@happyvertical/smrt-profiles 0.40.69 → 0.41.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.
Files changed (48) hide show
  1. package/AGENTS.md +10 -5
  2. package/dist/chunks/{ApiKey-Br9NGBnr.js → ApiKey-CMm509C4.js} +5 -7
  3. package/dist/chunks/ApiKey-CMm509C4.js.map +1 -0
  4. package/dist/chunks/{ApiKeyCollection-CrTjdMz8.js → ApiKeyCollection-C6p7XgTz.js} +3 -3
  5. package/dist/chunks/{ApiKeyCollection-CrTjdMz8.js.map → ApiKeyCollection-C6p7XgTz.js.map} +1 -1
  6. package/dist/chunks/{AuditLogCollection-BBIEI6BD.js → AuditLogCollection-lJfGFdIQ.js} +2 -2
  7. package/dist/chunks/{AuditLogCollection-BBIEI6BD.js.map → AuditLogCollection-lJfGFdIQ.js.map} +1 -1
  8. package/dist/chunks/{NostrIdentity-DF_a_WZk.js → NostrIdentity-CgdM4ng0.js} +6 -8
  9. package/dist/chunks/{NostrIdentity-DF_a_WZk.js.map → NostrIdentity-CgdM4ng0.js.map} +1 -1
  10. package/dist/chunks/{NostrIdentityCollection-BRYUgt47.js → NostrIdentityCollection-BnHZOsUI.js} +2 -2
  11. package/dist/chunks/{NostrIdentityCollection-BRYUgt47.js.map → NostrIdentityCollection-BnHZOsUI.js.map} +1 -1
  12. package/dist/chunks/{OidcIdentity-CFbUlB_Z.js → OidcIdentity-DANnAbfU.js} +3 -3
  13. package/dist/chunks/{OidcIdentity-CFbUlB_Z.js.map → OidcIdentity-DANnAbfU.js.map} +1 -1
  14. package/dist/chunks/{OidcIdentityCollection-BLxSaqJT.js → OidcIdentityCollection-eSM4gzHL.js} +3 -3
  15. package/dist/chunks/{OidcIdentityCollection-BLxSaqJT.js.map → OidcIdentityCollection-eSM4gzHL.js.map} +1 -1
  16. package/dist/chunks/{ProfileAssetCollection-FhW8q7_1.js → ProfileAssetCollection-A_PpEaDq.js} +3 -3
  17. package/dist/chunks/{ProfileAssetCollection-FhW8q7_1.js.map → ProfileAssetCollection-A_PpEaDq.js.map} +1 -1
  18. package/dist/chunks/{ProfileCollection-DdJ3W4GG.js → ProfileCollection-BsKhR5Dp.js} +27 -35
  19. package/dist/chunks/{ProfileCollection-DdJ3W4GG.js.map → ProfileCollection-BsKhR5Dp.js.map} +1 -1
  20. package/dist/chunks/{ProfileMetadataCollection-loKVv_pj.js → ProfileMetadataCollection-Ceq-QV-1.js} +2 -2
  21. package/dist/chunks/{ProfileMetadataCollection-loKVv_pj.js.map → ProfileMetadataCollection-Ceq-QV-1.js.map} +1 -1
  22. package/dist/chunks/{ProfileMetafieldCollection-CACMY1hl.js → ProfileMetafieldCollection-CM374Dbh.js} +2 -2
  23. package/dist/chunks/{ProfileMetafieldCollection-CACMY1hl.js.map → ProfileMetafieldCollection-CM374Dbh.js.map} +1 -1
  24. package/dist/chunks/{ProfileRelationshipCollection-CzQKfvMy.js → ProfileRelationshipCollection-DuPcpzp1.js} +5 -5
  25. package/dist/chunks/{ProfileRelationshipCollection-CzQKfvMy.js.map → ProfileRelationshipCollection-DuPcpzp1.js.map} +1 -1
  26. package/dist/chunks/{ProfileRelationshipTermCollection-DzmU48P6.js → ProfileRelationshipTermCollection-Bw32vYB8.js} +2 -2
  27. package/dist/chunks/{ProfileRelationshipTermCollection-DzmU48P6.js.map → ProfileRelationshipTermCollection-Bw32vYB8.js.map} +1 -1
  28. package/dist/chunks/{ProfileRelationshipType-BjoaCt-u.js → ProfileRelationshipType-CZm2X1qY.js} +2 -2
  29. package/dist/chunks/{ProfileRelationshipType-BjoaCt-u.js.map → ProfileRelationshipType-CZm2X1qY.js.map} +1 -1
  30. package/dist/chunks/{ProfileRelationshipTypeCollection-Bh4xJ4-I.js → ProfileRelationshipTypeCollection-CtoeCpp_.js} +3 -3
  31. package/dist/chunks/{ProfileRelationshipTypeCollection-Bh4xJ4-I.js.map → ProfileRelationshipTypeCollection-CtoeCpp_.js.map} +1 -1
  32. package/dist/chunks/{oidcProvisioningPrimitives-C9XSQNZj.js → oidcProvisioningPrimitives-Bzv4VTcn.js} +23 -8
  33. package/dist/chunks/oidcProvisioningPrimitives-Bzv4VTcn.js.map +1 -0
  34. package/dist/chunks/{resolveIdentity-BpkxPkz7.js → resolveIdentity-DusCiNZo.js} +17 -17
  35. package/dist/chunks/{resolveIdentity-BpkxPkz7.js.map → resolveIdentity-DusCiNZo.js.map} +1 -1
  36. package/dist/index.d.ts +12 -3
  37. package/dist/index.js +24 -26
  38. package/dist/index.js.map +1 -1
  39. package/dist/internal/oidc-provisioning.js +2 -2
  40. package/dist/manifest.json +399 -215
  41. package/dist/oidc-provisioning.d.ts +13 -1
  42. package/dist/smrt-knowledge.json +40 -5
  43. package/dist/types.d.ts +8 -2
  44. package/dist/utils.d.ts +8 -2
  45. package/dist/utils.js +4 -4
  46. package/package.json +6 -6
  47. package/dist/chunks/ApiKey-Br9NGBnr.js.map +0 -1
  48. package/dist/chunks/oidcProvisioningPrimitives-C9XSQNZj.js.map +0 -1
@@ -27,7 +27,19 @@ declare type DatabaseInterface_2 = Awaited<ReturnType<typeof getDatabase>>;
27
27
  /** Trusted integration insert after the caller proves Profile ownership. */
28
28
  export declare function insertOidcProvisioningIdentity<TIdentity>(db: DatabaseInterface, input: OidcProvisioningIdentityInput): Promise<TIdentity>;
29
29
 
30
- /** Detect an adapter error caused by an aborted transaction. */
30
+ /**
31
+ * Detect an adapter error caused by an aborted transaction.
32
+ *
33
+ * Reduced to the framework contract in #2366. The heuristic this replaced
34
+ * matched a bare `/transaction.*aborted/` against every message in the chain,
35
+ * which would misclassify a genuine foreign-key or check violation whose text
36
+ * happened to mention a transaction. Core now recognizes PostgreSQL's
37
+ * `25P02 in_failed_sql_transaction` from the driver's own SQLSTATE, whichever
38
+ * layer wrapped it.
39
+ *
40
+ * Kept as a named export because it is part of this package's provisioning
41
+ * API surface; the body is now a single delegation.
42
+ */
31
43
  export declare function isOidcAbortedTransactionError(error: unknown): boolean;
32
44
 
33
45
  /** Classify only the durable constraints used to converge OIDC retries. */
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-profiles",
6
- "packageVersion": "0.40.69",
6
+ "packageVersion": "0.41.0",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "2a54d08b660587efdc927ffb50e229c3c62d2722ef682b9dbf76d5b627588b9c",
11
- "packageJson": "900b60068fc240d7e23b66a33b0391cb61a0d7bcb4df7818997529bad6f0bc43",
12
- "agents": "7fa3e5a4ecd53e6483b58d60aee82a9ef1bca0c7433454b316fc09cef938248f"
10
+ "manifest": "43194fbefc159f78d93829497ba3c5dc6e965ccdd11b5e143e25a0cbbc43697f",
11
+ "packageJson": "957663a4a242f1188a54d532ab4cedec6842336107defa1015268bd27afd914c",
12
+ "agents": "475e041f9b6a04721dda5d475337a22f72c80c8fdc8be95acca5011535744b15"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -1432,6 +1432,11 @@
1432
1432
  "mode": "optional",
1433
1433
  "field": "tenantId"
1434
1434
  },
1435
+ "conflictColumns": [
1436
+ "tenant_id",
1437
+ "slug",
1438
+ "context"
1439
+ ],
1435
1440
  "surfaces": [
1436
1441
  {
1437
1442
  "kind": "api",
@@ -2776,6 +2781,12 @@
2776
2781
  "field": "tenantId"
2777
2782
  },
2778
2783
  "tableStrategy": "sti",
2784
+ "conflictColumns": [
2785
+ "tenant_id",
2786
+ "slug",
2787
+ "context",
2788
+ "_meta_type"
2789
+ ],
2779
2790
  "surfaces": [
2780
2791
  {
2781
2792
  "kind": "api",
@@ -3349,6 +3360,12 @@
3349
3360
  "field": "tenantId"
3350
3361
  },
3351
3362
  "tableStrategy": "sti",
3363
+ "conflictColumns": [
3364
+ "tenant_id",
3365
+ "slug",
3366
+ "context",
3367
+ "_meta_type"
3368
+ ],
3352
3369
  "surfaces": [
3353
3370
  {
3354
3371
  "kind": "api",
@@ -3840,6 +3857,12 @@
3840
3857
  "field": "tenantId"
3841
3858
  },
3842
3859
  "tableStrategy": "sti",
3860
+ "conflictColumns": [
3861
+ "tenant_id",
3862
+ "slug",
3863
+ "context",
3864
+ "_meta_type"
3865
+ ],
3843
3866
  "surfaces": [
3844
3867
  {
3845
3868
  "kind": "api",
@@ -4371,6 +4394,12 @@
4371
4394
  "field": "tenantId"
4372
4395
  },
4373
4396
  "tableStrategy": "sti",
4397
+ "conflictColumns": [
4398
+ "tenant_id",
4399
+ "slug",
4400
+ "context",
4401
+ "_meta_type"
4402
+ ],
4374
4403
  "surfaces": [
4375
4404
  {
4376
4405
  "kind": "api",
@@ -5757,6 +5786,12 @@
5757
5786
  "field": "tenantId"
5758
5787
  },
5759
5788
  "tableStrategy": "sti",
5789
+ "conflictColumns": [
5790
+ "tenant_id",
5791
+ "slug",
5792
+ "context",
5793
+ "_meta_type"
5794
+ ],
5760
5795
  "surfaces": [
5761
5796
  {
5762
5797
  "kind": "api",
@@ -8655,5 +8690,5 @@
8655
8690
  "polymorphicAssociations": 0,
8656
8691
  "uuidColumns": 61
8657
8692
  },
8658
- "agentDoc": "# @happyvertical/smrt-profiles\n\nCentral identity system with multi-auth, relationships, controlled metadata, and audit logging.\n\n## Models\n\n- **Profile** (STI base → Bot, Organization, Person): email (globally unique exact value), readonly indexed `emailKey` derived with `normalizeIdentityEmail()` for adapter-independent identity lookup, `typeId` FK to ProfileType, plus a `metadata` `@oneToMany('ProfileMetadata')` relationship for controlled per-profile values.\n- **ProfileAsset**: dedicated owned-asset join in `profile_assets` with `relationship` and `sortOrder`.\n- **ProfileRelationship**: bidirectional — creating one auto-creates reciprocal inverse. `contextProfileId` for tertiary relationships. `ProfileRelationshipTerm` tracks start/end dates.\n- **ProfileMetafield**: controlled vocabulary with `validationSchema`. **ProfileMetadata**: per-profile values linked to metafields.\n- **AuditLog**: action, resourceType/Id, `source` (web/cli/ci/webhook/mcp), `onBehalfOfId` for CI pass-through identity. `allowSuperAdminBypass: true`.\n\n## Auth Methods\n\n| Model | Pattern |\n|-------|---------|\n| NostrIdentity | Encrypted keypair (AES-256-GCM). Requires `SERVER_MASTER_SECRET` env var for decryption. NIP-05 address generation. |\n| OidcIdentity | Multiple issuers (Keycloak/Google/GitHub). Lookup by `issuer + subject` pair. Transactional provisioning derives the readonly nullable unique `identityKey` and backfills legacy rows. |\n| ApiKey | SHA-256 hashed. **Plaintext returned once only** on `generate()`. `keyPrefix` for identification. Scope-based with expiry. |\n| MagicLinkToken | One-time token with expiry for passwordless auth. |\n\n## Identity Resolution\n\nAuth helpers in `src/auth/` build profiles from external identity claims:\n\n- `resolveIdentity()` — top-level dispatcher that returns/creates a Profile from Nostr signatures, OIDC claims, magic link tokens, or API keys.\n- `createProfileFromOidc(claims, provider, options)` — creates `Profile` + `OidcIdentity` for first-time OIDC sign-in using a transaction-capable root database in `options.db`.\n- `ProfileCollection.findUniqueGlobalPersonByEmail(email)` — supported\n verified-identity lookup; fails closed on tenant-scoped, non-Person, or\n duplicate case-insensitive matches.\n- `ProfileCollection.requireCanonicalGlobalPerson(profileId, email?)` —\n validates an application-selected Profile against the same canonical global\n Person invariant.\n- `ProfileCollection.reserveCanonicalIdentityEmail(profileId, email?)` —\n validates and synchronizes the private unique\n `oidc_profile_email_reservations.email_key` used as the database arbiter for\n concurrent external-identity provisioning. Omitting `email` uses the\n Profile's stored address, moving or removing an existing reservation as the\n canonical Profile changes.\n- `createProfileFromNostr(email, nostrData)` — creates `Profile` + `NostrIdentity` for Nostr-authenticated users.\n\n## Key Methods\n\n- `Profile.getAssets()` / `addAsset()` / `removeAsset()` and the matching `ProfileCollection` wrappers — canonical owned asset helpers backed by `profile_assets`.\n- `Profile.addMetadata(metafieldSlug, value)` / `Profile.getMetadata()` — validates against metafield schema. `ProfileCollection.batchGetMetadata()` / `batchUpdateMetadata()` for bulk reads/writes.\n- `Profile.getRelationships({ direction: 'from'|'to'|'all' })` — direction matters.\n- `Profile.getRelationshipsFrom()` / `getRelationshipsTo()` — R10-generated `@oneToMany` accessors. ProfileRelationship has two FKs back to Profile, so each `@oneToMany` annotates its inverse explicitly (`{ foreignKey: 'fromProfileId' }` / `'toProfileId'`). Return raw `ProfileRelationship[]`; use `getRelationships()` for slug/direction filtering.\n- AI: `generateBio()` (uses `smrtProfiles.profile.generateBio` prompt via `@happyvertical/smrt-prompts`), `matches(criteria)` (delegates to `is()`).\n\n## Prompt Registry\n\n`generateBio()` is registered with `@happyvertical/smrt-prompts` so tenants can override template/model/params at runtime:\n\n```typescript\nimport { smrtProfilesGenerateBioPrompt } from '@happyvertical/smrt-profiles';\n// key: 'smrtProfiles.profile.generateBio'\n```\n\n## Gotchas\n\n- **SERVER_MASTER_SECRET required** for Nostr private key decryption — centralized key management\n- **API key never returned again**: `ApiKey.generate()` returns plaintext once; only `keyPrefix` visible later\n- **OIDC unique per issuer+subject**: same subject from different issuers =\n different identities. Both claims are opaque and case-sensitive; preserve\n exact whitespace after using trim only to reject blank values.\n- **OIDC identity mutations are trusted-only**: generated REST, MCP, and CLI\n surfaces are read-only. Link or update identities through transactional\n provisioning APIs so callers cannot rebind issuer/subject authority to an\n arbitrary Profile. Deprecated `OidcIdentity.findOrCreate()` preserves only\n transaction-safe exact-link reuse and refuses to create new links;\n `OidcIdentityCollection.linkToProfile()` and `Profile.linkOidcIdentity()`\n delegate to that same non-creating compatibility path.\n- **Apply schema migrations for identity race keys**: run `smrt db:status`,\n `smrt db:migrate`, then `smrt db:status` before deploying. Transactional OIDC\n provisioning populates `OidcIdentity.identityKey` and\n `oidc_profile_email_reservations`; legacy rows reserve an address only after\n safe validation. Stop/upgrade old Profile writers, then run the public,\n transactional and idempotent `backfillProfileEmailKeys(db)` from one deploy\n process. Verified-email provisioning and public email-key lookup require the\n standard `_smrt_backfills` readiness marker and fail closed immediately when\n it is absent. This keeps table scans in the explicit deploy step; runtime\n reads use the indexed key and validate only returned candidates.\n- **Public OIDC provisioning requires transactions**:\n `createProfileFromOidc()` owns a root transaction or uses a savepoint on an\n already-bound handle. Root adapters must expose `beginTransaction`;\n transaction-only handles are ambiguous and fail closed. Pass the root\n database for adapters without nested savepoints, including DuckDB;\n provisioning fails before durable writes if neither path is safe. Trusted\n framework packages use the private\n `@happyvertical/smrt-profiles/internal/oidc-provisioning` subpath instead of\n duplicating adapter probing, locking, or retry policy, and supply both exact\n issuer/subject and normalized-email lock keys in deterministic order. The\n coordinator additionally serializes all SQLite/DuckDB provisioning\n transactions per database URL because those adapters cannot overlap\n unrelated root transactions safely and retries bounded PostgreSQL\n deadlock/serialization failures. New OIDC Profiles use per-profile,\n non-semantic slugs so duplicate display names never invoke natural-key upsert.\n Caller-owned transactions never execute `_smrt_backfills` DDL; paths that\n perform canonical email lookup or reservation require the table to already\n exist or the caller must retry with the root database. Exact issuer/subject\n reuse skips the email-key readiness-marker lookup, but root coordination still\n initializes the shared tracker table. Caller-owned exact reuse does not\n consult the tracker and therefore does not require that table.\n- **Profile-only OIDC linking fails closed on existing email matches**:\n the typed canonical scenario contract is\n `src/testing/oidcProvisioningDecisionMatrix.ts`, executed by both Profiles\n and Users tests; keep public docs pointed at it instead of adding a second\n behavioral table.\n `createProfileFromOidc()` preserves exact issuer/subject reuse, including\n legacy tenant-scoped and non-Person links, but profiles cannot prove whether a\n User owns a same-email Profile. New identities therefore never attach to an\n existing email match through this helper; User/session provisioning still\n rejects unsafe linked Profiles before creating authentication state. Use\n `UserCollection.getOrCreateFromOidc()` for owner-aware verified-email reuse and\n its supported transaction-bound hooks. A pre-provisioned owned Person remains\n fail-closed unless the application explicitly uses `authorizeProfileOwner`\n to select both the canonical Profile and its existing approved sole owner;\n the users package revalidates those IDs, emails, and identity authority in\n the provisioning transaction.\n- **Email storage and identity matching differ**: `Profile.email` has an\n exact-value DB constraint. Identity boundaries query readonly indexed\n `Profile.emailKey`, derived by the shared TypeScript\n `normalizeIdentityEmail()` helper rather than adapter-specific SQL casing or\n trimming, and deliberately fail closed on duplicate normalized keys.\n- **Manifest objects must remain root-importable**: the non-API\n `OidcProfileEmailReservation` model and collection are public root exports\n because generated consumer registration imports every advertised manifest\n symbol from `@happyvertical/smrt-profiles`. Release-pack validation imports\n the actual generated register file from the packed tarball.\n- **Optional tenancy** on Profile; AuditLog allows super-admin bypass\n"
8693
+ "agentDoc": "# @happyvertical/smrt-profiles\n\nCentral identity system with multi-auth, relationships, controlled metadata, and audit logging.\n\n## Models\n\n- **Profile** (STI base → Bot, Organization, Person): email (optional; identity uniqueness is arbitrated by `oidc_profile_email_reservations.email_key`, not a DB constraint — legacy duplicates are tolerated and fail closed, #2359), readonly indexed `emailKey` derived with `normalizeIdentityEmail()` for adapter-independent identity lookup, `typeId` FK to ProfileType, plus a `metadata` `@oneToMany('ProfileMetadata')` relationship for controlled per-profile values.\n- **ProfileAsset**: dedicated owned-asset join in `profile_assets` with `relationship` and `sortOrder`.\n- **ProfileRelationship**: bidirectional — creating one auto-creates reciprocal inverse. `contextProfileId` for tertiary relationships. `ProfileRelationshipTerm` tracks start/end dates.\n- **ProfileMetafield**: controlled vocabulary with `validationSchema`. **ProfileMetadata**: per-profile values linked to metafields.\n- **AuditLog**: action, resourceType/Id, `source` (web/cli/ci/webhook/mcp), `onBehalfOfId` for CI pass-through identity. `allowSuperAdminBypass: true`.\n\n## Auth Methods\n\n| Model | Pattern |\n|-------|---------|\n| NostrIdentity | Encrypted keypair (AES-256-GCM). Requires `SERVER_MASTER_SECRET` env var for decryption. NIP-05 address generation. |\n| OidcIdentity | Multiple issuers (Keycloak/Google/GitHub). Lookup by `issuer + subject` pair. Transactional provisioning derives the readonly nullable unique `identityKey` and backfills legacy rows. |\n| ApiKey | SHA-256 hashed. **Plaintext returned once only** on `generate()`. `keyPrefix` for identification. Scope-based with expiry. |\n| MagicLinkToken | One-time token with expiry for passwordless auth. |\n\n## Identity Resolution\n\nAuth helpers in `src/auth/` build profiles from external identity claims:\n\n- `resolveIdentity()` — top-level dispatcher that returns/creates a Profile from Nostr signatures, OIDC claims, magic link tokens, or API keys.\n- `createProfileFromOidc(claims, provider, options)` — creates `Profile` + `OidcIdentity` for first-time OIDC sign-in using a transaction-capable root database in `options.db`.\n- `ProfileCollection.findUniqueGlobalPersonByEmail(email)` — supported\n verified-identity lookup; fails closed on tenant-scoped, non-Person, or\n duplicate case-insensitive matches.\n- `ProfileCollection.requireCanonicalGlobalPerson(profileId, email?)` —\n validates an application-selected Profile against the same canonical global\n Person invariant.\n- `ProfileCollection.reserveCanonicalIdentityEmail(profileId, email?)` —\n validates and synchronizes the private unique\n `oidc_profile_email_reservations.email_key` used as the database arbiter for\n concurrent external-identity provisioning. Omitting `email` uses the\n Profile's stored address, moving or removing an existing reservation as the\n canonical Profile changes.\n- `createProfileFromNostr(email, nostrData)` — creates `Profile` + `NostrIdentity` for Nostr-authenticated users.\n\n## Key Methods\n\n- `Profile.getAssets()` / `addAsset()` / `removeAsset()` and the matching `ProfileCollection` wrappers — canonical owned asset helpers backed by `profile_assets`.\n- `Profile.addMetadata(metafieldSlug, value)` / `Profile.getMetadata()` — validates against metafield schema. `ProfileCollection.batchGetMetadata()` / `batchUpdateMetadata()` for bulk reads/writes.\n- `Profile.getRelationships({ direction: 'from'|'to'|'all' })` — direction matters.\n- `Profile.getRelationshipsFrom()` / `getRelationshipsTo()` — R10-generated `@oneToMany` accessors. ProfileRelationship has two FKs back to Profile, so each `@oneToMany` annotates its inverse explicitly (`{ foreignKey: 'fromProfileId' }` / `'toProfileId'`). Return raw `ProfileRelationship[]`; use `getRelationships()` for slug/direction filtering.\n- AI: `generateBio()` (uses `smrtProfiles.profile.generateBio` prompt via `@happyvertical/smrt-prompts`), `matches(criteria)` (delegates to `is()`).\n\n## Prompt Registry\n\n`generateBio()` is registered with `@happyvertical/smrt-prompts` so tenants can override template/model/params at runtime:\n\n```typescript\nimport { smrtProfilesGenerateBioPrompt } from '@happyvertical/smrt-profiles';\n// key: 'smrtProfiles.profile.generateBio'\n```\n\n## Gotchas\n\n- **SERVER_MASTER_SECRET required** for Nostr private key decryption — centralized key management\n- **API key never returned again**: `ApiKey.generate()` returns plaintext once; only `keyPrefix` visible later\n- **OIDC unique per issuer+subject**: same subject from different issuers =\n different identities. Both claims are opaque and case-sensitive; preserve\n exact whitespace after using trim only to reject blank values.\n- **OIDC identity mutations are trusted-only**: generated REST, MCP, and CLI\n surfaces are read-only. Link or update identities through transactional\n provisioning APIs so callers cannot rebind issuer/subject authority to an\n arbitrary Profile. Deprecated `OidcIdentity.findOrCreate()` preserves only\n transaction-safe exact-link reuse and refuses to create new links;\n `OidcIdentityCollection.linkToProfile()` and `Profile.linkOidcIdentity()`\n delegate to that same non-creating compatibility path.\n- **Apply schema migrations for identity race keys**: run `smrt db:status`,\n `smrt db:migrate`, then `smrt db:status` before deploying. Transactional OIDC\n provisioning populates `OidcIdentity.identityKey` and\n `oidc_profile_email_reservations`; legacy rows reserve an address only after\n safe validation. Stop/upgrade old Profile writers, then run the public,\n transactional and idempotent `backfillProfileEmailKeys(db)` from one deploy\n process. Verified-email provisioning and public email-key lookup require the\n standard `_smrt_backfills` readiness marker and fail closed immediately when\n it is absent. This keeps table scans in the explicit deploy step; runtime\n reads use the indexed key and validate only returned candidates.\n- **Public OIDC provisioning requires transactions**:\n `createProfileFromOidc()` owns a root transaction or uses a savepoint on an\n already-bound handle. Root adapters must expose `beginTransaction`;\n transaction-only handles are ambiguous and fail closed. Pass the root\n database for adapters without nested savepoints, including DuckDB;\n provisioning fails before durable writes if neither path is safe. Trusted\n framework packages use the private\n `@happyvertical/smrt-profiles/internal/oidc-provisioning` subpath instead of\n duplicating adapter probing, locking, or retry policy, and supply both exact\n issuer/subject and normalized-email lock keys in deterministic order. The\n coordinator additionally serializes every root-handle statement it owns per\n database URL on SQLite/DuckDB — shared `_smrt_backfills` initialization, the\n provisioning transaction, and the post-commit rebind — because those adapters\n multiplex one native connection and cannot overlap unrelated root\n transactions safely. **Never overlap two statements on one such handle,\n transaction-bound or not** — no `Promise.all` over reads, not even\n primary-key rebinds — because DuckDB fails the losing prepared statement or\n aborts the process outright. It retries\n bounded PostgreSQL deadlock/serialization failures. New OIDC Profiles use per-profile,\n non-semantic slugs so duplicate display names never invoke natural-key upsert.\n Caller-owned transactions never execute `_smrt_backfills` DDL; paths that\n perform canonical email lookup or reservation require the table to already\n exist or the caller must retry with the root database. Exact issuer/subject\n reuse skips the email-key readiness-marker lookup, but root coordination still\n initializes the shared tracker table. Caller-owned exact reuse does not\n consult the tracker and therefore does not require that table.\n- **Profile-only OIDC linking fails closed on existing email matches**:\n the typed canonical scenario contract is\n `src/testing/oidcProvisioningDecisionMatrix.ts`, executed by both Profiles\n and Users tests; keep public docs pointed at it instead of adding a second\n behavioral table.\n `createProfileFromOidc()` preserves exact issuer/subject reuse, including\n legacy tenant-scoped and non-Person links, but profiles cannot prove whether a\n User owns a same-email Profile. New identities therefore never attach to an\n existing email match through this helper; User/session provisioning still\n rejects unsafe linked Profiles before creating authentication state. Use\n `UserCollection.getOrCreateFromOidc()` for owner-aware verified-email reuse and\n its supported transaction-bound hooks. A pre-provisioned owned Person remains\n fail-closed unless the application explicitly uses `authorizeProfileOwner`\n to select both the canonical Profile and its existing approved sole owner;\n the users package revalidates those IDs, emails, and identity authority in\n the provisioning transaction.\n- **Email storage and identity matching differ**: `Profile.email` has an\n exact-value DB constraint. Identity boundaries query readonly indexed\n `Profile.emailKey`, derived by the shared TypeScript\n `normalizeIdentityEmail()` helper rather than adapter-specific SQL casing or\n trimming, and deliberately fail closed on duplicate normalized keys.\n- **Manifest objects must remain root-importable**: the non-API\n `OidcProfileEmailReservation` model and collection are public root exports\n because generated consumer registration imports every advertised manifest\n symbol from `@happyvertical/smrt-profiles`. Release-pack validation imports\n the actual generated register file from the packed tarball.\n- **Optional tenancy** on Profile; AuditLog allows super-admin bypass\n"
8659
8694
  }
package/dist/types.d.ts CHANGED
@@ -8,7 +8,10 @@ declare class ApiKey extends SmrtObject {
8
8
  */
9
9
  profileId?: string;
10
10
  /**
11
- * SHA-256 hash of the API key
11
+ * SHA-256 hash of the API key.
12
+ *
13
+ * Indexed: `verify()` looks keys up by this hash on every authenticated
14
+ * request (#2364, epic #2382 finding A3).
12
15
  */
13
16
  keyHash: string;
14
17
  /**
@@ -158,7 +161,10 @@ declare class NostrIdentity extends SmrtObject {
158
161
  */
159
162
  profileId?: string;
160
163
  /**
161
- * Hex-encoded secp256k1 public key (64 characters)
164
+ * Hex-encoded secp256k1 public key (64 characters).
165
+ *
166
+ * Indexed: `findByPubkey()` looks identities up by this column (#2364,
167
+ * epic #2382 finding A3).
162
168
  */
163
169
  pubkey: string;
164
170
  /**
package/dist/utils.d.ts CHANGED
@@ -8,7 +8,10 @@ declare class ApiKey extends SmrtObject {
8
8
  */
9
9
  profileId?: string;
10
10
  /**
11
- * SHA-256 hash of the API key
11
+ * SHA-256 hash of the API key.
12
+ *
13
+ * Indexed: `verify()` looks keys up by this hash on every authenticated
14
+ * request (#2364, epic #2382 finding A3).
12
15
  */
13
16
  keyHash: string;
14
17
  /**
@@ -190,7 +193,10 @@ declare class NostrIdentity extends SmrtObject {
190
193
  */
191
194
  profileId?: string;
192
195
  /**
193
- * Hex-encoded secp256k1 public key (64 characters)
196
+ * Hex-encoded secp256k1 public key (64 characters).
197
+ *
198
+ * Indexed: `findByPubkey()` looks identities up by this column (#2364,
199
+ * epic #2382 finding A3).
194
200
  */
195
201
  pubkey: string;
196
202
  /**
package/dist/utils.js CHANGED
@@ -1,17 +1,17 @@
1
1
  //#region src/utils.ts
2
2
  async function getProfileMetadata(profileId, options = {}) {
3
- const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-loKVv_pj.js").then((n) => n.n);
3
+ const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-Ceq-QV-1.js").then((n) => n.n);
4
4
  return await (await ProfileMetadataCollection.create(options)).getMetadataObject(profileId);
5
5
  }
6
6
  async function setProfileMetadata(profileId, metafieldSlug, value, options = {}) {
7
- const { ProfileCollection } = await import("./chunks/ProfileCollection-DdJ3W4GG.js").then((n) => n.r);
7
+ const { ProfileCollection } = await import("./chunks/ProfileCollection-BsKhR5Dp.js").then((n) => n.r);
8
8
  const profile = await (await ProfileCollection.create(options)).get({ id: profileId });
9
9
  if (!profile) throw new Error(`Profile '${profileId}' not found`);
10
10
  await profile.addMetadata(metafieldSlug, value);
11
11
  }
12
12
  async function findProfilesByMeta(metafieldSlug, value, options = {}) {
13
- const { ProfileMetafieldCollection } = await import("./chunks/ProfileMetafieldCollection-CACMY1hl.js").then((n) => n.n);
14
- const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-loKVv_pj.js").then((n) => n.n);
13
+ const { ProfileMetafieldCollection } = await import("./chunks/ProfileMetafieldCollection-CM374Dbh.js").then((n) => n.n);
14
+ const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-Ceq-QV-1.js").then((n) => n.n);
15
15
  const metafield = await (await ProfileMetafieldCollection.create(options)).getBySlug(metafieldSlug);
16
16
  if (!metafield) throw new Error(`Metafield '${metafieldSlug}' not found`);
17
17
  return await (await ProfileMetadataCollection.create(options)).findProfilesByMetadata(metafield.id, value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-profiles",
3
- "version": "0.40.69",
3
+ "version": "0.41.0",
4
4
  "description": "Profile management system with relationships, metadata, and reciprocal associations for SMRT framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,10 +34,10 @@
34
34
  "@happyvertical/utils": "^0.88.0",
35
35
  "@noble/curves": "^1.9.7",
36
36
  "bech32": "^2.0.0",
37
- "@happyvertical/smrt-assets": "0.40.69",
38
- "@happyvertical/smrt-prompts": "0.40.69",
39
- "@happyvertical/smrt-tenancy": "0.40.69",
40
- "@happyvertical/smrt-core": "0.40.69"
37
+ "@happyvertical/smrt-assets": "0.41.0",
38
+ "@happyvertical/smrt-prompts": "0.41.0",
39
+ "@happyvertical/smrt-tenancy": "0.41.0",
40
+ "@happyvertical/smrt-core": "0.41.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@faker-js/faker": "^10.5.0",
@@ -46,7 +46,7 @@
46
46
  "typescript": "5.9.3",
47
47
  "vite": "8.1.4",
48
48
  "vitest": "4.1.10",
49
- "@happyvertical/smrt-vitest": "0.40.69"
49
+ "@happyvertical/smrt-vitest": "0.41.0"
50
50
  },
51
51
  "keywords": [
52
52
  "ai",
@@ -1 +0,0 @@
1
- {"version":3,"file":"ApiKey-Br9NGBnr.js","names":[],"sources":["../../src/models/ApiKey.ts"],"sourcesContent":["/**\n * ApiKey - API keys for CLI and CI authentication\n *\n * Provides secure API key management with hashing, scopes, and expiration.\n * Keys are stored as hashes - the plaintext is only returned on creation.\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport {\n foreignKey,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { Profile } from './Profile';\n\nexport interface ApiKeyOptions extends SmrtObjectOptions {\n profileId?: string;\n name?: string;\n scopes?: string[];\n expiresAt?: Date | null;\n}\n\nexport interface GenerateKeyResult {\n key: string;\n apiKey: ApiKey;\n}\n\n@smrt({\n tableName: 'api_keys',\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n // create/revoke are admin operations invoked in-process via the CLI; only\n // read-only list/get are exposed over HTTP.\n cli: { include: ['list', 'get', 'create', 'revoke'], skipApiCheck: true },\n})\nexport class ApiKey extends SmrtObject {\n /**\n * Link to the Profile (Person, Organization, Bot)\n */\n @foreignKey('Profile', { required: true })\n profileId?: string;\n\n /**\n * SHA-256 hash of the API key\n */\n keyHash: string = '';\n\n /**\n * First 8 characters of the key for identification (e.g., \"sk_live_a1b2...\")\n */\n keyPrefix: string = '';\n\n /**\n * Human-readable name for the key (e.g., \"CI Bot\", \"Local CLI\")\n */\n name: string = '';\n\n /**\n * Scopes/permissions for this key\n */\n scopes: string[] = [];\n\n /**\n * Last time this key was used\n */\n lastUsedAt: Date | null = null;\n\n /**\n * When this key expires (null = never)\n */\n expiresAt: Date | null = null;\n\n /**\n * When this key was revoked (null = active)\n */\n revokedAt: Date | null = null;\n\n constructor(options: ApiKeyOptions = {}) {\n super(options);\n if (options.profileId) this.profileId = options.profileId;\n if (options.name) this.name = options.name;\n if (options.scopes) this.scopes = options.scopes;\n if (options.expiresAt !== undefined) this.expiresAt = options.expiresAt;\n }\n\n /**\n * Get the linked Profile\n */\n async getProfile(): Promise<Profile | null> {\n return (await this.getRelated('profileId')) as Profile | null;\n }\n\n /**\n * Check if this key is valid (not expired, not revoked)\n */\n isValid(): boolean {\n if (this.revokedAt) return false;\n if (this.expiresAt && this.expiresAt < new Date()) return false;\n return true;\n }\n\n /**\n * Check if this key has a specific scope\n */\n hasScope(scope: string): boolean {\n return this.scopes.includes(scope) || this.scopes.includes('*');\n }\n\n /**\n * Revoke this key\n */\n async revoke(): Promise<void> {\n this.revokedAt = new Date();\n await this.save();\n }\n\n /**\n * Record usage of this key\n */\n async recordUsage(): Promise<void> {\n this.lastUsedAt = new Date();\n await this.save();\n }\n\n /**\n * Hash an API key\n */\n static hashKey(key: string): string {\n return createHash('sha256').update(key).digest('hex');\n }\n\n /**\n * Generate a new API key for a profile\n */\n static async generate(\n profile: Profile,\n options: {\n name: string;\n scopes?: string[];\n expiresAt?: Date | null;\n db?: SmrtObjectOptions['db'];\n },\n ): Promise<GenerateKeyResult> {\n // Generate a random key: sk_live_<32 random bytes as hex>\n const randomPart = randomBytes(32).toString('hex');\n const key = `sk_live_${randomPart}`;\n const keyPrefix = key.substring(0, 16);\n const keyHash = ApiKey.hashKey(key);\n\n const apiKey = new ApiKey({\n db: options.db || profile.options?.db,\n profileId: profile.id as string,\n name: options.name,\n scopes: options.scopes || ['*'],\n expiresAt: options.expiresAt ?? null,\n });\n apiKey.keyHash = keyHash;\n apiKey.keyPrefix = keyPrefix;\n\n await apiKey.initialize();\n await apiKey.save();\n\n return { key, apiKey };\n }\n\n /**\n * Verify an API key and return the ApiKey record if valid\n */\n static async verify(\n key: string,\n options: SmrtObjectOptions = {},\n ): Promise<ApiKey | null> {\n const keyHash = ApiKey.hashKey(key);\n\n const { ApiKeyCollection } = await import(\n '../collections/ApiKeyCollection'\n );\n const collection = await ApiKeyCollection.create(options);\n\n const apiKey = await collection.findOne({\n where: { keyHash },\n });\n\n if (!apiKey) return null;\n if (!apiKey.isValid()) return null;\n\n // Record usage\n await apiKey.recordUsage();\n\n return apiKey;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAoCO,IAAM,SAAN,cAAqB,WAAW;CAKrC;;;;CAKA,UAAkB;;;;CAKlB,YAAoB;;;;CAKpB,OAAe;;;;CAKf,SAAmB,CAAC;;;;CAKpB,aAA0B;;;;CAK1B,YAAyB;;;;CAKzB,YAAyB;CAEzB,YAAY,UAAyB,CAAC,GAAG;EACvC,MAAM,OAAO;EACb,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,MAAM,KAAK,OAAO,QAAQ;EACtC,IAAI,QAAQ,QAAQ,KAAK,SAAS,QAAQ;EAC1C,IAAI,QAAQ,cAAc,KAAA,GAAW,KAAK,YAAY,QAAQ;CAChE;;;;CAKA,MAAM,aAAsC;EAC1C,OAAQ,MAAM,KAAK,WAAW,WAAW;CAC3C;;;;CAKA,UAAmB;EACjB,IAAI,KAAK,WAAW,OAAO;EAC3B,IAAI,KAAK,aAAa,KAAK,4BAAY,IAAI,KAAK,GAAG,OAAO;EAC1D,OAAO;CACT;;;;CAKA,SAAS,OAAwB;EAC/B,OAAO,KAAK,OAAO,SAAS,KAAK,KAAK,KAAK,OAAO,SAAS,GAAG;CAChE;;;;CAKA,MAAM,SAAwB;EAC5B,KAAK,4BAAY,IAAI,KAAK;EAC1B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,MAAM,cAA6B;EACjC,KAAK,6BAAa,IAAI,KAAK;EAC3B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,OAAO,QAAQ,KAAqB;EAClC,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,GAAG,CAAA,CAAE,OAAO,KAAK;CACtD;;;;CAKA,aAAa,SACX,SACA,SAM4B;EAG5B,MAAM,MAAM,WADO,YAAY,EAAE,CAAA,CAAE,SAAS,KACrB;EACvB,MAAM,YAAY,IAAI,UAAU,GAAG,EAAE;EACrC,MAAM,UAAU,OAAO,QAAQ,GAAG;EAElC,MAAM,SAAS,IAAI,OAAO;GACxB,IAAI,QAAQ,MAAM,QAAQ,SAAS;GACnC,WAAW,QAAQ;GACnB,MAAM,QAAQ;GACd,QAAQ,QAAQ,UAAU,CAAC,GAAG;GAC9B,WAAW,QAAQ,aAAa;EAClC,CAAC;EACD,OAAO,UAAU;EACjB,OAAO,YAAY;EAEnB,MAAM,OAAO,WAAW;EACxB,MAAM,OAAO,KAAK;EAElB,OAAO;GAAE;GAAK;EAAO;CACvB;;;;CAKA,aAAa,OACX,KACA,UAA6B,CAAC,GACN;EACxB,MAAM,UAAU,OAAO,QAAQ,GAAG;EAElC,MAAM,EAAE,qBAAqB,MAAM,OACjC,iCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAIF,MAAM,SAAS,OAAM,MAFI,iBAAiB,OAAO,OAAO,EAAA,CAExB,QAAQ,EACtC,OAAO,EAAE,QAAQ,EACnB,CAAC;EAED,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,OAAO,QAAQ,GAAG,OAAO;EAG9B,MAAM,OAAO,YAAY;EAEzB,OAAO;CACT;AACF;AAvJE,gBAAA,CADC,WAAW,WAAW,EAAE,UAAU,KAAK,CAAC,CAAA,GAJ9B,OAKX,WAAA,aAAA,CAAA;AALW,SAAN,gBAAA,CARN,KAAK;CACJ,WAAW;CACX,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAGhC,KAAK;EAAE,SAAS;GAAC;GAAQ;GAAO;GAAU;EAAQ;EAAG,cAAc;CAAK;AAC1E,CAAC,CAAA,GACY,MAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"oidcProvisioningPrimitives-C9XSQNZj.js","names":[],"sources":["../../src/collections/ProfileTypeCollection.ts","../../src/models/ProfileTypes.ts","../../src/auth/oidcProvisioningPrimitives.ts"],"sourcesContent":["/**\n * ProfileTypeCollection - Collection manager for ProfileType objects\n *\n * Provides querying for profile type lookup table.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { isSystemContext } from '@happyvertical/smrt-tenancy';\nimport { ProfileType } from '../models/ProfileType';\n\nexport class ProfileTypeCollection extends SmrtCollection<ProfileType> {\n static readonly _itemClass = ProfileType;\n\n /**\n * Get profile type by slug\n *\n * @param slug - The slug to search for\n * @returns ProfileType instance or null\n */\n async getBySlug(slug: string): Promise<ProfileType | null> {\n return await this.get({ slug });\n }\n\n /**\n * Get or create a profile type by slug\n *\n * @param slug - The slug to search for\n * @param defaults - Default values if creating\n * @returns ProfileType instance\n */\n async getOrCreateBySlug(\n slug: string,\n defaults: { name: string; description?: string },\n ): Promise<ProfileType> {\n const existing = await this.getBySlug(slug);\n if (existing) return existing;\n\n const profileType = await this.create({ slug, ...defaults });\n await profileType.save();\n return profileType;\n }\n\n /**\n * Atomically create or load a global ProfileType from trusted system code.\n *\n * This intentionally bypasses tenant auto-population and therefore refuses\n * to run outside an explicit `withSystemContext()` boundary.\n */\n async getOrCreateGlobalBySlug(\n slug: string,\n defaults: { name: string; description?: string },\n ): Promise<ProfileType> {\n if (!isSystemContext()) {\n throw new Error(\n 'Global ProfileType creation requires an explicit system context.',\n );\n }\n\n const existing = await this.loadGlobalBySlug(slug);\n if (existing) return existing;\n\n // A plain SMRT create is an upsert and may replace the winning row's id\n // when two first-login transactions race. Keep the canonical id stable by\n // using a cross-adapter insert-if-absent and then hydrating the winner.\n await this.db.query(\n `INSERT INTO profile_types\n (id, slug, context, _meta_type, tenant_id, name, description)\n VALUES (?, ?, '', '@happyvertical/smrt-profiles:ProfileType', NULL, ?, ?)\n ON CONFLICT (slug, context, _meta_type) DO NOTHING`,\n crypto.randomUUID(),\n slug,\n defaults.name,\n defaults.description ?? null,\n );\n const profileType = await this.loadGlobalBySlug(slug);\n if (!profileType) {\n throw new Error(\n `Global ProfileType '${slug}' could not be created or loaded.`,\n );\n }\n return profileType;\n }\n\n private async loadGlobalBySlug(slug: string): Promise<ProfileType | null> {\n const result = await this.db.query(\n `SELECT id\n FROM profile_types\n WHERE slug = ?\n AND context = ''\n AND _meta_type = '@happyvertical/smrt-profiles:ProfileType'\n AND tenant_id IS NULL\n LIMIT 1`,\n slug,\n );\n const id = result.rows[0]?.id;\n return typeof id === 'string' ? this.get({ id }) : null;\n }\n}\n","/**\n * Profile subclasses for STI (Single Table Inheritance)\n *\n * These classes inherit from Profile and automatically use the shared\n * profiles table with _meta_type discriminator.\n */\n\nimport { smrt } from '@happyvertical/smrt-core';\nimport { Profile, type ProfileOptions } from './Profile';\n\n/**\n * Person profile type\n *\n * Represents individual people/users.\n */\n@smrt({\n tableStrategy: 'sti',\n})\nexport class Person extends Profile {\n constructor(options: ProfileOptions = {}) {\n super(options);\n // Person-specific initialization can go here\n }\n}\n\n/**\n * Organization profile type\n *\n * Represents companies, groups, institutions, and other organizational entities.\n */\n@smrt({\n tableStrategy: 'sti',\n})\nexport class Organization extends Profile {\n constructor(options: ProfileOptions = {}) {\n super(options);\n // Organization-specific initialization can go here\n }\n}\n\n/**\n * Bot profile type\n *\n * Represents automated agents, bots, and AI entities.\n */\n@smrt({\n tableStrategy: 'sti',\n})\nexport class Bot extends Profile {\n constructor(options: ProfileOptions = {}) {\n super(options);\n // Bot-specific initialization can go here\n }\n}\n","import type { getDatabase } from '@happyvertical/sql';\nimport { ProfileCollection } from '../collections/ProfileCollection';\nimport { ProfileTypeCollection } from '../collections/ProfileTypeCollection';\nimport { OidcIdentity } from '../models/OidcIdentity';\nimport type { Profile } from '../models/Profile';\nimport { Person } from '../models/ProfileTypes';\nimport { saveOidcRaceArbiter } from './oidcProvisioningCoordinator';\n\ntype DatabaseInterface = Awaited<ReturnType<typeof getDatabase>>;\n\nexport interface OidcProvisioningPersonInput {\n email?: string;\n name?: string;\n preferredUsername?: string;\n subject: string;\n}\n\nexport interface OidcProvisioningIdentityInput {\n email?: string;\n issuer: string;\n profileId: string;\n provider: string;\n subject: string;\n}\n\n/** Create and reserve a fresh global Person inside the caller's transaction. */\nexport async function createOidcProvisioningPerson(\n db: DatabaseInterface,\n input: OidcProvisioningPersonInput,\n): Promise<Profile> {\n const typeCollection = await ProfileTypeCollection.create({ db });\n const personType = await typeCollection.getOrCreateGlobalBySlug('person', {\n name: 'Person',\n description: 'Individual person profile',\n });\n const typeId = requireProvisioningId(personType.id, 'Person ProfileType');\n let profile: Profile = new Person({\n db,\n // OIDC display names are not identity keys. A per-profile slug keeps\n // SMRT's natural-key upsert from replacing an unrelated Person.\n slug: `oidc-${crypto.randomUUID()}`,\n tenantId: null,\n typeId,\n email: input.email ?? '',\n name: input.name || input.preferredUsername || input.subject,\n });\n await profile.initialize();\n await saveOidcRaceArbiter(() => profile.save());\n\n if (input.email) {\n const profiles = await ProfileCollection.create({ db });\n profile = await profiles.reserveCanonicalIdentityEmail(\n requireProvisioningId(profile.id, 'Created Profile'),\n input.email,\n );\n }\n return profile;\n}\n\n/** Insert the identity after the caller proves Profile ownership. */\nexport async function insertOidcProvisioningIdentity(\n db: DatabaseInterface,\n input: OidcProvisioningIdentityInput,\n): Promise<OidcIdentity> {\n const identity = new OidcIdentity({\n db,\n email: input.email ?? '',\n issuer: input.issuer,\n profileId: input.profileId,\n provider: input.provider,\n subject: input.subject,\n });\n await identity.initialize();\n await saveOidcRaceArbiter(() => identity.save());\n return identity;\n}\n\nfunction requireProvisioningId(value: unknown, label: string): string {\n if (typeof value !== 'string' || !value) {\n throw new Error(`${label} did not produce an id.`);\n }\n return value;\n}\n"],"mappings":";;;;;;;AAUO,IAAM,wBAAN,cAAoC,eAA4B;CACrE,OAAgB,aAAa;;;;;;;CAQ7B,MAAM,UAAU,MAA2C;EACzD,OAAO,MAAM,KAAK,IAAI,EAAE,KAAK,CAAC;CAChC;;;;;;;;CASA,MAAM,kBACJ,MACA,UACsB;EACtB,MAAM,WAAW,MAAM,KAAK,UAAU,IAAI;EAC1C,IAAI,UAAU,OAAO;EAErB,MAAM,cAAc,MAAM,KAAK,OAAO;GAAE;GAAM,GAAG;EAAS,CAAC;EAC3D,MAAM,YAAY,KAAK;EACvB,OAAO;CACT;;;;;;;CAQA,MAAM,wBACJ,MACA,UACsB;EACtB,IAAI,CAAC,gBAAgB,GACnB,MAAM,IAAI,MACR,kEACF;EAGF,MAAM,WAAW,MAAM,KAAK,iBAAiB,IAAI;EACjD,IAAI,UAAU,OAAO;EAKrB,MAAM,KAAK,GAAG,MACZ;;;4DAIA,OAAO,WAAW,GAClB,MACA,SAAS,MACT,SAAS,eAAe,IAC1B;EACA,MAAM,cAAc,MAAM,KAAK,iBAAiB,IAAI;EACpD,IAAI,CAAC,aACH,MAAM,IAAI,MACR,uBAAuB,KAAI,kCAC7B;EAEF,OAAO;CACT;CAEA,MAAc,iBAAiB,MAA2C;EAWxE,MAAM,MAAK,MAVU,KAAK,GAAG,MAC3B;;;;;;iBAOA,IACF,EAAA,CACkB,KAAK,EAAC,EAAG;EAC3B,OAAO,OAAO,OAAO,WAAW,KAAK,IAAI,EAAE,GAAG,CAAC,IAAI;CACrD;AACF;;;;;;;;;;;;;;;;AC/EO,IAAM,SAAN,cAAqB,QAAQ;CAClC,YAAY,UAA0B,CAAC,GAAG;EACxC,MAAM,OAAO;CAEf;AACF;AALa,SAAN,gBAAA,CAHN,KAAK,EACJ,eAAe,MACjB,CAAC,CAAA,GACY,MAAA;AAeN,IAAM,eAAN,cAA2B,QAAQ;CACxC,YAAY,UAA0B,CAAC,GAAG;EACxC,MAAM,OAAO;CAEf;AACF;AALa,eAAN,gBAAA,CAHN,KAAK,EACJ,eAAe,MACjB,CAAC,CAAA,GACY,YAAA;AAeN,IAAM,MAAN,cAAkB,QAAQ;CAC/B,YAAY,UAA0B,CAAC,GAAG;EACxC,MAAM,OAAO;CAEf;AACF;AALa,MAAN,gBAAA,CAHN,KAAK,EACJ,eAAe,MACjB,CAAC,CAAA,GACY,GAAA;;;ACtBb,eAAsB,6BACpB,IACA,OACkB;CAMlB,MAAM,SAAS,uBAAsB,OAJZ,MADI,sBAAsB,OAAO,EAAE,GAAG,CAAC,EAAA,CACxB,wBAAwB,UAAU;EACxE,MAAM;EACN,aAAa;CACf,CAAC,EAAA,CAC+C,IAAI,oBAAoB;CACxE,IAAI,UAAmB,IAAI,OAAO;EAChC;EAGA,MAAM,QAAQ,OAAO,WAAW;EAChC,UAAU;EACV;EACA,OAAO,MAAM,SAAS;EACtB,MAAM,MAAM,QAAQ,MAAM,qBAAqB,MAAM;CACvD,CAAC;CACD,MAAM,QAAQ,WAAW;CACzB,MAAM,0BAA0B,QAAQ,KAAK,CAAC;CAE9C,IAAI,MAAM,OAER,UAAU,OAAM,MADO,kBAAkB,OAAO,EAAE,GAAG,CAAC,EAAA,CAC7B,8BACvB,sBAAsB,QAAQ,IAAI,iBAAiB,GACnD,MAAM,KACR;CAEF,OAAO;AACT;AAGA,eAAsB,+BACpB,IACA,OACuB;CACvB,MAAM,WAAW,IAAI,aAAa;EAChC;EACA,OAAO,MAAM,SAAS;EACtB,QAAQ,MAAM;EACd,WAAW,MAAM;EACjB,UAAU,MAAM;EAChB,SAAS,MAAM;CACjB,CAAC;CACD,MAAM,SAAS,WAAW;CAC1B,MAAM,0BAA0B,SAAS,KAAK,CAAC;CAC/C,OAAO;AACT;AAEA,SAAS,sBAAsB,OAAgB,OAAuB;CACpE,IAAI,OAAO,UAAU,YAAY,CAAC,OAChC,MAAM,IAAI,MAAM,GAAG,MAAK,wBAAyB;CAEnD,OAAO;AACT"}