-
Notifications
You must be signed in to change notification settings - Fork 1
feat: unified system object architecture with namespace-based auto-derivation #912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
319e53a
798cccc
382e9c6
272ed8f
72bc3eb
615b762
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| | `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. | ||
|
|
@@ -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. | ||
|
|
||
| --- | ||
|
|
||
|
|
||
| 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" | ||
| } | ||
| } |
| 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'; |
| 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'; |
| 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, | ||
| }, | ||
| }); |
| 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"] | ||
| } |
There was a problem hiding this comment.
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.