From 319e53a73ab1cfd36eaa662826c2ddf14bbf4523 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:55:07 +0000 Subject: [PATCH 1/5] Initial plan From 798cccc52beb8932d2855243fbd214b232611f29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:05:42 +0000 Subject: [PATCH 2/5] feat: add namespace property to ObjectSchema with tableName auto-derivation, extend SystemObjectName with all system objects, create sys-namespaced object definitions across plugin-auth/plugin-security/plugin-audit Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/objects/sys-metadata.object.ts | 3 +- packages/plugins/plugin-audit/package.json | 27 ++++ packages/plugins/plugin-audit/src/index.ts | 11 ++ .../plugins/plugin-audit/src/objects/index.ts | 9 ++ .../src/objects/sys-audit-log.object.ts | 115 +++++++++++++++++ packages/plugins/plugin-audit/tsconfig.json | 9 ++ .../src/objects/auth-account.object.ts | 120 +----------------- .../src/objects/auth-session.object.ts | 88 +------------ .../src/objects/auth-user.object.ts | 96 +------------- .../src/objects/auth-verification.object.ts | 77 +---------- .../plugins/plugin-auth/src/objects/index.ts | 32 ++++- .../src/objects/sys-account.object.ts | 111 ++++++++++++++++ .../src/objects/sys-api-key.object.ts | 104 +++++++++++++++ .../src/objects/sys-invitation.object.ts | 93 ++++++++++++++ .../src/objects/sys-member.object.ts | 68 ++++++++++ .../src/objects/sys-organization.object.ts | 82 ++++++++++++ .../src/objects/sys-session.object.ts | 84 ++++++++++++ .../src/objects/sys-two-factor.object.ts | 73 +++++++++++ .../src/objects/sys-user.object.ts | 91 +++++++++++++ .../src/objects/sys-verification.object.ts | 75 +++++++++++ packages/plugins/plugin-security/src/index.ts | 3 + .../plugin-security/src/objects/index.ts | 10 ++ .../src/objects/sys-permission-set.object.ts | 94 ++++++++++++++ .../src/objects/sys-role.object.ts | 93 ++++++++++++++ packages/spec/src/data/object.test.ts | 103 +++++++++++++++ packages/spec/src/data/object.zod.ts | 18 ++- .../src/system/constants/system-names.test.ts | 44 +++++++ .../spec/src/system/constants/system-names.ts | 24 +++- pnpm-lock.yaml | 70 ++++++++++ 29 files changed, 1448 insertions(+), 379 deletions(-) create mode 100644 packages/plugins/plugin-audit/package.json create mode 100644 packages/plugins/plugin-audit/src/index.ts create mode 100644 packages/plugins/plugin-audit/src/objects/index.ts create mode 100644 packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts create mode 100644 packages/plugins/plugin-audit/tsconfig.json create mode 100644 packages/plugins/plugin-auth/src/objects/sys-account.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-api-key.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-invitation.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-member.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-organization.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-session.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-two-factor.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-user.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-verification.object.ts create mode 100644 packages/plugins/plugin-security/src/objects/index.ts create mode 100644 packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts create mode 100644 packages/plugins/plugin-security/src/objects/sys-role.object.ts diff --git a/packages/metadata/src/objects/sys-metadata.object.ts b/packages/metadata/src/objects/sys-metadata.object.ts index 90e583292..3678e4631 100644 --- a/packages/metadata/src/objects/sys-metadata.object.ts +++ b/packages/metadata/src/objects/sys-metadata.object.ts @@ -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', diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json new file mode 100644 index 000000000..d5e9d1923 --- /dev/null +++ b/packages/plugins/plugin-audit/package.json @@ -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" + } +} diff --git a/packages/plugins/plugin-audit/src/index.ts b/packages/plugins/plugin-audit/src/index.ts new file mode 100644 index 000000000..0241b2b67 --- /dev/null +++ b/packages/plugins/plugin-audit/src/index.ts @@ -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'; diff --git a/packages/plugins/plugin-audit/src/objects/index.ts b/packages/plugins/plugin-audit/src/objects/index.ts new file mode 100644 index 000000000..40cee1a97 --- /dev/null +++ b/packages/plugins/plugin-audit/src/objects/index.ts @@ -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'; diff --git a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts new file mode 100644 index 000000000..3586eb84f --- /dev/null +++ b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts @@ -0,0 +1,115 @@ +// 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, + }), + + 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 + trash: false, // Never soft-delete audit logs + mru: false, + clone: false, + }, +}); diff --git a/packages/plugins/plugin-audit/tsconfig.json b/packages/plugins/plugin-audit/tsconfig.json new file mode 100644 index 000000000..ead733427 --- /dev/null +++ b/packages/plugins/plugin-audit/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules", "**/*.test.ts"] +} diff --git a/packages/plugins/plugin-auth/src/objects/auth-account.object.ts b/packages/plugins/plugin-auth/src/objects/auth-account.object.ts index 5d1ac9d4a..2531bf0cd 100644 --- a/packages/plugins/plugin-auth/src/objects/auth-account.object.ts +++ b/packages/plugins/plugin-auth/src/objects/auth-account.object.ts @@ -1,121 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { ObjectSchema, Field } from '@objectstack/spec/data'; - /** - * Auth Account Object - * - * Uses better-auth's native schema for seamless migration: - * - id: string - * - created_at: Date - * - updated_at: Date - * - provider_id: string (e.g., 'google', 'github') - * - account_id: string (provider's user ID) - * - user_id: string (link to user table) - * - access_token: string | null - * - refresh_token: string | null - * - id_token: string | null - * - access_token_expires_at: Date | null - * - refresh_token_expires_at: Date | null - * - scope: string | null - * - password: string | null (for email/password provider) + * @deprecated Use `SysAccount` from `./sys-account.object` instead. + * This re-export is kept for backward compatibility. */ -export const AuthAccount = ObjectSchema.create({ - name: 'sys_account', - label: 'Account', - pluralLabel: 'Accounts', - icon: 'link', - description: 'OAuth and authentication provider accounts', - titleFormat: '{provider_id} - {account_id}', - compactLayout: ['provider_id', 'user_id', 'account_id'], - - fields: { - id: Field.text({ - label: 'Account ID', - required: true, - readonly: true, - }), - - created_at: Field.datetime({ - label: 'Created At', - defaultValue: 'NOW()', - readonly: true, - }), - - updated_at: Field.datetime({ - label: 'Updated At', - defaultValue: 'NOW()', - readonly: true, - }), - - provider_id: Field.text({ - label: 'Provider ID', - required: true, - description: 'OAuth provider identifier (google, github, etc.)', - }), - - account_id: Field.text({ - label: 'Provider Account ID', - required: true, - description: "User's ID in the provider's system", - }), - - user_id: Field.text({ - label: 'User ID', - required: true, - description: 'Link to user table', - }), - - access_token: Field.textarea({ - label: 'Access Token', - required: false, - }), - - refresh_token: Field.textarea({ - label: 'Refresh Token', - required: false, - }), - - id_token: Field.textarea({ - label: 'ID Token', - required: false, - }), - - access_token_expires_at: Field.datetime({ - label: 'Access Token Expires At', - required: false, - }), - - refresh_token_expires_at: Field.datetime({ - label: 'Refresh Token Expires At', - required: false, - }), - - scope: Field.text({ - label: 'OAuth Scope', - required: false, - }), - - password: Field.text({ - label: 'Password Hash', - required: false, - description: 'Hashed password for email/password provider', - }), - }, - - // Database indexes for performance - indexes: [ - { fields: ['user_id'], unique: false }, - { fields: ['provider_id', 'account_id'], unique: true }, - ], - - // Enable features - enable: { - trackHistory: false, - searchable: false, - apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: false, - }, -}); +export { SysAccount as AuthAccount } from './sys-account.object.js'; diff --git a/packages/plugins/plugin-auth/src/objects/auth-session.object.ts b/packages/plugins/plugin-auth/src/objects/auth-session.object.ts index b72402f96..3f375f61a 100644 --- a/packages/plugins/plugin-auth/src/objects/auth-session.object.ts +++ b/packages/plugins/plugin-auth/src/objects/auth-session.object.ts @@ -1,89 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { ObjectSchema, Field } from '@objectstack/spec/data'; - /** - * Auth Session Object - * - * Uses better-auth's native schema for seamless migration: - * - id: string - * - created_at: Date - * - updated_at: Date - * - user_id: string - * - expires_at: Date - * - token: string - * - ip_address: string | null - * - user_agent: string | null + * @deprecated Use `SysSession` from `./sys-session.object` instead. + * This re-export is kept for backward compatibility. */ -export const AuthSession = ObjectSchema.create({ - name: 'sys_session', - label: 'Session', - pluralLabel: 'Sessions', - icon: 'key', - description: 'Active user sessions', - titleFormat: 'Session {token}', - compactLayout: ['user_id', 'expires_at', 'ip_address'], - - fields: { - id: Field.text({ - label: 'Session ID', - required: true, - readonly: true, - }), - - created_at: Field.datetime({ - label: 'Created At', - defaultValue: 'NOW()', - readonly: true, - }), - - updated_at: Field.datetime({ - label: 'Updated At', - defaultValue: 'NOW()', - readonly: true, - }), - - user_id: Field.text({ - label: 'User ID', - required: true, - }), - - expires_at: Field.datetime({ - label: 'Expires At', - required: true, - }), - - token: Field.text({ - label: 'Session Token', - required: true, - }), - - ip_address: Field.text({ - label: 'IP Address', - required: false, - maxLength: 45, // Support IPv6 - }), - - user_agent: Field.textarea({ - label: 'User Agent', - required: false, - }), - }, - - // Database indexes for performance - indexes: [ - { fields: ['token'], unique: true }, - { fields: ['user_id'], unique: false }, - { fields: ['expires_at'], unique: false }, - ], - - // Enable features - enable: { - trackHistory: false, // Sessions don't need history tracking - searchable: false, - apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'delete'], // No update for sessions - trash: false, // Sessions should be hard deleted - mru: false, - }, -}); +export { SysSession as AuthSession } from './sys-session.object.js'; diff --git a/packages/plugins/plugin-auth/src/objects/auth-user.object.ts b/packages/plugins/plugin-auth/src/objects/auth-user.object.ts index b04c1601d..5df40bfef 100644 --- a/packages/plugins/plugin-auth/src/objects/auth-user.object.ts +++ b/packages/plugins/plugin-auth/src/objects/auth-user.object.ts @@ -1,97 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { ObjectSchema, Field } from '@objectstack/spec/data'; - /** - * Auth User Object - * - * Uses better-auth's native schema for seamless migration: - * - id: string - * - created_at: Date - * - updated_at: Date - * - email: string (unique, lowercase) - * - email_verified: boolean - * - name: string - * - image: string | null + * @deprecated Use `SysUser` from `./sys-user.object` instead. + * This re-export is kept for backward compatibility. */ -export const AuthUser = ObjectSchema.create({ - name: 'sys_user', - label: 'User', - pluralLabel: 'Users', - icon: 'user', - description: 'User accounts for authentication', - titleFormat: '{name} ({email})', - compactLayout: ['name', 'email', 'email_verified'], - - fields: { - // ID is auto-generated by ObjectQL - id: Field.text({ - label: 'User ID', - required: true, - readonly: true, - }), - - created_at: Field.datetime({ - label: 'Created At', - defaultValue: 'NOW()', - readonly: true, - }), - - updated_at: Field.datetime({ - label: 'Updated At', - defaultValue: 'NOW()', - readonly: true, - }), - - email: Field.email({ - label: 'Email', - required: true, - searchable: true, - }), - - email_verified: Field.boolean({ - label: 'Email Verified', - defaultValue: false, - }), - - name: Field.text({ - label: 'Name', - required: true, - searchable: true, - maxLength: 255, - }), - - image: Field.url({ - label: 'Profile Image', - required: false, - }), - }, - - // Database indexes for performance - indexes: [ - { fields: ['email'], unique: true }, - { fields: ['created_at'], unique: false }, - ], - - // Enable features - enable: { - trackHistory: true, - searchable: true, - apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'update', 'delete'], - trash: true, - mru: true, - }, - - // Validation Rules - validations: [ - { - name: 'email_unique', - type: 'unique', - severity: 'error', - message: 'Email must be unique', - fields: ['email'], - caseSensitive: false, - }, - ], -}); +export { SysUser as AuthUser } from './sys-user.object.js'; diff --git a/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts b/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts index 6618745b2..5411a6d70 100644 --- a/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts +++ b/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts @@ -1,78 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { ObjectSchema, Field } from '@objectstack/spec/data'; - /** - * Auth Verification Object - * - * Uses better-auth's native schema for seamless migration: - * - id: string - * - created_at: Date - * - updated_at: Date - * - value: string (verification token/code) - * - expires_at: Date - * - identifier: string (email or phone number) + * @deprecated Use `SysVerification` from `./sys-verification.object` instead. + * This re-export is kept for backward compatibility. */ -export const AuthVerification = ObjectSchema.create({ - name: 'sys_verification', - label: 'Verification', - pluralLabel: 'Verifications', - icon: 'shield-check', - description: 'Email and phone verification tokens', - titleFormat: 'Verification for {identifier}', - compactLayout: ['identifier', 'expires_at', 'created_at'], - - fields: { - id: Field.text({ - label: 'Verification ID', - required: true, - readonly: true, - }), - - created_at: Field.datetime({ - label: 'Created At', - defaultValue: 'NOW()', - readonly: true, - }), - - updated_at: Field.datetime({ - label: 'Updated At', - defaultValue: 'NOW()', - readonly: true, - }), - - value: Field.text({ - label: 'Verification Token', - required: true, - description: 'Token or code for verification', - }), - - expires_at: Field.datetime({ - label: 'Expires At', - required: true, - }), - - identifier: Field.text({ - label: 'Identifier', - required: true, - description: 'Email address or phone number', - }), - }, - - // Database indexes for performance - indexes: [ - { fields: ['value'], unique: true }, - { fields: ['identifier'], unique: false }, - { fields: ['expires_at'], unique: false }, - ], - - // Enable features - enable: { - trackHistory: false, - searchable: false, - apiEnabled: true, - apiMethods: ['get', 'create', 'delete'], // No list or update - trash: false, // Hard delete expired tokens - mru: false, - }, -}); +export { SysVerification as AuthVerification } from './sys-verification.object.js'; diff --git a/packages/plugins/plugin-auth/src/objects/index.ts b/packages/plugins/plugin-auth/src/objects/index.ts index 273f36b9b..660127b45 100644 --- a/packages/plugins/plugin-auth/src/objects/index.ts +++ b/packages/plugins/plugin-auth/src/objects/index.ts @@ -1,13 +1,37 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. /** - * Auth Objects - * - * ObjectQL-based object definitions for authentication database schema. - * These objects replace the need for third-party ORMs like drizzle-orm. + * Auth Plugin — System Object Definitions (sys namespace) + * + * Canonical ObjectSchema definitions for all authentication-related system objects. + * All objects belong to the `sys` namespace and follow the unified naming convention: + * - File: `sys-{name}.object.ts` + * - Export: `Sys{PascalCase}` + * - Object name: `{name}` (snake_case, no prefix) + * - Table name: `sys_{name}` (auto-derived from namespace) */ +// ── Core Auth Objects ────────────────────────────────────────────────────── +export { SysUser } from './sys-user.object.js'; +export { SysSession } from './sys-session.object.js'; +export { SysAccount } from './sys-account.object.js'; +export { SysVerification } from './sys-verification.object.js'; + +// ── Organization Objects ─────────────────────────────────────────────────── +export { SysOrganization } from './sys-organization.object.js'; +export { SysMember } from './sys-member.object.js'; +export { SysInvitation } from './sys-invitation.object.js'; + +// ── Additional Auth Objects ──────────────────────────────────────────────── +export { SysApiKey } from './sys-api-key.object.js'; +export { SysTwoFactor } from './sys-two-factor.object.js'; + +// ── Backward Compatibility (deprecated) ──────────────────────────────────── +/** @deprecated Use `SysUser` instead */ export { AuthUser } from './auth-user.object.js'; +/** @deprecated Use `SysSession` instead */ export { AuthSession } from './auth-session.object.js'; +/** @deprecated Use `SysAccount` instead */ export { AuthAccount } from './auth-account.object.js'; +/** @deprecated Use `SysVerification` instead */ export { AuthVerification } from './auth-verification.object.js'; diff --git a/packages/plugins/plugin-auth/src/objects/sys-account.object.ts b/packages/plugins/plugin-auth/src/objects/sys-account.object.ts new file mode 100644 index 000000000..c6cc423e3 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-account.object.ts @@ -0,0 +1,111 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_account — System Account Object + * + * OAuth / credential provider account record. + * Backed by better-auth's `account` model with ObjectStack field conventions. + * + * @namespace sys + */ +export const SysAccount = ObjectSchema.create({ + namespace: 'sys', + name: 'account', + label: 'Account', + pluralLabel: 'Accounts', + icon: 'link', + isSystem: true, + description: 'OAuth and authentication provider accounts', + titleFormat: '{provider_id} - {account_id}', + compactLayout: ['provider_id', 'user_id', 'account_id'], + + fields: { + id: Field.text({ + label: 'Account ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + provider_id: Field.text({ + label: 'Provider ID', + required: true, + description: 'OAuth provider identifier (google, github, etc.)', + }), + + account_id: Field.text({ + label: 'Provider Account ID', + required: true, + description: "User's ID in the provider's system", + }), + + user_id: Field.text({ + label: 'User ID', + required: true, + description: 'Link to user table', + }), + + access_token: Field.textarea({ + label: 'Access Token', + required: false, + }), + + refresh_token: Field.textarea({ + label: 'Refresh Token', + required: false, + }), + + id_token: Field.textarea({ + label: 'ID Token', + required: false, + }), + + access_token_expires_at: Field.datetime({ + label: 'Access Token Expires At', + required: false, + }), + + refresh_token_expires_at: Field.datetime({ + label: 'Refresh Token Expires At', + required: false, + }), + + scope: Field.text({ + label: 'OAuth Scope', + required: false, + }), + + password: Field.text({ + label: 'Password Hash', + required: false, + description: 'Hashed password for email/password provider', + }), + }, + + indexes: [ + { fields: ['user_id'], unique: false }, + { fields: ['provider_id', 'account_id'], unique: true }, + ], + + enable: { + trackHistory: false, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-api-key.object.ts b/packages/plugins/plugin-auth/src/objects/sys-api-key.object.ts new file mode 100644 index 000000000..1e59f100b --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-api-key.object.ts @@ -0,0 +1,104 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_api_key — System API Key Object + * + * API keys for programmatic/machine access to the platform. + * + * @namespace sys + */ +export const SysApiKey = ObjectSchema.create({ + namespace: 'sys', + name: 'api_key', + label: 'API Key', + pluralLabel: 'API Keys', + icon: 'key-round', + isSystem: true, + description: 'API keys for programmatic access', + titleFormat: '{name}', + compactLayout: ['name', 'user_id', 'expires_at'], + + fields: { + id: Field.text({ + label: 'API Key ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + name: Field.text({ + label: 'Name', + required: true, + maxLength: 255, + description: 'Human-readable label for the API key', + }), + + key: Field.text({ + label: 'Key', + required: true, + description: 'Hashed API key value', + }), + + prefix: Field.text({ + label: 'Prefix', + required: false, + maxLength: 16, + description: 'Visible prefix for identifying the key (e.g., "osk_")', + }), + + user_id: Field.text({ + label: 'User ID', + required: true, + description: 'Owner user of this API key', + }), + + scopes: Field.textarea({ + label: 'Scopes', + required: false, + description: 'JSON array of permission scopes', + }), + + expires_at: Field.datetime({ + label: 'Expires At', + required: false, + }), + + last_used_at: Field.datetime({ + label: 'Last Used At', + required: false, + }), + + revoked: Field.boolean({ + label: 'Revoked', + defaultValue: false, + }), + }, + + indexes: [ + { fields: ['key'], unique: true }, + { fields: ['user_id'] }, + { fields: ['prefix'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-invitation.object.ts b/packages/plugins/plugin-auth/src/objects/sys-invitation.object.ts new file mode 100644 index 000000000..4698c4230 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-invitation.object.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_invitation — System Invitation Object + * + * Organization invitation tokens for inviting users. + * Backed by better-auth's organization plugin. + * + * @namespace sys + */ +export const SysInvitation = ObjectSchema.create({ + namespace: 'sys', + name: 'invitation', + label: 'Invitation', + pluralLabel: 'Invitations', + icon: 'mail', + isSystem: true, + description: 'Organization invitations for user onboarding', + titleFormat: 'Invitation to {organization_id}', + compactLayout: ['email', 'organization_id', 'status'], + + fields: { + id: Field.text({ + label: 'Invitation ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + organization_id: Field.text({ + label: 'Organization ID', + required: true, + }), + + email: Field.email({ + label: 'Email', + required: true, + description: 'Email address of the invited user', + }), + + role: Field.text({ + label: 'Role', + required: false, + maxLength: 100, + description: 'Role to assign upon acceptance', + }), + + status: Field.select(['pending', 'accepted', 'rejected', 'expired', 'canceled'], { + label: 'Status', + required: true, + defaultValue: 'pending', + }), + + inviter_id: Field.text({ + label: 'Inviter ID', + required: true, + description: 'User ID of the person who sent the invitation', + }), + + expires_at: Field.datetime({ + label: 'Expires At', + required: true, + }), + + team_id: Field.text({ + label: 'Team ID', + required: false, + description: 'Optional team to assign upon acceptance', + }), + }, + + indexes: [ + { fields: ['organization_id'] }, + { fields: ['email'] }, + { fields: ['expires_at'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-member.object.ts b/packages/plugins/plugin-auth/src/objects/sys-member.object.ts new file mode 100644 index 000000000..28d400c76 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-member.object.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_member — System Member Object + * + * Organization membership linking users to organizations with roles. + * Backed by better-auth's organization plugin. + * + * @namespace sys + */ +export const SysMember = ObjectSchema.create({ + namespace: 'sys', + name: 'member', + label: 'Member', + pluralLabel: 'Members', + icon: 'user-check', + isSystem: true, + description: 'Organization membership records', + titleFormat: '{user_id} in {organization_id}', + compactLayout: ['user_id', 'organization_id', 'role'], + + fields: { + id: Field.text({ + label: 'Member ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + organization_id: Field.text({ + label: 'Organization ID', + required: true, + }), + + user_id: Field.text({ + label: 'User ID', + required: true, + }), + + role: Field.text({ + label: 'Role', + required: false, + description: 'Member role within the organization (e.g. admin, member)', + maxLength: 100, + }), + }, + + indexes: [ + { fields: ['organization_id', 'user_id'], unique: true }, + { fields: ['user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-organization.object.ts b/packages/plugins/plugin-auth/src/objects/sys-organization.object.ts new file mode 100644 index 000000000..14599fc46 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-organization.object.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_organization — System Organization Object + * + * Multi-organization support for the ObjectStack platform. + * Backed by better-auth's organization plugin. + * + * @namespace sys + */ +export const SysOrganization = ObjectSchema.create({ + namespace: 'sys', + name: 'organization', + label: 'Organization', + pluralLabel: 'Organizations', + icon: 'building-2', + isSystem: true, + description: 'Organizations for multi-tenant grouping', + titleFormat: '{name}', + compactLayout: ['name', 'slug', 'created_at'], + + fields: { + id: Field.text({ + label: 'Organization ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + name: Field.text({ + label: 'Name', + required: true, + searchable: true, + maxLength: 255, + }), + + slug: Field.text({ + label: 'Slug', + required: false, + maxLength: 255, + description: 'URL-friendly identifier', + }), + + logo: Field.url({ + label: 'Logo', + required: false, + }), + + metadata: Field.textarea({ + label: 'Metadata', + required: false, + description: 'JSON-serialized organization metadata', + }), + }, + + indexes: [ + { fields: ['slug'], unique: true }, + { fields: ['name'] }, + ], + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: true, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-session.object.ts b/packages/plugins/plugin-auth/src/objects/sys-session.object.ts new file mode 100644 index 000000000..0d0e14243 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-session.object.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_session — System Session Object + * + * Active user session record for the ObjectStack platform. + * Backed by better-auth's `session` model with ObjectStack field conventions. + * + * @namespace sys + */ +export const SysSession = ObjectSchema.create({ + namespace: 'sys', + name: 'session', + label: 'Session', + pluralLabel: 'Sessions', + icon: 'key', + isSystem: true, + description: 'Active user sessions', + titleFormat: 'Session {token}', + compactLayout: ['user_id', 'expires_at', 'ip_address'], + + fields: { + id: Field.text({ + label: 'Session ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + user_id: Field.text({ + label: 'User ID', + required: true, + }), + + expires_at: Field.datetime({ + label: 'Expires At', + required: true, + }), + + token: Field.text({ + label: 'Session Token', + required: true, + }), + + ip_address: Field.text({ + label: 'IP Address', + required: false, + maxLength: 45, // Support IPv6 + }), + + user_agent: Field.textarea({ + label: 'User Agent', + required: false, + }), + }, + + indexes: [ + { fields: ['token'], unique: true }, + { fields: ['user_id'], unique: false }, + { fields: ['expires_at'], unique: false }, + ], + + enable: { + trackHistory: false, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-two-factor.object.ts b/packages/plugins/plugin-auth/src/objects/sys-two-factor.object.ts new file mode 100644 index 000000000..8900650e0 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-two-factor.object.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_two_factor — System Two-Factor Object + * + * Two-factor authentication credentials (TOTP, backup codes). + * Backed by better-auth's two-factor plugin. + * + * @namespace sys + */ +export const SysTwoFactor = ObjectSchema.create({ + namespace: 'sys', + name: 'two_factor', + label: 'Two Factor', + pluralLabel: 'Two Factor Credentials', + icon: 'smartphone', + isSystem: true, + description: 'Two-factor authentication credentials', + titleFormat: 'Two-factor for {user_id}', + compactLayout: ['user_id', 'created_at'], + + fields: { + id: Field.text({ + label: 'Two Factor ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + user_id: Field.text({ + label: 'User ID', + required: true, + }), + + secret: Field.text({ + label: 'Secret', + required: true, + description: 'TOTP secret key', + }), + + backup_codes: Field.textarea({ + label: 'Backup Codes', + required: false, + description: 'JSON-serialized backup recovery codes', + }), + }, + + indexes: [ + { fields: ['user_id'], unique: true }, + ], + + enable: { + trackHistory: false, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'create', 'update', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-user.object.ts b/packages/plugins/plugin-auth/src/objects/sys-user.object.ts new file mode 100644 index 000000000..62d64de39 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-user.object.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_user — System User Object + * + * Canonical user identity record for the ObjectStack platform. + * Backed by better-auth's `user` model with ObjectStack field conventions. + * + * @namespace sys + */ +export const SysUser = ObjectSchema.create({ + namespace: 'sys', + name: 'user', + label: 'User', + pluralLabel: 'Users', + icon: 'user', + isSystem: true, + description: 'User accounts for authentication', + titleFormat: '{name} ({email})', + compactLayout: ['name', 'email', 'email_verified'], + + fields: { + id: Field.text({ + label: 'User ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + email: Field.email({ + label: 'Email', + required: true, + searchable: true, + }), + + email_verified: Field.boolean({ + label: 'Email Verified', + defaultValue: false, + }), + + name: Field.text({ + label: 'Name', + required: true, + searchable: true, + maxLength: 255, + }), + + image: Field.url({ + label: 'Profile Image', + required: false, + }), + }, + + indexes: [ + { fields: ['email'], unique: true }, + { fields: ['created_at'], unique: false }, + ], + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: true, + }, + + validations: [ + { + name: 'email_unique', + type: 'unique', + severity: 'error', + message: 'Email must be unique', + fields: ['email'], + caseSensitive: false, + }, + ], +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-verification.object.ts b/packages/plugins/plugin-auth/src/objects/sys-verification.object.ts new file mode 100644 index 000000000..b785b600e --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-verification.object.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_verification — System Verification Object + * + * Email and phone verification token record. + * Backed by better-auth's `verification` model with ObjectStack field conventions. + * + * @namespace sys + */ +export const SysVerification = ObjectSchema.create({ + namespace: 'sys', + name: 'verification', + label: 'Verification', + pluralLabel: 'Verifications', + icon: 'shield-check', + isSystem: true, + description: 'Email and phone verification tokens', + titleFormat: 'Verification for {identifier}', + compactLayout: ['identifier', 'expires_at', 'created_at'], + + fields: { + id: Field.text({ + label: 'Verification ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + value: Field.text({ + label: 'Verification Token', + required: true, + description: 'Token or code for verification', + }), + + expires_at: Field.datetime({ + label: 'Expires At', + required: true, + }), + + identifier: Field.text({ + label: 'Identifier', + required: true, + description: 'Email address or phone number', + }), + }, + + indexes: [ + { fields: ['value'], unique: true }, + { fields: ['identifier'], unique: false }, + { fields: ['expires_at'], unique: false }, + ], + + enable: { + trackHistory: false, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'create', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-security/src/index.ts b/packages/plugins/plugin-security/src/index.ts index cca27a626..244dcd06a 100644 --- a/packages/plugins/plugin-security/src/index.ts +++ b/packages/plugins/plugin-security/src/index.ts @@ -11,3 +11,6 @@ export { SecurityPlugin } from './security-plugin.js'; export { PermissionEvaluator } from './permission-evaluator.js'; export { RLSCompiler } from './rls-compiler.js'; export { FieldMasker } from './field-masker.js'; + +// System Object Definitions (sys namespace) +export { SysRole, SysPermissionSet } from './objects/index.js'; diff --git a/packages/plugins/plugin-security/src/objects/index.ts b/packages/plugins/plugin-security/src/objects/index.ts new file mode 100644 index 000000000..7acee6870 --- /dev/null +++ b/packages/plugins/plugin-security/src/objects/index.ts @@ -0,0 +1,10 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Security Plugin — System Object Definitions (sys namespace) + * + * Canonical ObjectSchema definitions for security-related system objects. + */ + +export { SysRole } from './sys-role.object.js'; +export { SysPermissionSet } from './sys-permission-set.object.js'; diff --git a/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts new file mode 100644 index 000000000..43b50bfea --- /dev/null +++ b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_permission_set — System Permission Set Object + * + * Named groupings of fine-grained permissions. + * Permission sets can be assigned to roles or directly to users + * for granular access control. + * + * @namespace sys + */ +export const SysPermissionSet = ObjectSchema.create({ + namespace: 'sys', + name: 'permission_set', + label: 'Permission Set', + pluralLabel: 'Permission Sets', + icon: 'lock', + isSystem: true, + description: 'Named permission groupings for fine-grained access control', + titleFormat: '{name}', + compactLayout: ['name', 'label', 'active'], + + fields: { + id: Field.text({ + label: 'Permission Set ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + name: Field.text({ + label: 'API Name', + required: true, + searchable: true, + maxLength: 100, + description: 'Unique machine name for the permission set', + }), + + label: Field.text({ + label: 'Display Name', + required: true, + maxLength: 255, + }), + + description: Field.textarea({ + label: 'Description', + required: false, + }), + + object_permissions: Field.textarea({ + label: 'Object Permissions', + required: false, + description: 'JSON-serialized object-level CRUD permissions', + }), + + field_permissions: Field.textarea({ + label: 'Field Permissions', + required: false, + description: 'JSON-serialized field-level read/write permissions', + }), + + active: Field.boolean({ + label: 'Active', + defaultValue: true, + }), + }, + + indexes: [ + { fields: ['name'], unique: true }, + { fields: ['active'] }, + ], + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: true, + }, +}); diff --git a/packages/plugins/plugin-security/src/objects/sys-role.object.ts b/packages/plugins/plugin-security/src/objects/sys-role.object.ts new file mode 100644 index 000000000..1057433e7 --- /dev/null +++ b/packages/plugins/plugin-security/src/objects/sys-role.object.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_role — System Role Object + * + * RBAC role definition for the ObjectStack platform. + * Roles group permissions and are assigned to users or members. + * + * @namespace sys + */ +export const SysRole = ObjectSchema.create({ + namespace: 'sys', + name: 'role', + label: 'Role', + pluralLabel: 'Roles', + icon: 'shield', + isSystem: true, + description: 'Role definitions for RBAC access control', + titleFormat: '{name}', + compactLayout: ['name', 'label', 'active'], + + fields: { + id: Field.text({ + label: 'Role ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + name: Field.text({ + label: 'API Name', + required: true, + searchable: true, + maxLength: 100, + description: 'Unique machine name for the role (e.g. admin, editor, viewer)', + }), + + label: Field.text({ + label: 'Display Name', + required: true, + maxLength: 255, + }), + + description: Field.textarea({ + label: 'Description', + required: false, + }), + + permissions: Field.textarea({ + label: 'Permissions', + required: false, + description: 'JSON-serialized array of permission strings', + }), + + active: Field.boolean({ + label: 'Active', + defaultValue: true, + }), + + is_default: Field.boolean({ + label: 'Default Role', + defaultValue: false, + description: 'Automatically assigned to new users', + }), + }, + + indexes: [ + { fields: ['name'], unique: true }, + { fields: ['active'] }, + ], + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: true, + }, +}); diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index 68ae9b88f..5db4bf954 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -744,3 +744,106 @@ describe('ObjectSchema.create()', () => { })).toThrow(); }); }); + +// ============================================================================ +// Namespace & Auto-Derivation +// ============================================================================ + +describe('ObjectSchema namespace', () => { + it('should accept namespace property', () => { + const result = ObjectSchema.safeParse({ + namespace: 'sys', + name: 'user', + fields: {}, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.namespace).toBe('sys'); + } + }); + + it('should accept object without namespace (optional)', () => { + const result = ObjectSchema.safeParse({ + name: 'account', + fields: {}, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.namespace).toBeUndefined(); + } + }); + + it('should reject invalid namespace format (must be lowercase alpha)', () => { + const result = ObjectSchema.safeParse({ + namespace: 'Sys', + name: 'user', + fields: {}, + }); + expect(result.success).toBe(false); + }); + + it('should reject namespace with underscores', () => { + const result = ObjectSchema.safeParse({ + namespace: 'my_ns', + name: 'user', + fields: {}, + }); + expect(result.success).toBe(false); + }); + + it('should reject namespace with hyphens', () => { + const result = ObjectSchema.safeParse({ + namespace: 'my-ns', + name: 'user', + fields: {}, + }); + expect(result.success).toBe(false); + }); +}); + +describe('ObjectSchema.create() namespace auto-derivation', () => { + it('should auto-derive tableName from namespace + name', () => { + const obj = ObjectSchema.create({ + namespace: 'sys', + name: 'user', + fields: {}, + }); + expect(obj.tableName).toBe('sys_user'); + }); + + it('should auto-derive tableName for multi-word name', () => { + const obj = ObjectSchema.create({ + namespace: 'sys', + name: 'audit_log', + fields: {}, + }); + expect(obj.tableName).toBe('sys_audit_log'); + }); + + it('should prefer explicit tableName over auto-derived', () => { + const obj = ObjectSchema.create({ + namespace: 'sys', + name: 'user', + tableName: 'custom_users', + fields: {}, + }); + expect(obj.tableName).toBe('custom_users'); + }); + + it('should not set tableName when namespace is absent', () => { + const obj = ObjectSchema.create({ + name: 'account', + fields: {}, + }); + expect(obj.tableName).toBeUndefined(); + }); + + it('should auto-derive tableName for business namespace', () => { + const obj = ObjectSchema.create({ + namespace: 'crm', + name: 'deal', + fields: {}, + }); + expect(obj.tableName).toBe('crm_deal'); + }); +}); diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index b8c4d5d1e..4960ef93a 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -244,6 +244,20 @@ const ObjectSchemaBase = z.object({ description: z.string().optional().describe('Developer documentation / description'), icon: z.string().optional().describe('Icon name (Lucide/Material) for UI representation'), + /** + * Namespace & Domain Classification + * + * Groups objects into logical domains for routing, permissions, and discovery. + * System objects use `'sys'`; business packages use their own namespace. + * + * When set, `tableName` is auto-derived as `{namespace}_{name}` by + * `ObjectSchema.create()` unless an explicit `tableName` is provided. + * + * @example namespace: 'sys' → tableName defaults to 'sys_user' + * @example namespace: 'crm' → tableName defaults to 'crm_account' + */ + namespace: z.string().regex(/^[a-z][a-z0-9]*$/).optional().describe('Logical domain namespace (e.g. "sys", "crm"). Used for routing, permissions, and auto-deriving tableName.'), + /** * Taxonomy & Organization */ @@ -256,7 +270,7 @@ const ObjectSchemaBase = z.object({ * Storage & Virtualization */ datasource: z.string().optional().default('default').describe('Target Datasource ID. "default" is the primary DB.'), - tableName: z.string().optional().describe('Physical table/collection name in the target datasource'), + tableName: z.string().optional().describe('Physical table/collection name in the target datasource. Auto-derived as {namespace}_{name} when namespace is set.'), /** * Data Model @@ -382,6 +396,8 @@ export const ObjectSchema = Object.assign(ObjectSchemaBase, { const withDefaults = { ...config, label: config.label ?? snakeCaseToLabel(config.name), + // Auto-derive tableName as {namespace}_{name} when namespace is set + tableName: config.tableName ?? (config.namespace ? `${config.namespace}_${config.name}` : undefined), }; return ObjectSchemaBase.parse(withDefaults) as Omit & Pick; }, diff --git a/packages/spec/src/system/constants/system-names.test.ts b/packages/spec/src/system/constants/system-names.test.ts index 798a648e0..acea481b5 100644 --- a/packages/spec/src/system/constants/system-names.test.ts +++ b/packages/spec/src/system/constants/system-names.test.ts @@ -15,6 +15,14 @@ describe('SystemObjectName', () => { expect(SystemObjectName.SESSION).toBe('sys_session'); expect(SystemObjectName.ACCOUNT).toBe('sys_account'); expect(SystemObjectName.VERIFICATION).toBe('sys_verification'); + expect(SystemObjectName.ORGANIZATION).toBe('sys_organization'); + expect(SystemObjectName.MEMBER).toBe('sys_member'); + expect(SystemObjectName.INVITATION).toBe('sys_invitation'); + expect(SystemObjectName.API_KEY).toBe('sys_api_key'); + expect(SystemObjectName.TWO_FACTOR).toBe('sys_two_factor'); + expect(SystemObjectName.ROLE).toBe('sys_role'); + expect(SystemObjectName.PERMISSION_SET).toBe('sys_permission_set'); + expect(SystemObjectName.AUDIT_LOG).toBe('sys_audit_log'); expect(SystemObjectName.METADATA).toBe('sys_metadata'); }); @@ -22,6 +30,26 @@ describe('SystemObjectName', () => { const names: readonly string[] = Object.values(SystemObjectName); expect(names).toContain('sys_user'); expect(names).toContain('sys_session'); + expect(names).toContain('sys_organization'); + expect(names).toContain('sys_role'); + expect(names).toContain('sys_audit_log'); + }); + + it('should have all expected keys', () => { + const keys = Object.keys(SystemObjectName); + expect(keys).toContain('USER'); + expect(keys).toContain('SESSION'); + expect(keys).toContain('ACCOUNT'); + expect(keys).toContain('VERIFICATION'); + expect(keys).toContain('ORGANIZATION'); + expect(keys).toContain('MEMBER'); + expect(keys).toContain('INVITATION'); + expect(keys).toContain('API_KEY'); + expect(keys).toContain('TWO_FACTOR'); + expect(keys).toContain('ROLE'); + expect(keys).toContain('PERMISSION_SET'); + expect(keys).toContain('AUDIT_LOG'); + expect(keys).toContain('METADATA'); }); }); @@ -64,6 +92,22 @@ describe('StorageNameMapping', () => { it('should fall back to name when tableName is undefined', () => { expect(StorageNameMapping.resolveTableName({ name: 'session', tableName: undefined })).toBe('session'); }); + + it('should auto-derive table name from namespace + name', () => { + expect(StorageNameMapping.resolveTableName({ name: 'user', namespace: 'sys' })).toBe('sys_user'); + }); + + it('should prefer explicit tableName over namespace derivation', () => { + expect(StorageNameMapping.resolveTableName({ name: 'user', namespace: 'sys', tableName: 'custom_users' })).toBe('custom_users'); + }); + + it('should derive multi-word name with namespace', () => { + expect(StorageNameMapping.resolveTableName({ name: 'audit_log', namespace: 'sys' })).toBe('sys_audit_log'); + }); + + it('should fall back to name when namespace is undefined', () => { + expect(StorageNameMapping.resolveTableName({ name: 'account', namespace: undefined })).toBe('account'); + }); }); describe('resolveColumnName', () => { diff --git a/packages/spec/src/system/constants/system-names.ts b/packages/spec/src/system/constants/system-names.ts index 4a3318efd..9eda294c7 100644 --- a/packages/spec/src/system/constants/system-names.ts +++ b/packages/spec/src/system/constants/system-names.ts @@ -28,6 +28,22 @@ export const SystemObjectName = { ACCOUNT: 'sys_account', /** Authentication: email / phone verification */ VERIFICATION: 'sys_verification', + /** Authentication: organization (multi-org support) */ + ORGANIZATION: 'sys_organization', + /** Authentication: organization member */ + MEMBER: 'sys_member', + /** Authentication: organization invitation */ + INVITATION: 'sys_invitation', + /** Authentication: API key for programmatic access */ + API_KEY: 'sys_api_key', + /** Authentication: two-factor authentication credentials */ + TWO_FACTOR: 'sys_two_factor', + /** Security: role definition for RBAC */ + ROLE: 'sys_role', + /** Security: permission set grouping */ + PERMISSION_SET: 'sys_permission_set', + /** Audit: system audit log */ + AUDIT_LOG: 'sys_audit_log', /** System metadata storage */ METADATA: 'sys_metadata', } as const; @@ -87,13 +103,13 @@ export type SystemFieldName = typeof SystemFieldName[keyof typeof SystemFieldNam export const StorageNameMapping = { /** * Resolve the physical table name for an object. - * Falls back to `object.name` when `tableName` is not set. + * Priority: explicit `tableName` → auto-derived `{namespace}_{name}` → `name`. * - * @param object - Object definition (at minimum `{ name: string; tableName?: string }`) + * @param object - Object definition (at minimum `{ name: string; namespace?: string; tableName?: string }`) * @returns The physical table / collection name to use in storage operations. */ - resolveTableName(object: { name: string; tableName?: string }): string { - return object.tableName ?? object.name; + resolveTableName(object: { name: string; namespace?: string; tableName?: string }): string { + return object.tableName ?? (object.namespace ? `${object.namespace}_${object.name}` : object.name); }, /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99624a05a..6211ed3fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -699,6 +699,22 @@ importers: specifier: ^4.0.18 version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.5)(typescript@5.9.3))(tsx@4.21.0) + packages/plugins/plugin-audit: + dependencies: + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + devDependencies: + '@types/node': + specifier: ^25.3.5 + version: 25.3.5 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.5)(typescript@5.9.3))(tsx@4.21.0) + packages/plugins/plugin-auth: dependencies: '@objectstack/core': @@ -1778,89 +1794,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -2027,24 +2059,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.1.6': resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} @@ -2772,131 +2808,157 @@ packages: resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.57.1': resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.2': resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.2': resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.2': resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.2': resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.2': resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.2': resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.2': resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.2': resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.2': resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.2': resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.2': resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.2': resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.2': resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} @@ -3126,24 +3188,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -5008,24 +5074,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} From 382e9c6da9ddf82ead13a73cfad8a296c66763a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:08:52 +0000 Subject: [PATCH 3/5] docs: update CHANGELOG.md and ROADMAP.md for system object architecture unification Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- CHANGELOG.md | 25 +++++++++++++++++++++++++ ROADMAP.md | 42 +++++++++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac05d203..3c5ede8bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ 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`, + `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`, `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`). + 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`). + ### Changed - **i18n route self-registration** — Moved i18n REST endpoint registration from `RestServer` to `I18nServicePlugin` (and kernel fallback). The i18n plugin now self-registers `/api/v1/i18n/*` diff --git a/ROADMAP.md b/ROADMAP.md index c6cf89da8..5943ab086 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -307,24 +307,48 @@ 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 | +| `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.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 +357,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. --- From 272ed8fa85d60380a429ada44a615e5330531994 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:10:25 +0000 Subject: [PATCH 4/5] fix: address code review feedback - improve namespace and audit log documentation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../plugins/plugin-audit/src/objects/sys-audit-log.object.ts | 3 ++- packages/spec/src/data/object.zod.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts index 3586eb84f..597d94f7a 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts @@ -44,6 +44,7 @@ export const SysAuditLog = ObjectSchema.create({ 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({ @@ -107,7 +108,7 @@ export const SysAuditLog = ObjectSchema.create({ trackHistory: false, // Audit logs are themselves the audit trail searchable: true, apiEnabled: true, - apiMethods: ['get', 'list'], // Read-only — audit logs are immutable + 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, diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 4960ef93a..8f444d881 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -253,10 +253,13 @@ const ObjectSchemaBase = z.object({ * When set, `tableName` is auto-derived as `{namespace}_{name}` by * `ObjectSchema.create()` unless an explicit `tableName` is provided. * + * Namespace must be a single lowercase word (no underscores or hyphens) + * to ensure clean auto-derivation of `{namespace}_{name}` table names. + * * @example namespace: 'sys' → tableName defaults to 'sys_user' * @example namespace: 'crm' → tableName defaults to 'crm_account' */ - namespace: z.string().regex(/^[a-z][a-z0-9]*$/).optional().describe('Logical domain namespace (e.g. "sys", "crm"). Used for routing, permissions, and auto-deriving tableName.'), + namespace: z.string().regex(/^[a-z][a-z0-9]*$/).optional().describe('Logical domain namespace — single lowercase word (e.g. "sys", "crm"). Used for routing, permissions, and auto-deriving tableName as {namespace}_{name}.'), /** * Taxonomy & Organization From 72bc3eb552f4f9fcae4498d4b84e4f537ed0f1ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:52:20 +0000 Subject: [PATCH 5/5] feat: add missing sys_team and sys_team_member objects, unify auth-schema-config to use SystemObjectName constants Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- CHANGELOG.md | 10 +-- ROADMAP.md | 2 + .../plugin-auth/src/auth-schema-config.ts | 12 ++-- .../plugins/plugin-auth/src/objects/index.ts | 2 + .../src/objects/sys-team-member.object.ts | 61 ++++++++++++++++ .../src/objects/sys-team.object.ts | 69 +++++++++++++++++++ .../src/system/constants/system-names.test.ts | 6 ++ .../spec/src/system/constants/system-names.ts | 4 ++ 8 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 packages/plugins/plugin-auth/src/objects/sys-team-member.object.ts create mode 100644 packages/plugins/plugin-auth/src/objects/sys-team.object.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5ede8bd..b0c1b8144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `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`, - `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`, `SysApiKey`, - `SysTwoFactor` object definitions with `namespace: 'sys'`. Existing objects (`SysUser`, `SysSession`, - `SysAccount`, `SysVerification`) migrated to use namespace convention. + `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 diff --git a/ROADMAP.md b/ROADMAP.md index 5943ab086..b936e70bf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -325,6 +325,8 @@ Objects now declare `namespace: 'sys'` and a short `name` (e.g., `name: 'user'`) | `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 | diff --git a/packages/plugins/plugin-auth/src/auth-schema-config.ts b/packages/plugins/plugin-auth/src/auth-schema-config.ts index ca99f7387..13d54d5b8 100644 --- a/packages/plugins/plugin-auth/src/auth-schema-config.ts +++ b/packages/plugins/plugin-auth/src/auth-schema-config.ts @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', diff --git a/packages/plugins/plugin-auth/src/objects/index.ts b/packages/plugins/plugin-auth/src/objects/index.ts index 660127b45..b84cb452c 100644 --- a/packages/plugins/plugin-auth/src/objects/index.ts +++ b/packages/plugins/plugin-auth/src/objects/index.ts @@ -21,6 +21,8 @@ export { SysVerification } from './sys-verification.object.js'; export { SysOrganization } from './sys-organization.object.js'; export { SysMember } from './sys-member.object.js'; export { SysInvitation } from './sys-invitation.object.js'; +export { SysTeam } from './sys-team.object.js'; +export { SysTeamMember } from './sys-team-member.object.js'; // ── Additional Auth Objects ──────────────────────────────────────────────── export { SysApiKey } from './sys-api-key.object.js'; diff --git a/packages/plugins/plugin-auth/src/objects/sys-team-member.object.ts b/packages/plugins/plugin-auth/src/objects/sys-team-member.object.ts new file mode 100644 index 000000000..ed98de21c --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-team-member.object.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_team_member — System Team Member Object + * + * Links users to teams within organizations. + * Backed by better-auth's organization plugin (teams feature). + * + * @namespace sys + */ +export const SysTeamMember = ObjectSchema.create({ + namespace: 'sys', + name: 'team_member', + label: 'Team Member', + pluralLabel: 'Team Members', + icon: 'user-plus', + isSystem: true, + description: 'Team membership records linking users to teams', + titleFormat: '{user_id} in {team_id}', + compactLayout: ['user_id', 'team_id', 'created_at'], + + fields: { + id: Field.text({ + label: 'Team Member ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + team_id: Field.text({ + label: 'Team ID', + required: true, + }), + + user_id: Field.text({ + label: 'User ID', + required: true, + }), + }, + + indexes: [ + { fields: ['team_id', 'user_id'], unique: true }, + { fields: ['user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'delete'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/sys-team.object.ts b/packages/plugins/plugin-auth/src/objects/sys-team.object.ts new file mode 100644 index 000000000..efeded841 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/sys-team.object.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_team — System Team Object + * + * Teams within an organization for fine-grained grouping. + * Backed by better-auth's organization plugin (teams feature). + * + * @namespace sys + */ +export const SysTeam = ObjectSchema.create({ + namespace: 'sys', + name: 'team', + label: 'Team', + pluralLabel: 'Teams', + icon: 'users', + isSystem: true, + description: 'Teams within organizations for fine-grained grouping', + titleFormat: '{name}', + compactLayout: ['name', 'organization_id', 'created_at'], + + fields: { + id: Field.text({ + label: 'Team ID', + required: true, + readonly: true, + }), + + created_at: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updated_at: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + name: Field.text({ + label: 'Name', + required: true, + searchable: true, + maxLength: 255, + }), + + organization_id: Field.text({ + label: 'Organization ID', + required: true, + }), + }, + + indexes: [ + { fields: ['organization_id'] }, + { fields: ['name', 'organization_id'], unique: true }, + ], + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: false, + }, +}); diff --git a/packages/spec/src/system/constants/system-names.test.ts b/packages/spec/src/system/constants/system-names.test.ts index acea481b5..29009f571 100644 --- a/packages/spec/src/system/constants/system-names.test.ts +++ b/packages/spec/src/system/constants/system-names.test.ts @@ -18,6 +18,8 @@ describe('SystemObjectName', () => { expect(SystemObjectName.ORGANIZATION).toBe('sys_organization'); expect(SystemObjectName.MEMBER).toBe('sys_member'); expect(SystemObjectName.INVITATION).toBe('sys_invitation'); + expect(SystemObjectName.TEAM).toBe('sys_team'); + expect(SystemObjectName.TEAM_MEMBER).toBe('sys_team_member'); expect(SystemObjectName.API_KEY).toBe('sys_api_key'); expect(SystemObjectName.TWO_FACTOR).toBe('sys_two_factor'); expect(SystemObjectName.ROLE).toBe('sys_role'); @@ -31,6 +33,8 @@ describe('SystemObjectName', () => { expect(names).toContain('sys_user'); expect(names).toContain('sys_session'); expect(names).toContain('sys_organization'); + expect(names).toContain('sys_team'); + expect(names).toContain('sys_team_member'); expect(names).toContain('sys_role'); expect(names).toContain('sys_audit_log'); }); @@ -44,6 +48,8 @@ describe('SystemObjectName', () => { expect(keys).toContain('ORGANIZATION'); expect(keys).toContain('MEMBER'); expect(keys).toContain('INVITATION'); + expect(keys).toContain('TEAM'); + expect(keys).toContain('TEAM_MEMBER'); expect(keys).toContain('API_KEY'); expect(keys).toContain('TWO_FACTOR'); expect(keys).toContain('ROLE'); diff --git a/packages/spec/src/system/constants/system-names.ts b/packages/spec/src/system/constants/system-names.ts index 9eda294c7..9b071c73b 100644 --- a/packages/spec/src/system/constants/system-names.ts +++ b/packages/spec/src/system/constants/system-names.ts @@ -34,6 +34,10 @@ export const SystemObjectName = { MEMBER: 'sys_member', /** Authentication: organization invitation */ INVITATION: 'sys_invitation', + /** Authentication: team within an organization */ + TEAM: 'sys_team', + /** Authentication: team membership */ + TEAM_MEMBER: 'sys_team_member', /** Authentication: API key for programmatic access */ API_KEY: 'sys_api_key', /** Authentication: two-factor authentication credentials */