@happyvertical/smrt-profiles 0.39.13 → 0.39.15

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 (54) hide show
  1. package/AGENTS.md +69 -5
  2. package/README.md +101 -50
  3. package/dist/chunks/{ApiKey-BrNyaaZN.js → ApiKey-Br9NGBnr.js} +3 -3
  4. package/dist/chunks/{ApiKey-BrNyaaZN.js.map → ApiKey-Br9NGBnr.js.map} +1 -1
  5. package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js → ApiKeyCollection-CrTjdMz8.js} +3 -3
  6. package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js.map → ApiKeyCollection-CrTjdMz8.js.map} +1 -1
  7. package/dist/chunks/{AuditLogCollection-CnIYU0aK.js → AuditLogCollection-BBIEI6BD.js} +2 -2
  8. package/dist/chunks/{AuditLogCollection-CnIYU0aK.js.map → AuditLogCollection-BBIEI6BD.js.map} +1 -1
  9. package/dist/chunks/{NostrIdentityCollection-CG6HGf6c.js → NostrIdentity-DF_a_WZk.js} +6 -125
  10. package/dist/chunks/NostrIdentity-DF_a_WZk.js.map +1 -0
  11. package/dist/chunks/NostrIdentityCollection-BRYUgt47.js +124 -0
  12. package/dist/chunks/NostrIdentityCollection-BRYUgt47.js.map +1 -0
  13. package/dist/chunks/OidcIdentity-ChkiT6hg.js +111 -0
  14. package/dist/chunks/OidcIdentity-ChkiT6hg.js.map +1 -0
  15. package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js +76 -0
  16. package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js.map +1 -0
  17. package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js → ProfileAssetCollection-CtlvXbGy.js} +3 -3
  18. package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js.map → ProfileAssetCollection-CtlvXbGy.js.map} +1 -1
  19. package/dist/chunks/ProfileCollection-Bix9yAiy.js +1161 -0
  20. package/dist/chunks/ProfileCollection-Bix9yAiy.js.map +1 -0
  21. package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js → ProfileMetadataCollection-loKVv_pj.js} +2 -2
  22. package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js.map → ProfileMetadataCollection-loKVv_pj.js.map} +1 -1
  23. package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js → ProfileMetafieldCollection-CACMY1hl.js} +2 -2
  24. package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js.map → ProfileMetafieldCollection-CACMY1hl.js.map} +1 -1
  25. package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js → ProfileRelationshipCollection-CzQKfvMy.js} +5 -5
  26. package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js.map → ProfileRelationshipCollection-CzQKfvMy.js.map} +1 -1
  27. package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js → ProfileRelationshipTermCollection-DzmU48P6.js} +2 -2
  28. package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js.map → ProfileRelationshipTermCollection-DzmU48P6.js.map} +1 -1
  29. package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js → ProfileRelationshipType-BjoaCt-u.js} +2 -2
  30. package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js.map → ProfileRelationshipType-BjoaCt-u.js.map} +1 -1
  31. package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js → ProfileRelationshipTypeCollection-Bh4xJ4-I.js} +3 -3
  32. package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js.map → ProfileRelationshipTypeCollection-Bh4xJ4-I.js.map} +1 -1
  33. package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js +138 -0
  34. package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js.map +1 -0
  35. package/dist/chunks/resolveIdentity-_h_2sGqJ.js +260 -0
  36. package/dist/chunks/resolveIdentity-_h_2sGqJ.js.map +1 -0
  37. package/dist/index.d.ts +127 -15
  38. package/dist/index.js +30 -273
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/oidc-provisioning.d.ts +17 -0
  41. package/dist/internal/oidc-provisioning.js +13 -0
  42. package/dist/internal/oidc-provisioning.js.map +1 -0
  43. package/dist/manifest.json +431 -46
  44. package/dist/oidc-provisioning.d.ts +83 -0
  45. package/dist/smrt-knowledge.json +105 -43
  46. package/dist/types.d.ts +26 -2
  47. package/dist/utils.d.ts +26 -2
  48. package/dist/utils.js +4 -4
  49. package/package.json +10 -6
  50. package/dist/chunks/NostrIdentityCollection-CG6HGf6c.js.map +0 -1
  51. package/dist/chunks/OidcIdentityCollection-DiLneB7J.js +0 -158
  52. package/dist/chunks/OidcIdentityCollection-DiLneB7J.js.map +0 -1
  53. package/dist/chunks/ProfileCollection-BOMOloTL.js +0 -641
  54. package/dist/chunks/ProfileCollection-BOMOloTL.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Asset } from '@happyvertical/smrt-assets';
2
+ import { getDatabase } from '@happyvertical/sql';
2
3
  import { PromptDefinition } from '@happyvertical/smrt-prompts';
3
4
  import { ResolvedPromptAI } from '@happyvertical/smrt-prompts';
4
5
  import { SmrtCollection } from '@happyvertical/smrt-core';
@@ -6,6 +7,13 @@ import { SmrtJunction } from '@happyvertical/smrt-core';
6
7
  import { SmrtObject } from '@happyvertical/smrt-core';
7
8
  import { SmrtObjectOptions } from '@happyvertical/smrt-core';
8
9
 
10
+ /** More than one legacy row maps the same opaque OIDC issuer and subject. */
11
+ export declare class AmbiguousOidcIdentityError extends Error {
12
+ readonly issuer: string;
13
+ readonly subject: string;
14
+ constructor(issuer: string, subject: string);
15
+ }
16
+
9
17
  export declare class ApiKey extends SmrtObject {
10
18
  /**
11
19
  * Link to the Profile (Person, Organization, Bot)
@@ -272,6 +280,19 @@ export declare interface AuthContext {
272
280
  db?: SmrtObjectOptions['db'];
273
281
  }
274
282
 
283
+ /**
284
+ * Populate adapter-independent Profile email keys after the schema migration.
285
+ *
286
+ * Run this from one deploy process after legacy writers are stopped or
287
+ * upgraded. Every read and update runs in one transaction, and repeats are a
288
+ * no-op.
289
+ */
290
+ export declare function backfillProfileEmailKeys(db: DatabaseInterface): Promise<BackfillProfileEmailKeysResult>;
291
+
292
+ export declare interface BackfillProfileEmailKeysResult {
293
+ updated: number;
294
+ }
295
+
275
296
  /**
276
297
  * Bot profile type
277
298
  *
@@ -281,6 +302,14 @@ export declare class Bot extends Profile {
281
302
  constructor(options?: ProfileOptions);
282
303
  }
283
304
 
305
+ /** A Profile cannot safely represent one global human identity. */
306
+ export declare class CanonicalPersonProfileError extends Error {
307
+ readonly code: CanonicalPersonProfileErrorCode;
308
+ constructor(code: CanonicalPersonProfileErrorCode, message: string);
309
+ }
310
+
311
+ export declare type CanonicalPersonProfileErrorCode = 'ambiguous_email' | 'email_key_backfill_required' | 'email_mismatch' | 'missing_profile' | 'non_person' | 'reservation_conflict' | 'tenant_scoped';
312
+
284
313
  /**
285
314
  * Compute the event ID (SHA-256 hash of serialized event)
286
315
  */
@@ -355,23 +384,21 @@ export declare function createProfileFromNostr(email: string, nostrData: {
355
384
  /**
356
385
  * Create a profile from OIDC claims if it doesn't exist
357
386
  *
358
- * Supports email-based account linking: if a user signs in with Google
359
- * and later with GitHub using the same email, they get the same profile.
360
- *
361
387
  * Resolution order:
362
388
  * 1. If OIDC identity (iss + sub) already exists → return linked profile
363
- * 2. If verified email provided, check if profile with same email exists → link new identity
389
+ * 2. If the email matches an existing Profile, fail closed because this
390
+ * package cannot prove whether a User owns it
364
391
  * 3. Otherwise, create new profile + identity
365
392
  *
366
393
  * Security considerations:
367
- * - Email-based linking only occurs when `email_verified` is true. This prevents
368
- * attackers from claiming unverified emails to hijack accounts.
369
- * - Linking is automatic and irreversible through this API. Multiple OIDC
370
- * identities from different providers sharing the same verified email will
371
- * be associated to the same Profile.
372
- * - If an OIDC provider does not supply an email, or the email changes later,
373
- * existing links are not automatically updated. New sign-ins without an email
374
- * or with a different email may result in a new Profile being created.
394
+ * - Existing issuer/subject links always keep their already-linked Profile,
395
+ * including legacy tenant-scoped or non-Person Profiles, and refresh the
396
+ * cached identity email. This exact-link compatibility does not perform
397
+ * email-based canonical reuse.
398
+ * - New identities never attach to an existing email match through this
399
+ * Profile-only helper because Profile ownership belongs to the users package.
400
+ * Use `UserCollection.getOrCreateFromOidc()` for owner-aware verified-email
401
+ * reuse and its supported pre-provision resolver hook.
375
402
  * - This function trusts the OIDC provider to assert correct email_verified status.
376
403
  * Only use with trusted providers.
377
404
  *
@@ -393,6 +420,8 @@ export declare function createProfileFromOidc(claims: {
393
420
  created: boolean;
394
421
  }>;
395
422
 
423
+ declare type DatabaseInterface = Awaited<ReturnType<typeof getDatabase>>;
424
+
396
425
  /**
397
426
  * Decrypt a private key using AES-256-GCM
398
427
  * @param encrypted - Encrypted key data
@@ -676,6 +705,14 @@ export declare interface Nip05Response {
676
705
  relays?: Record<string, string[]>;
677
706
  }
678
707
 
708
+ /**
709
+ * Canonicalize an email used as an external identity key.
710
+ *
711
+ * Keep this in application code: SQL `trim()` and `lower()` have
712
+ * adapter-specific whitespace and Unicode behavior.
713
+ */
714
+ export declare function normalizeIdentityEmail(email: string): string;
715
+
679
716
  export declare interface NostrEvent {
680
717
  id?: string;
681
718
  pubkey: string;
@@ -883,6 +920,14 @@ export declare class OidcIdentity extends SmrtObject {
883
920
  * OIDC subject claim - unique identifier from the provider
884
921
  */
885
922
  subject: string;
923
+ /**
924
+ * Stable issuer+subject key used as the database race arbiter.
925
+ *
926
+ * Nullable for legacy rows; every newly linked or reused identity backfills
927
+ * it. A separate unique constraint makes concurrent first login fail with a
928
+ * retryable conflict instead of creating two identities.
929
+ */
930
+ identityKey: string | null;
886
931
  /**
887
932
  * Cached email from the IdP (for display/lookup)
888
933
  */
@@ -900,8 +945,17 @@ export declare class OidcIdentity extends SmrtObject {
900
945
  * Find identity by issuer and subject
901
946
  */
902
947
  static findBySubject(issuer: string, subject: string, options?: SmrtObjectOptions): Promise<OidcIdentity | null>;
948
+ /** Build the collision-free natural key for one OIDC issuer subject. */
949
+ static buildIdentityKey(issuer: string, subject: string): string;
950
+ /** Keep the durable key derived from its natural-key source fields. */
951
+ save(): Promise<this>;
903
952
  /**
904
- * Find or create identity for a profile
953
+ * Reuse an existing exact identity for its unchanged Profile.
954
+ *
955
+ * @deprecated Authentication links must be created through transactional
956
+ * provisioning. This compatibility method only refreshes a unique mapping
957
+ * that already belongs to the supplied Profile, including legacy Profile
958
+ * types, and deliberately refuses to create or rebind authority.
905
959
  */
906
960
  static findOrCreate(profile: Profile, oidcData: {
907
961
  provider: string;
@@ -930,7 +984,11 @@ export declare class OidcIdentityCollection extends SmrtCollection<OidcIdentity>
930
984
  */
931
985
  findByProvider(provider: string): Promise<OidcIdentity[]>;
932
986
  /**
933
- * Link a new OIDC identity to a profile
987
+ * Reuse an existing exact OIDC identity for its unchanged Profile.
988
+ *
989
+ * @deprecated New authentication links require the owner-aware,
990
+ * transactional provisioning APIs. This compatibility helper may refresh a
991
+ * legacy Profile type, but refuses to create or rebind authority.
934
992
  */
935
993
  linkToProfile(profile: Profile, oidcData: {
936
994
  provider: string;
@@ -989,12 +1047,16 @@ export declare class Profile extends SmrtObject {
989
1047
  tenantId: string | null;
990
1048
  typeId?: string;
991
1049
  email?: string;
1050
+ /** Adapter-independent identity lookup key derived from email on save. */
1051
+ emailKey: string | null;
992
1052
  name: string;
993
1053
  description?: string;
994
1054
  metadata: ProfileMetadata[];
995
1055
  relationshipsFrom: ProfileRelationship[];
996
1056
  relationshipsTo: ProfileRelationship[];
997
1057
  constructor(options?: ProfileOptions);
1058
+ /** Keep the durable identity key derived from the public email field. */
1059
+ save(): Promise<this>;
998
1060
  /**
999
1061
  * Get the profile type slug for this profile
1000
1062
  *
@@ -1146,10 +1208,13 @@ export declare class Profile extends SmrtObject {
1146
1208
  */
1147
1209
  getOidcIdentities(): Promise<OidcIdentity[]>;
1148
1210
  /**
1149
- * Link a new OIDC identity to this profile
1211
+ * Reuse an existing exact OIDC identity for this unchanged Profile.
1150
1212
  *
1151
1213
  * @param oidcData - OIDC provider data
1152
1214
  * @returns The linked OIDC identity record
1215
+ * @deprecated New authentication links require owner-aware transactional
1216
+ * provisioning. This compatibility helper may refresh a legacy Profile
1217
+ * type, but refuses to create or rebind authority.
1153
1218
  */
1154
1219
  linkOidcIdentity(oidcData: {
1155
1220
  provider: string;
@@ -1200,6 +1265,8 @@ export declare class Profile extends SmrtObject {
1200
1265
  }): Promise<AuditLog>;
1201
1266
  }
1202
1267
 
1268
+ export declare const PROFILE_EMAIL_KEY_BACKFILL_NAME = "@happyvertical/smrt-profiles:profile-email-keys:v1";
1269
+
1203
1270
  export declare class ProfileAsset extends SmrtObject {
1204
1271
  tenantId: string | null;
1205
1272
  profileId: string;
@@ -1230,6 +1297,7 @@ export declare interface ProfileAssetOptions extends SmrtObjectOptions {
1230
1297
 
1231
1298
  export declare class ProfileCollection extends SmrtCollection<Profile> {
1232
1299
  static readonly _itemClass: typeof Profile;
1300
+ private emailKeysReadyPromise;
1233
1301
  /**
1234
1302
  * Find a profile by email address
1235
1303
  *
@@ -1237,6 +1305,30 @@ export declare class ProfileCollection extends SmrtCollection<Profile> {
1237
1305
  * @returns The matching profile or null
1238
1306
  */
1239
1307
  findByEmail(email: string): Promise<Profile | null>;
1308
+ /**
1309
+ * Resolve one unambiguous global Person by normalized email.
1310
+ *
1311
+ * Unlike `findByEmail()`, this identity-boundary helper deliberately reads
1312
+ * across tenant scopes and fails closed when any matching row is
1313
+ * tenant-scoped, is not a Person STI row, or when more than one row matches
1314
+ * case-insensitively. It is intended for verified external identities.
1315
+ */
1316
+ findUniqueGlobalPersonByEmail(email: string): Promise<Profile | null>;
1317
+ /**
1318
+ * Validate and hydrate a supplied canonical Profile.
1319
+ *
1320
+ * The Profile must be the sole case-insensitive match for its stored email.
1321
+ * When `email` is provided, that address must also match the stored email.
1322
+ */
1323
+ requireCanonicalGlobalPerson(profileId: string, email?: string): Promise<Profile>;
1324
+ /**
1325
+ * Reserve the normalized email for a previously validated canonical Person.
1326
+ *
1327
+ * The unique stored key turns a concurrent OIDC first-login race into a
1328
+ * retryable database conflict. Legacy Profiles remain unaffected until an
1329
+ * identity boundary safely claims them.
1330
+ */
1331
+ reserveCanonicalIdentityEmail(profileId: string, email?: string): Promise<Profile>;
1240
1332
  /**
1241
1333
  * Find profiles by type slug
1242
1334
  *
@@ -1244,6 +1336,15 @@ export declare class ProfileCollection extends SmrtCollection<Profile> {
1244
1336
  * @returns Array of matching profiles
1245
1337
  */
1246
1338
  findByType(typeSlug: string): Promise<Profile[]>;
1339
+ private requireDatabase;
1340
+ private loadCanonicalEmailRows;
1341
+ /** Require the deploy-time backfill marker before indexed identity reads. */
1342
+ private ensureEmailKeysReady;
1343
+ private checkEmailKeysReady;
1344
+ private isStaleEmailReservation;
1345
+ private assertRowEmailKeyCurrent;
1346
+ private assertCanonicalRows;
1347
+ private requireHydratedProfile;
1247
1348
  /**
1248
1349
  * Batch get metadata for multiple profiles
1249
1350
  *
@@ -1688,6 +1789,17 @@ export declare class ProfileTypeCollection extends SmrtCollection<ProfileType> {
1688
1789
  name: string;
1689
1790
  description?: string;
1690
1791
  }): Promise<ProfileType>;
1792
+ /**
1793
+ * Atomically create or load a global ProfileType from trusted system code.
1794
+ *
1795
+ * This intentionally bypasses tenant auto-population and therefore refuses
1796
+ * to run outside an explicit `withSystemContext()` boundary.
1797
+ */
1798
+ getOrCreateGlobalBySlug(slug: string, defaults: {
1799
+ name: string;
1800
+ description?: string;
1801
+ }): Promise<ProfileType>;
1802
+ private loadGlobalBySlug;
1691
1803
  }
1692
1804
 
1693
1805
  export declare interface ProfileTypeOptions extends SmrtObjectOptions {