diff --git a/.objectui-sha b/.objectui-sha index 7dddf6cbb2..2250732794 100644 --- a/.objectui-sha +++ b/.objectui-sha @@ -1 +1 @@ -a3a5abff8c75d0629c3abe520dbe448edace3155 +d6e7a84f7ccb32acad3fa435385ac15448df485e diff --git a/packages/platform-objects/src/identity/index.ts b/packages/platform-objects/src/identity/index.ts index a052443012..2627b192e1 100644 --- a/packages/platform-objects/src/identity/index.ts +++ b/packages/platform-objects/src/identity/index.ts @@ -35,3 +35,6 @@ export { SysOauthAccessToken } from './sys-oauth-access-token.object.js'; export { SysOauthRefreshToken } from './sys-oauth-refresh-token.object.js'; export { SysOauthConsent } from './sys-oauth-consent.object.js'; export { SysJwks } from './sys-jwks.object.js'; + +// ── External SSO (relying-party, @better-auth/sso) ───────────────── +export { SysSsoProvider } from './sys-sso-provider.object.js'; diff --git a/packages/platform-objects/src/identity/sys-sso-provider.object.ts b/packages/platform-objects/src/identity/sys-sso-provider.object.ts new file mode 100644 index 0000000000..8b4cdc39eb --- /dev/null +++ b/packages/platform-objects/src/identity/sys-sso-provider.object.ts @@ -0,0 +1,177 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_sso_provider — Registered external SSO identity provider (OIDC / SAML) + * + * Backed by `@better-auth/sso`'s `ssoProvider` model. Each row is an external + * IdP that THIS environment federates LOGIN to (the relying-party / client + * side) — e.g. the customer's Okta / Entra / Google Workspace. This is the + * per-environment SSO **mechanism** (ADR-0024): OPEN, configured in the env, + * and cloud-free for self-host. + * + * better-auth stores the protocol detail as a JSON blob in `oidc_config` + * (OIDC: clientId, clientSecret, endpoints, scopes, mapping, pkce, …) or + * `saml_config` (SAML: entryPoint, cert, identifierFormat, mapping, …) — not + * as separate columns. Field set mirrors `@better-auth/sso@1.6.20`'s + * `BaseSSOProvider`: issuer, oidcConfig, samlConfig, userId, providerId, + * organizationId, domain. + * + * All mutations route through better-auth's `/api/v1/auth/sso/*` endpoints + * (register / delete-provider) so config validation and secret handling run; + * the generic data layer is read-only (see `enable.apiMethods`). + * + * @namespace sys + */ +export const SysSsoProvider = ObjectSchema.create({ + name: 'sys_sso_provider', + label: 'SSO Provider', + pluralLabel: 'SSO Providers', + icon: 'shield-check', + isSystem: true, + managedBy: 'better-auth', + // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema. + protection: { + lock: 'full', + reason: 'Identity table managed by better-auth (@better-auth/sso) — see ADR-0024.', + docsUrl: 'https://docs.objectstack.ai/adr/0010-metadata-protection', + }, + description: 'External SSO identity providers (OIDC / SAML) this environment federates login to', + displayNameField: 'provider_id', + titleFormat: '{provider_id}', + compactLayout: ['provider_id', 'issuer', 'domain'], + + // All mutations go through @better-auth/sso's endpoints under + // /api/v1/auth/sso/* (register / delete-provider) rather than the generic + // data layer, so server-side config validation + secret handling run. + actions: [ + { + name: 'register_sso_provider', + label: 'Register SSO Provider', + icon: 'plus-circle', + variant: 'primary', + mode: 'create', + locations: ['list_toolbar'], + type: 'api', + method: 'POST', + target: '/api/v1/auth/sso/register', + refreshAfter: true, + params: [ + { name: 'providerId', label: 'Provider ID', type: 'text', required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' }, + { name: 'issuer', label: 'Issuer URL', type: 'text', required: true, helpText: 'IdP issuer / discovery base, e.g. https://acme.okta.com.' }, + { name: 'domain', label: 'Email Domain', type: 'text', required: true, helpText: 'Users with this email domain are routed to this IdP.' }, + { name: 'clientId', label: 'Client ID', type: 'text', required: true }, + { name: 'clientSecret', label: 'Client Secret', type: 'text', required: true }, + ], + }, + { + name: 'delete_sso_provider', + label: 'Delete SSO Provider', + icon: 'trash-2', + variant: 'danger', + mode: 'delete', + locations: ['list_item', 'record_header'], + type: 'api', + method: 'POST', + target: '/api/v1/auth/sso/delete-provider', + confirmText: 'Delete this SSO provider? Users from its domain will no longer be able to sign in through it.', + successMessage: 'SSO provider deleted', + refreshAfter: true, + params: [ + { name: 'providerId', field: 'provider_id', defaultFromRow: true, required: true }, + ], + }, + ], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_sso_provider' }, + columns: ['provider_id', 'issuer', 'domain', 'created_at'], + sort: [{ field: 'provider_id', order: 'asc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + provider_id: Field.text({ + label: 'Provider ID', + required: true, + searchable: true, + maxLength: 255, + description: 'Stable provider identifier (unique within the environment)', + group: 'Identity', + }), + + issuer: Field.text({ + label: 'Issuer', + required: true, + maxLength: 2048, + description: 'IdP issuer URL', + group: 'Identity', + }), + + domain: Field.text({ + label: 'Email Domain', + required: true, + maxLength: 255, + description: 'Email domain routed to this IdP (e.g. acme.com)', + group: 'Identity', + }), + + oidc_config: Field.textarea({ + label: 'OIDC Config', + required: false, + description: 'JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)', + group: 'Protocol', + }), + + saml_config: Field.textarea({ + label: 'SAML Config', + required: false, + description: 'JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)', + group: 'Protocol', + }), + + user_id: Field.lookup('sys_user', { + label: 'Registered By', + required: false, + description: 'User who registered this provider', + group: 'System', + }), + + organization_id: Field.text({ + label: 'Organization', + required: false, + maxLength: 255, + description: 'Organization scope (when org-scoped SSO is used)', + group: 'System', + }), + + created_at: Field.datetime({ label: 'Created At', defaultValue: 'NOW()', readonly: true, group: 'System' }), + updated_at: Field.datetime({ label: 'Updated At', defaultValue: 'NOW()', readonly: true, group: 'System' }), + }, + + indexes: [ + { fields: ['provider_id'], unique: true }, + { fields: ['domain'] }, + { fields: ['user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + // Mutations go through /api/v1/auth/sso/* (register / delete-provider); + // the generic data layer is read-only so sysadmins cannot bypass + // server-side validation / secret handling. + apiMethods: ['get', 'list'], + trash: false, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 9df7e2bc84..ed253b4bd2 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -20,6 +20,7 @@ "dependencies": { "@better-auth/core": "^1.6.20", "@better-auth/oauth-provider": "^1.6.20", + "@better-auth/sso": "^1.6.20", "@noble/hashes": "^2.2.0", "@objectstack/core": "workspace:*", "@objectstack/platform-objects": "workspace:*", diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index f17699a42e..d9d42eac8f 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -625,6 +625,8 @@ export class AuthManager { // override per-environment without touching the application bundle. const oidcEnv = (globalThis as any)?.process?.env?.OS_OIDC_PROVIDER_ENABLED; const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === 'true' : undefined; + const ssoEnv = (globalThis as any)?.process?.env?.OS_SSO_ENABLED; + const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === 'true' : undefined; const twoFactorFromEnv = readBooleanEnv('OS_AUTH_TWO_FACTOR'); const enabled = { organization: pluginConfig.organization ?? true, @@ -634,6 +636,7 @@ export class AuthManager { oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false, deviceAuthorization: pluginConfig.deviceAuthorization ?? false, admin: pluginConfig.admin ?? false, + sso: ssoFromEnv ?? (pluginConfig as any).sso ?? false, }; // bearer() — ALWAYS enabled. @@ -926,6 +929,26 @@ export class AuthManager { })); } + // External SSO (OIDC / SAML) relying-party — lets this environment federate + // login to a customer's own IdP (Okta / Entra / Google …). Per-env, runtime- + // registered providers live in `sys_sso_provider` (ADR-0024: the OPEN SSO + // mechanism — cloud-free for self-host). Endpoints mount under + // /api/v1/auth/sso/{register,providers,delete-provider,callback,…}. + // + // Toggle with `OS_SSO_ENABLED` (mirrors `OS_OIDC_PROVIDER_ENABLED`). + if (enabled.sso) { + const { sso } = await import('@better-auth/sso'); + // NOTE: unlike `oauthProvider`, @better-auth/sso hardcodes its `ssoProvider` + // model and accepts NO `schema` option (verified against 1.6.20 — no + // mergeSchema, runtime never reads options.schema). Its table mapping to + // `sys_sso_provider` must therefore be resolved by the better-auth adapter + // / a global model map, not per-plugin here (see AUTH_SSO_PROVIDER_SCHEMA; + // TODO confirm the resolved table name in E2E). + // provisionUser / organizationProvisioning will assign a default env role + // on first federated login (ADR-0024 V1); JIT works via account linking. + plugins.push(sso()); + } + // Device Authorization Grant (RFC 8628) — for CLI / TV-style devices. // Exposes the standard `/device/{code,token,approve,deny}` endpoints // and persists pending requests in `sys_device_code`. diff --git a/packages/plugins/plugin-auth/src/auth-schema-config.ts b/packages/plugins/plugin-auth/src/auth-schema-config.ts index fe4eabe33f..aaf856008f 100644 --- a/packages/plugins/plugin-auth/src/auth-schema-config.ts +++ b/packages/plugins/plugin-auth/src/auth-schema-config.ts @@ -650,6 +650,44 @@ export function buildOauthProviderPluginSchema() { */ export const buildOidcProviderPluginSchema = buildOauthProviderPluginSchema; +// --------------------------------------------------------------------------- +// SSO plugin – ssoProvider table (@better-auth/sso) +// --------------------------------------------------------------------------- + +/** + * `@better-auth/sso` plugin `ssoProvider` model mapping. + * + * Each row is an external OIDC/SAML IdP this environment federates login to + * (the relying-party side — ADR-0024's OPEN per-env SSO mechanism). The + * protocol detail lives in JSON blobs (`oidcConfig` / `samlConfig`); the model + * itself is thin. Mirrors @better-auth/sso@1.6.20's `BaseSSOProvider`. + * + * | camelCase (better-auth) | snake_case (ObjectStack) | + * |:------------------------|:-------------------------| + * | providerId | provider_id | + * | oidcConfig | oidc_config | + * | samlConfig | saml_config | + * | userId | user_id | + * | organizationId | organization_id | + * | issuer / domain | (same name — no remap) | + */ +export const AUTH_SSO_PROVIDER_SCHEMA = { + modelName: 'sys_sso_provider', + fields: { + providerId: 'provider_id', + oidcConfig: 'oidc_config', + samlConfig: 'saml_config', + userId: 'user_id', + organizationId: 'organization_id', + }, +} as const; + +// NOTE: there is intentionally no `buildSsoPluginSchema()`. Unlike +// `oauthProvider`, the @better-auth/sso plugin exposes NO `schema` option +// (verified vs 1.6.20), so the mapping above cannot be handed to the plugin — +// it must be consumed at the ADAPTER layer (AUTH_MODEL_TO_PROTOCOL + field +// resolution in objectql-adapter.ts). See ADR-0024. + // --------------------------------------------------------------------------- // Helper: build device-authorization plugin schema option // --------------------------------------------------------------------------- diff --git a/packages/plugins/plugin-auth/src/manifest.ts b/packages/plugins/plugin-auth/src/manifest.ts index b7fbe3a0c3..11814ed456 100644 --- a/packages/plugins/plugin-auth/src/manifest.ts +++ b/packages/plugins/plugin-auth/src/manifest.ts @@ -21,6 +21,7 @@ import { SysOauthRefreshToken, SysOrganization, SysSession, + SysSsoProvider, SysTeam, SysTeamMember, SysTwoFactor, @@ -52,6 +53,7 @@ export const authIdentityObjects: any[] = [ SysOauthConsent, SysJwks, SysDeviceCode, + SysSsoProvider, ]; /** Manifest header shared by compile-time config and runtime registration. */ diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.test.ts b/packages/plugins/plugin-auth/src/objectql-adapter.test.ts index a8a5cb9ba1..9633e71c04 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.test.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.test.ts @@ -24,6 +24,7 @@ import { } from './auth-schema-config'; import { SystemObjectName } from '@objectstack/spec/system'; import type { IDataEngine } from '@objectstack/core'; +import { sso } from '@better-auth/sso'; describe('AUTH_MODEL_TO_PROTOCOL mapping', () => { it('should map all four core better-auth models to sys_ protocol names', () => { @@ -279,3 +280,46 @@ describe('createObjectQLAdapter – legacy model name mapping', () => { expect(mockEngine.insert).toHaveBeenCalledWith('organization', { name: 'Acme' }); }); }); + +describe('createObjectQLAdapterFactory – schema-less plugin bridging (@better-auth/sso)', () => { + // The sso plugin exposes no `schema` option, so its `ssoProvider` table + + // camelCase fields are bridged at the adapter layer. Pass the plugin so + // better-auth's wrapper recognises the model (it validates against the + // merged schema before delegating to our adapter methods). + const makeAdapter = (findOneRow: any = { id: '1', provider_id: 'okta', oidc_config: '{"clientId":"x"}', domain: 'acme.com' }) => { + const engine = { + insert: vi.fn().mockImplementation((_m: string, d: any) => Promise.resolve({ id: '1', ...d })), + findOne: vi.fn().mockResolvedValue(findOneRow), + find: vi.fn().mockResolvedValue([]), + count: vi.fn().mockResolvedValue(0), + update: vi.fn().mockResolvedValue({ id: '1' }), + delete: vi.fn().mockResolvedValue(undefined), + } as unknown as IDataEngine; + const adapter: any = (createObjectQLAdapterFactory(engine) as any)({ plugins: [sso()] } as any); + return { engine, adapter }; + }; + + it('resolveProtocolName bridges ssoProvider -> sys_sso_provider', () => { + expect(resolveProtocolName('ssoProvider')).toBe('sys_sso_provider'); + expect(AUTH_MODEL_TO_PROTOCOL.ssoProvider).toBe('sys_sso_provider'); + }); + + it('maps the ssoProvider model + camelCase fields to sys_sso_provider snake columns on insert', async () => { + const { engine, adapter } = makeAdapter(); + await adapter.create({ model: 'ssoProvider', data: { providerId: 'okta', oidcConfig: '{"clientId":"x"}', domain: 'acme.com' } }); + const [tbl, payload] = (engine.insert as any).mock.calls[0]; + expect(tbl).toBe('sys_sso_provider'); + expect(payload).toMatchObject({ provider_id: 'okta', oidc_config: '{"clientId":"x"}', domain: 'acme.com' }); + expect(payload).not.toHaveProperty('oidcConfig'); + }); + + it('maps snake columns back to camelCase on read', async () => { + const { adapter } = makeAdapter(); + const row: any = await adapter.findOne({ + model: 'ssoProvider', + where: [{ field: 'providerId', value: 'okta', operator: 'eq', connector: 'AND' }], + }); + expect(row).toMatchObject({ providerId: 'okta', oidcConfig: '{"clientId":"x"}' }); + expect(row).not.toHaveProperty('oidc_config'); + }); +}); diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 04eaf28ebe..50f02f2aac 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -16,6 +16,15 @@ export const AUTH_MODEL_TO_PROTOCOL: Record = { session: SystemObjectName.SESSION, account: SystemObjectName.ACCOUNT, verification: SystemObjectName.VERIFICATION, + // @better-auth/sso has NO `schema` option (verified vs 1.6.20 — no + // mergeSchema, runtime never reads options.schema), so it cannot declare + // its modelName/fields. Bridge the table name here. NOTE: the ACTIVE + // factory adapter (createObjectQLAdapterFactory) passes the raw `model` + // to dataEngine and does NOT yet consult resolveProtocolName for plugin + // models — nor map sso's camelCase fields (oidcConfig→oidc_config …). + // Finishing the @better-auth/sso integration needs that adapter work + + // E2E (see ADR-0024 / sys_sso_provider). Off by default (OS_SSO_ENABLED). + ssoProvider: 'sys_sso_provider', }; /** @@ -152,6 +161,21 @@ function convertWhere(where: CleanedWhere[]): Record { * @returns better-auth AdapterFactory */ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) { + // Field-name bridging for better-auth plugins that expose NO `schema` option + // (e.g. @better-auth/sso): when a model is remapped via AUTH_MODEL_TO_PROTOCOL, + // its camelCase model fields are also converted to snake_case columns on the + // way in and back to camelCase on the way out. SCOPED by `objectName !== model` + // so core / schema-declared models are byte-for-byte untouched. + const camelToSnake = (s: string): string => s.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase()); + const snakeToCamel = (s: string): string => s.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()); + const remapKeys = (obj: Record, fn: (k: string) => string): Record => { + const out: Record = {}; + for (const k of Object.keys(obj)) out[fn(k)] = obj[k]; + return out; + }; + const remapWhere = (where: CleanedWhere[]): CleanedWhere[] => + where.map((c) => ({ ...c, field: camelToSnake(c.field) })); + return createAdapterFactory({ config: { adapterId: 'objectql', @@ -168,18 +192,25 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) { create: async >( { model, data, select: _select }: { model: string; data: T; select?: string[] }, ): Promise => { - const result = await dataEngine.insert(model, data); - return normaliseLegacyDates(model, result) as T; + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const result = await dataEngine.insert(objectName, bridged ? remapKeys(data, camelToSnake) : data); + const norm = normaliseLegacyDates(model, result); + return (bridged ? remapKeys(norm, snakeToCamel) : norm) as T; }, findOne: async ( { model, where, select, join: _join }: { model: string; where: CleanedWhere[]; select?: string[]; join?: any }, ): Promise => { - const filter = convertWhere(where); - - const result = await dataEngine.findOne(model, { where: filter, fields: select }); - - return result ? (normaliseLegacyDates(model, result) as T) : null; + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = convertWhere(bridged ? remapWhere(where) : where); + const fields = bridged && select ? select.map(camelToSnake) : select; + + const result = await dataEngine.findOne(objectName, { where: filter, fields }); + if (!result) return null; + const norm = normaliseLegacyDates(model, result); + return (bridged ? remapKeys(norm, snakeToCamel) : norm) as T; }, findMany: async ( @@ -188,51 +219,66 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) { offset?: number; sortBy?: { field: string; direction: 'asc' | 'desc' }; join?: any; }, ): Promise => { - const filter = where ? convertWhere(where) : {}; + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = where ? convertWhere(bridged ? remapWhere(where) : where) : {}; const orderBy = sortBy - ? [{ field: sortBy.field, order: sortBy.direction as 'asc' | 'desc' }] + ? [{ field: bridged ? camelToSnake(sortBy.field) : sortBy.field, order: sortBy.direction as 'asc' | 'desc' }] : undefined; - const results = await dataEngine.find(model, { + const results = await dataEngine.find(objectName, { where: filter, limit: limit || 100, offset, orderBy, }); - return results.map((r) => normaliseLegacyDates(model, r as Record)) as T[]; + return results.map((r) => { + const norm = normaliseLegacyDates(model, r as Record); + return bridged ? remapKeys(norm, snakeToCamel) : norm; + }) as T[]; }, count: async ( { model, where }: { model: string; where?: CleanedWhere[] }, ): Promise => { - const filter = where ? convertWhere(where) : {}; - return await dataEngine.count(model, { where: filter }); + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = where ? convertWhere(bridged ? remapWhere(where) : where) : {}; + return await dataEngine.count(objectName, { where: filter }); }, update: async ( { model, where, update }: { model: string; where: CleanedWhere[]; update: T }, ): Promise => { - const filter = convertWhere(where); + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = convertWhere(bridged ? remapWhere(where) : where); // ObjectQL requires an ID for updates – find the record first - const record = await dataEngine.findOne(model, { where: filter }); + const record = await dataEngine.findOne(objectName, { where: filter }); if (!record) return null; - const result = await dataEngine.update(model, { ...(update as any), id: record.id }); - return result ? (normaliseLegacyDates(model, result) as T) : null; + const patch = bridged ? remapKeys(update as any, camelToSnake) : (update as any); + const result = await dataEngine.update(objectName, { ...patch, id: record.id }); + if (!result) return null; + const norm = normaliseLegacyDates(model, result); + return (bridged ? remapKeys(norm, snakeToCamel) : norm) as T; }, updateMany: async ( { model, where, update }: { model: string; where: CleanedWhere[]; update: Record }, ): Promise => { - const filter = convertWhere(where); + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = convertWhere(bridged ? remapWhere(where) : where); // Sequential updates: ObjectQL requires an ID per update - const records = await dataEngine.find(model, { where: filter }); + const records = await dataEngine.find(objectName, { where: filter }); + const patch = bridged ? remapKeys(update, camelToSnake) : update; for (const record of records) { - await dataEngine.update(model, { ...update, id: record.id }); + await dataEngine.update(objectName, { ...patch, id: record.id }); } return records.length; }, @@ -240,22 +286,26 @@ export function createObjectQLAdapterFactory(dataEngine: IDataEngine) { delete: async ( { model, where }: { model: string; where: CleanedWhere[] }, ): Promise => { - const filter = convertWhere(where); + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = convertWhere(bridged ? remapWhere(where) : where); - const record = await dataEngine.findOne(model, { where: filter }); + const record = await dataEngine.findOne(objectName, { where: filter }); if (!record) return; - await dataEngine.delete(model, { where: { id: record.id } }); + await dataEngine.delete(objectName, { where: { id: record.id } }); }, deleteMany: async ( { model, where }: { model: string; where: CleanedWhere[] }, ): Promise => { - const filter = convertWhere(where); + const objectName = resolveProtocolName(model); + const bridged = objectName !== model; + const filter = convertWhere(bridged ? remapWhere(where) : where); - const records = await dataEngine.find(model, { where: filter }); + const records = await dataEngine.find(objectName, { where: filter }); for (const record of records) { - await dataEngine.delete(model, { where: { id: record.id } }); + await dataEngine.delete(objectName, { where: { id: record.id } }); } return records.length; }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da53cfac76..75b1ce6dbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1352,6 +1352,9 @@ importers: '@better-auth/oauth-provider': specifier: ^1.6.20 version: 1.6.20(8b7e5f0044af09e55a9541191cb216a7) + '@better-auth/sso': + specifier: ^1.6.20 + version: 1.6.20(8b7e5f0044af09e55a9541191cb216a7) '@noble/hashes': specifier: ^2.2.0 version: 2.2.0 @@ -2382,6 +2385,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@authenio/xml-encryption@2.0.2': + resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==} + engines: {node: '>=12'} + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -2690,6 +2697,15 @@ packages: prisma: optional: true + '@better-auth/sso@1.6.20': + resolution: {integrity: sha512-cT3dthGkfDAz/k9jTtBOfsBtxsEgbO2hs5nSQlb0FdO3L/8MLi19iaIoQd1dmEun93MNqenZmYEZrPDKWWiISQ==} + peerDependencies: + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + better-auth: ^1.6.20 + better-call: 1.3.6 + '@better-auth/telemetry@1.6.20': resolution: {integrity: sha512-3BhbY3naQDERvdJvJ7fGszVY6rpsVfc6c9uyBVZlC1coVEF/rkM0rIcjtMVI1GUH7vWy1wjR6qF5vQnMun3XNQ==} peerDependencies: @@ -3430,6 +3446,9 @@ packages: '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4703,6 +4722,14 @@ packages: engines: {node: '>= 20'} hasBin: true + '@xmldom/is-dom-node@1.0.1': + resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==} + engines: {node: '>= 16'} + + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -4794,6 +4821,9 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + aproba@2.1.0: resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} @@ -4820,6 +4850,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -5720,6 +5753,10 @@ packages: resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} hasBin: true + fast-xml-parser@5.9.3: + resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} + hasBin: true + fastify@5.8.5: resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} @@ -6297,6 +6334,9 @@ packages: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} + is-unsafe@1.0.1: + resolution: {integrity: sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==} + is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -7119,6 +7159,9 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-rsa@1.1.1: + resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==} + node-sarif-builder@3.4.0: resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} engines: {node: '>=20'} @@ -7849,6 +7892,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + samlify@2.13.1: + resolution: {integrity: sha512-vdYr/zohDGBbfWNU4miEzc1jmWOtkLySPViapC6nfGkv9KxzLq4UlGkKyryzwLw4jVlZk88Rw93HaCRVpe+t+g==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -8100,6 +8146,9 @@ packages: strnum@2.3.0: resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -8249,9 +8298,16 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.4.0: resolution: {integrity: sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==} + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tldts@7.4.0: resolution: {integrity: sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==} hasBin: true @@ -8682,6 +8738,13 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + xml-crypto@6.1.2: + resolution: {integrity: sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==} + engines: {node: '>=16'} + + xml-escape@1.1.0: + resolution: {integrity: sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==} + xml-naming@0.1.0: resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} engines: {node: '>=16.0.0'} @@ -8690,10 +8753,25 @@ packages: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + xmlbuilder@11.0.1: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xpath@0.0.32: + resolution: {integrity: sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==} + engines: {node: '>=0.6.0'} + + xpath@0.0.33: + resolution: {integrity: sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==} + engines: {node: '>=0.6.0'} + + xpath@0.0.34: + resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} + engines: {node: '>=0.6.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -8824,6 +8902,12 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@authenio/xml-encryption@2.0.2': + dependencies: + '@xmldom/xmldom': 0.8.13 + escape-html: 1.0.3 + xpath: 0.0.32 + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -9333,6 +9417,19 @@ snapshots: '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 + '@better-auth/sso@1.6.20(8b7e5f0044af09e55a9541191cb216a7)': + dependencies: + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + better-auth: 1.6.20(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.66.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.3(@typescript-eslint/types@8.61.1))(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.55.3(@typescript-eslint/types@8.61.1))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(better-sqlite3@12.11.1)(mongodb@7.3.0(socks@2.8.9))(mysql2@3.22.4(@types/node@26.0.0))(next@16.2.9(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.55.3(@typescript-eslint/types@8.61.1))(vitest@4.1.9) + better-call: 1.3.6(zod@4.4.3) + fast-xml-parser: 5.9.3 + jose: 6.2.3 + samlify: 2.13.1 + tldts: 6.1.86 + zod: 4.4.3 + '@better-auth/telemetry@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': dependencies: '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) @@ -10026,6 +10123,8 @@ snapshots: '@nodable/entities@2.1.0': {} + '@nodable/entities@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -11313,6 +11412,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@xmldom/is-dom-node@1.0.1': {} + + '@xmldom/xmldom@0.8.13': {} + abbrev@1.1.1: optional: true @@ -11401,6 +11504,8 @@ snapshots: any-promise@1.3.0: {} + anynum@1.0.1: {} + aproba@2.1.0: optional: true @@ -11424,6 +11529,10 @@ snapshots: array-union@2.1.0: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.4: @@ -12312,6 +12421,15 @@ snapshots: path-expression-matcher: 1.5.0 strnum: 2.3.0 + fast-xml-parser@5.9.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + is-unsafe: 1.0.1 + path-expression-matcher: 1.5.0 + strnum: 2.4.1 + xml-naming: 0.1.0 + fastify@5.8.5: dependencies: '@fastify/ajv-compiler': 4.0.5 @@ -13017,6 +13135,8 @@ snapshots: dependencies: better-path-resolve: 1.0.0 + is-unsafe@1.0.1: {} + is-windows@1.0.2: {} is-wsl@2.2.0: @@ -14099,6 +14219,10 @@ snapshots: node-mock-http@1.0.4: {} + node-rsa@1.1.1: + dependencies: + asn1: 0.2.6 + node-sarif-builder@3.4.0: dependencies: '@types/sarif': 2.1.7 @@ -14829,6 +14953,16 @@ snapshots: safer-buffer@2.1.2: {} + samlify@2.13.1: + dependencies: + '@authenio/xml-encryption': 2.0.2 + '@xmldom/xmldom': 0.8.13 + node-rsa: 1.1.1 + xml: 1.0.1 + xml-crypto: 6.1.2 + xml-escape: 1.1.0 + xpath: 0.0.34 + sax@1.6.0: {} scheduler@0.27.0: {} @@ -15137,6 +15271,10 @@ snapshots: strnum@2.3.0: {} + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -15330,8 +15468,14 @@ snapshots: tinyrainbow@3.1.0: {} + tldts-core@6.1.86: {} + tldts-core@7.4.0: {} + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + tldts@7.4.0: dependencies: tldts-core: 7.4.0 @@ -15700,6 +15844,14 @@ snapshots: dependencies: is-wsl: 3.1.1 + xml-crypto@6.1.2: + dependencies: + '@xmldom/is-dom-node': 1.0.1 + '@xmldom/xmldom': 0.8.13 + xpath: 0.0.33 + + xml-escape@1.1.0: {} + xml-naming@0.1.0: {} xml2js@0.5.0: @@ -15707,8 +15859,16 @@ snapshots: sax: 1.6.0 xmlbuilder: 11.0.1 + xml@1.0.1: {} + xmlbuilder@11.0.1: {} + xpath@0.0.32: {} + + xpath@0.0.33: {} + + xpath@0.0.34: {} + xtend@4.0.2: {} y18n@5.0.8: {}