@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.
- package/AGENTS.md +69 -5
- package/README.md +101 -50
- package/dist/chunks/{ApiKey-BrNyaaZN.js → ApiKey-Br9NGBnr.js} +3 -3
- package/dist/chunks/{ApiKey-BrNyaaZN.js.map → ApiKey-Br9NGBnr.js.map} +1 -1
- package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js → ApiKeyCollection-CrTjdMz8.js} +3 -3
- package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js.map → ApiKeyCollection-CrTjdMz8.js.map} +1 -1
- package/dist/chunks/{AuditLogCollection-CnIYU0aK.js → AuditLogCollection-BBIEI6BD.js} +2 -2
- package/dist/chunks/{AuditLogCollection-CnIYU0aK.js.map → AuditLogCollection-BBIEI6BD.js.map} +1 -1
- package/dist/chunks/{NostrIdentityCollection-CG6HGf6c.js → NostrIdentity-DF_a_WZk.js} +6 -125
- package/dist/chunks/NostrIdentity-DF_a_WZk.js.map +1 -0
- package/dist/chunks/NostrIdentityCollection-BRYUgt47.js +124 -0
- package/dist/chunks/NostrIdentityCollection-BRYUgt47.js.map +1 -0
- package/dist/chunks/OidcIdentity-ChkiT6hg.js +111 -0
- package/dist/chunks/OidcIdentity-ChkiT6hg.js.map +1 -0
- package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js +76 -0
- package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js.map +1 -0
- package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js → ProfileAssetCollection-CtlvXbGy.js} +3 -3
- package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js.map → ProfileAssetCollection-CtlvXbGy.js.map} +1 -1
- package/dist/chunks/ProfileCollection-Bix9yAiy.js +1161 -0
- package/dist/chunks/ProfileCollection-Bix9yAiy.js.map +1 -0
- package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js → ProfileMetadataCollection-loKVv_pj.js} +2 -2
- package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js.map → ProfileMetadataCollection-loKVv_pj.js.map} +1 -1
- package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js → ProfileMetafieldCollection-CACMY1hl.js} +2 -2
- package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js.map → ProfileMetafieldCollection-CACMY1hl.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js → ProfileRelationshipCollection-CzQKfvMy.js} +5 -5
- package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js.map → ProfileRelationshipCollection-CzQKfvMy.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js → ProfileRelationshipTermCollection-DzmU48P6.js} +2 -2
- package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js.map → ProfileRelationshipTermCollection-DzmU48P6.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js → ProfileRelationshipType-BjoaCt-u.js} +2 -2
- package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js.map → ProfileRelationshipType-BjoaCt-u.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js → ProfileRelationshipTypeCollection-Bh4xJ4-I.js} +3 -3
- package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js.map → ProfileRelationshipTypeCollection-Bh4xJ4-I.js.map} +1 -1
- package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js +138 -0
- package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js.map +1 -0
- package/dist/chunks/resolveIdentity-_h_2sGqJ.js +260 -0
- package/dist/chunks/resolveIdentity-_h_2sGqJ.js.map +1 -0
- package/dist/index.d.ts +127 -15
- package/dist/index.js +30 -273
- package/dist/index.js.map +1 -1
- package/dist/internal/oidc-provisioning.d.ts +17 -0
- package/dist/internal/oidc-provisioning.js +13 -0
- package/dist/internal/oidc-provisioning.js.map +1 -0
- package/dist/manifest.json +431 -46
- package/dist/oidc-provisioning.d.ts +83 -0
- package/dist/smrt-knowledge.json +105 -43
- package/dist/types.d.ts +26 -2
- package/dist/utils.d.ts +26 -2
- package/dist/utils.js +4 -4
- package/package.json +10 -6
- package/dist/chunks/NostrIdentityCollection-CG6HGf6c.js.map +0 -1
- package/dist/chunks/OidcIdentityCollection-DiLneB7J.js +0 -158
- package/dist/chunks/OidcIdentityCollection-DiLneB7J.js.map +0 -1
- package/dist/chunks/ProfileCollection-BOMOloTL.js +0 -641
- 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
|