@happyvertical/smrt-users 0.45.2 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-users",
5
- "packageVersion": "0.45.2",
5
+ "packageVersion": "0.46.0",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-users:AccessRequestCollection": {
8
8
  "name": "accessrequestcollection",
@@ -0,0 +1,30 @@
1
+ import { getDatabase } from '@happyvertical/sql';
2
+ type DatabaseInterface = Awaited<ReturnType<typeof getDatabase>>;
3
+ export declare const LEGACY_USER_PROFILE_BACKFILL_NAME = "@happyvertical/smrt-users:legacy-user-profiles:v1";
4
+ export interface BackfillLegacyUserProfilesResult {
5
+ created: number;
6
+ linked: number;
7
+ }
8
+ export type LegacyUserProfileBackfillErrorCode = 'duplicate_user_email' | 'missing_email' | 'profile_conflict' | 'profile_email_backfill_required' | 'profile_type_conflict' | 'reservation_conflict' | 'stale_user_email_key' | 'transaction_required' | 'user_email_backfill_required';
9
+ /** Fail-closed deploy-time legacy User/Profile migration error. */
10
+ export declare class LegacyUserProfileBackfillError extends Error {
11
+ readonly code: LegacyUserProfileBackfillErrorCode;
12
+ constructor(code: LegacyUserProfileBackfillErrorCode, message: string, options?: {
13
+ cause?: unknown;
14
+ });
15
+ }
16
+ /**
17
+ * Create and link canonical global Person Profiles for legacy Users whose
18
+ * `profile_id` is still null or blank.
19
+ *
20
+ * Run this from one deploy process after schema migration and both email-key
21
+ * backfills. Every preflight read and mutation uses one transaction. Existing
22
+ * same-email Profiles are rejected for explicit operator reconciliation; this
23
+ * migration never infers ownership and never creates an OIDC identity.
24
+ *
25
+ * The completion marker records a successful pass but does not skip later
26
+ * calls. A newly imported legacy User is therefore processed on the next run.
27
+ */
28
+ export declare function backfillLegacyUserProfiles(db: DatabaseInterface): Promise<BackfillLegacyUserProfilesResult>;
29
+ export {};
30
+ //# sourceMappingURL=backfillLegacyUserProfiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backfillLegacyUserProfiles.d.ts","sourceRoot":"","sources":["../../src/migrations/backfillLegacyUserProfiles.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,KAAK,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC;AAEjE,eAAO,MAAM,iCAAiC,sDACO,CAAC;AAEtD,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,kCAAkC,GAC1C,sBAAsB,GACtB,eAAe,GACf,kBAAkB,GAClB,iCAAiC,GACjC,uBAAuB,GACvB,sBAAsB,GACtB,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,CAAC;AAEnC,mEAAmE;AACnE,qBAAa,8BAA+B,SAAQ,KAAK;IAErD,QAAQ,CAAC,IAAI,EAAE,kCAAkC;gBAAxC,IAAI,EAAE,kCAAkC,EACjD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhC;AAQD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,GACpB,OAAO,CAAC,gCAAgC,CAAC,CA0M3C"}
@@ -3,15 +3,15 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-users",
6
- "packageVersion": "0.45.2",
6
+ "packageVersion": "0.46.0",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "fe8d99e875574123d286bdca134901aa03fe4176abc7abcec91f265b824318d7",
11
- "packageJson": "5ae1849a8722c284453553b4138db5af292de381c1d632d8ee7517a33c8c3409",
10
+ "manifest": "7da7105b15c6d1d1847a2568a3748e993e72763c5187623376beb76e96420ab6",
11
+ "packageJson": "2bcb2dc2464da67fe7ccc9a2ed46fe045c3b547a32d751d2bc8b5350c760ecb0",
12
12
  "agents": "ec09e41d9c5e1b55abf454eb5b88a98637094e2aa67bffce2f11db0dcb68b441",
13
13
  "moduleDoc:agents/permissions.md": "f801411ae51fc625ad99054e284fd1abc6e7b2a5555dd8797a83dfd30273db68",
14
- "moduleDoc:agents/oidc-provisioning.md": "cbab59f0819466de764514ba268026effdb9fa8b651285736e6c12d246b96ab9",
14
+ "moduleDoc:agents/oidc-provisioning.md": "5efa607ff23b9a693391e0c02670324a8cea4114d4968c9718f10a763da919c6",
15
15
  "moduleDoc:agents/mobile-auth.md": "25e85e83f43e3a46383a92fa809f3a003b4545c3026e78dbe1bccbbc35161e18",
16
16
  "moduleDoc:agents/retention.md": "425c0a34ed33969badfef699751bfa670bdf618eda65e079e00815620aabed68"
17
17
  },
@@ -9880,7 +9880,7 @@
9880
9880
  {
9881
9881
  "path": "agents/oidc-provisioning.md",
9882
9882
  "module": "oidc-provisioning",
9883
- "content": "# OIDC provisioning\n\nRead for changes to `src/collections/UserCollection.ts`, `src/services/OidcLoginService.ts`,\nOIDC handlers, or identity migrations. The canonical scenario matrix is\n`packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`.\n\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n- **RFC 9207 response issuer is exact.** OIDC callbacks validate a supplied\n `iss` against discovered metadata with exact string comparison before trusting\n a code or error. If metadata advertises issuer-response support, `iss` is\n required.\n- **Verified-email Profile reuse is fail-closed.** The typed canonical scenarios\n live in\n `packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`; both\n package suites execute that matrix and public docs reference it rather than\n maintaining another behavioral table. Default provisioning reuses only one\n unowned, global `Person`. Tenant-scoped, non-Person, duplicate-email,\n and already-owned matches fail before User/session creation. An existing\n issuer/subject link without a User must still be the unique global Person for\n the current verified claim email; once owned, the stable issuer/subject link\n reuses its canonical Person and owner.\n Issuer and subject are opaque, case-sensitive identifiers; preserve their\n exact value and use trim only to reject blank claims.\n- **`resolveProfile` is the application reconciliation boundary.** The\n SvelteKit handlers, `OidcLoginService`, and `getOrCreateFromOidc` accept the\n same hook inside the provisioning transaction. The service/handler path\n supplies protocol-validated claims; direct collection callers must validate\n and trust their claim source before calling `getOrCreateFromOidc`.\n Token/userinfo merging keeps `email` and `email_verified` paired to the same\n claim source; verification is never borrowed across sources.\n Resolver reads/writes use the supplied `db`, and the hook must be idempotent\n because a concurrent unique-key conflict can retry it. `undefined` chooses\n the secure default and `null` rejects, including exact issuer/subject reuse.\n For a new identity, a supplied Profile is still validated as the unique,\n unowned global Person for the verified email. For an exact existing identity,\n it must be the already-linked Profile and cannot rebind identity authority;\n stable-link owner and canonical-Person checks still apply. The hook receives a\n separate frozen claims snapshot; internal retry and persistence state is not\n exposed for mutation.\n- **Owned first binding requires `authorizeProfileOwner`.** An invitation or\n approval workflow may explicitly return both its pre-provisioned canonical\n global `Person` and existing approved `User` from this transaction-bound\n hook. `undefined` keeps the secure `profile_owned` default and `null`\n rejects. SMRT treats only the selected IDs as input, reloads them in the\n provisioning transaction, and requires `email_verified === true`, the unique\n canonical global Person for the normalized claim email, exactly one owner,\n that owner as the selected User, and the same normalized User email. The\n hook runs before identity/User/session creation and may be retried, so use\n only its supplied `db` and `users` handles and keep application authorization\n idempotent. Never authorize from email matching alone. Existing exact\n identities cannot be rebound; when `resolveProfile` is also present both\n hooks must select the same Profile.\n- **OIDC first login is atomic.** The Profile, `OidcIdentity`, and User are one\n transaction. The database arbiters are `OidcIdentity.identityKey`,\n private `oidc_profile_email_reservations.email_key`, `User.emailKey`, and the\n unique `User.profileId`; local callbacks acquire exact issuer/subject and normalized\n email locks in deterministic order so changed email claims also serialize.\n SQLite and DuckDB callbacks additionally serialize every root-handle statement\n the coordinator owns per database URL — `_smrt_backfills` initialization, the\n transaction, and the post-commit rebind — because one adapter multiplexes a\n single native connection and cannot safely overlap unrelated root\n transactions. Never overlap two statements on one such handle, inside a\n transaction or not: rebind and owner/email candidate reads are sequential,\n never `Promise.all`. Owner-authorized DuckDB callbacks use that same\n root-handle serialization;\n PostgreSQL deadlock and serialization errors use a bounded transaction retry.\n Newly provisioned Profiles use non-semantic per-profile slugs so equal IdP\n display names cannot trigger a natural-key upsert;\n run\n `smrt db:status`, `smrt db:migrate`, then `smrt db:status` before deployment.\n Stop or upgrade old writers first. Before migration, group\n non-null `users.profile_id` values, then reconcile duplicates. After\n migration, run public `backfillProfileEmailKeys(db)` followed by\n `backfillUserEmailKeys(db)` from one deploy process. Both use the shared\n TypeScript `normalizeIdentityEmail()` implementation transactionally and are\n idempotent; the User backfill fails before writes if normalized duplicates\n remain. Every OIDC path requires the Profile email-key readiness marker;\n creating a User or checking User email uniqueness additionally requires the\n User marker. A stable issuer/subject with an existing owning User skips only\n the User email-key lookup and marker. Full scans remain in the explicit deploy\n step; guarded runtime paths use indexed keys and validate only returned\n candidates. Multiple null links remain valid.\n Legacy race keys backfill only after canonical validation. Pass a root\n database on adapters such as DuckDB that cannot create nested savepoints.\n Root adapters must expose `beginTransaction`; transaction-only handles are\n ambiguous and fail closed before provisioning writes. Caller-owned\n transactions never run `_smrt_backfills` DDL and require that table to\n already exist; use the root database when initialization or recovery is\n needed.\n"
9883
+ "content": "# OIDC provisioning\n\nRead for changes to `src/collections/UserCollection.ts`, `src/services/OidcLoginService.ts`,\nOIDC handlers, or identity migrations. The canonical scenario matrix is\n`packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`.\n\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n- **RFC 9207 response issuer is exact.** OIDC callbacks validate a supplied\n `iss` against discovered metadata with exact string comparison before trusting\n a code or error. If metadata advertises issuer-response support, `iss` is\n required.\n- **Verified-email Profile reuse is fail-closed.** The typed canonical scenarios\n live in\n `packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`; both\n package suites execute that matrix and public docs reference it rather than\n maintaining another behavioral table. Default provisioning reuses only one\n unowned, global `Person`. Tenant-scoped, non-Person, duplicate-email,\n and already-owned matches fail before User/session creation. An existing\n issuer/subject link without a User must still be the unique global Person for\n the current verified claim email; once owned, the stable issuer/subject link\n reuses its canonical Person and owner.\n Issuer and subject are opaque, case-sensitive identifiers; preserve their\n exact value and use trim only to reject blank claims.\n- **`resolveProfile` is the application reconciliation boundary.** The\n SvelteKit handlers, `OidcLoginService`, and `getOrCreateFromOidc` accept the\n same hook inside the provisioning transaction. The service/handler path\n supplies protocol-validated claims; direct collection callers must validate\n and trust their claim source before calling `getOrCreateFromOidc`.\n Token/userinfo merging keeps `email` and `email_verified` paired to the same\n claim source; verification is never borrowed across sources.\n Resolver reads/writes use the supplied `db`, and the hook must be idempotent\n because a concurrent unique-key conflict can retry it. `undefined` chooses\n the secure default and `null` rejects, including exact issuer/subject reuse.\n For a new identity, a supplied Profile is still validated as the unique,\n unowned global Person for the verified email. For an exact existing identity,\n it must be the already-linked Profile and cannot rebind identity authority;\n stable-link owner and canonical-Person checks still apply. The hook receives a\n separate frozen claims snapshot; internal retry and persistence state is not\n exposed for mutation.\n- **Owned first binding requires `authorizeProfileOwner`.** An invitation or\n approval workflow may explicitly return both its pre-provisioned canonical\n global `Person` and existing approved `User` from this transaction-bound\n hook. `undefined` keeps the secure `profile_owned` default and `null`\n rejects. SMRT treats only the selected IDs as input, reloads them in the\n provisioning transaction, and requires `email_verified === true`, the unique\n canonical global Person for the normalized claim email, exactly one owner,\n that owner as the selected User, and the same normalized User email. The\n hook runs before identity/User/session creation and may be retried, so use\n only its supplied `db` and `users` handles and keep application authorization\n idempotent. Never authorize from email matching alone. Existing exact\n identities cannot be rebound; when `resolveProfile` is also present both\n hooks must select the same Profile.\n- **OIDC first login is atomic.** The Profile, `OidcIdentity`, and User are one\n transaction. The database arbiters are `OidcIdentity.identityKey`,\n private `oidc_profile_email_reservations.email_key`, `User.emailKey`, and the\n unique `User.profileId`; local callbacks acquire exact issuer/subject and normalized\n email locks in deterministic order so changed email claims also serialize.\n SQLite and DuckDB callbacks additionally serialize every root-handle statement\n the coordinator owns per database URL — `_smrt_backfills` initialization, the\n transaction, and the post-commit rebind — because one adapter multiplexes a\n single native connection and cannot safely overlap unrelated root\n transactions. Never overlap two statements on one such handle, inside a\n transaction or not: rebind and owner/email candidate reads are sequential,\n never `Promise.all`. Owner-authorized DuckDB callbacks use that same\n root-handle serialization;\n PostgreSQL deadlock and serialization errors use a bounded transaction retry.\n Newly provisioned Profiles use non-semantic per-profile slugs so equal IdP\n display names cannot trigger a natural-key upsert;\n run\n `smrt db:status`, `smrt db:migrate`, then `smrt db:status` before deployment.\n Stop or upgrade old writers first. Before migration, group\n non-null `users.profile_id` values, then reconcile duplicates. After\n migration, run public `backfillProfileEmailKeys(db)`,\n `backfillUserEmailKeys(db)`, then `backfillLegacyUserProfiles(db)` from one\n deploy process. The first two use the shared\n TypeScript `normalizeIdentityEmail()` implementation transactionally and are\n idempotent; the User backfill fails before writes if normalized duplicates\n remain. The legacy Profile backfill transactionally creates canonical global\n Persons for null/blank User links without creating OIDC identities or\n inferring ownership; reconcile any same-email Profile or reservation before\n retrying. Its marker records a completed pass but never skips newly imported\n legacy Users on a later run. Every OIDC path requires the Profile email-key\n readiness marker; creating a User or checking User email uniqueness\n additionally requires the User marker. A stable issuer/subject with an\n existing owning User skips only the User email-key lookup and marker. Full\n scans remain in the explicit deploy step; guarded runtime paths use indexed\n keys and validate only returned candidates. Multiple null links remain valid.\n Legacy race keys backfill only after canonical validation. Pass a root\n database on adapters such as DuckDB that cannot create nested savepoints.\n Root adapters must expose `beginTransaction`; transaction-only handles are\n ambiguous and fail closed before provisioning writes. Caller-owned\n transactions never run `_smrt_backfills` DDL and require that table to\n already exist; use the root database when initialization or recovery is\n needed.\n"
9884
9884
  },
9885
9885
  {
9886
9886
  "path": "agents/mobile-auth.md",
package/dist/sveltekit.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as decodeOidcTransaction, E as resolveOidcProviderConfig, S as OidcLoginService, T as getUsersOidcConfig, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as encodeOidcTransaction, x as OidcLoginError, y as SessionService } from "./chunks/TerminalAuthService-Buu2oc6x.js";
1
+ import { C as decodeOidcTransaction, E as resolveOidcProviderConfig, S as OidcLoginService, T as getUsersOidcConfig, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as encodeOidcTransaction, x as OidcLoginError, y as SessionService } from "./chunks/TerminalAuthService-C7z3uZ4w.js";
2
2
  import { DiscoveryArtifactValidationError, SMRT_APP_RESULT_CONTRACT, SMRT_APP_RESULT_SCHEMA, SMRT_APP_RESULT_VERSION, SMRT_DISCOVERY_CONFORMANCE_ARTIFACT_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_VERSION, SMRT_MCP_RESULT_METADATA_KEY, canonicalizeDiscoveryArtifact, createDiscoveryConformanceArtifact, deriveCommandRequirements, validateDiscoveryConformanceArtifact } from "./app-contract.js";
3
3
  import { ObjectRegistry } from "@happyvertical/smrt-core";
4
4
  import { createLogger } from "@happyvertical/logger";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-users",
3
- "version": "0.45.2",
3
+ "version": "0.46.0",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Multi-tenant user management for the SMRT framework - users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -49,13 +49,13 @@
49
49
  "@happyvertical/logger": "^0.89.4",
50
50
  "@happyvertical/sql": "^0.89.4",
51
51
  "jose": "^6.2.3",
52
- "@happyvertical/smrt-config": "0.45.2",
53
- "@happyvertical/smrt-core": "0.45.2",
54
- "@happyvertical/smrt-profiles": "0.45.2",
55
- "@happyvertical/smrt-tenancy": "0.45.2",
56
- "@happyvertical/smrt-types": "0.45.2",
57
- "@happyvertical/smrt-mobile-contract": "0.45.2",
58
- "@happyvertical/smrt-ui": "0.45.2"
52
+ "@happyvertical/smrt-config": "0.46.0",
53
+ "@happyvertical/smrt-core": "0.46.0",
54
+ "@happyvertical/smrt-mobile-contract": "0.46.0",
55
+ "@happyvertical/smrt-profiles": "0.46.0",
56
+ "@happyvertical/smrt-tenancy": "0.46.0",
57
+ "@happyvertical/smrt-types": "0.46.0",
58
+ "@happyvertical/smrt-ui": "0.46.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@sveltejs/package": "^2.5.8",
@@ -67,7 +67,7 @@
67
67
  "typescript": "5.9.3",
68
68
  "vite": "8.1.4",
69
69
  "vitest": "4.1.10",
70
- "@happyvertical/smrt-vitest": "0.45.2"
70
+ "@happyvertical/smrt-vitest": "0.46.0"
71
71
  },
72
72
  "keywords": [
73
73
  "smrt",
@@ -96,7 +96,7 @@
96
96
  "clean": "rm -rf dist",
97
97
  "dev": "npm run build:watch",
98
98
  "test": "vitest run",
99
- "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/permission-postgres-rls.test.ts src/__tests__/principal-permission-context-postgres.test.ts src/__tests__/oidc-provisioning-postgres.test.ts src/__tests__/terminal-auth-postgres.test.ts",
99
+ "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/permission-postgres-rls.test.ts src/__tests__/principal-permission-context-postgres.test.ts src/__tests__/oidc-provisioning-postgres.test.ts src/__tests__/legacy-user-profile-backfill-postgres.test.ts src/__tests__/terminal-auth-postgres.test.ts",
100
100
  "typecheck": "tsc --noEmit && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",
101
101
  "check": "pnpm exec svelte-check --tsconfig ./tsconfig.svelte.json",
102
102
  "verify:pack": "node ../../scripts/verify-package-types-exports.js ."