@odla-ai/chapter 0.51.0 → 0.52.1

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.
@@ -190,11 +190,11 @@ async function clerkVerifiedPrimaryEmail(db, userId) {
190
190
 
191
191
  // src/crm-person-projection.ts
192
192
  var import_crm = require("@odla-ai/crm");
193
- async function ensureProjectedPerson(deps2, input, options = {}) {
193
+ async function ensureProjectedPerson(deps, input, options = {}) {
194
194
  const email = String(input.email ?? "").trim().toLowerCase();
195
195
  if (!email) throw new Error("crm_projection_email_required");
196
196
  const find = async () => {
197
- const records = (await deps2.db.query({ crm_record: { $: {
197
+ const records = (await deps.db.query({ crm_record: { $: {
198
198
  where: { type: "person", primaryEmail: email },
199
199
  limit: 2
200
200
  } } })).crm_record ?? [];
@@ -207,7 +207,7 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
207
207
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
208
208
  const id6 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
209
209
  try {
210
- const created = await (0, import_crm.createRecord)({ ...deps2, db: deps2.db }, {
210
+ const created = await (0, import_crm.createRecord)({ ...deps, db: deps.db }, {
211
211
  id: id6,
212
212
  type: "person",
213
213
  input: { ...input, email },
@@ -223,14 +223,14 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
223
223
  }
224
224
 
225
225
  // src/crm-account-projection.ts
226
- async function syncAccountToCrm(deps2, userId) {
227
- const user = (await deps2.db.query({ $users: { $: { where: { id: userId }, limit: 1 } } })).$users?.[0];
226
+ async function syncAccountToCrm(deps, userId) {
227
+ const user = (await deps.db.query({ $users: { $: { where: { id: userId }, limit: 1 } } })).$users?.[0];
228
228
  if (!user || user.deleted === true || typeof user.email !== "string" || !user.email.trim()) return null;
229
- const linked = (await deps2.db.query({ crm_record: { $: { where: { type: "person", clerkUserId: userId }, limit: 2 } } })).crm_record ?? [];
229
+ const linked = (await deps.db.query({ crm_record: { $: { where: { type: "person", clerkUserId: userId }, limit: 2 } } })).crm_record ?? [];
230
230
  if (linked.length > 1) throw new Error("crm_projection_identity_ambiguous");
231
231
  if (linked.length === 1) return String(linked[0].id);
232
232
  const email = user.email.trim().toLowerCase();
233
- const peers = (await deps2.db.query({ $users: { $: { where: { email }, limit: 2 } } })).$users ?? [];
233
+ const peers = (await deps.db.query({ $users: { $: { where: { email }, limit: 2 } } })).$users ?? [];
234
234
  if (peers.length !== 1 || peers[0]?.id !== userId) throw new Error("crm_projection_identity_ambiguous");
235
235
  const userGuard = {
236
236
  ns: "$users",
@@ -239,14 +239,14 @@ async function syncAccountToCrm(deps2, userId) {
239
239
  equals: { email: user.email, ...user.deleted !== void 0 ? { deleted: user.deleted } : {} },
240
240
  ...user.deleted === void 0 ? { absent: ["deleted"] } : {}
241
241
  };
242
- const { record: record4 } = await ensureProjectedPerson(deps2, {
242
+ const { record: record4 } = await ensureProjectedPerson(deps, {
243
243
  name: typeof user.name === "string" && user.name.trim() ? user.name : email,
244
244
  email
245
245
  }, { stage: null, guards: [userGuard], emailConsent: { state: "unsubscribed", source: "account-no-marketing-consent" } });
246
246
  if (record4.clerkUserId === userId) return String(record4.id);
247
247
  if (record4.clerkUserId != null) throw new Error("crm_projection_identity_conflict");
248
248
  try {
249
- await deps2.db.transact([{ t: "update", ns: "crm_record", id: String(record4.id), attrs: { clerkUserId: userId } }], {
249
+ await deps.db.transact([{ t: "update", ns: "crm_record", id: String(record4.id), attrs: { clerkUserId: userId } }], {
250
250
  guards: [userGuard, {
251
251
  ns: "crm_record",
252
252
  id: String(record4.id),
@@ -262,12 +262,37 @@ async function syncAccountToCrm(deps2, userId) {
262
262
  });
263
263
  } catch (error) {
264
264
  if (error.code !== "transact_guard_failed") throw error;
265
- const current = (await deps2.db.query({ crm_record: { $: { where: { id: record4.id }, limit: 1 } } })).crm_record?.[0];
265
+ const current = (await deps.db.query({ crm_record: { $: { where: { id: record4.id }, limit: 1 } } })).crm_record?.[0];
266
266
  if (current?.clerkUserId !== userId) throw error;
267
267
  }
268
268
  return String(record4.id);
269
269
  }
270
270
 
271
+ // src/worker-alerts.ts
272
+ var consoleErrorRecorder = (error, report) => {
273
+ console.error("chapter_alert", JSON.stringify({ message: error.message, ...report }));
274
+ };
275
+ function reportChapterAlert(ctx, error, report) {
276
+ (ctx?.recordError ?? consoleErrorRecorder)(error, report);
277
+ }
278
+ var ACTIVATION_LIMIT = 50;
279
+ function networkCommercialAlerts(ctx, route) {
280
+ return {
281
+ exceptionsActivated(activated, activeCount) {
282
+ if (activated.length === 0) return;
283
+ reportChapterAlert(ctx, new Error("network commercial exception activated"), {
284
+ code: "network_commercial_exception_activated",
285
+ route,
286
+ attributes: {
287
+ "odla.commercial.active_exceptions": activeCount,
288
+ "odla.commercial.activated_exceptions": activated.length
289
+ },
290
+ artifacts: { exceptions: activated.slice(0, ACTIVATION_LIMIT).map(({ eventId, kind }) => ({ eventId, kind })) }
291
+ });
292
+ }
293
+ };
294
+ }
295
+
271
296
  // src/worker-context.ts
272
297
  var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
273
298
  function createWorkerContext(options) {
@@ -377,6 +402,7 @@ function createWorkerContext(options) {
377
402
  requirePaymentQuote: options.requirePaymentQuote === true,
378
403
  membershipAuthority: options.membershipAuthority,
379
404
  membershipAuthorityOperator: options.membershipAuthorityOperator,
405
+ recordError: options.recordError ?? consoleErrorRecorder,
380
406
  getPublicConfig,
381
407
  verifyUser,
382
408
  makeDb,
@@ -870,6 +896,17 @@ function projectNetworkCommercialEvents(input, sources) {
870
896
  };
871
897
  }
872
898
 
899
+ // src/network-commercial-fact-identity.ts
900
+ var stable = (value) => JSON.stringify(value, function(_key, item) {
901
+ return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
902
+ });
903
+ function sameNetworkCommercialFact(stored, incoming) {
904
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)) return false;
905
+ const comparable = { ...incoming };
906
+ if (!stored.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
907
+ return stable(comparable) === stable(stored);
908
+ }
909
+
873
910
  // src/network-commercial-store.ts
874
911
  var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
875
912
  var projectionId = (kind, sourceKey, providerId3) => `network-commercial:${kind}:${sourceKey}:${providerId3}`;
@@ -881,6 +918,7 @@ var materializedNamespaces = [
881
918
  "networkCommercialRefunds",
882
919
  "networkCommercialSeats"
883
920
  ];
921
+ var RESOLUTION_ATTRS = ["resolvedDigest", "reviewedDigest", "resolvedBy", "reviewId"];
884
922
  async function rows(db, ns) {
885
923
  const all = [];
886
924
  const pageSize = 500;
@@ -930,13 +968,14 @@ function projectionOps(projection, now) {
930
968
  }
931
969
  })));
932
970
  }
933
- async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
971
+ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now(), alerts) {
934
972
  const projection = await loadNetworkCommercialProjection(db, sources);
935
973
  const [existing, ...materialized] = await Promise.all([
936
974
  rows(db, "networkCommercialExceptions"),
937
975
  ...materializedNamespaces.map((ns) => rows(db, ns))
938
976
  ]);
939
977
  const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
978
+ const wasActive = new Set(existing.filter((row2) => row2.status === "active").map((row2) => String(row2.id)));
940
979
  const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
941
980
  const ops = [
942
981
  ...projectionOps(projection, now),
@@ -962,27 +1001,80 @@ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now =
962
1001
  }
963
1002
  }))
964
1003
  ];
965
- if (ops.length > 0) await db.transact(ops, { mutationId });
1004
+ if (ops.length === 0) return projection;
1005
+ const write = await db.transact(ops, { mutationId });
1006
+ if (!write.duplicate) {
1007
+ alerts?.exceptionsActivated(
1008
+ projection.exceptions.filter((item) => !wasActive.has(exceptionId(item.eventId, item.kind))),
1009
+ projection.exceptions.length
1010
+ );
1011
+ }
966
1012
  return projection;
967
1013
  }
968
- async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(), legacyDigest) {
969
- const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
970
- const existing = found.networkCommercialEvents?.[0];
971
- if (existing) {
972
- const projection2 = await loadNetworkCommercialProjection(db, sources);
973
- if (existing.digest === fact.digest || existing.digest === legacyDigest) return { status: "replayed", projection: projection2 };
974
- const id7 = exceptionId(fact.eventId, "collision");
975
- await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
976
- id: id7,
1014
+ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
1015
+ const id6 = exceptionId(fact.eventId, "collision");
1016
+ const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } } })).networkCommercialExceptions?.[0];
1017
+ const exact = stored.digest === fact.digest || stored.digest === legacyDigest;
1018
+ const canonical3 = !exact && sameNetworkCommercialFact(stored.fact, fact);
1019
+ if (exact || canonical3) {
1020
+ const recorded = exception?.incomingFact;
1021
+ const resolvable = canonical3 && exception?.status === "active" && (recorded === void 0 || sameNetworkCommercialFact(stored.fact, recorded));
1022
+ if (!resolvable) return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
1023
+ const reviewId = crypto.randomUUID();
1024
+ await db.transact([
1025
+ { t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
1026
+ id: reviewId,
1027
+ eventId: fact.eventId,
1028
+ actorUserId: "system",
1029
+ reviewedAt: now,
1030
+ previousDigest: String(stored.digest),
1031
+ providerDigest: fact.digest,
1032
+ scope: "network",
1033
+ previousState: exception,
1034
+ action: "canonical_replay",
1035
+ reason: "a resend matched the retained canonical fact"
1036
+ } },
1037
+ { t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
1038
+ status: "resolved",
1039
+ resolvedDigest: stored.digest,
1040
+ reviewedDigest: fact.digest,
1041
+ resolvedBy: "system",
1042
+ reviewId,
1043
+ updatedAt: now
1044
+ } }
1045
+ ], { mutationId: `network-commercial-resolve:${fact.eventId}:${fact.digest}`, guards: [
1046
+ { ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
1047
+ ] });
1048
+ const projection2 = await rebuildNetworkCommercialProjection(db, sources, `network-commercial-resolve-rebuild:${fact.eventId}:${fact.digest}`, now, alerts);
1049
+ return { status: "replayed", resolvedCollision: true, projection: projection2 };
1050
+ }
1051
+ if (exception?.status === "resolved" && exception.reviewedDigest === fact.digest) {
1052
+ return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
1053
+ }
1054
+ const stale = RESOLUTION_ATTRS.filter((attr3) => exception !== void 0 && Object.hasOwn(exception, attr3));
1055
+ await db.transact([
1056
+ ...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id: id6, attrs: stale }] : [],
1057
+ { t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
1058
+ id: id6,
977
1059
  eventId: fact.eventId,
978
1060
  kind: "collision",
979
1061
  status: "active",
980
1062
  incomingDigest: fact.digest,
1063
+ incomingFact: fact,
981
1064
  detail: { eventType: fact.eventType, objectId: fact.objectId },
982
1065
  updatedAt: now
983
- } }]);
984
- return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
1066
+ } }
1067
+ ]);
1068
+ const projection = await loadNetworkCommercialProjection(db, sources);
1069
+ if (exception?.status !== "active") {
1070
+ alerts?.exceptionsActivated([{ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } }], projection.exceptions.length);
985
1071
  }
1072
+ return { status: "collision", projection };
1073
+ }
1074
+ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(), legacyDigest, alerts) {
1075
+ const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
1076
+ const existing = found.networkCommercialEvents?.[0];
1077
+ if (existing) return collide(db, sources, existing, fact, now, legacyDigest, alerts);
986
1078
  const id6 = eventRowId(fact.eventId);
987
1079
  let lostRace = false;
988
1080
  let duplicate = false;
@@ -1008,25 +1100,15 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
1008
1100
  if (duplicate || lostRace) {
1009
1101
  const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
1010
1102
  const row2 = raced.networkCommercialEvents?.[0];
1011
- const projection2 = await loadNetworkCommercialProjection(db, sources);
1012
- if (row2?.digest === fact.digest || row2?.digest === legacyDigest) return { status: "replayed", projection: projection2 };
1013
- const id7 = exceptionId(fact.eventId, "collision");
1014
- await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
1015
- id: id7,
1016
- eventId: fact.eventId,
1017
- kind: "collision",
1018
- status: "active",
1019
- incomingDigest: fact.digest,
1020
- detail: { eventType: fact.eventType, objectId: fact.objectId },
1021
- updatedAt: now
1022
- } }]);
1023
- return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
1103
+ if (!row2) throw new Error("network commercial event vanished after a lost race");
1104
+ return collide(db, sources, row2, fact, now, legacyDigest, alerts);
1024
1105
  }
1025
1106
  const projection = await rebuildNetworkCommercialProjection(
1026
1107
  db,
1027
1108
  sources,
1028
1109
  `network-commercial-project:${fact.eventId}:${fact.digest}`,
1029
- now
1110
+ now,
1111
+ alerts
1030
1112
  );
1031
1113
  return { status: "stored", projection };
1032
1114
  }
@@ -1041,10 +1123,7 @@ async function readStripeRecoveryEvent(db, eventId) {
1041
1123
  const rawBody = JSON.stringify(response.body);
1042
1124
  return { event: response.body, rawBody, digest: await stripeEventDigest(rawBody) };
1043
1125
  }
1044
- var stable = (value) => JSON.stringify(value, function(_key, item) {
1045
- return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
1046
- });
1047
- async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now()) {
1126
+ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
1048
1127
  const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
1049
1128
  const rows5 = await db.query({
1050
1129
  networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
@@ -1054,9 +1133,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1054
1133
  if (!stored) throw new Error("stored event not found");
1055
1134
  const provider = await readStripeRecoveryEvent(db, eventId);
1056
1135
  const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
1057
- const comparable = { ...fact };
1058
- if (!stored.fact?.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
1059
- if (stable(comparable) !== stable(stored.fact)) throw new Error("Stripe facts differ; collision requires investigation");
1136
+ if (!sameNetworkCommercialFact(stored.fact, fact)) throw new Error("Stripe facts differ; collision requires investigation");
1060
1137
  const id6 = crypto.randomUUID();
1061
1138
  await db.transact([
1062
1139
  { t: "update", ns: "paymentRecoveryReviews", id: id6, attrs: {
@@ -1064,6 +1141,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1064
1141
  eventId,
1065
1142
  actorUserId,
1066
1143
  reviewedAt: now,
1144
+ action: "recover",
1067
1145
  previousDigest: stored.digest,
1068
1146
  providerDigest: provider.digest,
1069
1147
  scope: "network",
@@ -1074,7 +1152,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1074
1152
  t: "update",
1075
1153
  ns: "networkCommercialExceptions",
1076
1154
  id: exceptionId2,
1077
- attrs: { status: "resolved", resolvedDigest: provider.digest, updatedAt: now }
1155
+ attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId: id6, updatedAt: now }
1078
1156
  }] : []
1079
1157
  ], { mutationId: `payment-recovery:${id6}`, guards: [
1080
1158
  { ns: "networkCommercialEvents", id: eventRowId2, exists: true, equals: { digest: stored.digest } },
@@ -1085,7 +1163,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1085
1163
  ...exception ? { equals: { status: exception.status, updatedAt: exception.updatedAt } } : {}
1086
1164
  }
1087
1165
  ] });
1088
- await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now);
1166
+ await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now, alerts);
1089
1167
  return { recovered: true, eventId, reviewId: id6 };
1090
1168
  }
1091
1169
 
@@ -1242,12 +1320,12 @@ function applicationCrmFields(crm, app) {
1242
1320
  }
1243
1321
 
1244
1322
  // src/applicant-projection.ts
1245
- async function upsertPerson(deps2, opts) {
1323
+ async function upsertPerson(deps, opts) {
1246
1324
  const email = opts.email.toLowerCase();
1247
- const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
1248
- const { record: record4, created } = await ensureProjectedPerson(deps2, { ...opts.input, email });
1325
+ const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1326
+ const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
1249
1327
  if (!created) await (0, import_crm2.updateRecord)(crmDeps3, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
1250
- const initial = deps2.crm.initialStage("person");
1328
+ const initial = deps.crm.initialStage("person");
1251
1329
  if (!created && record4.stage == null && initial) {
1252
1330
  await (0, import_crm2.setStage)(crmDeps3, {
1253
1331
  id: String(record4.id),
@@ -1259,7 +1337,7 @@ async function upsertPerson(deps2, opts) {
1259
1337
  }
1260
1338
  return { recordId: String(record4.id) };
1261
1339
  }
1262
- async function projectApplicant(deps2, applicant) {
1340
+ async function projectApplicant(deps, applicant) {
1263
1341
  const base = sharedPersonInput({
1264
1342
  email: applicant.email,
1265
1343
  firstName: applicant.firstName,
@@ -1268,18 +1346,18 @@ async function projectApplicant(deps2, applicant) {
1268
1346
  linkedin: applicant.linkedin,
1269
1347
  hubRecordId: applicant.applicationId
1270
1348
  });
1271
- Object.assign(base, applicationCrmFields(deps2.crm, applicant));
1349
+ Object.assign(base, applicationCrmFields(deps.crm, applicant));
1272
1350
  const mutationId = `apply:${applicant.applicationId}`;
1273
1351
  const extra = applicant.extra ?? {};
1274
- if (Object.keys(extra).length === 0) return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
1352
+ if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
1275
1353
  try {
1276
- return await upsertPerson(deps2, { email: applicant.email, input: {
1354
+ return await upsertPerson(deps, { email: applicant.email, input: {
1277
1355
  ...base,
1278
1356
  ...extra,
1279
- ...applicationCrmFields(deps2.crm, applicant)
1357
+ ...applicationCrmFields(deps.crm, applicant)
1280
1358
  }, mutationId });
1281
1359
  } catch {
1282
- return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
1360
+ return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
1283
1361
  }
1284
1362
  }
1285
1363
 
@@ -1377,24 +1455,24 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
1377
1455
  if (input[nameField] === void 0) input[nameField] = record4.name;
1378
1456
  return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
1379
1457
  }
1380
- async function findSharedRecord(deps2, record4, tag) {
1458
+ async function findSharedRecord(deps, record4, tag) {
1381
1459
  const structured = await (0, import_crm3.getRecordByOrigin)(
1382
- { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId },
1460
+ { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1383
1461
  record4.sourceId,
1384
1462
  record4.sourceRecordId
1385
1463
  );
1386
1464
  if (structured) return structured;
1387
- const mapped = await deps2.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
1465
+ const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
1388
1466
  const mappedId = mapped.crm_tag?.[0]?.recordId;
1389
1467
  if (typeof mappedId === "string") {
1390
- const found = await deps2.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1468
+ const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1391
1469
  if (found.crm_record?.[0]) return found.crm_record[0];
1392
1470
  }
1393
- const def = deps2.crm.type(record4.type);
1471
+ const def = deps.crm.type(record4.type);
1394
1472
  const emailField = def.emailField;
1395
1473
  if (emailField && typeof record4.input[emailField] === "string") {
1396
1474
  const primaryEmail = record4.input[emailField].toLowerCase();
1397
- const found = await deps2.db.query({
1475
+ const found = await deps.db.query({
1398
1476
  crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
1399
1477
  });
1400
1478
  if (found.crm_record?.[0]) return found.crm_record[0];
@@ -1402,7 +1480,7 @@ async function findSharedRecord(deps2, record4, tag) {
1402
1480
  const domain = record4.input.domain;
1403
1481
  const domainSlot = def.fields.domain?.slot;
1404
1482
  if (typeof domain === "string" && domainSlot) {
1405
- const found = await deps2.db.query({
1483
+ const found = await deps.db.query({
1406
1484
  crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
1407
1485
  });
1408
1486
  if (found.crm_record?.[0]) return found.crm_record[0];
@@ -1410,14 +1488,14 @@ async function findSharedRecord(deps2, record4, tag) {
1410
1488
  const nameField = def.nameField ?? "name";
1411
1489
  const name = record4.input[nameField];
1412
1490
  if (record4.type === "company" && typeof name === "string" && name.trim()) {
1413
- const found = await deps2.db.query({
1491
+ const found = await deps.db.query({
1414
1492
  crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
1415
1493
  });
1416
1494
  if (found.crm_record?.[0]) return found.crm_record[0];
1417
1495
  }
1418
1496
  return void 0;
1419
1497
  }
1420
- async function projectSharedRecord(deps2, shared, options = {}) {
1498
+ async function projectSharedRecord(deps, shared, options = {}) {
1421
1499
  const record4 = normalizeSharedRecord(shared, options.sourceId);
1422
1500
  if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
1423
1501
  throw new Error("type, source site id, and source record id must be non-empty");
@@ -1426,14 +1504,14 @@ async function projectSharedRecord(deps2, shared, options = {}) {
1426
1504
  throw new Error("signed sender does not match payload source");
1427
1505
  }
1428
1506
  const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
1429
- const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
1430
- const existing = await findSharedRecord(deps2, record4, tag);
1507
+ const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1508
+ const existing = await findSharedRecord(deps, record4, tag);
1431
1509
  let recordId;
1432
1510
  if (existing && typeof existing.id === "string") {
1433
1511
  await (0, import_crm3.updateRecord)(crmDeps3, { id: existing.id, input: record4.input });
1434
1512
  recordId = existing.id;
1435
1513
  } else if (record4.type === "person" && typeof record4.input.email === "string") {
1436
- const projected = await ensureProjectedPerson(deps2, record4.input);
1514
+ const projected = await ensureProjectedPerson(deps, record4.input);
1437
1515
  recordId = String(projected.record.id);
1438
1516
  if (!projected.created) await (0, import_crm3.updateRecord)(crmDeps3, { id: recordId, input: record4.input });
1439
1517
  } else {
@@ -1444,8 +1522,8 @@ async function projectSharedRecord(deps2, shared, options = {}) {
1444
1522
  mutationId: `share-create:${tag}`
1445
1523
  });
1446
1524
  }
1447
- await deps2.db.transact(
1448
- [{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps2.now() } }],
1525
+ await deps.db.transact(
1526
+ [{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
1449
1527
  { mutationId: `share-map:${tag}:${recordId}` }
1450
1528
  );
1451
1529
  await (0, import_crm3.upsertRecordOrigin)(crmDeps3, {
@@ -1493,21 +1571,21 @@ function billingColumns(app) {
1493
1571
  if (billingStatus !== "canceled" && billingStatus !== "refunded" && Number.isFinite(renewal) && renewal > 0) cols.renewalAt = renewal;
1494
1572
  return cols;
1495
1573
  }
1496
- async function syncApplicationToCrm(deps2, opts) {
1574
+ async function syncApplicationToCrm(deps, opts) {
1497
1575
  if (!opts.app.id) throw new Error("crm_application_id_required");
1498
1576
  const emailKey = str(opts.app.email).toLowerCase();
1499
1577
  if (!emailKey) return null;
1500
- const recordDeps = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
1501
- const input = personInputFromApp(deps2.chapter, opts.app);
1578
+ const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1579
+ const input = personInputFromApp(deps.chapter, opts.app);
1502
1580
  const stage = opts.stage || void 0;
1503
- const { record: existing, created } = await ensureProjectedPerson(deps2, input, { stage });
1581
+ const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
1504
1582
  const recordId = String(existing.id);
1505
1583
  if (!created) await (0, import_crm4.updateRecord)(recordDeps, { id: recordId, input });
1506
1584
  if (!created && stage && existing.stage !== stage) {
1507
1585
  await (0, import_crm4.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
1508
1586
  }
1509
1587
  const billing = billingColumns(opts.app);
1510
- await deps2.db.transact([
1588
+ await deps.db.transact([
1511
1589
  { t: "update", ns: "crm_record", id: recordId, attrs: billing },
1512
1590
  // Optional typed dates are removed with a retract, never an invalid null.
1513
1591
  ...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
@@ -1515,8 +1593,8 @@ async function syncApplicationToCrm(deps2, opts) {
1515
1593
  await (0, import_crm4.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1516
1594
  return recordId;
1517
1595
  }
1518
- async function backfillCrm(deps2) {
1519
- const data = await deps2.db.query({
1596
+ async function backfillCrm(deps) {
1597
+ const data = await deps.db.query({
1520
1598
  applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
1521
1599
  });
1522
1600
  const seen = /* @__PURE__ */ new Set();
@@ -1527,7 +1605,7 @@ async function backfillCrm(deps2) {
1527
1605
  if (!key3 || seen.has(key3)) return;
1528
1606
  seen.add(key3);
1529
1607
  try {
1530
- await syncApplicationToCrm(deps2, { app, stage });
1608
+ await syncApplicationToCrm(deps, { app, stage });
1531
1609
  synced += 1;
1532
1610
  } catch (err) {
1533
1611
  errors.push({ email: key3, error: err instanceof Error ? err.message : String(err) });
@@ -1535,10 +1613,10 @@ async function backfillCrm(deps2) {
1535
1613
  };
1536
1614
  for (const a of data.applications ?? []) await run(a, str(a.status));
1537
1615
  for (let offset = 0; ; offset += 200) {
1538
- const users = (await deps2.db.query({ $users: { $: { order: { id: "asc" }, limit: 200, offset } } })).$users ?? [];
1616
+ const users = (await deps.db.query({ $users: { $: { order: { id: "asc" }, limit: 200, offset } } })).$users ?? [];
1539
1617
  for (const u of users) {
1540
1618
  try {
1541
- if (await syncAccountToCrm(deps2, String(u.id))) synced += 1;
1619
+ if (await syncAccountToCrm(deps, String(u.id))) synced += 1;
1542
1620
  } catch (err) {
1543
1621
  errors.push({ email: str(u.email), error: err instanceof Error ? err.message : String(err) });
1544
1622
  }
@@ -1578,26 +1656,32 @@ function planDelivery(input) {
1578
1656
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1579
1657
  const to = redirect ? input.group.debugEmail : input.to;
1580
1658
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1581
- const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
1659
+ const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
1582
1660
 
1583
1661
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1584
- return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
1662
+ return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
1585
1663
  }
1586
1664
 
1587
1665
  // src/notify.ts
1588
- var EMAIL_TEMPLATE_NAMES = ["adminNotification", "paymentConfirmation", "prepEmail", "onboardingInvite"];
1589
- async function sendTemplated(deps2, input) {
1590
- const { emailLog } = await deps2.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
1666
+ var EMAIL_TEMPLATE_NAMES = ["adminNotification", "submitConfirmation", "paymentConfirmation", "prepEmail", "onboardingInvite"];
1667
+ function fromAddress(configured, name) {
1668
+ const display = name?.trim();
1669
+ if (!display) return configured;
1670
+ const angled = configured.match(/<([^>]+)>/);
1671
+ return { name: display, email: (angled ? angled[1] : configured).trim() };
1672
+ }
1673
+ async function sendTemplated(deps, input) {
1674
+ const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
1591
1675
  const prior = Array.isArray(emailLog) ? emailLog : [];
1592
1676
  if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
1593
1677
  if (prior.length > 1 || prior.length === 1 && prior[0].deliveryState !== "retry_ready") {
1594
1678
  return { sent: false, reason: "delivery-uncertain" };
1595
1679
  }
1596
- const cloudflareReady = Boolean(deps2.sender && deps2.from);
1597
- const override = deps2.debugEmail?.trim();
1598
- const group = deps2.envName !== "prod" && deps2.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
1680
+ const cloudflareReady = Boolean(deps.sender && deps.from);
1681
+ const override = deps.debugEmail?.trim();
1682
+ const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
1599
1683
  const decision = planDelivery({
1600
- envName: deps2.envName,
1684
+ envName: deps.envName,
1601
1685
  group,
1602
1686
  template: input.template,
1603
1687
  to: input.to,
@@ -1622,14 +1706,14 @@ async function sendTemplated(deps2, input) {
1622
1706
  transport: decision.transport,
1623
1707
  redirected: decision.redirected,
1624
1708
  dedupeKey: input.dedupeKey,
1625
- sentAt: deps2.now(),
1709
+ sentAt: deps.now(),
1626
1710
  deliveryState: decision.transport === "log-only" ? "sent" : "sending",
1627
1711
  deliveryAttempt: attempt,
1628
1712
  error: decision.transport === "log-only" ? "" : "delivery-pending",
1629
1713
  ...input.applicationId ? { applicationId: input.applicationId } : {}
1630
1714
  };
1631
1715
  try {
1632
- const claim = await deps2.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
1716
+ const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
1633
1717
  mutationId: `email-claim:${id6}:${attempt}`,
1634
1718
  guards: [previous ? {
1635
1719
  ns: "emailLog",
@@ -1647,8 +1731,8 @@ async function sendTemplated(deps2, input) {
1647
1731
  if (decision.transport === "log-only") return { sent: true };
1648
1732
  let messageId;
1649
1733
  try {
1650
- const response = await deps2.sender.send({
1651
- from: deps2.from,
1734
+ const response = await deps.sender.send({
1735
+ from: fromAddress(deps.from, input.group.fromName),
1652
1736
  to: [decision.to],
1653
1737
  subject: String(row2.subject),
1654
1738
  text: String(row2.body),
@@ -1657,12 +1741,12 @@ async function sendTemplated(deps2, input) {
1657
1741
  if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
1658
1742
  messageId = response.messageId;
1659
1743
  } catch {
1660
- await deps2.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
1744
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
1661
1745
  guards: [{ ns: "emailLog", id: id6, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
1662
1746
  });
1663
1747
  return { sent: false, reason: "delivery-uncertain" };
1664
1748
  }
1665
- await deps2.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps2.now() } }], {
1749
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
1666
1750
  mutationId: `email-confirmed:${id6}:${attempt}`,
1667
1751
  guards: [{
1668
1752
  ns: "emailLog",
@@ -1681,6 +1765,7 @@ function emailGroupFrom(row2) {
1681
1765
  name: String(row2.name ?? ""),
1682
1766
  replyTo: str3(row2.replyTo) ?? "",
1683
1767
  debugEmail: str3(row2.debugEmail),
1768
+ fromName: str3(row2.fromName),
1684
1769
  refundPolicyText: str3(row2.refundPolicyText),
1685
1770
  commitmentText: str3(row2.commitmentText),
1686
1771
  normsText: str3(row2.normsText),
@@ -2811,24 +2896,22 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
2811
2896
  }
2812
2897
  }
2813
2898
 
2814
- // src/payment-notifications.ts
2815
- var groupFor = async (db, app) => {
2816
- const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
2817
- return result.groups?.[0];
2818
- };
2819
- var deps = (db, env) => ({
2820
- db,
2821
- envName: env.ODLA_ENV,
2822
- sender: env.SEND_EMAIL,
2823
- from: env.EMAIL_FROM,
2824
- debugEmail: env.ODLA_DEBUG_EMAIL,
2825
- now: () => Date.now(),
2826
- newId: () => crypto.randomUUID()
2827
- });
2828
- async function paymentConfirmationVariables(db, app) {
2899
+ // src/notification-variables.ts
2900
+ function lifecycleNotifyDeps(db, env) {
2901
+ return {
2902
+ db,
2903
+ envName: env.ODLA_ENV,
2904
+ sender: env.SEND_EMAIL,
2905
+ from: env.EMAIL_FROM,
2906
+ debugEmail: env.ODLA_DEBUG_EMAIL,
2907
+ now: () => Date.now(),
2908
+ newId: () => crypto.randomUUID()
2909
+ };
2910
+ }
2911
+ async function frozenCheckoutVariables(db, app) {
2829
2912
  const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
2830
2913
  const offer = intent?.membershipOffer, contract = offer?.contract;
2831
- const offering = contract?.offering;
2914
+ const offering = contract?.offering, quote = intent?.paymentQuote;
2832
2915
  const amount = contract?.dueTodayCents ?? intent?.priceCents;
2833
2916
  const currency = offering?.currency ?? intent?.currency;
2834
2917
  let formatted = "";
@@ -2836,9 +2919,75 @@ async function paymentConfirmationVariables(db, app) {
2836
2919
  formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
2837
2920
  }
2838
2921
  return {
2839
- firstName: typeof app.firstName === "string" ? app.firstName : "",
2840
2922
  paymentAmount: formatted,
2841
2923
  paymentAmountSummary: formatted ? ` of ${formatted}` : "",
2924
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
2925
+ };
2926
+ }
2927
+ async function catalogTierName(db, group, app) {
2928
+ try {
2929
+ return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
2930
+ } catch {
2931
+ return "";
2932
+ }
2933
+ }
2934
+ async function checkoutVariables(db, group, app) {
2935
+ const frozen = await frozenCheckoutVariables(db, app);
2936
+ return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
2937
+ }
2938
+ function formatInstant(at, timeZone) {
2939
+ return new Intl.DateTimeFormat("en-US", {
2940
+ weekday: "long",
2941
+ month: "long",
2942
+ day: "numeric",
2943
+ year: "numeric",
2944
+ hour: "numeric",
2945
+ minute: "2-digit",
2946
+ timeZoneName: "short",
2947
+ timeZone
2948
+ }).format(new Date(at));
2949
+ }
2950
+ var adminUrlFor = (origin) => `${origin}/admin/`;
2951
+ var membersUrlFor = (origin) => `${origin}/members/`;
2952
+ function applicantVariables(app) {
2953
+ const text8 = (v) => typeof v === "string" ? v : "";
2954
+ return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
2955
+ }
2956
+ function sampleVariables(origin, timeZone, now = Date.now()) {
2957
+ const startAt = formatInstant(now + 864e5, timeZone);
2958
+ return {
2959
+ firstName: "Sample",
2960
+ lastName: "Person",
2961
+ email: "sample@example.com",
2962
+ phone: "(555) 010-0100",
2963
+ state: "CA",
2964
+ tier: "Membership",
2965
+ tierName: "Membership",
2966
+ paymentAmount: "$100.00",
2967
+ paymentAmountSummary: " of $100.00",
2968
+ paymentNextStep: "Next, schedule your introduction call.",
2969
+ submitNextStep: "Next, schedule your introduction call.",
2970
+ startAt,
2971
+ endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
2972
+ timezone: timeZone,
2973
+ meetUrl: "https://meet.google.com/sample",
2974
+ htmlLink: "https://calendar.google.com/event?eid=sample",
2975
+ meetingTime: startAt,
2976
+ meetingLink: "https://meet.google.com/sample",
2977
+ adminUrl: adminUrlFor(origin),
2978
+ membersUrl: membersUrlFor(origin)
2979
+ };
2980
+ }
2981
+
2982
+ // src/payment-notifications.ts
2983
+ var groupFor = async (db, app) => {
2984
+ const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
2985
+ return result.groups?.[0];
2986
+ };
2987
+ async function paymentConfirmationVariables(db, app) {
2988
+ return {
2989
+ firstName: typeof app.firstName === "string" ? app.firstName : "",
2990
+ ...await frozenCheckoutVariables(db, app),
2842
2991
  paymentNextStep: app.status === "approved" ? "Your membership is approved." : typeof app.interviewWaivedAt === "number" ? "Your interview is waived. We are completing your membership approval." : "Next, schedule your introduction call."
2843
2992
  };
2844
2993
  }
@@ -2846,7 +2995,7 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
2846
2995
  if (typeof app.email !== "string" || !app.email) throw new Error("payment notice recipient missing");
2847
2996
  const group = await groupFor(db, app);
2848
2997
  if (!group) throw new Error("payment notice configuration missing");
2849
- const result = await sendTemplated(deps(db, env), {
2998
+ const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
2850
2999
  group: emailGroupFrom(group),
2851
3000
  template: "paymentConfirmation",
2852
3001
  to: app.email,
@@ -2856,21 +3005,20 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
2856
3005
  });
2857
3006
  if (!result.sent && !["disabled", "template-missing"].includes(result.reason ?? "")) throw new Error("payment notice reconciliation pending");
2858
3007
  }
2859
- async function notifyAdminOfPayment(db, env, eventId, app) {
3008
+ async function notifyAdminOfPayment(db, env, eventId, app, origin = "") {
2860
3009
  try {
2861
3010
  const group = await groupFor(db, app);
2862
3011
  if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
2863
- const text6 = (value) => typeof value === "string" ? value : "";
2864
- await sendTemplated(deps(db, env), {
3012
+ const { tierName: tierName2 } = await frozenCheckoutVariables(db, app);
3013
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
2865
3014
  group: emailGroupFrom(group),
2866
3015
  template: "adminNotification",
2867
3016
  to: group.notificationEmail,
2868
3017
  vars: {
2869
- firstName: text6(app.firstName),
2870
- lastName: text6(app.lastName),
2871
- email: text6(app.email),
2872
- phone: text6(app.phone),
2873
- state: text6(app.state)
3018
+ ...applicantVariables(app),
3019
+ tier: tierName2,
3020
+ tierName: tierName2,
3021
+ ...origin ? { adminUrl: adminUrlFor(origin), membersUrl: membersUrlFor(origin) } : {}
2874
3022
  },
2875
3023
  dedupeKey: `${eventId}:admin`,
2876
3024
  applicationId: String(app.id)
@@ -3190,6 +3338,26 @@ function reportStripePartition(level, eventId, event, disposition) {
3190
3338
  ...stripeEventIdentity(event)
3191
3339
  }));
3192
3340
  }
3341
+ var QUARANTINE_DISPOSITIONS = ["quarantine_unmatched", "quarantine_collision"];
3342
+ var QUEUE_LIMIT = 500;
3343
+ var QUEUE_IDS = 50;
3344
+ async function alertStripeQuarantine(db, ctx, eventId, disposition) {
3345
+ const pages = await Promise.all(QUARANTINE_DISPOSITIONS.map(async (value) => (await db.query({ stripeEventReceipts: { $: { where: { disposition: value }, limit: QUEUE_LIMIT } } })).stripeEventReceipts ?? []));
3346
+ const queued = pages.flat();
3347
+ reportChapterAlert(ctx, new Error("stripe webhook quarantined"), {
3348
+ code: "stripe_webhook_quarantined",
3349
+ attributes: {
3350
+ "odla.stripe.disposition": disposition,
3351
+ "odla.stripe.quarantined_count": queued.length,
3352
+ "odla.stripe.queue_truncated": pages.some((page) => page.length >= QUEUE_LIMIT)
3353
+ },
3354
+ artifacts: { eventId, eventIds: queued.slice(0, QUEUE_IDS).map((row2) => String(row2.eventId)) }
3355
+ });
3356
+ }
3357
+ async function reportStripeCollision(db, ctx, eventId, event) {
3358
+ reportStripePartition("error", eventId, event, "quarantine_collision");
3359
+ await alertStripeQuarantine(db, ctx, eventId, "quarantine_collision");
3360
+ }
3193
3361
  async function storeStripeReceipt(db, input) {
3194
3362
  const existing = await receiptFor(db, input.eventId);
3195
3363
  if (existing?.retryReady === true && existing.reviewedDigest === input.digest) {
@@ -3222,17 +3390,18 @@ async function storeStripeReceipt(db, input) {
3222
3390
  const raced = await receiptFor(db, input.eventId);
3223
3391
  return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
3224
3392
  }
3225
- async function quarantineStripeEvent(db, eventId, digest, event) {
3393
+ async function quarantineStripeEvent(db, eventId, digest, event, ctx) {
3226
3394
  const disposition = "quarantine_unmatched";
3227
3395
  const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
3228
3396
  if (stored === "collision") {
3229
- reportStripePartition("error", eventId, event, "quarantine_collision");
3397
+ await reportStripeCollision(db, ctx, eventId, event);
3230
3398
  return json({ ok: true, matched: false, collision: true });
3231
3399
  }
3232
3400
  reportStripePartition("warn", eventId, event, disposition);
3401
+ if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
3233
3402
  return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
3234
3403
  }
3235
- async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
3404
+ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx) {
3236
3405
  const existing = await receiptFor(db, eventId);
3237
3406
  if (!existing) return null;
3238
3407
  if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
@@ -3242,7 +3411,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
3242
3411
  collisionDigest: digest,
3243
3412
  retryReady: false
3244
3413
  } }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
3245
- reportStripePartition("error", eventId, event, "quarantine_collision");
3414
+ await reportStripeCollision(db, ctx, eventId, event);
3246
3415
  return json({ ok: true, matched: false, collision: true });
3247
3416
  }
3248
3417
  return json({ ok: true, matched: existing.matched === true, replayed: true });
@@ -3273,16 +3442,16 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3273
3442
  reportStripePartition("warn", eventId, event, "ignore_sibling");
3274
3443
  return json({ ok: true, matched: false, disposition: "ignore_sibling" });
3275
3444
  }
3276
- const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
3445
+ const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
3277
3446
  if (replay2) return replay2;
3278
3447
  const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
3279
3448
  const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
3280
3449
  if (!localIntent || disposition2 === "quarantine_unmatched") {
3281
- return quarantineStripeEvent(db, eventId, digest, event);
3450
+ return quarantineStripeEvent(db, eventId, digest, event, ctx);
3282
3451
  }
3283
3452
  if (event.kind === "gift_paid") {
3284
3453
  if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
3285
- return quarantineStripeEvent(db, eventId, digest, event);
3454
+ return quarantineStripeEvent(db, eventId, digest, event, ctx);
3286
3455
  }
3287
3456
  try {
3288
3457
  const issued = await issueGiftClaim(db, intent, {
@@ -3291,7 +3460,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3291
3460
  }, Date.now());
3292
3461
  const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
3293
3462
  if (stored3 === "collision") {
3294
- reportStripePartition("error", eventId, event, "quarantine_collision");
3463
+ await reportStripeCollision(db, ctx, eventId, event);
3295
3464
  return json({ ok: true, matched: false, collision: true });
3296
3465
  }
3297
3466
  return json({ ok: true, matched: true, claimId: issued.claimId });
@@ -3307,7 +3476,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3307
3476
  if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
3308
3477
  const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
3309
3478
  if (stored2 === "collision") {
3310
- reportStripePartition("error", eventId, event, "quarantine_collision");
3479
+ await reportStripeCollision(db, ctx, eventId, event);
3311
3480
  return json({ ok: true, matched: false, collision: true });
3312
3481
  }
3313
3482
  return json({ ok: true, matched: Boolean(claim) });
@@ -3317,12 +3486,12 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3317
3486
  reportStripePartition("warn", eventId, event, "ignore_sibling");
3318
3487
  return json({ ok: true, matched: false, disposition: "ignore_sibling" });
3319
3488
  }
3320
- const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
3489
+ const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
3321
3490
  if (replay) return replay;
3322
3491
  const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
3323
3492
  const disposition = classifyMemberStripeEvent(identity, target, localApplication);
3324
3493
  if (!localApplication || disposition === "quarantine_unmatched") {
3325
- return quarantineStripeEvent(db, eventId, digest, event);
3494
+ return quarantineStripeEvent(db, eventId, digest, event, ctx);
3326
3495
  }
3327
3496
  if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
3328
3497
  await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
@@ -3339,7 +3508,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3339
3508
  if (receipt.seatId !== event.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
3340
3509
  const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
3341
3510
  if (stored2 === "collision") {
3342
- reportStripePartition("error", eventId, event, "quarantine_collision");
3511
+ await reportStripeCollision(db, ctx, eventId, event);
3343
3512
  return json({ ok: true, matched: false, collision: true });
3344
3513
  }
3345
3514
  return json({ ok: true, seatId: event.seatId });
@@ -3363,7 +3532,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3363
3532
  if (intent) {
3364
3533
  const evidence = verifyInitialMembershipInvoice(intent, event.invoice);
3365
3534
  if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
3366
- if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event);
3535
+ if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event, ctx);
3367
3536
  if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event, renewalAt: evidence.renewalAt };
3368
3537
  }
3369
3538
  if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
@@ -3414,7 +3583,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3414
3583
  } catch {
3415
3584
  return json({ error: "payment notice reconciliation pending" }, 503);
3416
3585
  }
3417
- if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app);
3586
+ if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app, input.origin);
3418
3587
  if (typeof next.interviewWaivedAt === "number") {
3419
3588
  const approval = await approveApplication({
3420
3589
  db,
@@ -3429,15 +3598,132 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3429
3598
  }
3430
3599
  const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
3431
3600
  if (stored === "collision") {
3432
- reportStripePartition("error", eventId, event, "quarantine_collision");
3601
+ await reportStripeCollision(db, ctx, eventId, event);
3433
3602
  return json({ ok: true, matched: false, collision: true });
3434
3603
  }
3435
3604
  return json({ ok: true });
3436
3605
  }
3437
3606
 
3607
+ // src/network-commercial-resolve.ts
3608
+ var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
3609
+ var fail3 = (message, code) => Object.assign(new Error(message), { code });
3610
+ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
3611
+ if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
3612
+ const id6 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
3613
+ const found = await db.query({
3614
+ networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } },
3615
+ networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
3616
+ });
3617
+ const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
3618
+ if (exception?.status === "resolved" && exception.reviewId === reviewId) {
3619
+ return { eventId: input.eventId, reviewId, projection: await loadNetworkCommercialProjection(db, sources) };
3620
+ }
3621
+ if (!exception || exception.status !== "active") throw fail3("no active collision for this event", "collision_not_active");
3622
+ if (!stored) throw fail3("stored event not found", "event_not_found");
3623
+ const dismissed = typeof exception.incomingDigest === "string" ? exception.incomingDigest : void 0;
3624
+ const write = await db.transact([
3625
+ { t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
3626
+ id: reviewId,
3627
+ eventId: input.eventId,
3628
+ actorUserId: input.actorUserId,
3629
+ reviewedAt: now,
3630
+ previousDigest: String(stored.digest),
3631
+ providerDigest: dismissed ?? "",
3632
+ scope: "network",
3633
+ previousState: exception,
3634
+ action: "resolve",
3635
+ reason: input.reason
3636
+ } },
3637
+ { t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
3638
+ status: "resolved",
3639
+ resolvedDigest: stored.digest,
3640
+ ...dismissed ? { reviewedDigest: dismissed } : {},
3641
+ resolvedBy: input.actorUserId,
3642
+ reviewId,
3643
+ updatedAt: now
3644
+ } }
3645
+ ], { mutationId: input.mutationId, guards: [
3646
+ { ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
3647
+ ] });
3648
+ if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
3649
+ const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
3650
+ return { eventId: input.eventId, reviewId, projection };
3651
+ }
3652
+ async function retryNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
3653
+ if (!EVENT_ID.test(eventId)) throw fail3("invalid event reference", "invalid_event");
3654
+ const active = (await loadNetworkCommercialProjection(db, sources)).exceptions.filter((item) => item.eventId === eventId);
3655
+ if (active.length === 0) throw fail3("event has no active exception", "exception_not_active");
3656
+ const provider = await readStripeRecoveryEvent(db, eventId);
3657
+ const stored = (await db.query({ networkCommercialEvents: { $: { where: { eventId }, limit: 1 } } })).networkCommercialEvents?.[0];
3658
+ const reviewId = crypto.randomUUID();
3659
+ await db.transact([{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
3660
+ id: reviewId,
3661
+ eventId,
3662
+ actorUserId,
3663
+ reviewedAt: now,
3664
+ previousDigest: String(stored?.digest ?? ""),
3665
+ providerDigest: provider.digest,
3666
+ scope: "network",
3667
+ previousState: { exceptions: active },
3668
+ action: "retry"
3669
+ } }], { mutationId: `payment-recovery:${reviewId}` });
3670
+ const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
3671
+ const result = await ingestNetworkCommercialFact(db, sources, fact, now, await stripePayloadDigest(provider.rawBody), alerts);
3672
+ return {
3673
+ eventId,
3674
+ reviewId,
3675
+ status: result.status,
3676
+ resolvedCollision: result.status === "replayed" && result.resolvedCollision === true,
3677
+ exceptions: result.projection.exceptions.length
3678
+ };
3679
+ }
3680
+
3681
+ // src/worker-routes-network-commercial-recovery.ts
3682
+ var PATH = "/api/admin/payment-recovery";
3683
+ var handleNetworkCommercialRecovery = async (req, url, env, ctx) => {
3684
+ const model = ctx.chapter.networkCommercialReadModel;
3685
+ if (url.pathname !== PATH || !model) return null;
3686
+ if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
3687
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3688
+ if (!user) return json({ error: "unauthorized" }, 401);
3689
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "forbidden" }, 403);
3690
+ const db = raw2;
3691
+ if (req.method === "GET") {
3692
+ const { exceptions } = await loadNetworkCommercialProjection(db, model.sources);
3693
+ return json({
3694
+ scope: "network",
3695
+ count: exceptions.length,
3696
+ truncated: false,
3697
+ rows: exceptions.map((item) => ({
3698
+ eventId: item.eventId,
3699
+ kind: item.detail.eventType,
3700
+ disposition: item.kind,
3701
+ sourceKey: item.sourceKey ?? null,
3702
+ retryReady: /^evt_/.test(item.eventId)
3703
+ }))
3704
+ });
3705
+ }
3706
+ let eventId;
3707
+ try {
3708
+ eventId = String((await req.json()).eventId ?? "");
3709
+ } catch {
3710
+ return json({ error: "eventId required" }, 400);
3711
+ }
3712
+ try {
3713
+ const retried = await retryNetworkCommercialEvent(db, model.sources, eventId, user.userId, Date.now(), networkCommercialAlerts(ctx, PATH));
3714
+ return json({ ok: true, ...retried });
3715
+ } catch (error) {
3716
+ const code = error.code;
3717
+ if (code === "invalid_event") return json({ error: "invalid event reference" }, 400);
3718
+ if (code === "exception_not_active") return json({ error: "event has no active exception" }, 409);
3719
+ return json({ error: "Event recovery is pending. Check its provider evidence and retry." }, 503);
3720
+ }
3721
+ };
3722
+
3438
3723
  // src/worker-routes-payment-recovery.ts
3439
3724
  var handlePaymentRecovery = async (req, url, env, ctx) => {
3440
- if (url.pathname !== "/api/admin/payment-recovery" || ctx.chapter.mode !== "chapter") return null;
3725
+ if (url.pathname !== "/api/admin/payment-recovery") return null;
3726
+ if (ctx.chapter.mode !== "chapter") return handleNetworkCommercialRecovery(req, url, env, ctx);
3441
3727
  if (!["GET", "POST"].includes(req.method)) return json({ error: "method not allowed" }, 405);
3442
3728
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3443
3729
  if (!user) return json({ error: "unauthorized" }, 401);
@@ -3499,10 +3785,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
3499
3785
  };
3500
3786
 
3501
3787
  // src/worker-routes-admin-named-seat.ts
3502
- var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3788
+ var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3503
3789
  var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
3504
3790
  var handleAdminNamedSeat = async (req, url, env, ctx) => {
3505
- const match = url.pathname.match(PATH);
3791
+ const match = url.pathname.match(PATH2);
3506
3792
  if (!match) return null;
3507
3793
  const seats = ctx.membershipAuthority?.namedSeats;
3508
3794
  if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
@@ -3555,42 +3841,42 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
3555
3841
 
3556
3842
  // src/membership-seat-refund-stripe.ts
3557
3843
  var import_stripe6 = require("@odla-ai/stripe");
3558
- var fail3 = (code = "named_seat_refund_mismatch") => {
3844
+ var fail4 = (code = "named_seat_refund_mismatch") => {
3559
3845
  throw Object.assign(new Error(code), { code });
3560
3846
  };
3561
3847
  async function refundNamedSeatStripePayment(secretKey, seat) {
3562
- if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
3848
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
3563
3849
  const call = async (method, path, params, idempotencyKey) => {
3564
3850
  const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
3565
- if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
3851
+ if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
3566
3852
  return result.body;
3567
3853
  };
3568
3854
  const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
3569
3855
  const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
3570
- if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail3();
3856
+ if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail4();
3571
3857
  const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
3572
- if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail3();
3858
+ if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail4();
3573
3859
  const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
3574
- if (items.length > 1) fail3();
3860
+ if (items.length > 1) fail4();
3575
3861
  if (items.length && sub.status !== "canceled") {
3576
3862
  const item = items[0];
3577
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
3863
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail4();
3578
3864
  const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
3579
- if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
3865
+ if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
3580
3866
  }
3581
3867
  const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
3582
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
3868
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
3583
3869
  let refundedCents = 0, pendingCents = 0;
3584
3870
  const refundIds = [];
3585
3871
  for (const refund of list.data) {
3586
- if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail3();
3872
+ if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
3587
3873
  if (refund.status === "succeeded") {
3588
3874
  refundedCents += refund.amount;
3589
3875
  refundIds.push(refund.id);
3590
3876
  } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
3591
3877
  }
3592
- if (refundedCents + pendingCents > seat.amountCents) fail3();
3593
- if (pendingCents) fail3("named_seat_refund_pending");
3878
+ if (refundedCents + pendingCents > seat.amountCents) fail4();
3879
+ if (pendingCents) fail4("named_seat_refund_pending");
3594
3880
  if (refundedCents < seat.amountCents) {
3595
3881
  const remaining = seat.amountCents - refundedCents;
3596
3882
  const refund = await call("POST", "/v1/refunds", {
@@ -3599,8 +3885,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
3599
3885
  reason: "requested_by_customer",
3600
3886
  metadata
3601
3887
  }, `named-seat-refund:${seat.id}:${remaining}`);
3602
- if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
3603
- if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
3888
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
3889
+ if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
3604
3890
  refundedCents += remaining;
3605
3891
  refundIds.push(refund.id);
3606
3892
  }
@@ -3805,6 +4091,107 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3805
4091
  }
3806
4092
  };
3807
4093
 
4094
+ // src/membership-policy-versions.ts
4095
+ var LIMIT = 200;
4096
+ var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4097
+ var instant = (value) => {
4098
+ const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4099
+ return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
4100
+ };
4101
+ function provenance(row2) {
4102
+ const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4103
+ return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
4104
+ }
4105
+ var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
4106
+ function stateOf(item, all, now, window, rivals) {
4107
+ if (item.status !== "published") return { state: item.status };
4108
+ const mine = window(item);
4109
+ if (now < mine.from) return { state: "scheduled" };
4110
+ const closed = mine.until !== void 0 && mine.until <= now;
4111
+ const higher = all.filter((other) => other.id !== item.id && other.status === "published" && other.version > item.version && rivals(item, other)).map(window).filter((w) => w.from <= now && (w.until === void 0 || w.until > mine.from));
4112
+ if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
4113
+ const dates = higher.map((w) => Math.max(w.from, mine.from));
4114
+ if (closed) dates.push(mine.until);
4115
+ return { state: "superseded", supersededAt: Math.min(...dates) };
4116
+ }
4117
+ var mechanism = (policy) => policy.percentOffBps === 0 || policy.discountEligibleSkuIds.length === 0 ? "none" : policy.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
4118
+ var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
4119
+ var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
4120
+ function normalize(rows5, collection, parse, invalid2) {
4121
+ const out = [];
4122
+ for (const row2 of rows5) {
4123
+ try {
4124
+ out.push({ value: parse(row2.snapshot), row: row2 });
4125
+ } catch (error) {
4126
+ invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
4127
+ }
4128
+ }
4129
+ return out;
4130
+ }
4131
+ async function listMembershipPolicyVersions(db, input) {
4132
+ const where = input.chapterId ? { chapterId: input.chapterId } : {};
4133
+ const rows5 = await db.query({
4134
+ membershipOfferingVersion: { $: { where, limit: LIMIT } },
4135
+ discountPolicyVersion: { $: { where, limit: LIMIT } }
4136
+ });
4137
+ const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
4138
+ const invalid2 = [];
4139
+ const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
4140
+ const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
4141
+ const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
4142
+ return {
4143
+ now: input.now,
4144
+ chapterId: input.chapterId ?? null,
4145
+ offerings: offerings.map(({ value, row: row2 }) => ({
4146
+ ...value,
4147
+ ...provenance(row2),
4148
+ ...stateOf(
4149
+ value,
4150
+ offeringValues,
4151
+ input.now,
4152
+ (v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
4153
+ (a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
4154
+ )
4155
+ })).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
4156
+ discountPolicies: policies.map(({ value, row: row2 }) => ({
4157
+ ...value,
4158
+ ...provenance(row2),
4159
+ mechanism: mechanism(value),
4160
+ ...stateOf(
4161
+ value,
4162
+ policyValues,
4163
+ input.now,
4164
+ (v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
4165
+ (a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
4166
+ )
4167
+ })).sort((a, b) => byChapter(a, b) || b.version - a.version),
4168
+ invalid: invalid2,
4169
+ truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
4170
+ };
4171
+ }
4172
+
4173
+ // src/worker-routes-admin-membership-policy.ts
4174
+ var PATH3 = "/api/admin/membership-policy-versions";
4175
+ var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4176
+ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
4177
+ if (url.pathname !== PATH3) return null;
4178
+ const operator = ctx.membershipAuthorityOperator;
4179
+ if (!operator) return json({ error: "not found" }, 404);
4180
+ if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
4181
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4182
+ if (!user) return json({ error: "unauthorized" }, 401);
4183
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4184
+ const chapterId = url.searchParams.get("chapterId");
4185
+ if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
4186
+ let db;
4187
+ try {
4188
+ db = operator.makeDb(env);
4189
+ } catch {
4190
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4191
+ }
4192
+ return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
4193
+ };
4194
+
3808
4195
  // src/worker-routes-membership-cancel.ts
3809
4196
  var handleMembershipCancellation = async (req, url, env, ctx) => {
3810
4197
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4657,8 +5044,8 @@ var APPLICATION_LABELS = {
4657
5044
  state: "State"
4658
5045
  };
4659
5046
  function humanize(id6) {
4660
- const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
4661
- return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
5047
+ const text8 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
5048
+ return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id6;
4662
5049
  }
4663
5050
  function applicationDefs(chapter) {
4664
5051
  try {
@@ -4725,7 +5112,7 @@ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
4725
5112
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
4726
5113
  var CURRENCY = /^[a-z]{3}$/;
4727
5114
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
4728
- var text2 = (value, path, errors, max) => {
5115
+ var text3 = (value, path, errors, max) => {
4729
5116
  if (typeof value !== "string" || !value.trim()) {
4730
5117
  errors.push(`${path} is required`);
4731
5118
  return "";
@@ -4753,23 +5140,23 @@ function validateGiftOfferCatalog(input, tierIds) {
4753
5140
  }
4754
5141
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
4755
5142
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
4756
- const id6 = text2(item.id, `${path}.id`, errors, 80);
5143
+ const id6 = text3(item.id, `${path}.id`, errors, 80);
4757
5144
  if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4758
5145
  if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
4759
5146
  ids2.add(id6);
4760
5147
  const targetRaw = record(item.target);
4761
5148
  let target = { type: "entitlement", entitlementKey: "invalid" };
4762
5149
  if (targetRaw?.type === "tier") {
4763
- const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
5150
+ const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
4764
5151
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
4765
5152
  target = { type: "tier", tierId };
4766
5153
  } else if (targetRaw?.type === "entitlement") {
4767
- const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
5154
+ const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
4768
5155
  if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
4769
5156
  target = { type: "entitlement", entitlementKey };
4770
5157
  } else errors.push(`${path}.target.type must be tier or entitlement`);
4771
5158
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
4772
- const stripePriceId = text2(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
5159
+ const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
4773
5160
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
4774
5161
  if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
4775
5162
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -4791,9 +5178,9 @@ function validateGiftOfferCatalog(input, tierIds) {
4791
5178
  redemptionLifetimeHours: lifetime,
4792
5179
  ...activeFrom !== void 0 ? { activeFrom } : {},
4793
5180
  ...activeUntil !== void 0 ? { activeUntil } : {},
4794
- title: text2(item.title, `${path}.title`, errors, 160),
4795
- description: text2(item.description, `${path}.description`, errors, 2e3),
4796
- ctaLabel: text2(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
5181
+ title: text3(item.title, `${path}.title`, errors, 160),
5182
+ description: text3(item.description, `${path}.description`, errors, 2e3),
5183
+ ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
4797
5184
  });
4798
5185
  }
4799
5186
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -5011,22 +5398,22 @@ async function createPaymentQuote(input) {
5011
5398
  // src/membership-quote-projection.ts
5012
5399
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
5013
5400
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
5014
- var fail4 = (code) => {
5401
+ var fail5 = (code) => {
5015
5402
  throw Object.assign(new Error(code), { code });
5016
5403
  };
5017
5404
  var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
5018
5405
  async function restore2(row2, target, now, allowExpired = false) {
5019
- if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail4("membership_quote_unavailable");
5020
- if (row2.status !== "ready") return fail4("membership_quote_pending");
5406
+ if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail5("membership_quote_unavailable");
5407
+ if (row2.status !== "ready") return fail5("membership_quote_pending");
5021
5408
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
5022
5409
  ...target,
5023
5410
  requestId: row2.requestId
5024
5411
  }), now, lifetimeMs: 6e4 });
5025
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
5412
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail5("membership_quote_expired");
5026
5413
  const tier = row2.tier, quote = row2.quote;
5027
5414
  const offering = offer.contract.offering;
5028
5415
  if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
5029
- return fail4("membership_catalog_mismatch");
5416
+ return fail5("membership_catalog_mismatch");
5030
5417
  }
5031
5418
  const expected = await createPaymentQuote({
5032
5419
  ...target,
@@ -5037,7 +5424,7 @@ async function restore2(row2, target, now, allowExpired = false) {
5037
5424
  presentation: quote.presentation,
5038
5425
  founding: foundingOf(offer)
5039
5426
  });
5040
- if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
5427
+ if (quote.digest !== expected.digest) return fail5("membership_quote_conflict");
5041
5428
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
5042
5429
  }
5043
5430
  async function readMembershipQuote(db, target, now) {
@@ -5045,9 +5432,9 @@ async function readMembershipQuote(db, target, now) {
5045
5432
  }
5046
5433
  async function prepareMembershipQuote(db, input, issue) {
5047
5434
  const { applicationId: id6, now, app } = input;
5048
- if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail4("membership_quote_unavailable");
5435
+ if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail5("membership_quote_unavailable");
5049
5436
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
5050
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
5437
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail5("membership_quote_conflict");
5051
5438
  if (row2?.status === "ready") {
5052
5439
  try {
5053
5440
  return await restore2(row2, input, now);
@@ -5092,7 +5479,7 @@ async function prepareMembershipQuote(db, input, issue) {
5092
5479
  }
5093
5480
  if (row2?.status === "ready") return restore2(row2, input, now);
5094
5481
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
5095
- return fail4("membership_quote_pending");
5482
+ return fail5("membership_quote_pending");
5096
5483
  }
5097
5484
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
5098
5485
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -5356,17 +5743,17 @@ async function readBoundedRequestText(req, maxBytes) {
5356
5743
  if (!reader) return "";
5357
5744
  const decoder = new TextDecoder();
5358
5745
  let bytes = 0;
5359
- let text6 = "";
5746
+ let text8 = "";
5360
5747
  for (; ; ) {
5361
5748
  const { done, value } = await reader.read();
5362
5749
  if (req.signal.aborted) return invalid2();
5363
5750
  if (done) {
5364
5751
  complete = true;
5365
- return text6 + decoder.decode();
5752
+ return text8 + decoder.decode();
5366
5753
  }
5367
5754
  bytes += value.byteLength;
5368
5755
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
5369
- text6 += decoder.decode(value, { stream: true });
5756
+ text8 += decoder.decode(value, { stream: true });
5370
5757
  }
5371
5758
  } catch {
5372
5759
  return invalid2();
@@ -5719,37 +6106,61 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
5719
6106
  } catch {
5720
6107
  }
5721
6108
  }
5722
- async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
6109
+
6110
+ // src/submit-notifications.ts
6111
+ async function notifyAdminOfApplication(db, env, notice) {
5723
6112
  try {
5724
- const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
5725
- const group = Array.isArray(groups) ? groups[0] : void 0;
5726
- if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
5727
- const stringValue = (value) => typeof value === "string" ? value : "";
5728
- await sendTemplated(
5729
- {
5730
- db,
5731
- envName: env.ODLA_ENV,
5732
- debugEmail: env.ODLA_DEBUG_EMAIL,
5733
- sender: env.SEND_EMAIL,
5734
- from: env.EMAIL_FROM,
5735
- now: () => Date.now(),
5736
- newId: () => crypto.randomUUID()
6113
+ const { group, application: app } = notice;
6114
+ if (typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
6115
+ const { tierName: tierName2 } = await checkoutVariables(db, group, app);
6116
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
6117
+ group: emailGroupFrom(group),
6118
+ template: "adminNotification",
6119
+ to: group.notificationEmail,
6120
+ vars: { ...applicantVariables(app), tier: tierName2, tierName: tierName2, adminUrl: adminUrlFor(notice.origin), membersUrl: membersUrlFor(notice.origin) },
6121
+ dedupeKey: `apply:${String(app.id)}:admin`,
6122
+ applicationId: String(app.id)
6123
+ });
6124
+ } catch {
6125
+ }
6126
+ }
6127
+ var FREE_CONTINUATIONS = /* @__PURE__ */ new Set(["booking", "accepted", "approve"]);
6128
+ function shouldConfirmSubmit(state, group) {
6129
+ if (state !== null && FREE_CONTINUATIONS.has(state)) return true;
6130
+ const templates = group.emailTemplates;
6131
+ return templates?.paymentConfirmation?.enabled === false;
6132
+ }
6133
+ function submitNextStep(state) {
6134
+ switch (state) {
6135
+ case "accepted":
6136
+ return "Your membership is approved.";
6137
+ case "booking":
6138
+ return "Next, schedule your introduction call.";
6139
+ case "payment":
6140
+ case "paymentPending":
6141
+ case "gift":
6142
+ return "Next, complete your membership payment.";
6143
+ default:
6144
+ return "";
6145
+ }
6146
+ }
6147
+ async function notifySubmitConfirmed(db, env, notice) {
6148
+ try {
6149
+ const { group, application: app } = notice;
6150
+ if (typeof app.email !== "string" || !app.email || !shouldConfirmSubmit(notice.state, group)) return;
6151
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
6152
+ group: emailGroupFrom(group),
6153
+ template: "submitConfirmation",
6154
+ to: app.email,
6155
+ vars: {
6156
+ ...applicantVariables(app),
6157
+ ...await checkoutVariables(db, group, app),
6158
+ submitNextStep: submitNextStep(notice.state),
6159
+ membersUrl: membersUrlFor(notice.origin)
5737
6160
  },
5738
- {
5739
- group: emailGroupFrom(group),
5740
- template: "adminNotification",
5741
- to: group.notificationEmail,
5742
- vars: {
5743
- firstName: stringValue(fields.firstName),
5744
- lastName: stringValue(fields.lastName),
5745
- email: stringValue(fields.email),
5746
- phone: stringValue(fields.phone),
5747
- state: stringValue(fields.state)
5748
- },
5749
- dedupeKey: `apply:${applicationId}:admin`,
5750
- applicationId
5751
- }
5752
- );
6161
+ dedupeKey: `apply:${String(app.id)}:confirm`,
6162
+ applicationId: String(app.id)
6163
+ });
5753
6164
  } catch {
5754
6165
  }
5755
6166
  }
@@ -5947,17 +6358,17 @@ var handleMember = async (req, url, env, ctx) => {
5947
6358
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
5948
6359
  return json(provisioningFailure.body, provisioningFailure.status);
5949
6360
  }
5950
- if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
5951
- await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical3);
5952
- }
5953
6361
  const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
5954
6362
  const group = Array.isArray(groups) ? groups[0] : void 0;
5955
- const state = canonical3 && group ? await continueJoin(db, ctx, env, url.origin, canonical3, group) : null;
6363
+ const notice = group ? { group, application: canonical3, origin: url.origin } : null;
6364
+ if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
6365
+ const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
5956
6366
  if (!state) return json({
5957
6367
  error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
5958
6368
  code: "application_continuation_unavailable",
5959
6369
  id: result.id
5960
6370
  }, 503);
6371
+ if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
5961
6372
  const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
5962
6373
  if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
5963
6374
  return json({ error: "application changed; reload its current state" }, 409);
@@ -6077,10 +6488,10 @@ function boundedString(value, path, errors, max, required = false) {
6077
6488
  errors.push(`${path} must be a string`);
6078
6489
  return void 0;
6079
6490
  }
6080
- const text6 = value.trim();
6081
- if (required && !text6) errors.push(`${path} is required`);
6082
- if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
6083
- return text6;
6491
+ const text8 = value.trim();
6492
+ if (required && !text8) errors.push(`${path} is required`);
6493
+ if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
6494
+ return text8;
6084
6495
  }
6085
6496
  function validateMembershipPageDocument(input, options = {}) {
6086
6497
  const errors = [];
@@ -6129,8 +6540,8 @@ function validateMembershipPageDocument(input, options = {}) {
6129
6540
  else {
6130
6541
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
6131
6542
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
6132
- const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
6133
- return text6 ? [text6] : [];
6543
+ const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
6544
+ return text8 ? [text8] : [];
6134
6545
  });
6135
6546
  }
6136
6547
  }
@@ -6224,7 +6635,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
6224
6635
  function unknownKeys(value, allowed2, path, errors) {
6225
6636
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6226
6637
  }
6227
- function text3(value, path, errors, max = 65536) {
6638
+ function text4(value, path, errors, max = 65536) {
6228
6639
  if (typeof value !== "string" || !value.trim()) {
6229
6640
  errors.push(`${path} is required`);
6230
6641
  return "";
@@ -6247,11 +6658,11 @@ function tiersOf(value, errors) {
6247
6658
  return [];
6248
6659
  }
6249
6660
  unknownKeys(item, TIER_KEYS2, path, errors);
6250
- const id6 = text3(item.id, `${path}.id`, errors, 80);
6661
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
6251
6662
  if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6252
6663
  if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
6253
6664
  seen.add(id6);
6254
- const name = text3(item.name, `${path}.name`, errors, 160);
6665
+ const name = text4(item.name, `${path}.name`, errors, 160);
6255
6666
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
6256
6667
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
6257
6668
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -6269,7 +6680,7 @@ function tiersOf(value, errors) {
6269
6680
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
6270
6681
  errors.push(`${path}.interval must be year or month`);
6271
6682
  }
6272
- const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
6683
+ const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
6273
6684
  return [{
6274
6685
  id: id6,
6275
6686
  name,
@@ -6327,7 +6738,7 @@ function policyOf(value, errors, tiers) {
6327
6738
  const result = {};
6328
6739
  for (const key3 of allowed2) {
6329
6740
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
6330
- result[key3] = text3(input[key3], `document.policy.${key3}`, errors);
6741
+ result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
6331
6742
  }
6332
6743
  const tierPolicies = {};
6333
6744
  if (input.tierPolicies !== void 0) {
@@ -6342,8 +6753,8 @@ function policyOf(value, errors, tiers) {
6342
6753
  }
6343
6754
  unknownKeys(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
6344
6755
  tierPolicies[id6] = {
6345
- refundPolicyText: text3(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
6346
- ...policy.trustCopy !== void 0 ? { trustCopy: text3(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
6756
+ refundPolicyText: text4(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
6757
+ ...policy.trustCopy !== void 0 ? { trustCopy: text4(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
6347
6758
  };
6348
6759
  }
6349
6760
  }
@@ -6383,9 +6794,9 @@ function validateSignupControlDocument(input) {
6383
6794
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
6384
6795
  const source = recordOf(root.source);
6385
6796
  const target = recordOf(root.target);
6386
- const appId = text3(source?.appId, "document.source.appId", errors, 63);
6387
- const chapterId = text3(target?.chapterId, "document.target.chapterId", errors, 63);
6388
- const environment = text3(target?.environment, "document.target.environment", errors, 63);
6797
+ const appId = text4(source?.appId, "document.source.appId", errors, 63);
6798
+ const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
6799
+ const environment = text4(target?.environment, "document.target.environment", errors, 63);
6389
6800
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
6390
6801
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
6391
6802
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -7564,7 +7975,7 @@ async function deliverAdmissionGrants(options) {
7564
7975
  }
7565
7976
 
7566
7977
  // src/worker-routes-network-admission.ts
7567
- var PATH2 = "/api/admin/network/admissions";
7978
+ var PATH4 = "/api/admin/network/admissions";
7568
7979
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7569
7980
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7570
7981
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -7644,11 +8055,11 @@ async function parseBody(req) {
7644
8055
  }
7645
8056
  }
7646
8057
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
7647
- if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
8058
+ if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7648
8059
  const auth = await gate2(req, env, ctx);
7649
8060
  if ("response" in auth) return auth.response;
7650
8061
  const db = auth.db;
7651
- if (url.pathname === PATH2 && req.method === "GET") {
8062
+ if (url.pathname === PATH4 && req.method === "GET") {
7652
8063
  const targetId2 = url.searchParams.get("targetId") ?? "";
7653
8064
  const target2 = targetFor(ctx, targetId2);
7654
8065
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -7749,6 +8160,41 @@ async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
7749
8160
 
7750
8161
  // src/worker-routes-schedule.ts
7751
8162
  var import_calendar = require("@odla-ai/calendar");
8163
+
8164
+ // src/booking-notifications.ts
8165
+ async function bookingConfirmationVariables(db, group, app, meeting) {
8166
+ const startAt = formatInstant(meeting.startAt, meeting.timezone);
8167
+ const meetUrl = meeting.meetUrl ?? "", htmlLink = meeting.htmlLink ?? "";
8168
+ return {
8169
+ ...applicantVariables(app),
8170
+ ...await checkoutVariables(db, group, app),
8171
+ startAt,
8172
+ endAt: formatInstant(meeting.endAt, meeting.timezone),
8173
+ timezone: meeting.timezone,
8174
+ meetUrl,
8175
+ htmlLink,
8176
+ // The placeholders the seeded template used before these variables existed.
8177
+ meetingTime: startAt,
8178
+ meetingLink: meetUrl || htmlLink
8179
+ };
8180
+ }
8181
+ async function notifyBookingConfirmed(db, env, input) {
8182
+ const app = input.application;
8183
+ if (typeof app.email !== "string" || !app.email) return;
8184
+ try {
8185
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
8186
+ group: emailGroupFrom(input.group),
8187
+ template: "prepEmail",
8188
+ to: app.email,
8189
+ vars: await bookingConfirmationVariables(db, input.group, app, input.meeting),
8190
+ dedupeKey: `prep:${String(app.id)}`,
8191
+ applicationId: String(app.id)
8192
+ });
8193
+ } catch {
8194
+ }
8195
+ }
8196
+
8197
+ // src/worker-routes-schedule.ts
7752
8198
  function errCode(err) {
7753
8199
  if (err && typeof err === "object") {
7754
8200
  const code = err.code;
@@ -7863,12 +8309,7 @@ async function bookSlot(req, env, ctx) {
7863
8309
  await db.transact([meetingOp, appOp]);
7864
8310
  const booked = { ...app, ...bookingAttrs };
7865
8311
  await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
7866
- if (typeof app.email === "string" && app.email) {
7867
- await sendTemplated(
7868
- { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL, now: () => Date.now(), newId: () => crypto.randomUUID() },
7869
- { group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
7870
- ).catch(() => void 0);
7871
- }
8312
+ await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
7872
8313
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
7873
8314
  }
7874
8315
  var handleSchedule = async (req, url, env, ctx) => {
@@ -7899,21 +8340,21 @@ var import_stripe7 = require("@odla-ai/stripe");
7899
8340
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
7900
8341
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
7901
8342
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
7902
- function fail5(code = "refund_target_mismatch") {
8343
+ function fail6(code = "refund_target_mismatch") {
7903
8344
  throw Object.assign(new Error(code), { code });
7904
8345
  }
7905
8346
  async function refundSubscriptionPayment(secretKey, input) {
7906
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
8347
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
7907
8348
  const call = async (method, path, params, key3) => {
7908
8349
  const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7909
- if (!result.ok) fail5("stripe_error");
7910
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
8350
+ if (!result.ok) fail6("stripe_error");
8351
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
7911
8352
  return result.body;
7912
8353
  };
7913
8354
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
7914
- if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
8355
+ if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail6();
7915
8356
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
7916
- if (row(sub.metadata)[key3] !== value) fail5();
8357
+ if (row(sub.metadata)[key3] !== value) fail6();
7917
8358
  }
7918
8359
  let cursor;
7919
8360
  let earliest;
@@ -7926,55 +8367,55 @@ async function refundSubscriptionPayment(secretKey, input) {
7926
8367
  limit: 100,
7927
8368
  ...cursor ? { starting_after: cursor } : {}
7928
8369
  });
7929
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
8370
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail6("stripe_invalid_response");
7930
8371
  for (const value of list.data) {
7931
8372
  const invoice2 = row(value);
7932
8373
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
7933
- if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail5();
8374
+ if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail6();
7934
8375
  seen.add(id2(invoice2));
7935
8376
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
7936
8377
  }
7937
8378
  if (!list.has_more) break;
7938
- if (!list.data.length || page === 9) fail5("refund_history_incomplete");
8379
+ if (!list.data.length || page === 9) fail6("refund_history_incomplete");
7939
8380
  cursor = id2(list.data[list.data.length - 1]);
7940
8381
  }
7941
- if (!earliest) fail5("no_charge");
8382
+ if (!earliest) fail6("no_charge");
7942
8383
  const invoice = earliest;
7943
- if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
8384
+ if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail6();
7944
8385
  let chargeId = id2(invoice.charge);
7945
8386
  let paymentIntentId = id2(invoice.payment_intent);
7946
8387
  if (!chargeId && !paymentIntentId) {
7947
8388
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
7948
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
8389
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail6("refund_payment_ambiguous");
7949
8390
  const payment = row(payments.data[0]);
7950
- if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
8391
+ if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail6();
7951
8392
  paymentIntentId = id2(row(payment.payment).payment_intent);
7952
8393
  }
7953
8394
  if (!chargeId) {
7954
- if (!providerId2(paymentIntentId, "pi")) fail5();
8395
+ if (!providerId2(paymentIntentId, "pi")) fail6();
7955
8396
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
7956
- if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
8397
+ if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail6();
7957
8398
  chargeId = id2(payment.latest_charge);
7958
8399
  }
7959
- if (!providerId2(chargeId, "ch")) fail5();
8400
+ if (!providerId2(chargeId, "ch")) fail6();
7960
8401
  const charge = await call("GET", `/v1/charges/${chargeId}`);
7961
- if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail5();
7962
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
8402
+ if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail6();
8403
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail6();
7963
8404
  if (charge.amount_refunded !== charge.amount) {
7964
8405
  const refund = await call("POST", "/v1/refunds", {
7965
8406
  charge: chargeId,
7966
8407
  ...input.metadata ? { metadata: input.metadata } : {}
7967
8408
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
7968
- if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail5("refund_not_confirmed");
8409
+ if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail6("refund_not_confirmed");
7969
8410
  }
7970
8411
  let subscriptionCanceled = sub.status === "canceled";
7971
8412
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
7972
8413
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
7973
- if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
8414
+ if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail6("refund_cancel_incomplete");
7974
8415
  subscriptionCanceled = true;
7975
8416
  }
7976
8417
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
7977
- if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail5("refund_not_confirmed");
8418
+ if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail6("refund_not_confirmed");
7978
8419
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
7979
8420
  }
7980
8421
 
@@ -8040,7 +8481,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
8040
8481
  idempotencyKey ? { idempotencyKey } : {}
8041
8482
  );
8042
8483
  }
8043
- function fail6(op, r) {
8484
+ function fail7(op, r) {
8044
8485
  const err = new Error(`stripe ${op} failed: ${r.status}`);
8045
8486
  err.code = "stripe_error";
8046
8487
  throw err;
@@ -8090,7 +8531,7 @@ function createStripeProvider(config) {
8090
8531
  { email: input.email, name: input.name, metadata: meta },
8091
8532
  `cus:${input.applicationId}`
8092
8533
  );
8093
- if (!cust.ok) fail6("customer create", cust);
8534
+ if (!cust.ok) fail7("customer create", cust);
8094
8535
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
8095
8536
  customerId = cust.body.id;
8096
8537
  }
@@ -8122,7 +8563,7 @@ function createStripeProvider(config) {
8122
8563
  // retry between create and the db write can't orphan a second one.
8123
8564
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
8124
8565
  );
8125
- if (!sub.ok) fail6("subscription create", sub);
8566
+ if (!sub.ok) fail7("subscription create", sub);
8126
8567
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
8127
8568
  const subscriptionId = sub.body.id;
8128
8569
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -8132,14 +8573,14 @@ function createStripeProvider(config) {
8132
8573
  "expand[2]": "discounts",
8133
8574
  "expand[3]": "latest_invoice.discounts"
8134
8575
  });
8135
- if (!expanded.ok) fail6("subscription invoice evidence", expanded);
8576
+ if (!expanded.ok) fail7("subscription invoice evidence", expanded);
8136
8577
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
8137
8578
  sub = expanded;
8138
8579
  }
8139
8580
  verifySubscriptionResult(sub.body, input, customerId, meta);
8140
8581
  const invoice = sub.body.latest_invoice;
8141
8582
  const clientSecret2 = clientSecretOf(sub.body);
8142
- if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
8583
+ if (!clientSecret2) fail7("subscription confirmation-secret missing", sub);
8143
8584
  await tagSubscriptionPayment(
8144
8585
  sk,
8145
8586
  invoice,
@@ -8964,6 +9405,58 @@ var handleNamedSeat = async (req, url, env, ctx) => {
8964
9405
  }
8965
9406
  };
8966
9407
 
9408
+ // src/named-seat-accepted-template.ts
9409
+ var SEAT_ACCEPTED_TEMPLATE = {
9410
+ enabled: true,
9411
+ subject: "{{recipientName}} accepted your {{chapterName}} seat",
9412
+ text: "Hi {{firstName}},\n\n{{recipientName}} has accepted the {{tierName}} seat you purchased at {{chapterName}}. They will complete their own application and any membership requirements from here; nothing more is needed from you.\n\nWarmly,\n{{chapterName}}"
9413
+ };
9414
+
9415
+ // src/named-seat-accepted-notice.ts
9416
+ var text5 = (value) => typeof value === "string" ? value : "";
9417
+ async function notifyNamedSeatAccepted(db, env, ctx, input) {
9418
+ const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
9419
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
9420
+ return { sent: false, reason: "purchaser-missing" };
9421
+ }
9422
+ const group = await membershipLoad(db, "groups", ctx.chapter.id);
9423
+ if (!group) return { sent: false, reason: "group-missing" };
9424
+ const config = emailGroupFrom(group);
9425
+ config.emailTemplates = { seatAccepted: SEAT_ACCEPTED_TEMPLATE, ...config.emailTemplates };
9426
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
9427
+ return sendTemplated({
9428
+ db,
9429
+ envName: env.ODLA_ENV,
9430
+ sender: env.SEND_EMAIL,
9431
+ from: env.EMAIL_FROM,
9432
+ debugEmail: env.ODLA_DEBUG_EMAIL,
9433
+ now: () => Date.now(),
9434
+ newId: () => crypto.randomUUID()
9435
+ }, {
9436
+ group: config,
9437
+ template: "seatAccepted",
9438
+ to: text5(purchaser.email),
9439
+ dedupeKey: `named-seat-accepted:${input.seatId}`,
9440
+ applicationId: input.primaryApplicationId,
9441
+ vars: {
9442
+ firstName: text5(purchaser.firstName),
9443
+ purchaserName,
9444
+ recipientName: input.recipientName,
9445
+ tierName: await tierName(db, group, input.skuId),
9446
+ chapterName: ctx.chapter.name,
9447
+ membersUrl: `${input.origin}/members/`
9448
+ }
9449
+ });
9450
+ }
9451
+ async function tierName(db, group, skuId) {
9452
+ try {
9453
+ const tier = (await loadTiers(db, group)).find((candidate) => candidate.id === skuId);
9454
+ if (tier) return tier.name;
9455
+ } catch {
9456
+ }
9457
+ return skuId;
9458
+ }
9459
+
8967
9460
  // src/worker-routes-named-seat-recipient.ts
8968
9461
  var handleNamedSeatRecipient = async (req, url, env, ctx) => {
8969
9462
  if (url.pathname === "/api/named-seat/invitation") {
@@ -9081,6 +9574,13 @@ var handleNamedSeatRecipient = async (req, url, env, ctx) => {
9081
9574
  const current = await membershipLoad(db, "applications", body.applicationId), group = await membershipLoad(db, "groups", ctx.chapter.id);
9082
9575
  if (!current || current.namedSeatClaimPending !== false || !current.namedSeatAcceptedAt || !group) throw new Error("seat application reconciliation pending");
9083
9576
  if (current.clerkUserId !== user.userId || current.groupId !== ctx.chapter.id || current.tierId !== invitation.skuId || stripeRuntimeIdentity(current.stripeRuntime) !== runtime) throw new Error("seat application identity changed");
9577
+ await notifyNamedSeatAccepted(db, env, ctx, {
9578
+ seatId: body.seatId,
9579
+ primaryApplicationId: invitation.primaryApplicationId,
9580
+ recipientName: invitation.recipientName,
9581
+ skuId: invitation.skuId,
9582
+ origin: url.origin
9583
+ }).catch(() => console.warn("named_seat_accepted_notice_incomplete", { seatId: body.seatId }));
9084
9584
  await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) }).catch(() => void 0);
9085
9585
  const state = await continueJoin(db, ctx, env, url.origin, current, group);
9086
9586
  if (!state) throw new Error("seat Journey reconciliation pending");
@@ -9441,8 +9941,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
9441
9941
  };
9442
9942
 
9443
9943
  // src/crm-account-repair.ts
9444
- async function repairAccountPeoplePage(deps2, after) {
9445
- const users = (await deps2.db.query({ $users: { $: {
9944
+ async function repairAccountPeoplePage(deps, after) {
9945
+ const users = (await deps.db.query({ $users: { $: {
9446
9946
  ...after ? { where: { id: { $gt: after } } } : {},
9447
9947
  order: { id: "asc" },
9448
9948
  limit: 50
@@ -9451,7 +9951,7 @@ async function repairAccountPeoplePage(deps2, after) {
9451
9951
  const errors = [];
9452
9952
  for (const user of users) {
9453
9953
  try {
9454
- if (await syncAccountToCrm(deps2, String(user.id))) synced++;
9954
+ if (await syncAccountToCrm(deps, String(user.id))) synced++;
9455
9955
  } catch (error) {
9456
9956
  const message = error instanceof Error ? error.message : "";
9457
9957
  errors.push({ userId: String(user.id), code: /^crm_projection_identity_(ambiguous|archived|conflict)$/.test(message) ? message : "crm_projection_unavailable" });
@@ -9463,8 +9963,8 @@ async function repairAccountPeoplePage(deps2, after) {
9463
9963
  // src/crm-application-repair.ts
9464
9964
  var import_crm11 = require("@odla-ai/crm");
9465
9965
  async function repairApplicationLink(db, chapter, recordId, actorId) {
9466
- const deps2 = { crm: chapter.crm, db };
9467
- const record4 = await (0, import_crm11.getRecord)(deps2, recordId);
9966
+ const deps = { crm: chapter.crm, db };
9967
+ const record4 = await (0, import_crm11.getRecord)(deps, recordId);
9468
9968
  if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
9469
9969
  if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
9470
9970
  const fields = record4.fields;
@@ -9482,7 +9982,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
9482
9982
  }
9483
9983
  const app = [...applications.values()][0];
9484
9984
  if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
9485
- await (0, import_crm11.updateRecord)(deps2, {
9985
+ await (0, import_crm11.updateRecord)(deps, {
9486
9986
  id: recordId,
9487
9987
  input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
9488
9988
  expectedUpdatedAt: record4.updatedAt,
@@ -10180,15 +10680,6 @@ async function loadGroup2(db, id6) {
10180
10680
  return groups?.[0] ?? null;
10181
10681
  }
10182
10682
  var str2 = (v, d = "") => typeof v === "string" ? v : d;
10183
- var notifyDeps = (db, env) => ({
10184
- db,
10185
- debugEmail: env.ODLA_DEBUG_EMAIL,
10186
- envName: env.ODLA_ENV,
10187
- sender: env.SEND_EMAIL,
10188
- from: env.EMAIL_FROM,
10189
- now: () => Date.now(),
10190
- newId: () => crypto.randomUUID()
10191
- });
10192
10683
  var handleAdminGroupEmail = async (req, url, env, ctx) => {
10193
10684
  if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
10194
10685
  const got = await gate5(req, env, ctx);
@@ -10207,6 +10698,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
10207
10698
  groupId: group.id,
10208
10699
  name: group.name,
10209
10700
  replyTo: str2(group.replyTo),
10701
+ fromName: str2(group.fromName),
10210
10702
  notificationEmail: str2(group.notificationEmail),
10211
10703
  debugEmail: str2(group.debugEmail),
10212
10704
  emailTemplates,
@@ -10233,11 +10725,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
10233
10725
  for (const key3 of EMAIL_TEMPLATE_NAMES) {
10234
10726
  const t = templates[key3];
10235
10727
  const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
10236
- const text6 = typeof t?.text === "string" ? t.text : "";
10237
- if (!subject || !text6.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
10728
+ const text8 = typeof t?.text === "string" ? t.text : "";
10729
+ if (!subject || !text8.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
10238
10730
  if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
10239
- if (text6.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
10240
- clean[key3] = { subject, text: text6, enabled: t?.enabled !== false };
10731
+ if (text8.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
10732
+ clean[key3] = { subject, text: text8, enabled: t?.enabled !== false };
10241
10733
  }
10242
10734
  const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
10243
10735
  const notificationEmail = str2(body.notificationEmail).trim();
@@ -10246,13 +10738,15 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
10246
10738
  if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
10247
10739
  if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
10248
10740
  if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
10741
+ const fromName = str2(body.fromName).trim();
10742
+ if (/[\r\n<>]/.test(fromName) || fromName.length > 100) return json({ error: "from name must be one line of at most 100 characters with no angle brackets" }, 400);
10249
10743
  const commitmentText = str2(body.commitmentText);
10250
10744
  const normsText = str2(body.normsText);
10251
10745
  if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
10252
10746
  const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
10253
10747
  await db.transact([
10254
10748
  { t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
10255
- { t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail } }
10749
+ { t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
10256
10750
  ]);
10257
10751
  return json({ ok: true });
10258
10752
  };
@@ -10301,11 +10795,11 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
10301
10795
  }
10302
10796
  const group = await loadGroup2(db, ctx.chapter.id);
10303
10797
  if (!group) return json({ error: "not found" }, 404);
10304
- const res = await sendTemplated(notifyDeps(db, env), {
10798
+ const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
10305
10799
  group: emailGroupFrom(group),
10306
10800
  template,
10307
10801
  to: str2(group.notificationEmail),
10308
- vars: { firstName: "Sample", lastName: "Person", email: "sample@example.com", phone: "(555) 010-0100", state: "CA", adminUrl: `${url.origin}/admin/`, membersUrl: `${url.origin}/members/` },
10802
+ vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
10309
10803
  dedupeKey: `test:${template}:${Date.now()}`,
10310
10804
  force: true
10311
10805
  });
@@ -10332,7 +10826,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
10332
10826
  const app = data.applications?.[0] ?? null;
10333
10827
  if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
10334
10828
  const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
10335
- const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl: `${url.origin}/admin/`, membersUrl: `${url.origin}/members/` } : null;
10829
+ const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl: adminUrlFor(url.origin), membersUrl: membersUrlFor(url.origin) } : null;
10336
10830
  const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
10337
10831
  let mailBody = typeof r.body === "string" ? r.body : null;
10338
10832
  if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
@@ -10356,6 +10850,28 @@ var handleAdminComms = async (req, url, env, ctx) => {
10356
10850
  var import_crm13 = require("@odla-ai/crm");
10357
10851
  var import_db9 = require("@odla-ai/db");
10358
10852
 
10853
+ // src/network-edge.ts
10854
+ var EDGE_UNPAIRED = "edge_unpaired";
10855
+ function pairingCommand(leaderId, targetId, env) {
10856
+ const scope2 = env ? ` --env ${env}` : "";
10857
+ return `odla-ai chapter edge pair --leader ${leaderId} --follower ${targetId}${scope2}`;
10858
+ }
10859
+ function edgeUnpaired(leaderId, target, env) {
10860
+ return {
10861
+ code: EDGE_UNPAIRED,
10862
+ target: target.id,
10863
+ secretName: target.secretName,
10864
+ message: `edge to "${target.id}" is unpaired: leader vault slot "${target.secretName}" is empty`,
10865
+ hint: pairingCommand(leaderId, target.id, env)
10866
+ };
10867
+ }
10868
+ function unpairedResponse(unpaired) {
10869
+ return new Response(JSON.stringify({ error: unpaired.message, ...unpaired }), {
10870
+ status: 502,
10871
+ headers: { "content-type": "application/json" }
10872
+ });
10873
+ }
10874
+
10359
10875
  // src/worker-network-push.ts
10360
10876
  var import_crm12 = require("@odla-ai/crm");
10361
10877
  var import_db8 = require("@odla-ai/db");
@@ -10368,15 +10884,15 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
10368
10884
  newId: () => crypto.randomUUID()
10369
10885
  };
10370
10886
  if (!secret) {
10371
- const error = `vault secret "${target.secretName}" is missing`;
10887
+ const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
10372
10888
  await (0, import_crm12.recordDeliveryAttempt)(crmDeps3, {
10373
10889
  recordId: record4.id,
10374
10890
  targetId: target.id,
10375
10891
  status: "failed",
10376
10892
  payloadVersion: 2,
10377
- error
10893
+ error: unpaired.message
10378
10894
  });
10379
- return { id: target.id, name: target.name, ok: false, error };
10895
+ return { id: target.id, name: target.name, ok: false, error: unpaired.message, code: unpaired.code, hint: unpaired.hint };
10380
10896
  }
10381
10897
  let payload;
10382
10898
  try {
@@ -10517,17 +11033,20 @@ async function snapshotOne(db, env, ctx, target) {
10517
11033
  publicSite: { available: null }
10518
11034
  };
10519
11035
  const secret = await getVaultSecret(db, target.secretName);
10520
- if (!secret) return {
10521
- ...base,
10522
- available: false,
10523
- transport: {
10524
- kind: transportKind,
10525
- ...target.binding ? { binding: target.binding } : {},
10526
- reachable: null,
10527
- error: { code: "edge_secret_missing", message: "edge secret is missing" }
10528
- },
10529
- error: "edge secret is missing"
10530
- };
11036
+ if (!secret) {
11037
+ const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
11038
+ return {
11039
+ ...base,
11040
+ available: false,
11041
+ transport: {
11042
+ kind: transportKind,
11043
+ ...target.binding ? { binding: target.binding } : {},
11044
+ reachable: null,
11045
+ error: unpaired
11046
+ },
11047
+ error: unpaired.message
11048
+ };
11049
+ }
10531
11050
  const destination = new URL("/api/network/snapshot", target.url);
10532
11051
  try {
10533
11052
  const transport = resolveNetworkTargetTransport(env, target);
@@ -11002,7 +11521,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
11002
11521
  if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
11003
11522
  return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
11004
11523
  }
11005
- const deps2 = { crm: ctx.chapter.crm, db };
11524
+ const deps = { crm: ctx.chapter.crm, db };
11006
11525
  if (hasFields && ctx.chapter.mode === "chapter" && record4.type === "person") {
11007
11526
  const updated2 = await updateChapterPerson({
11008
11527
  db,
@@ -11021,7 +11540,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
11021
11540
  record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
11022
11541
  });
11023
11542
  }
11024
- const updated = hasFields ? await (0, import_crm17.updateRecord)(deps2, {
11543
+ const updated = hasFields ? await (0, import_crm17.updateRecord)(deps, {
11025
11544
  id: record4.id,
11026
11545
  input: fields,
11027
11546
  mutationId: activityId,
@@ -11032,7 +11551,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
11032
11551
  authorId: `network:${verified.sender}`,
11033
11552
  meta: auditMeta
11034
11553
  }
11035
- }) : await (0, import_crm17.setStage)(deps2, {
11554
+ }) : await (0, import_crm17.setStage)(deps, {
11036
11555
  id: record4.id,
11037
11556
  to: input.stage,
11038
11557
  authorId: `network:${verified.sender}`,
@@ -11132,7 +11651,10 @@ function targetFor2(ctx, id6) {
11132
11651
  }
11133
11652
  async function readTargetRecords(db, env, ctx, target, query) {
11134
11653
  const secret = await getVaultSecret(db, target.secretName);
11135
- if (!secret) return { ok: false, error: "edge secret is missing" };
11654
+ if (!secret) {
11655
+ const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
11656
+ return { ok: false, error: unpaired.message, unpaired };
11657
+ }
11136
11658
  const destination = new URL("/api/network/records", target.url);
11137
11659
  destination.search = query.toString();
11138
11660
  try {
@@ -11169,7 +11691,7 @@ async function mutateTargetRecord(db, env, ctx, target, input) {
11169
11691
  return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
11170
11692
  }
11171
11693
  const secret = await getVaultSecret(db, target.secretName);
11172
- if (!secret) return json({ error: "edge secret is missing", code: "edge_unavailable" }, 502);
11694
+ if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
11173
11695
  const destination = new URL("/api/network/records", target.url);
11174
11696
  const body = JSON.stringify(input);
11175
11697
  try {
@@ -11251,7 +11773,9 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
11251
11773
  if (window.search) query.set("search", window.search);
11252
11774
  if (window.stage) query.set("stage", window.stage);
11253
11775
  const result = await readTargetRecords(got.db, env, ctx, target, query);
11254
- if (!result.ok) return json({ error: result.error }, result.status ?? 502);
11776
+ if (!result.ok) {
11777
+ return result.unpaired ? unpairedResponse(result.unpaired) : json({ error: result.error }, result.status ?? 502);
11778
+ }
11255
11779
  if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
11256
11780
  return json({ error: "follower did not apply the requested record query" }, 502);
11257
11781
  }
@@ -11503,7 +12027,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
11503
12027
  body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
11504
12028
  }
11505
12029
  const secret = await getVaultSecret(got.db, target.secretName);
11506
- if (!secret) return json({ error: "edge secret is missing" }, 502);
12030
+ if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
11507
12031
  const destination = new URL("/api/network/shared-notes", target.url);
11508
12032
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
11509
12033
  try {
@@ -11557,11 +12081,11 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
11557
12081
  if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
11558
12082
  const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
11559
12083
  const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
11560
- const provenance = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
12084
+ const provenance2 = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
11561
12085
  const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
11562
12086
  if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
11563
12087
  if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
11564
- if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
12088
+ if (!provenance2 || provenance2.length > 500) errors.push("provenance is required and bounded");
11565
12089
  if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
11566
12090
  if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
11567
12091
  errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
@@ -11629,7 +12153,7 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
11629
12153
  sourceKind: "external_reimport",
11630
12154
  sourceSystem,
11631
12155
  sourceRecordId,
11632
- provenance,
12156
+ provenance: provenance2,
11633
12157
  approved: true,
11634
12158
  isRealPerson: true,
11635
12159
  displayName,
@@ -12334,7 +12858,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
12334
12858
  }
12335
12859
 
12336
12860
  // src/worker-routes-network-person-registry.ts
12337
- var PATH3 = "/api/admin/network-person-registry";
12861
+ var PATH5 = "/api/admin/network-person-registry";
12338
12862
  var MAX_BODY2 = 8 * 1024 * 1024;
12339
12863
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
12340
12864
  var positiveRevision = (value) => {
@@ -12359,7 +12883,7 @@ function registryError(error) {
12359
12883
  return null;
12360
12884
  }
12361
12885
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12362
- if (url.pathname !== PATH3) return null;
12886
+ if (url.pathname !== PATH5) return null;
12363
12887
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
12364
12888
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
12365
12889
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -12371,9 +12895,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12371
12895
  if (req.method === "POST") {
12372
12896
  let body;
12373
12897
  try {
12374
- const text6 = await req.text();
12375
- if (new TextEncoder().encode(text6).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
12376
- const parsed = JSON.parse(text6);
12898
+ const text8 = await req.text();
12899
+ if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
12900
+ const parsed = JSON.parse(text8);
12377
12901
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
12378
12902
  body = parsed;
12379
12903
  } catch {
@@ -12542,7 +13066,7 @@ var import_db13 = require("@odla-ai/db");
12542
13066
  // src/network-commercial-parity.ts
12543
13067
  var DIGEST = /^[0-9a-f]{64}$/;
12544
13068
  var MAX_RECORDS = 500;
12545
- var text4 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
13069
+ var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
12546
13070
  function parityMessage(partition, kind, value) {
12547
13071
  return [
12548
13072
  "chapter-commercial-parity",
@@ -12574,18 +13098,18 @@ async function digestWithKey(key3, partition, kind, value) {
12574
13098
  async function buildChapterCommercialParitySnapshot(input) {
12575
13099
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
12576
13100
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
12577
- const eligible = input.rows.filter((row2) => text4(row2.id) && (text4(row2.stripeCustomerId) || text4(row2.stripeSubscriptionId)));
13101
+ const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
12578
13102
  const selected2 = eligible.slice(0, maxRecords);
12579
13103
  const key3 = await parityKey(input.secret);
12580
13104
  const records = await Promise.all(selected2.map(async (row2) => {
12581
- const applicationId = text4(row2.id);
12582
- const customerId = text4(row2.stripeCustomerId);
12583
- const subscriptionId = text4(row2.stripeSubscriptionId);
13105
+ const applicationId = text6(row2.id);
13106
+ const customerId = text6(row2.stripeCustomerId);
13107
+ const subscriptionId = text6(row2.stripeSubscriptionId);
12584
13108
  return {
12585
13109
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
12586
13110
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
12587
13111
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
12588
- status: text4(row2.status, 64),
13112
+ status: text6(row2.status, 64),
12589
13113
  canceled: row2.canceled === true
12590
13114
  };
12591
13115
  }));
@@ -12611,7 +13135,7 @@ function validSnapshot(snapshot, source) {
12611
13135
  if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
12612
13136
  const seen = /* @__PURE__ */ new Set();
12613
13137
  for (const record4 of records) {
12614
- if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text4(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
13138
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text6(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
12615
13139
  const key3 = recordKey(record4);
12616
13140
  if (seen.has(key3)) return false;
12617
13141
  seen.add(key3);
@@ -12622,16 +13146,16 @@ async function reconcileCommercialParitySnapshot(input) {
12622
13146
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
12623
13147
  const sourceKey = networkCommercialSourceKey(input.source);
12624
13148
  const key3 = await parityKey(input.secret);
12625
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text4(row2.applicationId) && text4(row2.objectId)).map(async (row2) => {
12626
- const customerId = text4(row2.customerId);
12627
- const subscriptionId = text4(row2.subscriptionId) ?? text4(row2.objectId);
13149
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
13150
+ const customerId = text6(row2.customerId);
13151
+ const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
12628
13152
  return {
12629
13153
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
12630
13154
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
12631
13155
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
12632
13156
  status: null,
12633
13157
  canceled: false,
12634
- stripeStatus: text4(row2.status, 64),
13158
+ stripeStatus: text6(row2.status, 64),
12635
13159
  stripeCanceled: row2.status === "canceled"
12636
13160
  };
12637
13161
  }));
@@ -12757,7 +13281,7 @@ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
12757
13281
  async function reconcileTarget(db, env, ctx, target, projection) {
12758
13282
  const base = { targetId: target.id, name: target.name, url: target.url };
12759
13283
  const secret = await getVaultSecret(db, target.secretName);
12760
- if (!secret) return { ...base, available: false, ok: false, error: { code: "edge_secret_missing" } };
13284
+ if (!secret) return { ...base, available: false, ok: false, error: edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV) };
12761
13285
  const destination = new URL(PARITY_PATH, target.url);
12762
13286
  try {
12763
13287
  const transport = resolveNetworkTargetTransport(env, target);
@@ -12913,11 +13437,23 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
12913
13437
  const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
12914
13438
  const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
12915
13439
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
12916
- const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact, Date.now(), await stripePayloadDigest(body));
13440
+ const result = await ingestNetworkCommercialFact(
13441
+ db,
13442
+ ctx.chapter.networkCommercialReadModel.sources,
13443
+ fact,
13444
+ Date.now(),
13445
+ await stripePayloadDigest(body),
13446
+ networkCommercialAlerts(ctx, WEBHOOK_PATH)
13447
+ );
12917
13448
  if (result.status === "collision") {
12918
13449
  console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
12919
13450
  }
12920
- return json({ ok: true, status: result.status, eventId: fact.eventId });
13451
+ return json({
13452
+ ok: true,
13453
+ status: result.status,
13454
+ eventId: fact.eventId,
13455
+ ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
13456
+ });
12921
13457
  } catch (error) {
12922
13458
  if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
12923
13459
  throw error;
@@ -12941,18 +13477,39 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12941
13477
  } catch {
12942
13478
  return json({ error: "invalid JSON body" }, 400);
12943
13479
  }
13480
+ const alerts = networkCommercialAlerts(ctx, ADMIN_PATH2);
12944
13481
  if (parsed.command === "recover-event") {
12945
13482
  try {
12946
13483
  return json(await recoverNetworkCommercialEvent(
12947
13484
  db,
12948
13485
  ctx.chapter.networkCommercialReadModel.sources,
12949
13486
  String(parsed.eventId ?? ""),
12950
- got.user.userId
13487
+ got.user.userId,
13488
+ Date.now(),
13489
+ alerts
12951
13490
  ));
12952
13491
  } catch (error) {
12953
13492
  return json({ error: error instanceof Error ? error.message : "event recovery failed" }, 409);
12954
13493
  }
12955
13494
  }
13495
+ if (parsed.command === "resolve") {
13496
+ if (typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) return json({ error: "resolve requires a valid mutationId" }, 400);
13497
+ const reason = typeof parsed.reason === "string" ? parsed.reason.trim() : "";
13498
+ if (!reason || reason.length > 500) return json({ error: "resolve requires a reason of at most 500 characters" }, 400);
13499
+ try {
13500
+ const result = await resolveNetworkCommercialCollision(
13501
+ db,
13502
+ ctx.chapter.networkCommercialReadModel.sources,
13503
+ { eventId: String(parsed.eventId ?? ""), mutationId: parsed.mutationId, actorUserId: got.user.userId, reason },
13504
+ Date.now(),
13505
+ alerts
13506
+ );
13507
+ return json({ resolved: true, eventId: result.eventId, reviewId: result.reviewId, ...safeProjection(result.projection) });
13508
+ } catch (error) {
13509
+ const code = error.code;
13510
+ return json({ error: error instanceof Error ? error.message : "resolve failed", code }, code === "invalid_event" ? 400 : 409);
13511
+ }
13512
+ }
12956
13513
  if (parsed.command === "reconcile-chapters") {
12957
13514
  const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
12958
13515
  return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
@@ -12980,9 +13537,9 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12980
13537
  }
12981
13538
  }
12982
13539
  if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
12983
- return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also requires a valid mutationId" }, 400);
13540
+ return json({ error: "rebuild, resolve, recover-event, reconcile-stripe, or reconcile-chapters command is required; rebuild and resolve also require a valid mutationId" }, 400);
12984
13541
  }
12985
- const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
13542
+ const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId, Date.now(), alerts);
12986
13543
  return json({ rebuilt: true, ...safeProjection(projection) });
12987
13544
  }
12988
13545
  return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
@@ -13092,7 +13649,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
13092
13649
  function unknownKeys3(value, allowed2, path, errors) {
13093
13650
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
13094
13651
  }
13095
- function text5(value, path, errors, max = MAX_TEXT2, required = true) {
13652
+ function text7(value, path, errors, max = MAX_TEXT2, required = true) {
13096
13653
  if (value === void 0 && !required) return void 0;
13097
13654
  if (typeof value !== "string" || required && !value.trim()) {
13098
13655
  errors.push(`${path} must be a non-empty string`);
@@ -13118,7 +13675,7 @@ function scopeValue(value, path, errors) {
13118
13675
  return { kind: raw2.kind };
13119
13676
  }
13120
13677
  if (raw2.kind === "chapter") {
13121
- const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
13678
+ const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
13122
13679
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
13123
13680
  return { kind: "chapter", chapterId };
13124
13681
  }
@@ -13132,22 +13689,22 @@ function resourceValue(value, path, errors) {
13132
13689
  return null;
13133
13690
  }
13134
13691
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
13135
- const id6 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
13692
+ const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
13136
13693
  if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
13137
- const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
13694
+ const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
13138
13695
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
13139
- const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
13140
- const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
13141
- const status2 = text5(raw2.status, `${path}.status`, errors) ?? "";
13696
+ const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
13697
+ const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
13698
+ const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
13142
13699
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
13143
13700
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
13144
13701
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
13145
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
13702
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
13146
13703
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
13147
13704
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
13148
13705
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
13149
- const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
13150
- const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
13706
+ const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
13707
+ const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
13151
13708
  if (url) {
13152
13709
  try {
13153
13710
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -13159,9 +13716,9 @@ function resourceValue(value, path, errors) {
13159
13716
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
13160
13717
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
13161
13718
  const brandAsset = asset ? {
13162
- bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
13163
- assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
13164
- ...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
13719
+ bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
13720
+ assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
13721
+ ...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
13165
13722
  } : void 0;
13166
13723
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
13167
13724
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -13176,21 +13733,21 @@ function resourceValue(value, path, errors) {
13176
13733
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
13177
13734
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
13178
13735
  const promotedFrom = promotedRaw ? {
13179
- chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
13180
- resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
13736
+ chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
13737
+ resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
13181
13738
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
13182
13739
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
13183
- promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
13740
+ promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
13184
13741
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
13185
13742
  } : void 0;
13186
- const provenance = {
13187
- createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
13743
+ const provenance2 = {
13744
+ createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
13188
13745
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
13189
- updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
13746
+ updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
13190
13747
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
13191
13748
  ...promotedFrom ? { promotedFrom } : {}
13192
13749
  };
13193
- return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
13750
+ return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
13194
13751
  }
13195
13752
  function validateBuilderLibraryDocument(value, options = {}) {
13196
13753
  const errors = [];
@@ -13205,7 +13762,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
13205
13762
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
13206
13763
  const sourceType = sourceRaw?.type;
13207
13764
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
13208
- const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
13765
+ const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
13209
13766
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
13210
13767
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
13211
13768
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -13213,8 +13770,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
13213
13770
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
13214
13771
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
13215
13772
  const target = targetRaw ? {
13216
- appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
13217
- environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
13773
+ appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
13774
+ environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
13218
13775
  } : void 0;
13219
13776
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
13220
13777
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15226,7 +15783,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
15226
15783
  };
15227
15784
  signal.addEventListener("abort", cancel, { once: true });
15228
15785
  let size = 0;
15229
- let text6 = "";
15786
+ let text8 = "";
15230
15787
  const decoder = new TextDecoder();
15231
15788
  try {
15232
15789
  while (true) {
@@ -15236,10 +15793,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
15236
15793
  if (done) break;
15237
15794
  size += value.byteLength;
15238
15795
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
15239
- text6 += decoder.decode(value, { stream: true });
15796
+ text8 += decoder.decode(value, { stream: true });
15240
15797
  }
15241
15798
  try {
15242
- return JSON.parse(text6 + decoder.decode());
15799
+ return JSON.parse(text8 + decoder.decode());
15243
15800
  } catch {
15244
15801
  throw new TypeError(`invalid ${label}`);
15245
15802
  }
@@ -15901,6 +16458,7 @@ var BUILTIN_ROUTES = [
15901
16458
  handlePaymentRecovery,
15902
16459
  handleAdminNamedSeat,
15903
16460
  handleAdminMembershipSeats,
16461
+ handleAdminMembershipPolicyVersions,
15904
16462
  handleAdminMeetings,
15905
16463
  handleAdminScheduling,
15906
16464
  // Roster + identity