@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
@@ -0,0 +1,1161 @@
1
+ import { y as __exportAll } from "./NostrIdentity-DF_a_WZk.js";
2
+ import { SmrtCollection, SmrtObject, ValidationError, field, foreignKey, oneToMany, smrt } from "@happyvertical/smrt-core";
3
+ import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
4
+ import { TenantScoped, queryGlobal, queryWithGlobals, tenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
5
+ import { BackfillTableUnavailableError, BackfillTracker } from "@happyvertical/smrt-core/migrations";
6
+ import { addOwnedAssetFromCollection, assertValidOwnedAssetRelationship, assertValidOwnedAssetSortOrder, getOwnedAssetsFromCollection, removeOwnedAssetFromCollection, resolveOwnedAssetsById } from "@happyvertical/smrt-assets";
7
+ //#region src/prompts.ts
8
+ var smrtProfilesGenerateBioPrompt = definePrompt({
9
+ key: "smrtProfiles.profile.generateBio",
10
+ template: `Write a short, professional bio for this person.
11
+
12
+ Profile name: {profileName}
13
+ Profile description: {profileDescription}
14
+
15
+ Return only the bio text, with no commentary or surrounding quotation marks.`,
16
+ editable: {
17
+ template: true,
18
+ profile: true,
19
+ model: true,
20
+ params: true
21
+ }
22
+ });
23
+ function promptMessageOptions(ai) {
24
+ return {
25
+ ...ai.params || {},
26
+ ...ai.model ? { model: ai.model } : {},
27
+ ...typeof ai.temperature === "number" ? { temperature: ai.temperature } : {},
28
+ ...typeof ai.maxTokens === "number" ? { maxTokens: ai.maxTokens } : {}
29
+ };
30
+ }
31
+ //#endregion
32
+ //#region src/auth/normalizeIdentityEmail.ts
33
+ function normalizeIdentityEmail(email) {
34
+ const normalized = email.trim().toLowerCase();
35
+ if (!normalized) throw new Error("Identity email normalization requires a non-blank email.");
36
+ return normalized;
37
+ }
38
+ //#endregion
39
+ //#region src/auth/oidcProvisioningCoordinator.ts
40
+ var provisioningLocks = /* @__PURE__ */ new Map();
41
+ var sqliteRetryLocks = /* @__PURE__ */ new Map();
42
+ var savepointSequence = 0;
43
+ var OIDC_RACE_FIELD_NAMES = /* @__PURE__ */ new Set([
44
+ "email_key",
45
+ "emailKey",
46
+ "identity_key",
47
+ "identityKey",
48
+ "profile_id",
49
+ "profileId"
50
+ ]);
51
+ var OIDC_RACE_MESSAGE_PATTERNS = [
52
+ /oidc_identities[._].*identity_key/iu,
53
+ /oidc_identities_identity_key/iu,
54
+ /oidc_profile_email_reservations[._].*(?:email_key|profile_id)/iu,
55
+ /profile_types_slug_context_meta_type/iu
56
+ ];
57
+ var OidcRaceArbiterConflict = class extends Error {
58
+ constructor(cause) {
59
+ super("An OIDC provisioning race arbiter reported a unique conflict.", { cause });
60
+ this.name = "OidcRaceArbiterConflict";
61
+ }
62
+ };
63
+ var SavepointUnavailableError = class extends Error {
64
+ constructor(cause) {
65
+ super("OIDC provisioning could not start a transaction savepoint.", { cause });
66
+ this.name = "SavepointUnavailableError";
67
+ }
68
+ };
69
+ async function coordinateOidcProvisioning(options) {
70
+ const rootBackfillTableReady = typeof options.db.beginTransaction === "function";
71
+ if (rootBackfillTableReady) await new BackfillTracker({ db: options.db }).initialize();
72
+ return withProvisioningLocks(options.db, options.lockKeys, async () => {
73
+ const result = await retryProvisioning(options, () => withProvisioningTransaction(options.db, (tx) => {
74
+ if (rootBackfillTableReady) BackfillTracker.inheritInitialization(tx, options.db);
75
+ else BackfillTracker.requireExistingTable(tx);
76
+ return options.provision(tx);
77
+ }, options.createTransactionError));
78
+ return rootBackfillTableReady && options.rebindRootResult ? options.rebindRootResult(result, options.db) : result;
79
+ });
80
+ }
81
+ async function withProvisioningLocks(db, lockKeys, callback) {
82
+ const keys = [.../* @__PURE__ */ new Set([...!isPostgresDatabase(db) || typeof db.beginTransaction !== "function" ? ["adapter-transaction"] : [], ...lockKeys])].sort();
83
+ const acquire = (index) => index >= keys.length ? callback() : withProvisioningLock(db, keys[index], () => acquire(index + 1));
84
+ return acquire(0);
85
+ }
86
+ function isOidcAbortedTransactionError(error) {
87
+ const seen = /* @__PURE__ */ new Set();
88
+ let current = error;
89
+ while (current instanceof Error && !seen.has(current)) {
90
+ seen.add(current);
91
+ if (/current transaction is aborted|transaction.*aborted/iu.test(current.message)) return true;
92
+ current = current.cause;
93
+ }
94
+ return false;
95
+ }
96
+ function testRaceMessagePattern(pattern, message) {
97
+ pattern.lastIndex = 0;
98
+ try {
99
+ return pattern.test(message);
100
+ } finally {
101
+ pattern.lastIndex = 0;
102
+ }
103
+ }
104
+ function isOidcProvisioningRaceConflict(error, options = {}) {
105
+ if (isOidcAbortedTransactionError(error)) return true;
106
+ const pending = [error];
107
+ const seen = /* @__PURE__ */ new Set();
108
+ const messagePatterns = [...OIDC_RACE_MESSAGE_PATTERNS, ...options.messagePatterns ?? []];
109
+ while (pending.length > 0) {
110
+ const current = pending.pop();
111
+ if (!current || typeof current !== "object" || seen.has(current)) continue;
112
+ seen.add(current);
113
+ if (current instanceof OidcRaceArbiterConflict) return true;
114
+ if (current instanceof Error && current.name === "CanonicalPersonProfileError" && current.code === "reservation_conflict") return true;
115
+ if (current instanceof ValidationError && current.code === "VALIDATION_UNIQUE_CONSTRAINT" && OIDC_RACE_FIELD_NAMES.has(String(current.details?.fieldName ?? ""))) return true;
116
+ if (current instanceof Error && messagePatterns.some((pattern) => testRaceMessagePattern(pattern, current.message))) return true;
117
+ const details = current;
118
+ pending.push(details.cause, details.context, details.details, details.originalError);
119
+ if (current instanceof AggregateError) pending.push(...current.errors);
120
+ }
121
+ return false;
122
+ }
123
+ async function saveOidcRaceArbiter(operation) {
124
+ try {
125
+ return await operation();
126
+ } catch (error) {
127
+ if (error instanceof ValidationError && error.code === "VALIDATION_UNIQUE_CONSTRAINT" || isOidcAbortedTransactionError(error)) throw new OidcRaceArbiterConflict(error);
128
+ throw error;
129
+ }
130
+ }
131
+ async function withProvisioningLock(db, lockKey, callback) {
132
+ const databaseKey = getDatabaseLockKey(db);
133
+ const locks = provisioningLocks.get(databaseKey) ?? /* @__PURE__ */ new Map();
134
+ provisioningLocks.set(databaseKey, locks);
135
+ const previous = locks.get(lockKey) ?? Promise.resolve();
136
+ let release;
137
+ const current = new Promise((resolve) => {
138
+ release = resolve;
139
+ });
140
+ const queued = previous.then(() => current);
141
+ locks.set(lockKey, queued);
142
+ await previous;
143
+ try {
144
+ return await callback();
145
+ } finally {
146
+ release();
147
+ if (locks.get(lockKey) === queued) locks.delete(lockKey);
148
+ if (locks.size === 0) provisioningLocks.delete(databaseKey);
149
+ }
150
+ }
151
+ function getDatabaseLockKey(db) {
152
+ const url = db.url;
153
+ if (isPostgresDatabase(db) && typeof db.beginTransaction !== "function" && db.client !== null && (typeof db.client === "object" || typeof db.client === "function")) return db.client;
154
+ return url && !isPostgresDatabase(db) ? url : db;
155
+ }
156
+ function isPostgresDatabase(db) {
157
+ return /^postgres(?:ql)?:/iu.test(db.url ?? "");
158
+ }
159
+ async function retryProvisioning(options, callback) {
160
+ let serializeSqliteRetry = false;
161
+ for (let attempt = 0; attempt < 8; attempt += 1) try {
162
+ return await (serializeSqliteRetry ? withSqliteRetryLock(options.db, callback) : callback());
163
+ } catch (error) {
164
+ if (error instanceof BackfillTableUnavailableError) {
165
+ if (typeof options.db.beginTransaction === "function" && attempt < 7) {
166
+ BackfillTracker.invalidateInitialization(options.db);
167
+ await new BackfillTracker({ db: options.db }).initialize();
168
+ continue;
169
+ }
170
+ throw options.createTransactionError("Transaction-bound OIDC provisioning requires an existing _smrt_backfills table; pass the root database so SMRT can initialize it safely.", error);
171
+ }
172
+ const sqliteBusy = isSqliteBusyError(error);
173
+ const postgresTransactionRetry = isPostgresTransactionRetryError(error);
174
+ const raceConflict = options.isRaceConflict(error);
175
+ if (!sqliteBusy && !postgresTransactionRetry && !raceConflict) throw error;
176
+ if (sqliteBusy && attempt < 7) {
177
+ serializeSqliteRetry = true;
178
+ await waitForRetry(Math.min(100 * 2 ** attempt, 1e3));
179
+ continue;
180
+ }
181
+ if (postgresTransactionRetry && attempt < 7) {
182
+ await waitForRetry(Math.min(25 * 2 ** attempt, 500));
183
+ continue;
184
+ }
185
+ if (raceConflict && attempt === 0) continue;
186
+ throw options.createConcurrencyError(error);
187
+ }
188
+ throw options.createConcurrencyError();
189
+ }
190
+ function isPostgresTransactionRetryError(error) {
191
+ const pending = [error];
192
+ const seen = /* @__PURE__ */ new Set();
193
+ const messagePattern = /deadlock detected|could not serialize access|serialization failure/iu;
194
+ while (pending.length > 0) {
195
+ const current = pending.pop();
196
+ if (!current || typeof current !== "object" || seen.has(current)) continue;
197
+ seen.add(current);
198
+ const details = current;
199
+ if (details.code === "40P01" || details.code === "40001") return true;
200
+ if (current instanceof Error && messagePattern.test(current.message)) return true;
201
+ pending.push(details.cause, details.context, details.originalError);
202
+ if (current instanceof AggregateError) pending.push(...current.errors);
203
+ }
204
+ return false;
205
+ }
206
+ async function withSqliteRetryLock(db, callback) {
207
+ const key = db.url || db;
208
+ const previous = sqliteRetryLocks.get(key) ?? Promise.resolve();
209
+ let release;
210
+ const current = new Promise((resolve) => {
211
+ release = resolve;
212
+ });
213
+ const queued = previous.then(() => current);
214
+ sqliteRetryLocks.set(key, queued);
215
+ await previous;
216
+ try {
217
+ return await callback();
218
+ } finally {
219
+ release();
220
+ if (sqliteRetryLocks.get(key) === queued) sqliteRetryLocks.delete(key);
221
+ }
222
+ }
223
+ function isSqliteBusyError(error) {
224
+ const pending = [error];
225
+ const seen = /* @__PURE__ */ new Set();
226
+ const busyPattern = /SQLITE_(?:BUSY|LOCKED)|database(?: table)? is locked|cannot (?:commit transaction|release savepoint) - SQL statements in progress/iu;
227
+ while (pending.length > 0) {
228
+ const current = pending.pop();
229
+ if (typeof current === "string") {
230
+ if (busyPattern.test(current)) return true;
231
+ continue;
232
+ }
233
+ if (!current || typeof current !== "object" || seen.has(current)) continue;
234
+ seen.add(current);
235
+ if (current instanceof Error) {
236
+ if (busyPattern.test(current.message)) return true;
237
+ pending.push(current.cause);
238
+ if (current instanceof AggregateError) pending.push(...current.errors);
239
+ }
240
+ const details = current;
241
+ pending.push(details.context, details.details, details.originalError);
242
+ }
243
+ return false;
244
+ }
245
+ async function waitForRetry(milliseconds) {
246
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
247
+ }
248
+ async function withProvisioningTransaction(db, callback, createTransactionError) {
249
+ if (typeof db.beginTransaction === "function") {
250
+ if (db.transaction) return db.transaction((tx2) => callback(tx2));
251
+ const tx = await db.beginTransaction();
252
+ try {
253
+ const result = await callback(tx);
254
+ await tx.commit();
255
+ return result;
256
+ } catch (error) {
257
+ try {
258
+ if (tx.isActive()) await tx.rollback();
259
+ } catch (rollbackError) {
260
+ throw createTransactionError("OIDC provisioning could not roll back its root transaction.", new AggregateError([error, rollbackError]));
261
+ }
262
+ throw error;
263
+ }
264
+ }
265
+ try {
266
+ return await withSavepoint(db, () => callback(db), createTransactionError);
267
+ } catch (error) {
268
+ if (!(error instanceof SavepointUnavailableError)) throw error;
269
+ throw createTransactionError("OIDC provisioning requires a root database or a transaction handle that supports savepoints.", error.cause);
270
+ }
271
+ }
272
+ async function withSavepoint(db, callback, createTransactionError) {
273
+ savepointSequence = (savepointSequence + 1) % Number.MAX_SAFE_INTEGER;
274
+ const savepoint = `smrt_oidc_${savepointSequence.toString(36)}`;
275
+ try {
276
+ await db.query(`SAVEPOINT ${savepoint}`);
277
+ } catch (error) {
278
+ throw new SavepointUnavailableError(error);
279
+ }
280
+ try {
281
+ const result = await callback();
282
+ await db.query(`RELEASE SAVEPOINT ${savepoint}`);
283
+ return result;
284
+ } catch (error) {
285
+ try {
286
+ await db.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
287
+ await db.query(`RELEASE SAVEPOINT ${savepoint}`);
288
+ } catch (rollbackError) {
289
+ throw createTransactionError("OIDC provisioning could not roll back its transaction savepoint.", new AggregateError([error, rollbackError]));
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ //#endregion
295
+ //#region src/migrations/backfillProfileEmailKeys.ts
296
+ var PROFILE_EMAIL_KEY_BACKFILL_NAME = "@happyvertical/smrt-profiles:profile-email-keys:v1";
297
+ async function backfillProfileEmailKeys(db) {
298
+ if (!db.transaction) throw new Error("Profile email-key backfill requires a root database with transaction().");
299
+ await new BackfillTracker({ db }).initialize();
300
+ return db.transaction(async (tx) => {
301
+ BackfillTracker.inheritInitialization(tx, db);
302
+ const result = await tx.query("SELECT id, email, email_key FROM profiles ORDER BY id");
303
+ const updates = [];
304
+ for (const row of result.rows) {
305
+ const id = typeof row.id === "string" ? row.id : "";
306
+ if (!id) throw new Error("Profile email-key backfill found a row without an id.");
307
+ const email = typeof row.email === "string" ? row.email : "";
308
+ const emailKey = email.trim() ? normalizeIdentityEmail(email) : null;
309
+ if ((typeof row.email_key === "string" ? row.email_key : null) !== emailKey) updates.push({
310
+ emailKey,
311
+ id
312
+ });
313
+ }
314
+ for (const update of updates) await tx.query("UPDATE profiles SET email_key = ? WHERE id = ?", update.emailKey, update.id);
315
+ await new BackfillTracker({ db: tx }).recordApplied(PROFILE_EMAIL_KEY_BACKFILL_NAME, {
316
+ description: "Canonical Profile identity email keys are current.",
317
+ packageName: "@happyvertical/smrt-profiles"
318
+ });
319
+ return { updated: updates.length };
320
+ });
321
+ }
322
+ //#endregion
323
+ //#region src/models/OidcProfileEmailReservation.ts
324
+ var __defProp$2 = Object.defineProperty;
325
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
326
+ var __decorateClass$2 = (decorators, target, key, kind) => {
327
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
328
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
329
+ if (kind && result) __defProp$2(target, key, result);
330
+ return result;
331
+ };
332
+ var OidcProfileEmailReservation = class extends SmrtObject {
333
+ profileId;
334
+ emailKey = "";
335
+ constructor(options = {}) {
336
+ super(options);
337
+ if (options.profileId !== void 0) this.profileId = options.profileId;
338
+ if (options.emailKey !== void 0) this.emailKey = options.emailKey;
339
+ }
340
+ };
341
+ __decorateClass$2([foreignKey("Profile", {
342
+ required: true,
343
+ unique: true
344
+ })], OidcProfileEmailReservation.prototype, "profileId", 2);
345
+ __decorateClass$2([field({
346
+ type: "text",
347
+ required: true,
348
+ unique: true,
349
+ readonly: true
350
+ })], OidcProfileEmailReservation.prototype, "emailKey", 2);
351
+ OidcProfileEmailReservation = __decorateClass$2([smrt({
352
+ tableName: "oidc_profile_email_reservations",
353
+ api: false,
354
+ mcp: false,
355
+ cli: false
356
+ })], OidcProfileEmailReservation);
357
+ //#endregion
358
+ //#region src/models/ProfileType.ts
359
+ var ProfileType_exports = /* @__PURE__ */ __exportAll({ ProfileType: () => ProfileType });
360
+ var __defProp$1 = Object.defineProperty;
361
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
362
+ var __decorateClass$1 = (decorators, target, key, kind) => {
363
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
364
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
365
+ if (kind && result) __defProp$1(target, key, result);
366
+ return result;
367
+ };
368
+ var ProfileType = class extends SmrtObject {
369
+ tenantId = null;
370
+ name = "";
371
+ description;
372
+ constructor(options = {}) {
373
+ super(options);
374
+ if (options.name) this.name = options.name;
375
+ if (options.description !== void 0) this.description = options.description;
376
+ }
377
+ /**
378
+ * Convenience method for slug-based lookup
379
+ *
380
+ * @param slug - The slug to search for
381
+ * @returns ProfileType instance or null if not found
382
+ */
383
+ static async getBySlug(_slug) {
384
+ return null;
385
+ }
386
+ };
387
+ __decorateClass$1([tenantId({ nullable: true })], ProfileType.prototype, "tenantId", 2);
388
+ __decorateClass$1([field({ required: true })], ProfileType.prototype, "name", 2);
389
+ ProfileType = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
390
+ tableStrategy: "sti",
391
+ api: { include: [
392
+ "list",
393
+ "get",
394
+ "create",
395
+ "update"
396
+ ] },
397
+ mcp: { include: ["list", "get"] },
398
+ cli: true
399
+ })], ProfileType);
400
+ //#endregion
401
+ //#region src/models/Profile.ts
402
+ var __defProp = Object.defineProperty;
403
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
404
+ var __decorateClass = (decorators, target, key, kind) => {
405
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
406
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
407
+ if (kind && result) __defProp(target, key, result);
408
+ return result;
409
+ };
410
+ var Profile = class extends SmrtObject {
411
+ tenantId = null;
412
+ typeId;
413
+ email;
414
+ emailKey = null;
415
+ name = "";
416
+ description;
417
+ metadata = [];
418
+ relationshipsFrom = [];
419
+ relationshipsTo = [];
420
+ constructor(options = {}) {
421
+ super(options);
422
+ if (options.typeId !== void 0) this.typeId = options.typeId;
423
+ if (options.email !== void 0) this.email = options.email;
424
+ if (options.name) this.name = options.name;
425
+ if (options.description !== void 0) this.description = options.description;
426
+ }
427
+ /** Keep the durable identity key derived from the public email field. */
428
+ async save() {
429
+ this.emailKey = this.email?.trim() ? normalizeIdentityEmail(this.email) : null;
430
+ return super.save();
431
+ }
432
+ /**
433
+ * Get the profile type slug for this profile
434
+ *
435
+ * @returns The slug of the profile type
436
+ */
437
+ async getTypeSlug() {
438
+ return (await this.loadRelated("typeId"))?.slug || "";
439
+ }
440
+ /**
441
+ * Set the profile type by slug
442
+ *
443
+ * @param slug - The slug of the profile type
444
+ * @throws Error if profile type not found
445
+ */
446
+ async setTypeBySlug(slug) {
447
+ const type = await ProfileType.getBySlug(slug);
448
+ if (!type) throw new Error(`Profile type '${slug}' not found`);
449
+ this.typeId = type.id;
450
+ }
451
+ /**
452
+ * Add metadata to this profile
453
+ *
454
+ * @param metafieldSlug - The slug of the metafield
455
+ * @param value - The value to set
456
+ */
457
+ async addMetadata(metafieldSlug, value) {
458
+ const { ProfileMetafieldCollection } = await import("./ProfileMetafieldCollection-CACMY1hl.js").then((n) => n.n);
459
+ const { ProfileMetadataCollection } = await import("./ProfileMetadataCollection-loKVv_pj.js").then((n) => n.n);
460
+ const metafield = await (await ProfileMetafieldCollection.create(this.options)).getBySlug(metafieldSlug);
461
+ if (!metafield) throw new Error(`Metafield '${metafieldSlug}' not found`);
462
+ await metafield.validateValue(value);
463
+ const metadataCollection = await ProfileMetadataCollection.create(this.options);
464
+ const existing = await metadataCollection.list({
465
+ where: {
466
+ profileId: this.id,
467
+ metafieldId: metafield.id
468
+ },
469
+ limit: 1
470
+ });
471
+ if (existing.length > 0) {
472
+ const metadata = existing[0];
473
+ metadata.value = String(value);
474
+ await metadata.save();
475
+ } else {
476
+ if (!this.id || !metafield.id) throw new Error("Profile.addMetadata requires a persisted profile and metafield (missing id)");
477
+ await (await metadataCollection.create({
478
+ profileId: this.id,
479
+ metafieldId: metafield.id,
480
+ value: String(value)
481
+ })).save();
482
+ }
483
+ }
484
+ /**
485
+ * Get all metadata for this profile as key-value object
486
+ *
487
+ * @returns Object with metafield slugs as keys
488
+ */
489
+ async getMetadata() {
490
+ const { ProfileMetadataCollection } = await import("./ProfileMetadataCollection-loKVv_pj.js").then((n) => n.n);
491
+ return await (await ProfileMetadataCollection.create(this.options)).getMetadataObject(this.id);
492
+ }
493
+ /**
494
+ * Update multiple metadata values
495
+ *
496
+ * @param metadata - Object with metafield slugs as keys and values
497
+ */
498
+ async updateMetadata(metadata) {
499
+ for (const [metafieldSlug, value] of Object.entries(metadata)) await this.addMetadata(metafieldSlug, value);
500
+ }
501
+ /**
502
+ * Remove metadata by metafield slug
503
+ *
504
+ * @param metafieldSlug - The slug of the metafield to remove
505
+ */
506
+ async removeMetadata(metafieldSlug) {
507
+ const { ProfileMetafieldCollection } = await import("./ProfileMetafieldCollection-CACMY1hl.js").then((n) => n.n);
508
+ const { ProfileMetadataCollection } = await import("./ProfileMetadataCollection-loKVv_pj.js").then((n) => n.n);
509
+ const metafield = await (await ProfileMetafieldCollection.create(this.options)).getBySlug(metafieldSlug);
510
+ if (!metafield) throw new Error(`Metafield '${metafieldSlug}' not found`);
511
+ const existing = await (await ProfileMetadataCollection.create(this.options)).list({
512
+ where: {
513
+ profileId: this.id,
514
+ metafieldId: metafield.id
515
+ },
516
+ limit: 1
517
+ });
518
+ if (existing.length > 0) await existing[0].delete();
519
+ }
520
+ async getProfileAssetCollection() {
521
+ const { ProfileAssetCollection } = await import("./ProfileAssetCollection-CtlvXbGy.js").then((n) => n.n);
522
+ return ProfileAssetCollection.create({ db: this.db });
523
+ }
524
+ async getAssets(relationship) {
525
+ if (!this.id) return [];
526
+ const linkedAssets = await (await this.getProfileAssetCollection()).byLeft(this.id, relationship ? { relationship } : {});
527
+ return resolveOwnedAssetsById(this.db, linkedAssets.map((link) => link.assetId), this.tenantId);
528
+ }
529
+ async addAsset(asset, relationship = "attachment", sortOrder = 0) {
530
+ if (!this.id || !asset.id) throw new Error("Cannot associate unsaved profile or asset");
531
+ assertValidOwnedAssetRelationship(relationship);
532
+ assertValidOwnedAssetSortOrder(sortOrder);
533
+ await (await this.getProfileAssetCollection()).attach(this.id, asset.id, {
534
+ relationship,
535
+ sortOrder,
536
+ tenantId: this.tenantId
537
+ });
538
+ }
539
+ async removeAsset(assetId, relationship) {
540
+ if (!this.id) return;
541
+ await (await this.getProfileAssetCollection()).detach(this.id, assetId, relationship ? { relationship } : {});
542
+ }
543
+ /**
544
+ * Add a relationship to another profile
545
+ *
546
+ * @param toProfile - The target profile
547
+ * @param relationshipSlug - The type of relationship
548
+ * @param contextProfile - Optional context profile for tertiary relationships
549
+ */
550
+ async addRelationship(toProfile, relationshipSlug, contextProfile) {
551
+ const { ProfileRelationshipTypeCollection } = await import("./ProfileRelationshipTypeCollection-Bh4xJ4-I.js").then((n) => n.n);
552
+ const { ProfileRelationshipCollection } = await import("./ProfileRelationshipCollection-CzQKfvMy.js").then((n) => n.n);
553
+ const { ProfileRelationshipType } = await import("./ProfileRelationshipType-BjoaCt-u.js").then((n) => n.n);
554
+ const relationshipType = await (await ProfileRelationshipTypeCollection.create(this.options)).getBySlug(relationshipSlug);
555
+ if (!relationshipType) throw new Error(`Relationship type '${relationshipSlug}' not found`);
556
+ const relationshipCollection = await ProfileRelationshipCollection.create(this.options);
557
+ if (!await relationshipCollection.exists(this.id, toProfile.id, relationshipType.id)) {
558
+ await (await relationshipCollection.create({
559
+ fromProfileId: this.id ?? void 0,
560
+ toProfileId: toProfile.id ?? void 0,
561
+ typeId: relationshipType.id ?? void 0,
562
+ contextProfileId: contextProfile?.id ?? void 0
563
+ })).save();
564
+ if (relationshipType.reciprocal) {
565
+ const handler = ProfileRelationshipType.getReciprocalHandler(relationshipSlug);
566
+ if (handler) await handler(this, toProfile, contextProfile);
567
+ }
568
+ }
569
+ }
570
+ /**
571
+ * Get all relationships for this profile
572
+ *
573
+ * @param options - Filter options (typeSlug, direction)
574
+ * @returns Array of ProfileRelationship instances
575
+ */
576
+ async getRelationships(options) {
577
+ const { ProfileRelationshipCollection } = await import("./ProfileRelationshipCollection-CzQKfvMy.js").then((n) => n.n);
578
+ const { ProfileRelationshipTypeCollection } = await import("./ProfileRelationshipTypeCollection-Bh4xJ4-I.js").then((n) => n.n);
579
+ const relationshipCollection = await ProfileRelationshipCollection.create(this.options);
580
+ const direction = options?.direction || "all";
581
+ let typeId;
582
+ if (options?.typeSlug) typeId = (await (await ProfileRelationshipTypeCollection.create(this.options)).getBySlug(options.typeSlug))?.id ?? void 0;
583
+ if (direction === "from") return await relationshipCollection.getFromProfile(this.id, typeId);
584
+ else if (direction === "to") return await relationshipCollection.getToProfile(this.id, typeId);
585
+ else return await relationshipCollection.getForProfile(this.id, typeId);
586
+ }
587
+ /**
588
+ * Get related profiles
589
+ *
590
+ * @param relationshipSlug - Optional filter by relationship type slug
591
+ * @returns Array of related Profile instances
592
+ */
593
+ async getRelatedProfiles(relationshipSlug) {
594
+ const { ProfileCollection } = await Promise.resolve().then(() => ProfileCollection_exports);
595
+ const relationships = await this.getRelationships({
596
+ typeSlug: relationshipSlug,
597
+ direction: "all"
598
+ });
599
+ const profileCollection = await ProfileCollection.create(this.options);
600
+ const relatedProfiles = [];
601
+ const seenIds = /* @__PURE__ */ new Set();
602
+ for (const relationship of relationships) {
603
+ const fromId = String(relationship.fromProfileId);
604
+ const toId = String(relationship.toProfileId);
605
+ const otherId = fromId === String(this.id) ? toId : fromId;
606
+ if (!seenIds.has(otherId)) {
607
+ seenIds.add(otherId);
608
+ const profile = await profileCollection.get({ id: otherId });
609
+ if (profile) relatedProfiles.push(profile);
610
+ }
611
+ }
612
+ return relatedProfiles;
613
+ }
614
+ /**
615
+ * Remove a relationship to another profile
616
+ *
617
+ * @param toProfile - The target profile
618
+ * @param relationshipSlug - The type of relationship to remove
619
+ */
620
+ async removeRelationship(toProfile, relationshipSlug) {
621
+ const { ProfileRelationshipTypeCollection } = await import("./ProfileRelationshipTypeCollection-Bh4xJ4-I.js").then((n) => n.n);
622
+ const { ProfileRelationshipCollection } = await import("./ProfileRelationshipCollection-CzQKfvMy.js").then((n) => n.n);
623
+ const relationshipType = await (await ProfileRelationshipTypeCollection.create(this.options)).getBySlug(relationshipSlug);
624
+ if (!relationshipType) throw new Error(`Relationship type '${relationshipSlug}' not found`);
625
+ const relationshipCollection = await ProfileRelationshipCollection.create(this.options);
626
+ const relationships = await relationshipCollection.list({ where: {
627
+ fromProfileId: this.id,
628
+ toProfileId: toProfile.id,
629
+ typeId: relationshipType.id
630
+ } });
631
+ for (const relationship of relationships) await relationship.delete();
632
+ if (relationshipType.reciprocal) {
633
+ const inverseRelationships = await relationshipCollection.list({ where: {
634
+ fromProfileId: toProfile.id,
635
+ toProfileId: this.id,
636
+ typeId: relationshipType.id
637
+ } });
638
+ for (const relationship of inverseRelationships) await relationship.delete();
639
+ }
640
+ }
641
+ /**
642
+ * AI-powered: Generate a professional bio for this profile
643
+ *
644
+ * Uses the `smrtProfiles.profile.generateBio` prompt registered via
645
+ * `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
646
+ * overrides of the template, model, and parameters at runtime.
647
+ *
648
+ * @returns Generated bio text
649
+ */
650
+ async generateBio() {
651
+ const db = this.options.db ?? this.options.persistence;
652
+ const resolvedPrompt = await resolvePrompt(smrtProfilesGenerateBioPrompt.key, {
653
+ db,
654
+ tenantId: this.tenantId,
655
+ variables: {
656
+ profileName: this.name || "",
657
+ profileDescription: this.description || ""
658
+ }
659
+ });
660
+ return (await (await this.getAiClient()).message(resolvedPrompt.text, promptMessageOptions(resolvedPrompt.ai))).trim();
661
+ }
662
+ /**
663
+ * AI-powered: Check if profile matches criteria
664
+ *
665
+ * @param criteria - Criteria to match against
666
+ * @returns True if matches criteria
667
+ */
668
+ async matches(criteria) {
669
+ return await this.is(criteria);
670
+ }
671
+ /**
672
+ * Find profiles by metadata key-value pair
673
+ *
674
+ * @param metafieldSlug - The metafield slug to search
675
+ * @param value - The value to match
676
+ * @returns Array of matching profiles
677
+ */
678
+ static async findByMetadata(_metafieldSlug, _value) {
679
+ return [];
680
+ }
681
+ /**
682
+ * Find profiles by type slug
683
+ *
684
+ * @param typeSlug - The profile type slug
685
+ * @returns Array of matching profiles
686
+ */
687
+ static async findByType(_typeSlug) {
688
+ return [];
689
+ }
690
+ /**
691
+ * Find related profiles for a given profile
692
+ *
693
+ * @param profileId - The profile UUID
694
+ * @param relationshipSlug - Optional filter by relationship type
695
+ * @returns Array of related profiles
696
+ */
697
+ static async findRelated(_profileId, _relationshipSlug) {
698
+ return [];
699
+ }
700
+ /**
701
+ * Search profiles by email
702
+ *
703
+ * @param email - The email to search for
704
+ * @returns Profile or null if not found
705
+ */
706
+ static async searchByEmail(_email) {
707
+ return null;
708
+ }
709
+ /**
710
+ * Get all API keys for this profile
711
+ *
712
+ * @returns Array of API keys
713
+ */
714
+ async getApiKeys() {
715
+ const { ApiKeyCollection } = await import("./ApiKeyCollection-CrTjdMz8.js").then((n) => n.n);
716
+ return await (await ApiKeyCollection.create(this.options)).findByProfile(this.id);
717
+ }
718
+ /**
719
+ * Get active (non-revoked, non-expired) API keys for this profile
720
+ *
721
+ * @returns Array of active API keys
722
+ */
723
+ async getActiveApiKeys() {
724
+ const { ApiKeyCollection } = await import("./ApiKeyCollection-CrTjdMz8.js").then((n) => n.n);
725
+ return await (await ApiKeyCollection.create(this.options)).findActiveByProfile(this.id);
726
+ }
727
+ /**
728
+ * Generate a new API key for this profile
729
+ *
730
+ * @param options - Key options (name, scopes, expiration)
731
+ * @returns The generated key (plaintext) and ApiKey record
732
+ */
733
+ async generateApiKey(options) {
734
+ const { ApiKey } = await import("./ApiKey-Br9NGBnr.js").then((n) => n.n);
735
+ return await ApiKey.generate(this, {
736
+ ...options,
737
+ db: this.options?.db
738
+ });
739
+ }
740
+ /**
741
+ * Get all OIDC identities linked to this profile
742
+ *
743
+ * @returns Array of OIDC identity records
744
+ */
745
+ async getOidcIdentities() {
746
+ const { OidcIdentityCollection } = await import("./OidcIdentityCollection-Ce4yv8p0.js").then((n) => n.r);
747
+ return await (await OidcIdentityCollection.create(this.options)).findByProfile(this.id);
748
+ }
749
+ /**
750
+ * Reuse an existing exact OIDC identity for this unchanged Profile.
751
+ *
752
+ * @param oidcData - OIDC provider data
753
+ * @returns The linked OIDC identity record
754
+ * @deprecated New authentication links require owner-aware transactional
755
+ * provisioning. This compatibility helper may refresh a legacy Profile
756
+ * type, but refuses to create or rebind authority.
757
+ */
758
+ async linkOidcIdentity(oidcData) {
759
+ const { OidcIdentityCollection } = await import("./OidcIdentityCollection-Ce4yv8p0.js").then((n) => n.r);
760
+ return await (await OidcIdentityCollection.create(this.options)).linkToProfile(this, oidcData);
761
+ }
762
+ /**
763
+ * Get all Nostr identities linked to this profile
764
+ *
765
+ * @returns Array of Nostr identity records
766
+ */
767
+ async getNostrIdentities() {
768
+ const { NostrIdentityCollection } = await import("./NostrIdentityCollection-BRYUgt47.js").then((n) => n.n);
769
+ return await (await NostrIdentityCollection.create(this.options)).findByProfile(this.id);
770
+ }
771
+ /**
772
+ * Link a new Nostr identity to this profile
773
+ *
774
+ * @param nostrData - Nostr identity data (encrypted keypair)
775
+ * @returns The linked Nostr identity record
776
+ */
777
+ async linkNostrIdentity(nostrData) {
778
+ const { NostrIdentityCollection } = await import("./NostrIdentityCollection-BRYUgt47.js").then((n) => n.n);
779
+ return await (await NostrIdentityCollection.create(this.options)).linkToProfile(this, nostrData);
780
+ }
781
+ /**
782
+ * Get audit logs for actions performed by this profile
783
+ *
784
+ * @param limit - Maximum number of logs to return
785
+ * @returns Array of audit log entries
786
+ */
787
+ async getAuditLogs(limit = 50) {
788
+ const { AuditLogCollection } = await import("./AuditLogCollection-BBIEI6BD.js").then((n) => n.n);
789
+ return await (await AuditLogCollection.create(this.options)).getRecentActivity(this.id, limit);
790
+ }
791
+ /**
792
+ * Record an audit log entry for an action by this profile
793
+ *
794
+ * @param options - Audit log options
795
+ * @returns The created audit log entry
796
+ */
797
+ async recordAction(options) {
798
+ const { AuditLogCollection } = await import("./AuditLogCollection-BBIEI6BD.js").then((n) => n.n);
799
+ return await (await AuditLogCollection.create(this.options)).record({
800
+ profile: this,
801
+ ...options
802
+ });
803
+ }
804
+ };
805
+ __decorateClass([tenantId({ nullable: true })], Profile.prototype, "tenantId", 2);
806
+ __decorateClass([foreignKey("ProfileType", { required: true })], Profile.prototype, "typeId", 2);
807
+ __decorateClass([field({ unique: true })], Profile.prototype, "email", 2);
808
+ __decorateClass([field({
809
+ type: "text",
810
+ nullable: true,
811
+ indexed: true,
812
+ readonly: true
813
+ })], Profile.prototype, "emailKey", 2);
814
+ __decorateClass([field({ required: true })], Profile.prototype, "name", 2);
815
+ __decorateClass([oneToMany("ProfileMetadata")], Profile.prototype, "metadata", 2);
816
+ __decorateClass([oneToMany("ProfileRelationship", { foreignKey: "fromProfileId" })], Profile.prototype, "relationshipsFrom", 2);
817
+ __decorateClass([oneToMany("ProfileRelationship", { foreignKey: "toProfileId" })], Profile.prototype, "relationshipsTo", 2);
818
+ Profile = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
819
+ tableStrategy: "sti",
820
+ api: { include: [
821
+ "list",
822
+ "get",
823
+ "create",
824
+ "update",
825
+ "delete"
826
+ ] },
827
+ mcp: { include: [
828
+ "list",
829
+ "get",
830
+ "create",
831
+ "update"
832
+ ] },
833
+ cli: true
834
+ })], Profile);
835
+ //#endregion
836
+ //#region src/collections/OidcProfileEmailReservationCollection.ts
837
+ var OidcProfileEmailReservationCollection = class extends SmrtCollection {
838
+ static _itemClass = OidcProfileEmailReservation;
839
+ };
840
+ //#endregion
841
+ //#region src/collections/ProfileCollection.ts
842
+ var ProfileCollection_exports = /* @__PURE__ */ __exportAll({
843
+ CanonicalPersonProfileError: () => CanonicalPersonProfileError,
844
+ ProfileCollection: () => ProfileCollection
845
+ });
846
+ var PERSON_META_TYPES = /* @__PURE__ */ new Set(["@happyvertical/smrt-profiles:Person", "Person"]);
847
+ var CanonicalPersonProfileError = class extends Error {
848
+ constructor(code, message) {
849
+ super(message);
850
+ this.code = code;
851
+ this.name = "CanonicalPersonProfileError";
852
+ }
853
+ code;
854
+ };
855
+ var ProfileCollection = class extends SmrtCollection {
856
+ static _itemClass = Profile;
857
+ emailKeysReadyPromise = null;
858
+ /**
859
+ * Find a profile by email address
860
+ *
861
+ * @param email - The email address to search for
862
+ * @returns The matching profile or null
863
+ */
864
+ async findByEmail(email) {
865
+ const normalizedEmail = normalizeIdentityEmail(email);
866
+ return (await this.list({
867
+ where: { email: normalizedEmail },
868
+ limit: 1
869
+ }))[0] ?? null;
870
+ }
871
+ /**
872
+ * Resolve one unambiguous global Person by normalized email.
873
+ *
874
+ * Unlike `findByEmail()`, this identity-boundary helper deliberately reads
875
+ * across tenant scopes and fails closed when any matching row is
876
+ * tenant-scoped, is not a Person STI row, or when more than one row matches
877
+ * case-insensitively. It is intended for verified external identities.
878
+ */
879
+ async findUniqueGlobalPersonByEmail(email) {
880
+ const normalizedEmail = normalizeIdentityEmail(email);
881
+ const rows = await this.loadCanonicalEmailRows(normalizedEmail);
882
+ if (rows.length === 0) return null;
883
+ this.assertCanonicalRows(rows, normalizedEmail);
884
+ return this.requireHydratedProfile(readRequiredString(rows[0], "id"));
885
+ }
886
+ /**
887
+ * Validate and hydrate a supplied canonical Profile.
888
+ *
889
+ * The Profile must be the sole case-insensitive match for its stored email.
890
+ * When `email` is provided, that address must also match the stored email.
891
+ */
892
+ async requireCanonicalGlobalPerson(profileId, email) {
893
+ const db = this.requireDatabase();
894
+ await this.ensureEmailKeysReady();
895
+ const row = (await withSystemContext(() => db.query(`SELECT id, tenant_id, _meta_type, email, email_key
896
+ FROM profiles
897
+ WHERE id = ?
898
+ LIMIT 1`, profileId))).rows[0];
899
+ if (!row) throw new CanonicalPersonProfileError("missing_profile", `Profile ${profileId} does not exist.`);
900
+ this.assertRowEmailKeyCurrent(row);
901
+ this.assertCanonicalRows([row]);
902
+ const storedEmail = readString(row, "email");
903
+ const normalizedEmail = email !== void 0 ? normalizeIdentityEmail(email) : storedEmail ? normalizeIdentityEmail(storedEmail) : void 0;
904
+ if (normalizedEmail !== void 0) {
905
+ const matches = await this.loadCanonicalEmailRows(normalizedEmail);
906
+ this.assertCanonicalRows(matches, normalizedEmail);
907
+ if (matches.length !== 1 || readRequiredString(matches[0], "id") !== profileId) throw new CanonicalPersonProfileError("email_mismatch", `Profile ${profileId} is not the unique global Person for ${normalizedEmail}.`);
908
+ }
909
+ return this.requireHydratedProfile(profileId);
910
+ }
911
+ /**
912
+ * Reserve the normalized email for a previously validated canonical Person.
913
+ *
914
+ * The unique stored key turns a concurrent OIDC first-login race into a
915
+ * retryable database conflict. Legacy Profiles remain unaffected until an
916
+ * identity boundary safely claims them.
917
+ */
918
+ async reserveCanonicalIdentityEmail(profileId, email) {
919
+ const profile = await this.requireCanonicalGlobalPerson(profileId, email);
920
+ const normalizedEmail = email !== void 0 ? normalizeIdentityEmail(email) : profile.email?.trim() ? normalizeIdentityEmail(profile.email) : null;
921
+ const db = this.requireDatabase();
922
+ const reservations = await OidcProfileEmailReservationCollection.create({ db });
923
+ const byProfile = await reservations.list({
924
+ where: { profileId },
925
+ limit: 2
926
+ });
927
+ if (byProfile.length > 1) throw new CanonicalPersonProfileError("ambiguous_email", `Multiple canonical identity reservations exist for Profile ${profileId}.`);
928
+ if (normalizedEmail === null) {
929
+ await byProfile[0]?.delete();
930
+ return profile;
931
+ }
932
+ const byEmail = await reservations.list({
933
+ where: { emailKey: normalizedEmail },
934
+ limit: 2
935
+ });
936
+ const activeEmailReservations = [];
937
+ for (const reservation of byEmail) if (reservation.profileId !== profileId && await this.isStaleEmailReservation(reservation, normalizedEmail)) await reservation.delete();
938
+ else activeEmailReservations.push(reservation);
939
+ if (activeEmailReservations.find((reservation) => reservation.profileId !== profileId)) throw new CanonicalPersonProfileError("reservation_conflict", `${normalizedEmail} is already reserved for a different canonical Person.`);
940
+ if (activeEmailReservations.length > 1) throw new CanonicalPersonProfileError("ambiguous_email", `Multiple canonical identity reservations exist for ${normalizedEmail}.`);
941
+ const existing = byProfile[0];
942
+ if (existing) {
943
+ if (existing.emailKey !== normalizedEmail) {
944
+ existing.emailKey = normalizedEmail;
945
+ await saveIdentityEmailReservation(existing, normalizedEmail);
946
+ }
947
+ return profile;
948
+ }
949
+ if (!activeEmailReservations[0]) {
950
+ const reservation = new OidcProfileEmailReservation({
951
+ db,
952
+ emailKey: normalizedEmail,
953
+ profileId
954
+ });
955
+ await reservation.initialize();
956
+ await saveIdentityEmailReservation(reservation, normalizedEmail);
957
+ }
958
+ return profile;
959
+ }
960
+ /**
961
+ * Find profiles by type slug
962
+ *
963
+ * @param typeSlug - The profile type slug to filter by
964
+ * @returns Array of matching profiles
965
+ */
966
+ async findByType(typeSlug) {
967
+ const allProfiles = await this.list({});
968
+ const filtered = [];
969
+ for (const profile of allProfiles) if (await profile.getTypeSlug() === typeSlug) filtered.push(profile);
970
+ return filtered;
971
+ }
972
+ requireDatabase() {
973
+ const db = this.options.db;
974
+ if (!db || typeof db !== "object" || !("query" in db)) throw new Error("Canonical Profile identity resolution requires an initialized database.");
975
+ return db;
976
+ }
977
+ async loadCanonicalEmailRows(normalizedEmail) {
978
+ const db = this.requireDatabase();
979
+ await this.ensureEmailKeysReady();
980
+ const rows = (await withSystemContext(async () => {
981
+ return db.query(`SELECT id, tenant_id, _meta_type, email, email_key
982
+ FROM profiles
983
+ WHERE email_key = ?
984
+ ORDER BY created_at ASC, id ASC
985
+ LIMIT 2`, normalizedEmail);
986
+ })).rows;
987
+ for (const row of rows) this.assertRowEmailKeyCurrent(row, normalizedEmail);
988
+ return rows;
989
+ }
990
+ /** Require the deploy-time backfill marker before indexed identity reads. */
991
+ async ensureEmailKeysReady() {
992
+ if (!this.emailKeysReadyPromise) this.emailKeysReadyPromise = this.checkEmailKeysReady().catch((error) => {
993
+ this.emailKeysReadyPromise = null;
994
+ throw error;
995
+ });
996
+ return this.emailKeysReadyPromise;
997
+ }
998
+ async checkEmailKeysReady() {
999
+ if (await new BackfillTracker({ db: this.requireDatabase() }).isApplied("@happyvertical/smrt-profiles:profile-email-keys:v1")) return;
1000
+ throw new CanonicalPersonProfileError("email_key_backfill_required", "Profile identity email keys are not marked ready; run backfillProfileEmailKeys() before identity lookup.");
1001
+ }
1002
+ async isStaleEmailReservation(reservation, reservedEmail) {
1003
+ const profileId = reservation.profileId;
1004
+ if (!profileId) return true;
1005
+ const row = (await this.requireDatabase().query("SELECT email FROM profiles WHERE id = ? LIMIT 1", profileId)).rows[0];
1006
+ if (!row) return true;
1007
+ const email = readString(row, "email");
1008
+ return !email?.trim() || normalizeIdentityEmail(email) !== reservedEmail;
1009
+ }
1010
+ assertRowEmailKeyCurrent(row, normalizedEmail) {
1011
+ const email = readString(row, "email") ?? "";
1012
+ const expectedKey = email.trim() ? normalizeIdentityEmail(email) : null;
1013
+ if ((readString(row, "email_key") ?? null) !== expectedKey || normalizedEmail !== void 0 && expectedKey !== normalizedEmail) throw new CanonicalPersonProfileError("email_key_backfill_required", `Profile ${String(row.id ?? "<unknown>")} has a missing or stale identity email key; run backfillProfileEmailKeys() before identity lookup.`);
1014
+ }
1015
+ assertCanonicalRows(rows, normalizedEmail) {
1016
+ const tenantScoped = rows.find((row) => readString(row, "tenant_id"));
1017
+ if (tenantScoped) throw new CanonicalPersonProfileError("tenant_scoped", `Profile ${readRequiredString(tenantScoped, "id")} for ${normalizedEmail ?? "the supplied identity"} is tenant-scoped.`);
1018
+ const nonPerson = rows.find((row) => !PERSON_META_TYPES.has(readString(row, "_meta_type") ?? ""));
1019
+ if (nonPerson) throw new CanonicalPersonProfileError("non_person", `Profile ${readRequiredString(nonPerson, "id")} for ${normalizedEmail ?? "the supplied identity"} is not a Person.`);
1020
+ if (rows.length > 1) throw new CanonicalPersonProfileError("ambiguous_email", `Multiple Profiles use ${normalizedEmail ?? "the supplied identity email"}.`);
1021
+ }
1022
+ async requireHydratedProfile(profileId) {
1023
+ const profile = await withSystemContext(() => this.get({ id: profileId }));
1024
+ if (!profile) throw new CanonicalPersonProfileError("missing_profile", `Profile ${profileId} could not be hydrated.`);
1025
+ return profile;
1026
+ }
1027
+ /**
1028
+ * Batch get metadata for multiple profiles
1029
+ *
1030
+ * @param profileIds - Array of profile UUIDs
1031
+ * @returns Map of profile ID to metadata object
1032
+ */
1033
+ async batchGetMetadata(profileIds) {
1034
+ const result = /* @__PURE__ */ new Map();
1035
+ for (const profileId of profileIds) {
1036
+ const profile = await this.get({ id: profileId });
1037
+ if (profile) {
1038
+ const metadata = await profile.getMetadata();
1039
+ result.set(profileId, metadata);
1040
+ }
1041
+ }
1042
+ return result;
1043
+ }
1044
+ /**
1045
+ * Batch update metadata for multiple profiles
1046
+ *
1047
+ * @param updates - Array of { profileId, data } objects
1048
+ */
1049
+ async batchUpdateMetadata(updates) {
1050
+ for (const update of updates) {
1051
+ const profile = await this.get({ id: update.profileId });
1052
+ if (profile) await profile.updateMetadata(update.data);
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Find related profiles for a given profile
1057
+ *
1058
+ * @param profileId - The profile UUID
1059
+ * @param relationshipSlug - Optional filter by relationship type
1060
+ * @returns Array of related profiles
1061
+ */
1062
+ async findRelated(profileId, relationshipSlug) {
1063
+ const profile = await this.get({ id: profileId });
1064
+ if (!profile) return [];
1065
+ return await profile.getRelatedProfiles(relationshipSlug);
1066
+ }
1067
+ async getAssets(profileId, relationship) {
1068
+ return getOwnedAssetsFromCollection(this, profileId, relationship);
1069
+ }
1070
+ async addAsset(profileId, asset, relationship = "attachment", sortOrder = 0) {
1071
+ await addOwnedAssetFromCollection(this, "Profile", profileId, asset, relationship, sortOrder);
1072
+ }
1073
+ async removeAsset(profileId, assetId, relationship) {
1074
+ await removeOwnedAssetFromCollection(this, "Profile", profileId, assetId, relationship);
1075
+ }
1076
+ /**
1077
+ * Get the relationship network for a profile up to a maximum depth
1078
+ *
1079
+ * @param profileId - The starting profile UUID
1080
+ * @param options - Configuration options
1081
+ * @returns Map of profile ID to depth level
1082
+ */
1083
+ async getRelationshipNetwork(profileId, options = {}) {
1084
+ const maxDepth = options.maxDepth || 2;
1085
+ const network = /* @__PURE__ */ new Map();
1086
+ const visited = /* @__PURE__ */ new Set();
1087
+ const queue = [{
1088
+ id: profileId,
1089
+ depth: 0
1090
+ }];
1091
+ while (queue.length > 0) {
1092
+ const current = queue.shift();
1093
+ if (!current) break;
1094
+ if (visited.has(current.id) || current.depth > maxDepth) continue;
1095
+ visited.add(current.id);
1096
+ network.set(current.id, current.depth);
1097
+ if (current.depth < maxDepth) {
1098
+ const related = await this.findRelated(current.id);
1099
+ for (const profile of related) if (profile.id && !visited.has(profile.id)) queue.push({
1100
+ id: profile.id,
1101
+ depth: current.depth + 1
1102
+ });
1103
+ }
1104
+ }
1105
+ return network;
1106
+ }
1107
+ /**
1108
+ * Find all profiles belonging to a specific tenant
1109
+ *
1110
+ * @param tenantId - The tenant UUID to filter by
1111
+ * @returns Array of profiles for this tenant
1112
+ */
1113
+ async findByTenant(tenantId) {
1114
+ return this.list({ where: { tenantId } });
1115
+ }
1116
+ /**
1117
+ * Find all global profiles (no tenant association).
1118
+ *
1119
+ * Routes through the shared tenant-global helper so it does not throw under
1120
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1121
+ * flagged as an isolation violation). (#1600)
1122
+ *
1123
+ * @returns Array of profiles with null tenantId
1124
+ */
1125
+ async findGlobal() {
1126
+ return queryGlobal(this);
1127
+ }
1128
+ /**
1129
+ * Find profiles belonging to a tenant plus all global profiles.
1130
+ *
1131
+ * Fails closed if an active tenant context requests a different tenant's
1132
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1133
+ *
1134
+ * @param tenantId - The tenant UUID to include
1135
+ * @returns Array of tenant-specific and global profiles
1136
+ */
1137
+ async findWithGlobals(tenantId) {
1138
+ return queryWithGlobals(this, tenantId, "Profile.findWithGlobals");
1139
+ }
1140
+ };
1141
+ async function saveIdentityEmailReservation(reservation, normalizedEmail) {
1142
+ try {
1143
+ await reservation.save();
1144
+ } catch (error) {
1145
+ if (error instanceof ValidationError && error.code === "VALIDATION_UNIQUE_CONSTRAINT" || isOidcAbortedTransactionError(error)) throw new CanonicalPersonProfileError("reservation_conflict", `${normalizedEmail} was concurrently reserved for another canonical Person.`);
1146
+ throw error;
1147
+ }
1148
+ }
1149
+ function readString(row, key) {
1150
+ const value = row[key];
1151
+ return typeof value === "string" && value.length > 0 ? value : null;
1152
+ }
1153
+ function readRequiredString(row, key) {
1154
+ const value = readString(row, key);
1155
+ if (!value) throw new Error(`Canonical Profile identity row is missing ${key}.`);
1156
+ return value;
1157
+ }
1158
+ //#endregion
1159
+ export { ProfileType as a, backfillProfileEmailKeys as c, isOidcProvisioningRaceConflict as d, saveOidcRaceArbiter as f, smrtProfilesGenerateBioPrompt as h, Profile as i, coordinateOidcProvisioning as l, promptMessageOptions as m, ProfileCollection as n, ProfileType_exports as o, normalizeIdentityEmail as p, ProfileCollection_exports as r, PROFILE_EMAIL_KEY_BACKFILL_NAME as s, CanonicalPersonProfileError as t, isOidcAbortedTransactionError as u };
1160
+
1161
+ //# sourceMappingURL=ProfileCollection-Bix9yAiy.js.map