Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **ObjectSchema `namespace` property** — New optional `namespace` field on `ObjectSchema` for logical domain
classification (e.g., `'sys'`, `'crm'`). When set, `tableName` is auto-derived as `{namespace}_{name}` by
`ObjectSchema.create()` unless an explicit `tableName` is provided. This decouples the logical object name
from the physical table name and enables unified routing, permissions, and discovery by domain.
- **SystemObjectName constants** — Extended with all system objects: `ORGANIZATION`, `MEMBER`, `INVITATION`,
`TEAM`, `TEAM_MEMBER`, `API_KEY`, `TWO_FACTOR`, `ROLE`, `PERMISSION_SET`, `AUDIT_LOG` (in addition to
existing `USER`, `SESSION`, `ACCOUNT`, `VERIFICATION`, `METADATA`).
- **plugin-auth system objects** — Added `SysOrganization`, `SysMember`, `SysInvitation`, `SysTeam`,
`SysTeamMember`, `SysApiKey`, `SysTwoFactor` object definitions with `namespace: 'sys'`. Existing objects
(`SysUser`, `SysSession`, `SysAccount`, `SysVerification`) migrated to use namespace convention.
- **plugin-security system objects** — Added `SysRole` and `SysPermissionSet` object definitions.
- **plugin-audit** — New plugin package with `SysAuditLog` immutable audit trail object definition.
- **StorageNameMapping.resolveTableName()** — Now supports namespace-aware auto-derivation
(`{namespace}_{name}` fallback when no explicit `tableName` is set).

### Changed
- **System object naming convention** — All system objects now use `namespace: 'sys'` with short `name`
(e.g., `name: 'user'` instead of `name: 'sys_user'`). The `sys_` prefix is auto-derived via
`tableName` = `{namespace}_{name}`. File naming follows `sys-{name}.object.ts` pattern.
- **plugin-auth object exports** — New canonical exports use `Sys*` prefix (e.g., `SysUser`, `SysSession`).
Comment on lines +26 to +30
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Unreleased section now contains two consecutive "### Changed" headings (one starting at line 26 and another at line 35). This breaks Keep-a-Changelog formatting and makes it unclear which bullets belong to which section. Please merge these into a single "### Changed" section (or otherwise restructure the headings) so there's only one "Changed" block per release section.

Copilot uses AI. Check for mistakes.
Legacy `Auth*` exports are preserved as deprecated re-exports for backward compatibility.
- **sys_metadata object** — Migrated to `namespace: 'sys'`, `name: 'metadata'` convention (tableName
auto-derived as `sys_metadata`).
- **Locale code fallback** — New `resolveLocale()` helper in `@objectstack/core` that resolves
locale codes through a 4-step fallback chain: exact match → case-insensitive match
(`zh-cn` → `zh-CN`) → base language match (`zh-CN` → `zh`) → variant expansion
Expand Down
44 changes: 35 additions & 9 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,24 +307,50 @@ The following renames are planned for packages that implement core service contr
### System Object Naming Convention (`sys_` Prefix)

> **Adopted:** 2026-02-19
> **Updated:** 2026-03-11 — Namespace-based architecture with auto-derivation
> **Scope:** All system kernel objects in `SystemObjectName` constants.

All system kernel objects use the `sys_` prefix to clearly distinguish platform-internal objects from
All system kernel objects use the `sys` namespace to clearly distinguish platform-internal objects from
business/custom objects, aligning with industry best practices (e.g., ServiceNow `sys_user`, `sys_audit`).

| Constant Key | Protocol Name | Description |
|:---|:---|:---|
| `SystemObjectName.USER` | `sys_user` | Authentication: user identity |
| `SystemObjectName.SESSION` | `sys_session` | Authentication: active session |
| `SystemObjectName.ACCOUNT` | `sys_account` | Authentication: OAuth / credential account |
| `SystemObjectName.VERIFICATION` | `sys_verification` | Authentication: email / phone verification |
| `SystemObjectName.METADATA` | `sys_metadata` | System metadata storage |
Objects now declare `namespace: 'sys'` and a short `name` (e.g., `name: 'user'`). The physical table name
`sys_user` is auto-derived as `{namespace}_{name}` by `ObjectSchema.create()`.

| Constant Key | Protocol Name | Plugin | Description |
|:---|:---|:---|:---|
| `SystemObjectName.USER` | `sys_user` | plugin-auth | Authentication: user identity |
Comment on lines +316 to +321
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this section, the table column is labeled "Protocol Name" but the values shown (e.g. sys_user) are described elsewhere in the section as the physical table name auto-derived from {namespace}_{name}. Consider renaming the column to something like "Physical Table" / "Derived Table Name" to avoid confusing protocol identifiers with storage names. Also, since this section now describes a namespace-based convention, the section title still referencing a "sys_ Prefix" is misleading.

Copilot uses AI. Check for mistakes.
| `SystemObjectName.SESSION` | `sys_session` | plugin-auth | Authentication: active session |
| `SystemObjectName.ACCOUNT` | `sys_account` | plugin-auth | Authentication: OAuth / credential account |
| `SystemObjectName.VERIFICATION` | `sys_verification` | plugin-auth | Authentication: email / phone verification |
| `SystemObjectName.ORGANIZATION` | `sys_organization` | plugin-auth | Authentication: organization (multi-org) |
| `SystemObjectName.MEMBER` | `sys_member` | plugin-auth | Authentication: organization member |
| `SystemObjectName.INVITATION` | `sys_invitation` | plugin-auth | Authentication: organization invitation |
| `SystemObjectName.TEAM` | `sys_team` | plugin-auth | Authentication: team within an organization |
| `SystemObjectName.TEAM_MEMBER` | `sys_team_member` | plugin-auth | Authentication: team membership |
| `SystemObjectName.API_KEY` | `sys_api_key` | plugin-auth | Authentication: API key for programmatic access |
| `SystemObjectName.TWO_FACTOR` | `sys_two_factor` | plugin-auth | Authentication: two-factor credentials |
| `SystemObjectName.ROLE` | `sys_role` | plugin-security | Security: RBAC role definition |
| `SystemObjectName.PERMISSION_SET` | `sys_permission_set` | plugin-security | Security: permission set grouping |
| `SystemObjectName.AUDIT_LOG` | `sys_audit_log` | plugin-audit | Audit: immutable audit trail |
| `SystemObjectName.METADATA` | `sys_metadata` | metadata | System metadata storage |

**Object Definition Convention:**
- File naming: `sys-{name}.object.ts` (e.g., `sys-user.object.ts`, `sys-role.object.ts`)
- Export naming: `Sys{PascalCase}` (e.g., `SysUser`, `SysRole`, `SysAuditLog`)
- Object schema: `namespace: 'sys'`, `name: '{short_name}'` (no `sys_` prefix in name)
- Table derivation: `tableName` auto-derived as `sys_{name}` unless explicitly overridden

**Rationale:**
- Prevents naming collisions between system objects and business objects (e.g., a CRM `account` vs. `sys_account`)
- Aligns with ServiceNow and similar platforms that use `sys_` as a reserved namespace
- ObjectStack already uses namespace + FQN for business object isolation; the `sys_` prefix completes the picture for kernel-level objects
- Physical storage table names can differ via `ObjectSchema.tableName` + `StorageNameMapping.resolveTableName()` for backward compatibility
- Namespace-based auto-derivation eliminates manual `tableName` boilerplate and ensures consistency

**Plugin Architecture:**
- Each plugin (plugin-auth, plugin-security, plugin-audit) owns and registers its own `sys` namespace objects
- Plugins remain decoupled and optional — consumers aggregate all `sys` objects at runtime
- Object definitions follow the ObjectSchema protocol with `isSystem: true`

**Migration (v3.x → v4.0):**
- v3.x: The `SystemObjectName` constants now emit `sys_`-prefixed names. Implementations using `StorageNameMapping.resolveTableName()` can set `tableName` to preserve legacy physical table names during the transition.
Expand All @@ -333,7 +359,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow
- v3.x: **Bug fix** — `AuthManager.createDatabaseConfig()` now wraps the ObjectQL adapter as a `DBAdapterInstance` factory function (`(options) => DBAdapter`). Previously the raw adapter object was passed, which fell through to the Kysely adapter path and failed silently. `AuthManager.handleRequest()` and `AuthPlugin.registerAuthRoutes()` now inspect `response.status >= 500` and log the error body, since better-auth catches internal errors and returns 500 Responses without throwing.
- v3.x: **Bug fix** — `AuthPlugin` now defers HTTP route registration to a `kernel:ready` hook instead of doing it synchronously in `start()`. This makes the plugin resilient to plugin loading order — the `http-server` service is guaranteed to be available after all plugins complete their init/start phases. The CLI `serve` command also registers `HonoServerPlugin` before config plugins (with duplicate detection) for the same reason.
- v3.x: **Bug fix** — Studio `useApiDiscovery` hook no longer hardcodes auth endpoints as `/api/auth/...`. The `discover()` callback now fetches `/api/v1/discovery` and reads `routes.auth` to dynamically construct auth endpoint paths (falling back to `/api/v1/auth`). The session endpoint is corrected from `/session` to `/get-session` to align with better-auth's `AuthEndpointPaths.getSession`.
- v4.0: Legacy un-prefixed aliases will be fully removed.
- v4.0: Legacy un-prefixed aliases and `Auth*` export names will be fully removed.

---

Expand Down
3 changes: 2 additions & 1 deletion packages/metadata/src/objects/sys-metadata.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
* @see MetadataRecordSchema in metadata-persistence.zod.ts
*/
export const SysMetadataObject = ObjectSchema.create({
name: 'sys_metadata',
namespace: 'sys',
name: 'metadata',
label: 'System Metadata',
pluralLabel: 'System Metadata',
icon: 'settings',
Expand Down
27 changes: 27 additions & 0 deletions packages/plugins/plugin-audit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@objectstack/plugin-audit",
"version": "3.2.5",
"license": "Apache-2.0",
"description": "Audit Plugin for ObjectStack — System audit log object and audit trail",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"scripts": {
"build": "tsup --config ../../../tsup.config.ts",
"test": "vitest run"
},
"dependencies": {
"@objectstack/spec": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.3.5",
"typescript": "^5.0.0",
"vitest": "^4.0.18"
}
}
11 changes: 11 additions & 0 deletions packages/plugins/plugin-audit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* @objectstack/plugin-audit
*
* Audit Plugin for ObjectStack
* Provides the sys_audit_log system object definition for immutable audit trails.
*/

// System Object Definitions (sys namespace)
export { SysAuditLog } from './objects/index.js';
9 changes: 9 additions & 0 deletions packages/plugins/plugin-audit/src/objects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Audit Plugin — System Object Definitions (sys namespace)
*
* Canonical ObjectSchema definitions for audit-related system objects.
*/

export { SysAuditLog } from './sys-audit-log.object.js';
116 changes: 116 additions & 0 deletions packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { ObjectSchema, Field } from '@objectstack/spec/data';

/**
* sys_audit_log — System Audit Log Object
*
* Immutable audit trail for all significant platform events.
* Records who did what, when, and the before/after state.
*
* @namespace sys
*/
export const SysAuditLog = ObjectSchema.create({
namespace: 'sys',
name: 'audit_log',
label: 'Audit Log',
pluralLabel: 'Audit Logs',
icon: 'scroll-text',
isSystem: true,
description: 'Immutable audit trail for platform events',
titleFormat: '{action} on {object_name} by {user_id}',
compactLayout: ['action', 'object_name', 'user_id', 'created_at'],

fields: {
id: Field.text({
label: 'Audit Log ID',
required: true,
readonly: true,
}),

created_at: Field.datetime({
label: 'Timestamp',
required: true,
defaultValue: 'NOW()',
readonly: true,
}),

user_id: Field.text({
label: 'User ID',
required: false,
description: 'User who performed the action (null for system actions)',
}),

action: Field.select(['create', 'update', 'delete', 'restore', 'login', 'logout', 'permission_change', 'config_change', 'export', 'import'], {
label: 'Action',
required: true,
description: 'Action type (snake_case). Values: create, update, delete, restore, login, logout, permission_change, config_change, export, import',
}),

object_name: Field.text({
label: 'Object Name',
required: false,
maxLength: 255,
description: 'Target object (e.g. sys_user, project_task)',
}),

record_id: Field.text({
label: 'Record ID',
required: false,
description: 'ID of the affected record',
}),

old_value: Field.textarea({
label: 'Old Value',
required: false,
description: 'JSON-serialized previous state',
}),

new_value: Field.textarea({
label: 'New Value',
required: false,
description: 'JSON-serialized new state',
}),

ip_address: Field.text({
label: 'IP Address',
required: false,
maxLength: 45,
}),

user_agent: Field.textarea({
label: 'User Agent',
required: false,
}),

tenant_id: Field.text({
label: 'Tenant ID',
required: false,
description: 'Tenant context for multi-tenant isolation',
}),

metadata: Field.textarea({
label: 'Metadata',
required: false,
description: 'JSON-serialized additional context',
}),
},

indexes: [
{ fields: ['created_at'] },
{ fields: ['user_id'] },
{ fields: ['object_name', 'record_id'] },
{ fields: ['action'] },
{ fields: ['tenant_id'] },
],

enable: {
trackHistory: false, // Audit logs are themselves the audit trail
searchable: true,
apiEnabled: true,
apiMethods: ['get', 'list'], // Read-only — audit logs are immutable; creation happens via internal system hooks only
trash: false, // Never soft-delete audit logs
mru: false,
clone: false,
},
});
9 changes: 9 additions & 0 deletions packages/plugins/plugin-audit/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules", "**/*.test.ts"]
}
12 changes: 6 additions & 6 deletions packages/plugins/plugin-auth/src/auth-schema-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const AUTH_VERIFICATION_CONFIG = {
* | updatedAt | updated_at |
*/
export const AUTH_ORGANIZATION_SCHEMA = {
modelName: 'sys_organization',
modelName: SystemObjectName.ORGANIZATION, // 'sys_organization'
fields: {
createdAt: 'created_at',
updatedAt: 'updated_at',
Expand All @@ -178,7 +178,7 @@ export const AUTH_ORGANIZATION_SCHEMA = {
* | createdAt | created_at |
*/
export const AUTH_MEMBER_SCHEMA = {
modelName: 'sys_member',
modelName: SystemObjectName.MEMBER, // 'sys_member'
fields: {
organizationId: 'organization_id',
userId: 'user_id',
Expand All @@ -202,7 +202,7 @@ export const AUTH_MEMBER_SCHEMA = {
* | teamId | team_id |
*/
export const AUTH_INVITATION_SCHEMA = {
modelName: 'sys_invitation',
modelName: SystemObjectName.INVITATION, // 'sys_invitation'
fields: {
organizationId: 'organization_id',
inviterId: 'inviter_id',
Expand Down Expand Up @@ -240,7 +240,7 @@ export const AUTH_ORG_SESSION_FIELDS = {
* | updatedAt | updated_at |
*/
export const AUTH_TEAM_SCHEMA = {
modelName: 'sys_team',
modelName: SystemObjectName.TEAM, // 'sys_team'
fields: {
organizationId: 'organization_id',
createdAt: 'created_at',
Expand All @@ -262,7 +262,7 @@ export const AUTH_TEAM_SCHEMA = {
* | createdAt | created_at |
*/
export const AUTH_TEAM_MEMBER_SCHEMA = {
modelName: 'sys_team_member',
modelName: SystemObjectName.TEAM_MEMBER, // 'sys_team_member'
fields: {
teamId: 'team_id',
userId: 'user_id',
Expand All @@ -283,7 +283,7 @@ export const AUTH_TEAM_MEMBER_SCHEMA = {
* | userId | user_id |
*/
export const AUTH_TWO_FACTOR_SCHEMA = {
modelName: 'sys_two_factor',
modelName: SystemObjectName.TWO_FACTOR, // 'sys_two_factor'
fields: {
backupCodes: 'backup_codes',
userId: 'user_id',
Expand Down
Loading
Loading