@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.
@@ -143,11 +143,11 @@ async function clerkVerifiedPrimaryEmail(db, userId) {
143
143
 
144
144
  // src/crm-person-projection.ts
145
145
  import { createRecord } from "@odla-ai/crm";
146
- async function ensureProjectedPerson(deps2, input, options = {}) {
146
+ async function ensureProjectedPerson(deps, input, options = {}) {
147
147
  const email = String(input.email ?? "").trim().toLowerCase();
148
148
  if (!email) throw new Error("crm_projection_email_required");
149
149
  const find = async () => {
150
- const records = (await deps2.db.query({ crm_record: { $: {
150
+ const records = (await deps.db.query({ crm_record: { $: {
151
151
  where: { type: "person", primaryEmail: email },
152
152
  limit: 2
153
153
  } } })).crm_record ?? [];
@@ -160,7 +160,7 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
160
160
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
161
161
  const id6 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
162
162
  try {
163
- const created = await createRecord({ ...deps2, db: deps2.db }, {
163
+ const created = await createRecord({ ...deps, db: deps.db }, {
164
164
  id: id6,
165
165
  type: "person",
166
166
  input: { ...input, email },
@@ -176,14 +176,14 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
176
176
  }
177
177
 
178
178
  // src/crm-account-projection.ts
179
- async function syncAccountToCrm(deps2, userId) {
180
- const user = (await deps2.db.query({ $users: { $: { where: { id: userId }, limit: 1 } } })).$users?.[0];
179
+ async function syncAccountToCrm(deps, userId) {
180
+ const user = (await deps.db.query({ $users: { $: { where: { id: userId }, limit: 1 } } })).$users?.[0];
181
181
  if (!user || user.deleted === true || typeof user.email !== "string" || !user.email.trim()) return null;
182
- const linked = (await deps2.db.query({ crm_record: { $: { where: { type: "person", clerkUserId: userId }, limit: 2 } } })).crm_record ?? [];
182
+ const linked = (await deps.db.query({ crm_record: { $: { where: { type: "person", clerkUserId: userId }, limit: 2 } } })).crm_record ?? [];
183
183
  if (linked.length > 1) throw new Error("crm_projection_identity_ambiguous");
184
184
  if (linked.length === 1) return String(linked[0].id);
185
185
  const email = user.email.trim().toLowerCase();
186
- const peers = (await deps2.db.query({ $users: { $: { where: { email }, limit: 2 } } })).$users ?? [];
186
+ const peers = (await deps.db.query({ $users: { $: { where: { email }, limit: 2 } } })).$users ?? [];
187
187
  if (peers.length !== 1 || peers[0]?.id !== userId) throw new Error("crm_projection_identity_ambiguous");
188
188
  const userGuard = {
189
189
  ns: "$users",
@@ -192,14 +192,14 @@ async function syncAccountToCrm(deps2, userId) {
192
192
  equals: { email: user.email, ...user.deleted !== void 0 ? { deleted: user.deleted } : {} },
193
193
  ...user.deleted === void 0 ? { absent: ["deleted"] } : {}
194
194
  };
195
- const { record: record4 } = await ensureProjectedPerson(deps2, {
195
+ const { record: record4 } = await ensureProjectedPerson(deps, {
196
196
  name: typeof user.name === "string" && user.name.trim() ? user.name : email,
197
197
  email
198
198
  }, { stage: null, guards: [userGuard], emailConsent: { state: "unsubscribed", source: "account-no-marketing-consent" } });
199
199
  if (record4.clerkUserId === userId) return String(record4.id);
200
200
  if (record4.clerkUserId != null) throw new Error("crm_projection_identity_conflict");
201
201
  try {
202
- await deps2.db.transact([{ t: "update", ns: "crm_record", id: String(record4.id), attrs: { clerkUserId: userId } }], {
202
+ await deps.db.transact([{ t: "update", ns: "crm_record", id: String(record4.id), attrs: { clerkUserId: userId } }], {
203
203
  guards: [userGuard, {
204
204
  ns: "crm_record",
205
205
  id: String(record4.id),
@@ -215,12 +215,37 @@ async function syncAccountToCrm(deps2, userId) {
215
215
  });
216
216
  } catch (error) {
217
217
  if (error.code !== "transact_guard_failed") throw error;
218
- const current = (await deps2.db.query({ crm_record: { $: { where: { id: record4.id }, limit: 1 } } })).crm_record?.[0];
218
+ const current = (await deps.db.query({ crm_record: { $: { where: { id: record4.id }, limit: 1 } } })).crm_record?.[0];
219
219
  if (current?.clerkUserId !== userId) throw error;
220
220
  }
221
221
  return String(record4.id);
222
222
  }
223
223
 
224
+ // src/worker-alerts.ts
225
+ var consoleErrorRecorder = (error, report) => {
226
+ console.error("chapter_alert", JSON.stringify({ message: error.message, ...report }));
227
+ };
228
+ function reportChapterAlert(ctx, error, report) {
229
+ (ctx?.recordError ?? consoleErrorRecorder)(error, report);
230
+ }
231
+ var ACTIVATION_LIMIT = 50;
232
+ function networkCommercialAlerts(ctx, route) {
233
+ return {
234
+ exceptionsActivated(activated, activeCount) {
235
+ if (activated.length === 0) return;
236
+ reportChapterAlert(ctx, new Error("network commercial exception activated"), {
237
+ code: "network_commercial_exception_activated",
238
+ route,
239
+ attributes: {
240
+ "odla.commercial.active_exceptions": activeCount,
241
+ "odla.commercial.activated_exceptions": activated.length
242
+ },
243
+ artifacts: { exceptions: activated.slice(0, ACTIVATION_LIMIT).map(({ eventId, kind }) => ({ eventId, kind })) }
244
+ });
245
+ }
246
+ };
247
+ }
248
+
224
249
  // src/worker-context.ts
225
250
  var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
226
251
  function createWorkerContext(options) {
@@ -330,6 +355,7 @@ function createWorkerContext(options) {
330
355
  requirePaymentQuote: options.requirePaymentQuote === true,
331
356
  membershipAuthority: options.membershipAuthority,
332
357
  membershipAuthorityOperator: options.membershipAuthorityOperator,
358
+ recordError: options.recordError ?? consoleErrorRecorder,
333
359
  getPublicConfig,
334
360
  verifyUser,
335
361
  makeDb,
@@ -823,6 +849,17 @@ function projectNetworkCommercialEvents(input, sources) {
823
849
  };
824
850
  }
825
851
 
852
+ // src/network-commercial-fact-identity.ts
853
+ var stable = (value) => JSON.stringify(value, function(_key, item) {
854
+ return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
855
+ });
856
+ function sameNetworkCommercialFact(stored, incoming) {
857
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)) return false;
858
+ const comparable = { ...incoming };
859
+ if (!stored.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
860
+ return stable(comparable) === stable(stored);
861
+ }
862
+
826
863
  // src/network-commercial-store.ts
827
864
  var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
828
865
  var projectionId = (kind, sourceKey, providerId3) => `network-commercial:${kind}:${sourceKey}:${providerId3}`;
@@ -834,6 +871,7 @@ var materializedNamespaces = [
834
871
  "networkCommercialRefunds",
835
872
  "networkCommercialSeats"
836
873
  ];
874
+ var RESOLUTION_ATTRS = ["resolvedDigest", "reviewedDigest", "resolvedBy", "reviewId"];
837
875
  async function rows(db, ns) {
838
876
  const all = [];
839
877
  const pageSize = 500;
@@ -883,13 +921,14 @@ function projectionOps(projection, now) {
883
921
  }
884
922
  })));
885
923
  }
886
- async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
924
+ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now(), alerts) {
887
925
  const projection = await loadNetworkCommercialProjection(db, sources);
888
926
  const [existing, ...materialized] = await Promise.all([
889
927
  rows(db, "networkCommercialExceptions"),
890
928
  ...materializedNamespaces.map((ns) => rows(db, ns))
891
929
  ]);
892
930
  const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
931
+ const wasActive = new Set(existing.filter((row2) => row2.status === "active").map((row2) => String(row2.id)));
893
932
  const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
894
933
  const ops = [
895
934
  ...projectionOps(projection, now),
@@ -915,27 +954,80 @@ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now =
915
954
  }
916
955
  }))
917
956
  ];
918
- if (ops.length > 0) await db.transact(ops, { mutationId });
957
+ if (ops.length === 0) return projection;
958
+ const write = await db.transact(ops, { mutationId });
959
+ if (!write.duplicate) {
960
+ alerts?.exceptionsActivated(
961
+ projection.exceptions.filter((item) => !wasActive.has(exceptionId(item.eventId, item.kind))),
962
+ projection.exceptions.length
963
+ );
964
+ }
919
965
  return projection;
920
966
  }
921
- async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(), legacyDigest) {
922
- const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
923
- const existing = found.networkCommercialEvents?.[0];
924
- if (existing) {
925
- const projection2 = await loadNetworkCommercialProjection(db, sources);
926
- if (existing.digest === fact.digest || existing.digest === legacyDigest) return { status: "replayed", projection: projection2 };
927
- const id7 = exceptionId(fact.eventId, "collision");
928
- await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
929
- id: id7,
967
+ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
968
+ const id6 = exceptionId(fact.eventId, "collision");
969
+ const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } } })).networkCommercialExceptions?.[0];
970
+ const exact = stored.digest === fact.digest || stored.digest === legacyDigest;
971
+ const canonical3 = !exact && sameNetworkCommercialFact(stored.fact, fact);
972
+ if (exact || canonical3) {
973
+ const recorded = exception?.incomingFact;
974
+ const resolvable = canonical3 && exception?.status === "active" && (recorded === void 0 || sameNetworkCommercialFact(stored.fact, recorded));
975
+ if (!resolvable) return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
976
+ const reviewId = crypto.randomUUID();
977
+ await db.transact([
978
+ { t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
979
+ id: reviewId,
980
+ eventId: fact.eventId,
981
+ actorUserId: "system",
982
+ reviewedAt: now,
983
+ previousDigest: String(stored.digest),
984
+ providerDigest: fact.digest,
985
+ scope: "network",
986
+ previousState: exception,
987
+ action: "canonical_replay",
988
+ reason: "a resend matched the retained canonical fact"
989
+ } },
990
+ { t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
991
+ status: "resolved",
992
+ resolvedDigest: stored.digest,
993
+ reviewedDigest: fact.digest,
994
+ resolvedBy: "system",
995
+ reviewId,
996
+ updatedAt: now
997
+ } }
998
+ ], { mutationId: `network-commercial-resolve:${fact.eventId}:${fact.digest}`, guards: [
999
+ { ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
1000
+ ] });
1001
+ const projection2 = await rebuildNetworkCommercialProjection(db, sources, `network-commercial-resolve-rebuild:${fact.eventId}:${fact.digest}`, now, alerts);
1002
+ return { status: "replayed", resolvedCollision: true, projection: projection2 };
1003
+ }
1004
+ if (exception?.status === "resolved" && exception.reviewedDigest === fact.digest) {
1005
+ return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
1006
+ }
1007
+ const stale = RESOLUTION_ATTRS.filter((attr3) => exception !== void 0 && Object.hasOwn(exception, attr3));
1008
+ await db.transact([
1009
+ ...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id: id6, attrs: stale }] : [],
1010
+ { t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
1011
+ id: id6,
930
1012
  eventId: fact.eventId,
931
1013
  kind: "collision",
932
1014
  status: "active",
933
1015
  incomingDigest: fact.digest,
1016
+ incomingFact: fact,
934
1017
  detail: { eventType: fact.eventType, objectId: fact.objectId },
935
1018
  updatedAt: now
936
- } }]);
937
- return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
1019
+ } }
1020
+ ]);
1021
+ const projection = await loadNetworkCommercialProjection(db, sources);
1022
+ if (exception?.status !== "active") {
1023
+ alerts?.exceptionsActivated([{ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } }], projection.exceptions.length);
938
1024
  }
1025
+ return { status: "collision", projection };
1026
+ }
1027
+ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(), legacyDigest, alerts) {
1028
+ const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
1029
+ const existing = found.networkCommercialEvents?.[0];
1030
+ if (existing) return collide(db, sources, existing, fact, now, legacyDigest, alerts);
939
1031
  const id6 = eventRowId(fact.eventId);
940
1032
  let lostRace = false;
941
1033
  let duplicate = false;
@@ -961,25 +1053,15 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
961
1053
  if (duplicate || lostRace) {
962
1054
  const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
963
1055
  const row2 = raced.networkCommercialEvents?.[0];
964
- const projection2 = await loadNetworkCommercialProjection(db, sources);
965
- if (row2?.digest === fact.digest || row2?.digest === legacyDigest) return { status: "replayed", projection: projection2 };
966
- const id7 = exceptionId(fact.eventId, "collision");
967
- await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
968
- id: id7,
969
- eventId: fact.eventId,
970
- kind: "collision",
971
- status: "active",
972
- incomingDigest: fact.digest,
973
- detail: { eventType: fact.eventType, objectId: fact.objectId },
974
- updatedAt: now
975
- } }]);
976
- return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
1056
+ if (!row2) throw new Error("network commercial event vanished after a lost race");
1057
+ return collide(db, sources, row2, fact, now, legacyDigest, alerts);
977
1058
  }
978
1059
  const projection = await rebuildNetworkCommercialProjection(
979
1060
  db,
980
1061
  sources,
981
1062
  `network-commercial-project:${fact.eventId}:${fact.digest}`,
982
- now
1063
+ now,
1064
+ alerts
983
1065
  );
984
1066
  return { status: "stored", projection };
985
1067
  }
@@ -994,10 +1076,7 @@ async function readStripeRecoveryEvent(db, eventId) {
994
1076
  const rawBody = JSON.stringify(response.body);
995
1077
  return { event: response.body, rawBody, digest: await stripeEventDigest(rawBody) };
996
1078
  }
997
- var stable = (value) => JSON.stringify(value, function(_key, item) {
998
- return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
999
- });
1000
- async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now()) {
1079
+ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
1001
1080
  const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
1002
1081
  const rows5 = await db.query({
1003
1082
  networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
@@ -1007,9 +1086,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1007
1086
  if (!stored) throw new Error("stored event not found");
1008
1087
  const provider = await readStripeRecoveryEvent(db, eventId);
1009
1088
  const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
1010
- const comparable = { ...fact };
1011
- if (!stored.fact?.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
1012
- if (stable(comparable) !== stable(stored.fact)) throw new Error("Stripe facts differ; collision requires investigation");
1089
+ if (!sameNetworkCommercialFact(stored.fact, fact)) throw new Error("Stripe facts differ; collision requires investigation");
1013
1090
  const id6 = crypto.randomUUID();
1014
1091
  await db.transact([
1015
1092
  { t: "update", ns: "paymentRecoveryReviews", id: id6, attrs: {
@@ -1017,6 +1094,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1017
1094
  eventId,
1018
1095
  actorUserId,
1019
1096
  reviewedAt: now,
1097
+ action: "recover",
1020
1098
  previousDigest: stored.digest,
1021
1099
  providerDigest: provider.digest,
1022
1100
  scope: "network",
@@ -1027,7 +1105,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1027
1105
  t: "update",
1028
1106
  ns: "networkCommercialExceptions",
1029
1107
  id: exceptionId2,
1030
- attrs: { status: "resolved", resolvedDigest: provider.digest, updatedAt: now }
1108
+ attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId: id6, updatedAt: now }
1031
1109
  }] : []
1032
1110
  ], { mutationId: `payment-recovery:${id6}`, guards: [
1033
1111
  { ns: "networkCommercialEvents", id: eventRowId2, exists: true, equals: { digest: stored.digest } },
@@ -1038,7 +1116,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
1038
1116
  ...exception ? { equals: { status: exception.status, updatedAt: exception.updatedAt } } : {}
1039
1117
  }
1040
1118
  ] });
1041
- await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now);
1119
+ await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now, alerts);
1042
1120
  return { recovered: true, eventId, reviewId: id6 };
1043
1121
  }
1044
1122
 
@@ -1200,12 +1278,12 @@ function applicationCrmFields(crm, app) {
1200
1278
  }
1201
1279
 
1202
1280
  // src/applicant-projection.ts
1203
- async function upsertPerson(deps2, opts) {
1281
+ async function upsertPerson(deps, opts) {
1204
1282
  const email = opts.email.toLowerCase();
1205
- const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
1206
- const { record: record4, created } = await ensureProjectedPerson(deps2, { ...opts.input, email });
1283
+ const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1284
+ const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
1207
1285
  if (!created) await updateRecord(crmDeps3, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
1208
- const initial = deps2.crm.initialStage("person");
1286
+ const initial = deps.crm.initialStage("person");
1209
1287
  if (!created && record4.stage == null && initial) {
1210
1288
  await setStage(crmDeps3, {
1211
1289
  id: String(record4.id),
@@ -1217,7 +1295,7 @@ async function upsertPerson(deps2, opts) {
1217
1295
  }
1218
1296
  return { recordId: String(record4.id) };
1219
1297
  }
1220
- async function projectApplicant(deps2, applicant) {
1298
+ async function projectApplicant(deps, applicant) {
1221
1299
  const base = sharedPersonInput({
1222
1300
  email: applicant.email,
1223
1301
  firstName: applicant.firstName,
@@ -1226,18 +1304,18 @@ async function projectApplicant(deps2, applicant) {
1226
1304
  linkedin: applicant.linkedin,
1227
1305
  hubRecordId: applicant.applicationId
1228
1306
  });
1229
- Object.assign(base, applicationCrmFields(deps2.crm, applicant));
1307
+ Object.assign(base, applicationCrmFields(deps.crm, applicant));
1230
1308
  const mutationId = `apply:${applicant.applicationId}`;
1231
1309
  const extra = applicant.extra ?? {};
1232
- if (Object.keys(extra).length === 0) return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
1310
+ if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
1233
1311
  try {
1234
- return await upsertPerson(deps2, { email: applicant.email, input: {
1312
+ return await upsertPerson(deps, { email: applicant.email, input: {
1235
1313
  ...base,
1236
1314
  ...extra,
1237
- ...applicationCrmFields(deps2.crm, applicant)
1315
+ ...applicationCrmFields(deps.crm, applicant)
1238
1316
  }, mutationId });
1239
1317
  } catch {
1240
- return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
1318
+ return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
1241
1319
  }
1242
1320
  }
1243
1321
 
@@ -1335,24 +1413,24 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
1335
1413
  if (input[nameField] === void 0) input[nameField] = record4.name;
1336
1414
  return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
1337
1415
  }
1338
- async function findSharedRecord(deps2, record4, tag) {
1416
+ async function findSharedRecord(deps, record4, tag) {
1339
1417
  const structured = await getRecordByOrigin(
1340
- { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId },
1418
+ { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1341
1419
  record4.sourceId,
1342
1420
  record4.sourceRecordId
1343
1421
  );
1344
1422
  if (structured) return structured;
1345
- const mapped = await deps2.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
1423
+ const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
1346
1424
  const mappedId = mapped.crm_tag?.[0]?.recordId;
1347
1425
  if (typeof mappedId === "string") {
1348
- const found = await deps2.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1426
+ const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1349
1427
  if (found.crm_record?.[0]) return found.crm_record[0];
1350
1428
  }
1351
- const def = deps2.crm.type(record4.type);
1429
+ const def = deps.crm.type(record4.type);
1352
1430
  const emailField = def.emailField;
1353
1431
  if (emailField && typeof record4.input[emailField] === "string") {
1354
1432
  const primaryEmail = record4.input[emailField].toLowerCase();
1355
- const found = await deps2.db.query({
1433
+ const found = await deps.db.query({
1356
1434
  crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
1357
1435
  });
1358
1436
  if (found.crm_record?.[0]) return found.crm_record[0];
@@ -1360,7 +1438,7 @@ async function findSharedRecord(deps2, record4, tag) {
1360
1438
  const domain = record4.input.domain;
1361
1439
  const domainSlot = def.fields.domain?.slot;
1362
1440
  if (typeof domain === "string" && domainSlot) {
1363
- const found = await deps2.db.query({
1441
+ const found = await deps.db.query({
1364
1442
  crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
1365
1443
  });
1366
1444
  if (found.crm_record?.[0]) return found.crm_record[0];
@@ -1368,14 +1446,14 @@ async function findSharedRecord(deps2, record4, tag) {
1368
1446
  const nameField = def.nameField ?? "name";
1369
1447
  const name = record4.input[nameField];
1370
1448
  if (record4.type === "company" && typeof name === "string" && name.trim()) {
1371
- const found = await deps2.db.query({
1449
+ const found = await deps.db.query({
1372
1450
  crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
1373
1451
  });
1374
1452
  if (found.crm_record?.[0]) return found.crm_record[0];
1375
1453
  }
1376
1454
  return void 0;
1377
1455
  }
1378
- async function projectSharedRecord(deps2, shared, options = {}) {
1456
+ async function projectSharedRecord(deps, shared, options = {}) {
1379
1457
  const record4 = normalizeSharedRecord(shared, options.sourceId);
1380
1458
  if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
1381
1459
  throw new Error("type, source site id, and source record id must be non-empty");
@@ -1384,14 +1462,14 @@ async function projectSharedRecord(deps2, shared, options = {}) {
1384
1462
  throw new Error("signed sender does not match payload source");
1385
1463
  }
1386
1464
  const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
1387
- const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
1388
- const existing = await findSharedRecord(deps2, record4, tag);
1465
+ const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1466
+ const existing = await findSharedRecord(deps, record4, tag);
1389
1467
  let recordId;
1390
1468
  if (existing && typeof existing.id === "string") {
1391
1469
  await updateRecord2(crmDeps3, { id: existing.id, input: record4.input });
1392
1470
  recordId = existing.id;
1393
1471
  } else if (record4.type === "person" && typeof record4.input.email === "string") {
1394
- const projected = await ensureProjectedPerson(deps2, record4.input);
1472
+ const projected = await ensureProjectedPerson(deps, record4.input);
1395
1473
  recordId = String(projected.record.id);
1396
1474
  if (!projected.created) await updateRecord2(crmDeps3, { id: recordId, input: record4.input });
1397
1475
  } else {
@@ -1402,8 +1480,8 @@ async function projectSharedRecord(deps2, shared, options = {}) {
1402
1480
  mutationId: `share-create:${tag}`
1403
1481
  });
1404
1482
  }
1405
- await deps2.db.transact(
1406
- [{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps2.now() } }],
1483
+ await deps.db.transact(
1484
+ [{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
1407
1485
  { mutationId: `share-map:${tag}:${recordId}` }
1408
1486
  );
1409
1487
  await upsertRecordOrigin(crmDeps3, {
@@ -1451,21 +1529,21 @@ function billingColumns(app) {
1451
1529
  if (billingStatus !== "canceled" && billingStatus !== "refunded" && Number.isFinite(renewal) && renewal > 0) cols.renewalAt = renewal;
1452
1530
  return cols;
1453
1531
  }
1454
- async function syncApplicationToCrm(deps2, opts) {
1532
+ async function syncApplicationToCrm(deps, opts) {
1455
1533
  if (!opts.app.id) throw new Error("crm_application_id_required");
1456
1534
  const emailKey = str(opts.app.email).toLowerCase();
1457
1535
  if (!emailKey) return null;
1458
- const recordDeps = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
1459
- const input = personInputFromApp(deps2.chapter, opts.app);
1536
+ const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1537
+ const input = personInputFromApp(deps.chapter, opts.app);
1460
1538
  const stage = opts.stage || void 0;
1461
- const { record: existing, created } = await ensureProjectedPerson(deps2, input, { stage });
1539
+ const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
1462
1540
  const recordId = String(existing.id);
1463
1541
  if (!created) await updateRecord3(recordDeps, { id: recordId, input });
1464
1542
  if (!created && stage && existing.stage !== stage) {
1465
1543
  await setStage2(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
1466
1544
  }
1467
1545
  const billing = billingColumns(opts.app);
1468
- await deps2.db.transact([
1546
+ await deps.db.transact([
1469
1547
  { t: "update", ns: "crm_record", id: recordId, attrs: billing },
1470
1548
  // Optional typed dates are removed with a retract, never an invalid null.
1471
1549
  ...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
@@ -1473,8 +1551,8 @@ async function syncApplicationToCrm(deps2, opts) {
1473
1551
  await linkIdentity(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1474
1552
  return recordId;
1475
1553
  }
1476
- async function backfillCrm(deps2) {
1477
- const data = await deps2.db.query({
1554
+ async function backfillCrm(deps) {
1555
+ const data = await deps.db.query({
1478
1556
  applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
1479
1557
  });
1480
1558
  const seen = /* @__PURE__ */ new Set();
@@ -1485,7 +1563,7 @@ async function backfillCrm(deps2) {
1485
1563
  if (!key3 || seen.has(key3)) return;
1486
1564
  seen.add(key3);
1487
1565
  try {
1488
- await syncApplicationToCrm(deps2, { app, stage });
1566
+ await syncApplicationToCrm(deps, { app, stage });
1489
1567
  synced += 1;
1490
1568
  } catch (err) {
1491
1569
  errors.push({ email: key3, error: err instanceof Error ? err.message : String(err) });
@@ -1493,10 +1571,10 @@ async function backfillCrm(deps2) {
1493
1571
  };
1494
1572
  for (const a of data.applications ?? []) await run(a, str(a.status));
1495
1573
  for (let offset = 0; ; offset += 200) {
1496
- const users = (await deps2.db.query({ $users: { $: { order: { id: "asc" }, limit: 200, offset } } })).$users ?? [];
1574
+ const users = (await deps.db.query({ $users: { $: { order: { id: "asc" }, limit: 200, offset } } })).$users ?? [];
1497
1575
  for (const u of users) {
1498
1576
  try {
1499
- if (await syncAccountToCrm(deps2, String(u.id))) synced += 1;
1577
+ if (await syncAccountToCrm(deps, String(u.id))) synced += 1;
1500
1578
  } catch (err) {
1501
1579
  errors.push({ email: str(u.email), error: err instanceof Error ? err.message : String(err) });
1502
1580
  }
@@ -1536,26 +1614,32 @@ function planDelivery(input) {
1536
1614
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1537
1615
  const to = redirect ? input.group.debugEmail : input.to;
1538
1616
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1539
- const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
1617
+ const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
1540
1618
 
1541
1619
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1542
- return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
1620
+ return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
1543
1621
  }
1544
1622
 
1545
1623
  // src/notify.ts
1546
- var EMAIL_TEMPLATE_NAMES = ["adminNotification", "paymentConfirmation", "prepEmail", "onboardingInvite"];
1547
- async function sendTemplated(deps2, input) {
1548
- const { emailLog } = await deps2.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
1624
+ var EMAIL_TEMPLATE_NAMES = ["adminNotification", "submitConfirmation", "paymentConfirmation", "prepEmail", "onboardingInvite"];
1625
+ function fromAddress(configured, name) {
1626
+ const display = name?.trim();
1627
+ if (!display) return configured;
1628
+ const angled = configured.match(/<([^>]+)>/);
1629
+ return { name: display, email: (angled ? angled[1] : configured).trim() };
1630
+ }
1631
+ async function sendTemplated(deps, input) {
1632
+ const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
1549
1633
  const prior = Array.isArray(emailLog) ? emailLog : [];
1550
1634
  if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
1551
1635
  if (prior.length > 1 || prior.length === 1 && prior[0].deliveryState !== "retry_ready") {
1552
1636
  return { sent: false, reason: "delivery-uncertain" };
1553
1637
  }
1554
- const cloudflareReady = Boolean(deps2.sender && deps2.from);
1555
- const override = deps2.debugEmail?.trim();
1556
- const group = deps2.envName !== "prod" && deps2.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
1638
+ const cloudflareReady = Boolean(deps.sender && deps.from);
1639
+ const override = deps.debugEmail?.trim();
1640
+ const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
1557
1641
  const decision = planDelivery({
1558
- envName: deps2.envName,
1642
+ envName: deps.envName,
1559
1643
  group,
1560
1644
  template: input.template,
1561
1645
  to: input.to,
@@ -1580,14 +1664,14 @@ async function sendTemplated(deps2, input) {
1580
1664
  transport: decision.transport,
1581
1665
  redirected: decision.redirected,
1582
1666
  dedupeKey: input.dedupeKey,
1583
- sentAt: deps2.now(),
1667
+ sentAt: deps.now(),
1584
1668
  deliveryState: decision.transport === "log-only" ? "sent" : "sending",
1585
1669
  deliveryAttempt: attempt,
1586
1670
  error: decision.transport === "log-only" ? "" : "delivery-pending",
1587
1671
  ...input.applicationId ? { applicationId: input.applicationId } : {}
1588
1672
  };
1589
1673
  try {
1590
- const claim = await deps2.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
1674
+ const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
1591
1675
  mutationId: `email-claim:${id6}:${attempt}`,
1592
1676
  guards: [previous ? {
1593
1677
  ns: "emailLog",
@@ -1605,8 +1689,8 @@ async function sendTemplated(deps2, input) {
1605
1689
  if (decision.transport === "log-only") return { sent: true };
1606
1690
  let messageId;
1607
1691
  try {
1608
- const response = await deps2.sender.send({
1609
- from: deps2.from,
1692
+ const response = await deps.sender.send({
1693
+ from: fromAddress(deps.from, input.group.fromName),
1610
1694
  to: [decision.to],
1611
1695
  subject: String(row2.subject),
1612
1696
  text: String(row2.body),
@@ -1615,12 +1699,12 @@ async function sendTemplated(deps2, input) {
1615
1699
  if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
1616
1700
  messageId = response.messageId;
1617
1701
  } catch {
1618
- await deps2.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
1702
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
1619
1703
  guards: [{ ns: "emailLog", id: id6, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
1620
1704
  });
1621
1705
  return { sent: false, reason: "delivery-uncertain" };
1622
1706
  }
1623
- await deps2.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps2.now() } }], {
1707
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
1624
1708
  mutationId: `email-confirmed:${id6}:${attempt}`,
1625
1709
  guards: [{
1626
1710
  ns: "emailLog",
@@ -1639,6 +1723,7 @@ function emailGroupFrom(row2) {
1639
1723
  name: String(row2.name ?? ""),
1640
1724
  replyTo: str3(row2.replyTo) ?? "",
1641
1725
  debugEmail: str3(row2.debugEmail),
1726
+ fromName: str3(row2.fromName),
1642
1727
  refundPolicyText: str3(row2.refundPolicyText),
1643
1728
  commitmentText: str3(row2.commitmentText),
1644
1729
  normsText: str3(row2.normsText),
@@ -2769,24 +2854,22 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
2769
2854
  }
2770
2855
  }
2771
2856
 
2772
- // src/payment-notifications.ts
2773
- var groupFor = async (db, app) => {
2774
- const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
2775
- return result.groups?.[0];
2776
- };
2777
- var deps = (db, env) => ({
2778
- db,
2779
- envName: env.ODLA_ENV,
2780
- sender: env.SEND_EMAIL,
2781
- from: env.EMAIL_FROM,
2782
- debugEmail: env.ODLA_DEBUG_EMAIL,
2783
- now: () => Date.now(),
2784
- newId: () => crypto.randomUUID()
2785
- });
2786
- async function paymentConfirmationVariables(db, app) {
2857
+ // src/notification-variables.ts
2858
+ function lifecycleNotifyDeps(db, env) {
2859
+ return {
2860
+ db,
2861
+ envName: env.ODLA_ENV,
2862
+ sender: env.SEND_EMAIL,
2863
+ from: env.EMAIL_FROM,
2864
+ debugEmail: env.ODLA_DEBUG_EMAIL,
2865
+ now: () => Date.now(),
2866
+ newId: () => crypto.randomUUID()
2867
+ };
2868
+ }
2869
+ async function frozenCheckoutVariables(db, app) {
2787
2870
  const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
2788
2871
  const offer = intent?.membershipOffer, contract = offer?.contract;
2789
- const offering = contract?.offering;
2872
+ const offering = contract?.offering, quote = intent?.paymentQuote;
2790
2873
  const amount = contract?.dueTodayCents ?? intent?.priceCents;
2791
2874
  const currency = offering?.currency ?? intent?.currency;
2792
2875
  let formatted = "";
@@ -2794,9 +2877,75 @@ async function paymentConfirmationVariables(db, app) {
2794
2877
  formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
2795
2878
  }
2796
2879
  return {
2797
- firstName: typeof app.firstName === "string" ? app.firstName : "",
2798
2880
  paymentAmount: formatted,
2799
2881
  paymentAmountSummary: formatted ? ` of ${formatted}` : "",
2882
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
2883
+ };
2884
+ }
2885
+ async function catalogTierName(db, group, app) {
2886
+ try {
2887
+ return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
2888
+ } catch {
2889
+ return "";
2890
+ }
2891
+ }
2892
+ async function checkoutVariables(db, group, app) {
2893
+ const frozen = await frozenCheckoutVariables(db, app);
2894
+ return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
2895
+ }
2896
+ function formatInstant(at, timeZone) {
2897
+ return new Intl.DateTimeFormat("en-US", {
2898
+ weekday: "long",
2899
+ month: "long",
2900
+ day: "numeric",
2901
+ year: "numeric",
2902
+ hour: "numeric",
2903
+ minute: "2-digit",
2904
+ timeZoneName: "short",
2905
+ timeZone
2906
+ }).format(new Date(at));
2907
+ }
2908
+ var adminUrlFor = (origin) => `${origin}/admin/`;
2909
+ var membersUrlFor = (origin) => `${origin}/members/`;
2910
+ function applicantVariables(app) {
2911
+ const text8 = (v) => typeof v === "string" ? v : "";
2912
+ return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
2913
+ }
2914
+ function sampleVariables(origin, timeZone, now = Date.now()) {
2915
+ const startAt = formatInstant(now + 864e5, timeZone);
2916
+ return {
2917
+ firstName: "Sample",
2918
+ lastName: "Person",
2919
+ email: "sample@example.com",
2920
+ phone: "(555) 010-0100",
2921
+ state: "CA",
2922
+ tier: "Membership",
2923
+ tierName: "Membership",
2924
+ paymentAmount: "$100.00",
2925
+ paymentAmountSummary: " of $100.00",
2926
+ paymentNextStep: "Next, schedule your introduction call.",
2927
+ submitNextStep: "Next, schedule your introduction call.",
2928
+ startAt,
2929
+ endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
2930
+ timezone: timeZone,
2931
+ meetUrl: "https://meet.google.com/sample",
2932
+ htmlLink: "https://calendar.google.com/event?eid=sample",
2933
+ meetingTime: startAt,
2934
+ meetingLink: "https://meet.google.com/sample",
2935
+ adminUrl: adminUrlFor(origin),
2936
+ membersUrl: membersUrlFor(origin)
2937
+ };
2938
+ }
2939
+
2940
+ // src/payment-notifications.ts
2941
+ var groupFor = async (db, app) => {
2942
+ const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
2943
+ return result.groups?.[0];
2944
+ };
2945
+ async function paymentConfirmationVariables(db, app) {
2946
+ return {
2947
+ firstName: typeof app.firstName === "string" ? app.firstName : "",
2948
+ ...await frozenCheckoutVariables(db, app),
2800
2949
  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."
2801
2950
  };
2802
2951
  }
@@ -2804,7 +2953,7 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
2804
2953
  if (typeof app.email !== "string" || !app.email) throw new Error("payment notice recipient missing");
2805
2954
  const group = await groupFor(db, app);
2806
2955
  if (!group) throw new Error("payment notice configuration missing");
2807
- const result = await sendTemplated(deps(db, env), {
2956
+ const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
2808
2957
  group: emailGroupFrom(group),
2809
2958
  template: "paymentConfirmation",
2810
2959
  to: app.email,
@@ -2814,21 +2963,20 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
2814
2963
  });
2815
2964
  if (!result.sent && !["disabled", "template-missing"].includes(result.reason ?? "")) throw new Error("payment notice reconciliation pending");
2816
2965
  }
2817
- async function notifyAdminOfPayment(db, env, eventId, app) {
2966
+ async function notifyAdminOfPayment(db, env, eventId, app, origin = "") {
2818
2967
  try {
2819
2968
  const group = await groupFor(db, app);
2820
2969
  if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
2821
- const text6 = (value) => typeof value === "string" ? value : "";
2822
- await sendTemplated(deps(db, env), {
2970
+ const { tierName: tierName2 } = await frozenCheckoutVariables(db, app);
2971
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
2823
2972
  group: emailGroupFrom(group),
2824
2973
  template: "adminNotification",
2825
2974
  to: group.notificationEmail,
2826
2975
  vars: {
2827
- firstName: text6(app.firstName),
2828
- lastName: text6(app.lastName),
2829
- email: text6(app.email),
2830
- phone: text6(app.phone),
2831
- state: text6(app.state)
2976
+ ...applicantVariables(app),
2977
+ tier: tierName2,
2978
+ tierName: tierName2,
2979
+ ...origin ? { adminUrl: adminUrlFor(origin), membersUrl: membersUrlFor(origin) } : {}
2832
2980
  },
2833
2981
  dedupeKey: `${eventId}:admin`,
2834
2982
  applicationId: String(app.id)
@@ -3148,6 +3296,26 @@ function reportStripePartition(level, eventId, event, disposition) {
3148
3296
  ...stripeEventIdentity(event)
3149
3297
  }));
3150
3298
  }
3299
+ var QUARANTINE_DISPOSITIONS = ["quarantine_unmatched", "quarantine_collision"];
3300
+ var QUEUE_LIMIT = 500;
3301
+ var QUEUE_IDS = 50;
3302
+ async function alertStripeQuarantine(db, ctx, eventId, disposition) {
3303
+ const pages = await Promise.all(QUARANTINE_DISPOSITIONS.map(async (value) => (await db.query({ stripeEventReceipts: { $: { where: { disposition: value }, limit: QUEUE_LIMIT } } })).stripeEventReceipts ?? []));
3304
+ const queued = pages.flat();
3305
+ reportChapterAlert(ctx, new Error("stripe webhook quarantined"), {
3306
+ code: "stripe_webhook_quarantined",
3307
+ attributes: {
3308
+ "odla.stripe.disposition": disposition,
3309
+ "odla.stripe.quarantined_count": queued.length,
3310
+ "odla.stripe.queue_truncated": pages.some((page) => page.length >= QUEUE_LIMIT)
3311
+ },
3312
+ artifacts: { eventId, eventIds: queued.slice(0, QUEUE_IDS).map((row2) => String(row2.eventId)) }
3313
+ });
3314
+ }
3315
+ async function reportStripeCollision(db, ctx, eventId, event) {
3316
+ reportStripePartition("error", eventId, event, "quarantine_collision");
3317
+ await alertStripeQuarantine(db, ctx, eventId, "quarantine_collision");
3318
+ }
3151
3319
  async function storeStripeReceipt(db, input) {
3152
3320
  const existing = await receiptFor(db, input.eventId);
3153
3321
  if (existing?.retryReady === true && existing.reviewedDigest === input.digest) {
@@ -3180,17 +3348,18 @@ async function storeStripeReceipt(db, input) {
3180
3348
  const raced = await receiptFor(db, input.eventId);
3181
3349
  return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
3182
3350
  }
3183
- async function quarantineStripeEvent(db, eventId, digest, event) {
3351
+ async function quarantineStripeEvent(db, eventId, digest, event, ctx) {
3184
3352
  const disposition = "quarantine_unmatched";
3185
3353
  const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
3186
3354
  if (stored === "collision") {
3187
- reportStripePartition("error", eventId, event, "quarantine_collision");
3355
+ await reportStripeCollision(db, ctx, eventId, event);
3188
3356
  return json({ ok: true, matched: false, collision: true });
3189
3357
  }
3190
3358
  reportStripePartition("warn", eventId, event, disposition);
3359
+ if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
3191
3360
  return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
3192
3361
  }
3193
- async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
3362
+ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx) {
3194
3363
  const existing = await receiptFor(db, eventId);
3195
3364
  if (!existing) return null;
3196
3365
  if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
@@ -3200,7 +3369,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
3200
3369
  collisionDigest: digest,
3201
3370
  retryReady: false
3202
3371
  } }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
3203
- reportStripePartition("error", eventId, event, "quarantine_collision");
3372
+ await reportStripeCollision(db, ctx, eventId, event);
3204
3373
  return json({ ok: true, matched: false, collision: true });
3205
3374
  }
3206
3375
  return json({ ok: true, matched: existing.matched === true, replayed: true });
@@ -3231,16 +3400,16 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3231
3400
  reportStripePartition("warn", eventId, event, "ignore_sibling");
3232
3401
  return json({ ok: true, matched: false, disposition: "ignore_sibling" });
3233
3402
  }
3234
- const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
3403
+ const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
3235
3404
  if (replay2) return replay2;
3236
3405
  const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
3237
3406
  const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
3238
3407
  if (!localIntent || disposition2 === "quarantine_unmatched") {
3239
- return quarantineStripeEvent(db, eventId, digest, event);
3408
+ return quarantineStripeEvent(db, eventId, digest, event, ctx);
3240
3409
  }
3241
3410
  if (event.kind === "gift_paid") {
3242
3411
  if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
3243
- return quarantineStripeEvent(db, eventId, digest, event);
3412
+ return quarantineStripeEvent(db, eventId, digest, event, ctx);
3244
3413
  }
3245
3414
  try {
3246
3415
  const issued = await issueGiftClaim(db, intent, {
@@ -3249,7 +3418,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3249
3418
  }, Date.now());
3250
3419
  const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
3251
3420
  if (stored3 === "collision") {
3252
- reportStripePartition("error", eventId, event, "quarantine_collision");
3421
+ await reportStripeCollision(db, ctx, eventId, event);
3253
3422
  return json({ ok: true, matched: false, collision: true });
3254
3423
  }
3255
3424
  return json({ ok: true, matched: true, claimId: issued.claimId });
@@ -3265,7 +3434,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3265
3434
  if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
3266
3435
  const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
3267
3436
  if (stored2 === "collision") {
3268
- reportStripePartition("error", eventId, event, "quarantine_collision");
3437
+ await reportStripeCollision(db, ctx, eventId, event);
3269
3438
  return json({ ok: true, matched: false, collision: true });
3270
3439
  }
3271
3440
  return json({ ok: true, matched: Boolean(claim) });
@@ -3275,12 +3444,12 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3275
3444
  reportStripePartition("warn", eventId, event, "ignore_sibling");
3276
3445
  return json({ ok: true, matched: false, disposition: "ignore_sibling" });
3277
3446
  }
3278
- const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
3447
+ const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
3279
3448
  if (replay) return replay;
3280
3449
  const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
3281
3450
  const disposition = classifyMemberStripeEvent(identity, target, localApplication);
3282
3451
  if (!localApplication || disposition === "quarantine_unmatched") {
3283
- return quarantineStripeEvent(db, eventId, digest, event);
3452
+ return quarantineStripeEvent(db, eventId, digest, event, ctx);
3284
3453
  }
3285
3454
  if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
3286
3455
  await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
@@ -3297,7 +3466,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3297
3466
  if (receipt.seatId !== event.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
3298
3467
  const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
3299
3468
  if (stored2 === "collision") {
3300
- reportStripePartition("error", eventId, event, "quarantine_collision");
3469
+ await reportStripeCollision(db, ctx, eventId, event);
3301
3470
  return json({ ok: true, matched: false, collision: true });
3302
3471
  }
3303
3472
  return json({ ok: true, seatId: event.seatId });
@@ -3321,7 +3490,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3321
3490
  if (intent) {
3322
3491
  const evidence = verifyInitialMembershipInvoice(intent, event.invoice);
3323
3492
  if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
3324
- if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event);
3493
+ if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event, ctx);
3325
3494
  if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event, renewalAt: evidence.renewalAt };
3326
3495
  }
3327
3496
  if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
@@ -3372,7 +3541,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3372
3541
  } catch {
3373
3542
  return json({ error: "payment notice reconciliation pending" }, 503);
3374
3543
  }
3375
- if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app);
3544
+ if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app, input.origin);
3376
3545
  if (typeof next.interviewWaivedAt === "number") {
3377
3546
  const approval = await approveApplication({
3378
3547
  db,
@@ -3387,15 +3556,132 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3387
3556
  }
3388
3557
  const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
3389
3558
  if (stored === "collision") {
3390
- reportStripePartition("error", eventId, event, "quarantine_collision");
3559
+ await reportStripeCollision(db, ctx, eventId, event);
3391
3560
  return json({ ok: true, matched: false, collision: true });
3392
3561
  }
3393
3562
  return json({ ok: true });
3394
3563
  }
3395
3564
 
3565
+ // src/network-commercial-resolve.ts
3566
+ var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
3567
+ var fail3 = (message, code) => Object.assign(new Error(message), { code });
3568
+ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
3569
+ if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
3570
+ const id6 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
3571
+ const found = await db.query({
3572
+ networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } },
3573
+ networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
3574
+ });
3575
+ const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
3576
+ if (exception?.status === "resolved" && exception.reviewId === reviewId) {
3577
+ return { eventId: input.eventId, reviewId, projection: await loadNetworkCommercialProjection(db, sources) };
3578
+ }
3579
+ if (!exception || exception.status !== "active") throw fail3("no active collision for this event", "collision_not_active");
3580
+ if (!stored) throw fail3("stored event not found", "event_not_found");
3581
+ const dismissed = typeof exception.incomingDigest === "string" ? exception.incomingDigest : void 0;
3582
+ const write = await db.transact([
3583
+ { t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
3584
+ id: reviewId,
3585
+ eventId: input.eventId,
3586
+ actorUserId: input.actorUserId,
3587
+ reviewedAt: now,
3588
+ previousDigest: String(stored.digest),
3589
+ providerDigest: dismissed ?? "",
3590
+ scope: "network",
3591
+ previousState: exception,
3592
+ action: "resolve",
3593
+ reason: input.reason
3594
+ } },
3595
+ { t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
3596
+ status: "resolved",
3597
+ resolvedDigest: stored.digest,
3598
+ ...dismissed ? { reviewedDigest: dismissed } : {},
3599
+ resolvedBy: input.actorUserId,
3600
+ reviewId,
3601
+ updatedAt: now
3602
+ } }
3603
+ ], { mutationId: input.mutationId, guards: [
3604
+ { ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
3605
+ ] });
3606
+ if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
3607
+ const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
3608
+ return { eventId: input.eventId, reviewId, projection };
3609
+ }
3610
+ async function retryNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
3611
+ if (!EVENT_ID.test(eventId)) throw fail3("invalid event reference", "invalid_event");
3612
+ const active = (await loadNetworkCommercialProjection(db, sources)).exceptions.filter((item) => item.eventId === eventId);
3613
+ if (active.length === 0) throw fail3("event has no active exception", "exception_not_active");
3614
+ const provider = await readStripeRecoveryEvent(db, eventId);
3615
+ const stored = (await db.query({ networkCommercialEvents: { $: { where: { eventId }, limit: 1 } } })).networkCommercialEvents?.[0];
3616
+ const reviewId = crypto.randomUUID();
3617
+ await db.transact([{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
3618
+ id: reviewId,
3619
+ eventId,
3620
+ actorUserId,
3621
+ reviewedAt: now,
3622
+ previousDigest: String(stored?.digest ?? ""),
3623
+ providerDigest: provider.digest,
3624
+ scope: "network",
3625
+ previousState: { exceptions: active },
3626
+ action: "retry"
3627
+ } }], { mutationId: `payment-recovery:${reviewId}` });
3628
+ const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
3629
+ const result = await ingestNetworkCommercialFact(db, sources, fact, now, await stripePayloadDigest(provider.rawBody), alerts);
3630
+ return {
3631
+ eventId,
3632
+ reviewId,
3633
+ status: result.status,
3634
+ resolvedCollision: result.status === "replayed" && result.resolvedCollision === true,
3635
+ exceptions: result.projection.exceptions.length
3636
+ };
3637
+ }
3638
+
3639
+ // src/worker-routes-network-commercial-recovery.ts
3640
+ var PATH = "/api/admin/payment-recovery";
3641
+ var handleNetworkCommercialRecovery = async (req, url, env, ctx) => {
3642
+ const model = ctx.chapter.networkCommercialReadModel;
3643
+ if (url.pathname !== PATH || !model) return null;
3644
+ if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
3645
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3646
+ if (!user) return json({ error: "unauthorized" }, 401);
3647
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "forbidden" }, 403);
3648
+ const db = raw2;
3649
+ if (req.method === "GET") {
3650
+ const { exceptions } = await loadNetworkCommercialProjection(db, model.sources);
3651
+ return json({
3652
+ scope: "network",
3653
+ count: exceptions.length,
3654
+ truncated: false,
3655
+ rows: exceptions.map((item) => ({
3656
+ eventId: item.eventId,
3657
+ kind: item.detail.eventType,
3658
+ disposition: item.kind,
3659
+ sourceKey: item.sourceKey ?? null,
3660
+ retryReady: /^evt_/.test(item.eventId)
3661
+ }))
3662
+ });
3663
+ }
3664
+ let eventId;
3665
+ try {
3666
+ eventId = String((await req.json()).eventId ?? "");
3667
+ } catch {
3668
+ return json({ error: "eventId required" }, 400);
3669
+ }
3670
+ try {
3671
+ const retried = await retryNetworkCommercialEvent(db, model.sources, eventId, user.userId, Date.now(), networkCommercialAlerts(ctx, PATH));
3672
+ return json({ ok: true, ...retried });
3673
+ } catch (error) {
3674
+ const code = error.code;
3675
+ if (code === "invalid_event") return json({ error: "invalid event reference" }, 400);
3676
+ if (code === "exception_not_active") return json({ error: "event has no active exception" }, 409);
3677
+ return json({ error: "Event recovery is pending. Check its provider evidence and retry." }, 503);
3678
+ }
3679
+ };
3680
+
3396
3681
  // src/worker-routes-payment-recovery.ts
3397
3682
  var handlePaymentRecovery = async (req, url, env, ctx) => {
3398
- if (url.pathname !== "/api/admin/payment-recovery" || ctx.chapter.mode !== "chapter") return null;
3683
+ if (url.pathname !== "/api/admin/payment-recovery") return null;
3684
+ if (ctx.chapter.mode !== "chapter") return handleNetworkCommercialRecovery(req, url, env, ctx);
3399
3685
  if (!["GET", "POST"].includes(req.method)) return json({ error: "method not allowed" }, 405);
3400
3686
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3401
3687
  if (!user) return json({ error: "unauthorized" }, 401);
@@ -3457,10 +3743,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
3457
3743
  };
3458
3744
 
3459
3745
  // src/worker-routes-admin-named-seat.ts
3460
- var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3746
+ var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3461
3747
  var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
3462
3748
  var handleAdminNamedSeat = async (req, url, env, ctx) => {
3463
- const match = url.pathname.match(PATH);
3749
+ const match = url.pathname.match(PATH2);
3464
3750
  if (!match) return null;
3465
3751
  const seats = ctx.membershipAuthority?.namedSeats;
3466
3752
  if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
@@ -3513,42 +3799,42 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
3513
3799
 
3514
3800
  // src/membership-seat-refund-stripe.ts
3515
3801
  import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
3516
- var fail3 = (code = "named_seat_refund_mismatch") => {
3802
+ var fail4 = (code = "named_seat_refund_mismatch") => {
3517
3803
  throw Object.assign(new Error(code), { code });
3518
3804
  };
3519
3805
  async function refundNamedSeatStripePayment(secretKey, seat) {
3520
- if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
3806
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
3521
3807
  const call = async (method, path, params, idempotencyKey) => {
3522
3808
  const result = await stripeRequest4({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
3523
- if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
3809
+ if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
3524
3810
  return result.body;
3525
3811
  };
3526
3812
  const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
3527
3813
  const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
3528
- 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();
3814
+ 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();
3529
3815
  const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
3530
- 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();
3816
+ 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();
3531
3817
  const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
3532
- if (items.length > 1) fail3();
3818
+ if (items.length > 1) fail4();
3533
3819
  if (items.length && sub.status !== "canceled") {
3534
3820
  const item = items[0];
3535
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
3821
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail4();
3536
3822
  const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
3537
- if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
3823
+ if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
3538
3824
  }
3539
3825
  const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
3540
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
3826
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
3541
3827
  let refundedCents = 0, pendingCents = 0;
3542
3828
  const refundIds = [];
3543
3829
  for (const refund of list.data) {
3544
- 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();
3830
+ 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();
3545
3831
  if (refund.status === "succeeded") {
3546
3832
  refundedCents += refund.amount;
3547
3833
  refundIds.push(refund.id);
3548
3834
  } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
3549
3835
  }
3550
- if (refundedCents + pendingCents > seat.amountCents) fail3();
3551
- if (pendingCents) fail3("named_seat_refund_pending");
3836
+ if (refundedCents + pendingCents > seat.amountCents) fail4();
3837
+ if (pendingCents) fail4("named_seat_refund_pending");
3552
3838
  if (refundedCents < seat.amountCents) {
3553
3839
  const remaining = seat.amountCents - refundedCents;
3554
3840
  const refund = await call("POST", "/v1/refunds", {
@@ -3557,8 +3843,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
3557
3843
  reason: "requested_by_customer",
3558
3844
  metadata
3559
3845
  }, `named-seat-refund:${seat.id}:${remaining}`);
3560
- if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
3561
- if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
3846
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
3847
+ if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
3562
3848
  refundedCents += remaining;
3563
3849
  refundIds.push(refund.id);
3564
3850
  }
@@ -3763,6 +4049,107 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3763
4049
  }
3764
4050
  };
3765
4051
 
4052
+ // src/membership-policy-versions.ts
4053
+ var LIMIT = 200;
4054
+ var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4055
+ var instant = (value) => {
4056
+ const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4057
+ return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
4058
+ };
4059
+ function provenance(row2) {
4060
+ const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4061
+ return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
4062
+ }
4063
+ var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
4064
+ function stateOf(item, all, now, window, rivals) {
4065
+ if (item.status !== "published") return { state: item.status };
4066
+ const mine = window(item);
4067
+ if (now < mine.from) return { state: "scheduled" };
4068
+ const closed = mine.until !== void 0 && mine.until <= now;
4069
+ 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));
4070
+ if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
4071
+ const dates = higher.map((w) => Math.max(w.from, mine.from));
4072
+ if (closed) dates.push(mine.until);
4073
+ return { state: "superseded", supersededAt: Math.min(...dates) };
4074
+ }
4075
+ 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";
4076
+ var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
4077
+ var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
4078
+ function normalize(rows5, collection, parse, invalid2) {
4079
+ const out = [];
4080
+ for (const row2 of rows5) {
4081
+ try {
4082
+ out.push({ value: parse(row2.snapshot), row: row2 });
4083
+ } catch (error) {
4084
+ invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
4085
+ }
4086
+ }
4087
+ return out;
4088
+ }
4089
+ async function listMembershipPolicyVersions(db, input) {
4090
+ const where = input.chapterId ? { chapterId: input.chapterId } : {};
4091
+ const rows5 = await db.query({
4092
+ membershipOfferingVersion: { $: { where, limit: LIMIT } },
4093
+ discountPolicyVersion: { $: { where, limit: LIMIT } }
4094
+ });
4095
+ const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
4096
+ const invalid2 = [];
4097
+ const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
4098
+ const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
4099
+ const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
4100
+ return {
4101
+ now: input.now,
4102
+ chapterId: input.chapterId ?? null,
4103
+ offerings: offerings.map(({ value, row: row2 }) => ({
4104
+ ...value,
4105
+ ...provenance(row2),
4106
+ ...stateOf(
4107
+ value,
4108
+ offeringValues,
4109
+ input.now,
4110
+ (v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
4111
+ (a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
4112
+ )
4113
+ })).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
4114
+ discountPolicies: policies.map(({ value, row: row2 }) => ({
4115
+ ...value,
4116
+ ...provenance(row2),
4117
+ mechanism: mechanism(value),
4118
+ ...stateOf(
4119
+ value,
4120
+ policyValues,
4121
+ input.now,
4122
+ (v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
4123
+ (a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
4124
+ )
4125
+ })).sort((a, b) => byChapter(a, b) || b.version - a.version),
4126
+ invalid: invalid2,
4127
+ truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
4128
+ };
4129
+ }
4130
+
4131
+ // src/worker-routes-admin-membership-policy.ts
4132
+ var PATH3 = "/api/admin/membership-policy-versions";
4133
+ var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4134
+ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
4135
+ if (url.pathname !== PATH3) return null;
4136
+ const operator = ctx.membershipAuthorityOperator;
4137
+ if (!operator) return json({ error: "not found" }, 404);
4138
+ if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
4139
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4140
+ if (!user) return json({ error: "unauthorized" }, 401);
4141
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4142
+ const chapterId = url.searchParams.get("chapterId");
4143
+ if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
4144
+ let db;
4145
+ try {
4146
+ db = operator.makeDb(env);
4147
+ } catch {
4148
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4149
+ }
4150
+ return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
4151
+ };
4152
+
3766
4153
  // src/worker-routes-membership-cancel.ts
3767
4154
  var handleMembershipCancellation = async (req, url, env, ctx) => {
3768
4155
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4615,8 +5002,8 @@ var APPLICATION_LABELS = {
4615
5002
  state: "State"
4616
5003
  };
4617
5004
  function humanize(id6) {
4618
- const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
4619
- return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
5005
+ const text8 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
5006
+ return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id6;
4620
5007
  }
4621
5008
  function applicationDefs(chapter) {
4622
5009
  try {
@@ -4683,7 +5070,7 @@ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
4683
5070
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
4684
5071
  var CURRENCY = /^[a-z]{3}$/;
4685
5072
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
4686
- var text2 = (value, path, errors, max) => {
5073
+ var text3 = (value, path, errors, max) => {
4687
5074
  if (typeof value !== "string" || !value.trim()) {
4688
5075
  errors.push(`${path} is required`);
4689
5076
  return "";
@@ -4711,23 +5098,23 @@ function validateGiftOfferCatalog(input, tierIds) {
4711
5098
  }
4712
5099
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
4713
5100
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
4714
- const id6 = text2(item.id, `${path}.id`, errors, 80);
5101
+ const id6 = text3(item.id, `${path}.id`, errors, 80);
4715
5102
  if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4716
5103
  if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
4717
5104
  ids2.add(id6);
4718
5105
  const targetRaw = record(item.target);
4719
5106
  let target = { type: "entitlement", entitlementKey: "invalid" };
4720
5107
  if (targetRaw?.type === "tier") {
4721
- const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
5108
+ const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
4722
5109
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
4723
5110
  target = { type: "tier", tierId };
4724
5111
  } else if (targetRaw?.type === "entitlement") {
4725
- const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
5112
+ const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
4726
5113
  if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
4727
5114
  target = { type: "entitlement", entitlementKey };
4728
5115
  } else errors.push(`${path}.target.type must be tier or entitlement`);
4729
5116
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
4730
- const stripePriceId = text2(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
5117
+ const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
4731
5118
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
4732
5119
  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`);
4733
5120
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -4749,9 +5136,9 @@ function validateGiftOfferCatalog(input, tierIds) {
4749
5136
  redemptionLifetimeHours: lifetime,
4750
5137
  ...activeFrom !== void 0 ? { activeFrom } : {},
4751
5138
  ...activeUntil !== void 0 ? { activeUntil } : {},
4752
- title: text2(item.title, `${path}.title`, errors, 160),
4753
- description: text2(item.description, `${path}.description`, errors, 2e3),
4754
- ctaLabel: text2(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
5139
+ title: text3(item.title, `${path}.title`, errors, 160),
5140
+ description: text3(item.description, `${path}.description`, errors, 2e3),
5141
+ ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
4755
5142
  });
4756
5143
  }
4757
5144
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -4969,22 +5356,22 @@ async function createPaymentQuote(input) {
4969
5356
  // src/membership-quote-projection.ts
4970
5357
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
4971
5358
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
4972
- var fail4 = (code) => {
5359
+ var fail5 = (code) => {
4973
5360
  throw Object.assign(new Error(code), { code });
4974
5361
  };
4975
5362
  var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
4976
5363
  async function restore2(row2, target, now, allowExpired = false) {
4977
- 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");
4978
- if (row2.status !== "ready") return fail4("membership_quote_pending");
5364
+ 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");
5365
+ if (row2.status !== "ready") return fail5("membership_quote_pending");
4979
5366
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
4980
5367
  ...target,
4981
5368
  requestId: row2.requestId
4982
5369
  }), now, lifetimeMs: 6e4 });
4983
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
5370
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail5("membership_quote_expired");
4984
5371
  const tier = row2.tier, quote = row2.quote;
4985
5372
  const offering = offer.contract.offering;
4986
5373
  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) {
4987
- return fail4("membership_catalog_mismatch");
5374
+ return fail5("membership_catalog_mismatch");
4988
5375
  }
4989
5376
  const expected = await createPaymentQuote({
4990
5377
  ...target,
@@ -4995,7 +5382,7 @@ async function restore2(row2, target, now, allowExpired = false) {
4995
5382
  presentation: quote.presentation,
4996
5383
  founding: foundingOf(offer)
4997
5384
  });
4998
- if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
5385
+ if (quote.digest !== expected.digest) return fail5("membership_quote_conflict");
4999
5386
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
5000
5387
  }
5001
5388
  async function readMembershipQuote(db, target, now) {
@@ -5003,9 +5390,9 @@ async function readMembershipQuote(db, target, now) {
5003
5390
  }
5004
5391
  async function prepareMembershipQuote(db, input, issue) {
5005
5392
  const { applicationId: id6, now, app } = input;
5006
- 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");
5393
+ 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");
5007
5394
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
5008
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
5395
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail5("membership_quote_conflict");
5009
5396
  if (row2?.status === "ready") {
5010
5397
  try {
5011
5398
  return await restore2(row2, input, now);
@@ -5050,7 +5437,7 @@ async function prepareMembershipQuote(db, input, issue) {
5050
5437
  }
5051
5438
  if (row2?.status === "ready") return restore2(row2, input, now);
5052
5439
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
5053
- return fail4("membership_quote_pending");
5440
+ return fail5("membership_quote_pending");
5054
5441
  }
5055
5442
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
5056
5443
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -5314,17 +5701,17 @@ async function readBoundedRequestText(req, maxBytes) {
5314
5701
  if (!reader) return "";
5315
5702
  const decoder = new TextDecoder();
5316
5703
  let bytes = 0;
5317
- let text6 = "";
5704
+ let text8 = "";
5318
5705
  for (; ; ) {
5319
5706
  const { done, value } = await reader.read();
5320
5707
  if (req.signal.aborted) return invalid2();
5321
5708
  if (done) {
5322
5709
  complete = true;
5323
- return text6 + decoder.decode();
5710
+ return text8 + decoder.decode();
5324
5711
  }
5325
5712
  bytes += value.byteLength;
5326
5713
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
5327
- text6 += decoder.decode(value, { stream: true });
5714
+ text8 += decoder.decode(value, { stream: true });
5328
5715
  }
5329
5716
  } catch {
5330
5717
  return invalid2();
@@ -5677,37 +6064,61 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
5677
6064
  } catch {
5678
6065
  }
5679
6066
  }
5680
- async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
6067
+
6068
+ // src/submit-notifications.ts
6069
+ async function notifyAdminOfApplication(db, env, notice) {
5681
6070
  try {
5682
- const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
5683
- const group = Array.isArray(groups) ? groups[0] : void 0;
5684
- if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
5685
- const stringValue = (value) => typeof value === "string" ? value : "";
5686
- await sendTemplated(
5687
- {
5688
- db,
5689
- envName: env.ODLA_ENV,
5690
- debugEmail: env.ODLA_DEBUG_EMAIL,
5691
- sender: env.SEND_EMAIL,
5692
- from: env.EMAIL_FROM,
5693
- now: () => Date.now(),
5694
- newId: () => crypto.randomUUID()
6071
+ const { group, application: app } = notice;
6072
+ if (typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
6073
+ const { tierName: tierName2 } = await checkoutVariables(db, group, app);
6074
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
6075
+ group: emailGroupFrom(group),
6076
+ template: "adminNotification",
6077
+ to: group.notificationEmail,
6078
+ vars: { ...applicantVariables(app), tier: tierName2, tierName: tierName2, adminUrl: adminUrlFor(notice.origin), membersUrl: membersUrlFor(notice.origin) },
6079
+ dedupeKey: `apply:${String(app.id)}:admin`,
6080
+ applicationId: String(app.id)
6081
+ });
6082
+ } catch {
6083
+ }
6084
+ }
6085
+ var FREE_CONTINUATIONS = /* @__PURE__ */ new Set(["booking", "accepted", "approve"]);
6086
+ function shouldConfirmSubmit(state, group) {
6087
+ if (state !== null && FREE_CONTINUATIONS.has(state)) return true;
6088
+ const templates = group.emailTemplates;
6089
+ return templates?.paymentConfirmation?.enabled === false;
6090
+ }
6091
+ function submitNextStep(state) {
6092
+ switch (state) {
6093
+ case "accepted":
6094
+ return "Your membership is approved.";
6095
+ case "booking":
6096
+ return "Next, schedule your introduction call.";
6097
+ case "payment":
6098
+ case "paymentPending":
6099
+ case "gift":
6100
+ return "Next, complete your membership payment.";
6101
+ default:
6102
+ return "";
6103
+ }
6104
+ }
6105
+ async function notifySubmitConfirmed(db, env, notice) {
6106
+ try {
6107
+ const { group, application: app } = notice;
6108
+ if (typeof app.email !== "string" || !app.email || !shouldConfirmSubmit(notice.state, group)) return;
6109
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
6110
+ group: emailGroupFrom(group),
6111
+ template: "submitConfirmation",
6112
+ to: app.email,
6113
+ vars: {
6114
+ ...applicantVariables(app),
6115
+ ...await checkoutVariables(db, group, app),
6116
+ submitNextStep: submitNextStep(notice.state),
6117
+ membersUrl: membersUrlFor(notice.origin)
5695
6118
  },
5696
- {
5697
- group: emailGroupFrom(group),
5698
- template: "adminNotification",
5699
- to: group.notificationEmail,
5700
- vars: {
5701
- firstName: stringValue(fields.firstName),
5702
- lastName: stringValue(fields.lastName),
5703
- email: stringValue(fields.email),
5704
- phone: stringValue(fields.phone),
5705
- state: stringValue(fields.state)
5706
- },
5707
- dedupeKey: `apply:${applicationId}:admin`,
5708
- applicationId
5709
- }
5710
- );
6119
+ dedupeKey: `apply:${String(app.id)}:confirm`,
6120
+ applicationId: String(app.id)
6121
+ });
5711
6122
  } catch {
5712
6123
  }
5713
6124
  }
@@ -5905,17 +6316,17 @@ var handleMember = async (req, url, env, ctx) => {
5905
6316
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
5906
6317
  return json(provisioningFailure.body, provisioningFailure.status);
5907
6318
  }
5908
- if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
5909
- await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical3);
5910
- }
5911
6319
  const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
5912
6320
  const group = Array.isArray(groups) ? groups[0] : void 0;
5913
- const state = canonical3 && group ? await continueJoin(db, ctx, env, url.origin, canonical3, group) : null;
6321
+ const notice = group ? { group, application: canonical3, origin: url.origin } : null;
6322
+ if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
6323
+ const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
5914
6324
  if (!state) return json({
5915
6325
  error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
5916
6326
  code: "application_continuation_unavailable",
5917
6327
  id: result.id
5918
6328
  }, 503);
6329
+ if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
5919
6330
  const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
5920
6331
  if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
5921
6332
  return json({ error: "application changed; reload its current state" }, 409);
@@ -6035,10 +6446,10 @@ function boundedString(value, path, errors, max, required = false) {
6035
6446
  errors.push(`${path} must be a string`);
6036
6447
  return void 0;
6037
6448
  }
6038
- const text6 = value.trim();
6039
- if (required && !text6) errors.push(`${path} is required`);
6040
- if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
6041
- return text6;
6449
+ const text8 = value.trim();
6450
+ if (required && !text8) errors.push(`${path} is required`);
6451
+ if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
6452
+ return text8;
6042
6453
  }
6043
6454
  function validateMembershipPageDocument(input, options = {}) {
6044
6455
  const errors = [];
@@ -6087,8 +6498,8 @@ function validateMembershipPageDocument(input, options = {}) {
6087
6498
  else {
6088
6499
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
6089
6500
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
6090
- const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
6091
- return text6 ? [text6] : [];
6501
+ const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
6502
+ return text8 ? [text8] : [];
6092
6503
  });
6093
6504
  }
6094
6505
  }
@@ -6182,7 +6593,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
6182
6593
  function unknownKeys(value, allowed2, path, errors) {
6183
6594
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6184
6595
  }
6185
- function text3(value, path, errors, max = 65536) {
6596
+ function text4(value, path, errors, max = 65536) {
6186
6597
  if (typeof value !== "string" || !value.trim()) {
6187
6598
  errors.push(`${path} is required`);
6188
6599
  return "";
@@ -6205,11 +6616,11 @@ function tiersOf(value, errors) {
6205
6616
  return [];
6206
6617
  }
6207
6618
  unknownKeys(item, TIER_KEYS2, path, errors);
6208
- const id6 = text3(item.id, `${path}.id`, errors, 80);
6619
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
6209
6620
  if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6210
6621
  if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
6211
6622
  seen.add(id6);
6212
- const name = text3(item.name, `${path}.name`, errors, 160);
6623
+ const name = text4(item.name, `${path}.name`, errors, 160);
6213
6624
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
6214
6625
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
6215
6626
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -6227,7 +6638,7 @@ function tiersOf(value, errors) {
6227
6638
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
6228
6639
  errors.push(`${path}.interval must be year or month`);
6229
6640
  }
6230
- const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
6641
+ const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
6231
6642
  return [{
6232
6643
  id: id6,
6233
6644
  name,
@@ -6285,7 +6696,7 @@ function policyOf(value, errors, tiers) {
6285
6696
  const result = {};
6286
6697
  for (const key3 of allowed2) {
6287
6698
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
6288
- result[key3] = text3(input[key3], `document.policy.${key3}`, errors);
6699
+ result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
6289
6700
  }
6290
6701
  const tierPolicies = {};
6291
6702
  if (input.tierPolicies !== void 0) {
@@ -6300,8 +6711,8 @@ function policyOf(value, errors, tiers) {
6300
6711
  }
6301
6712
  unknownKeys(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
6302
6713
  tierPolicies[id6] = {
6303
- refundPolicyText: text3(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
6304
- ...policy.trustCopy !== void 0 ? { trustCopy: text3(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
6714
+ refundPolicyText: text4(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
6715
+ ...policy.trustCopy !== void 0 ? { trustCopy: text4(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
6305
6716
  };
6306
6717
  }
6307
6718
  }
@@ -6341,9 +6752,9 @@ function validateSignupControlDocument(input) {
6341
6752
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
6342
6753
  const source = recordOf(root.source);
6343
6754
  const target = recordOf(root.target);
6344
- const appId = text3(source?.appId, "document.source.appId", errors, 63);
6345
- const chapterId = text3(target?.chapterId, "document.target.chapterId", errors, 63);
6346
- const environment = text3(target?.environment, "document.target.environment", errors, 63);
6755
+ const appId = text4(source?.appId, "document.source.appId", errors, 63);
6756
+ const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
6757
+ const environment = text4(target?.environment, "document.target.environment", errors, 63);
6347
6758
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
6348
6759
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
6349
6760
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -7522,7 +7933,7 @@ async function deliverAdmissionGrants(options) {
7522
7933
  }
7523
7934
 
7524
7935
  // src/worker-routes-network-admission.ts
7525
- var PATH2 = "/api/admin/network/admissions";
7936
+ var PATH4 = "/api/admin/network/admissions";
7526
7937
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7527
7938
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7528
7939
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -7602,11 +8013,11 @@ async function parseBody(req) {
7602
8013
  }
7603
8014
  }
7604
8015
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
7605
- if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
8016
+ if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7606
8017
  const auth = await gate2(req, env, ctx);
7607
8018
  if ("response" in auth) return auth.response;
7608
8019
  const db = auth.db;
7609
- if (url.pathname === PATH2 && req.method === "GET") {
8020
+ if (url.pathname === PATH4 && req.method === "GET") {
7610
8021
  const targetId2 = url.searchParams.get("targetId") ?? "";
7611
8022
  const target2 = targetFor(ctx, targetId2);
7612
8023
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -7707,6 +8118,41 @@ async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
7707
8118
 
7708
8119
  // src/worker-routes-schedule.ts
7709
8120
  import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
8121
+
8122
+ // src/booking-notifications.ts
8123
+ async function bookingConfirmationVariables(db, group, app, meeting) {
8124
+ const startAt = formatInstant(meeting.startAt, meeting.timezone);
8125
+ const meetUrl = meeting.meetUrl ?? "", htmlLink = meeting.htmlLink ?? "";
8126
+ return {
8127
+ ...applicantVariables(app),
8128
+ ...await checkoutVariables(db, group, app),
8129
+ startAt,
8130
+ endAt: formatInstant(meeting.endAt, meeting.timezone),
8131
+ timezone: meeting.timezone,
8132
+ meetUrl,
8133
+ htmlLink,
8134
+ // The placeholders the seeded template used before these variables existed.
8135
+ meetingTime: startAt,
8136
+ meetingLink: meetUrl || htmlLink
8137
+ };
8138
+ }
8139
+ async function notifyBookingConfirmed(db, env, input) {
8140
+ const app = input.application;
8141
+ if (typeof app.email !== "string" || !app.email) return;
8142
+ try {
8143
+ await sendTemplated(lifecycleNotifyDeps(db, env), {
8144
+ group: emailGroupFrom(input.group),
8145
+ template: "prepEmail",
8146
+ to: app.email,
8147
+ vars: await bookingConfirmationVariables(db, input.group, app, input.meeting),
8148
+ dedupeKey: `prep:${String(app.id)}`,
8149
+ applicationId: String(app.id)
8150
+ });
8151
+ } catch {
8152
+ }
8153
+ }
8154
+
8155
+ // src/worker-routes-schedule.ts
7710
8156
  function errCode(err) {
7711
8157
  if (err && typeof err === "object") {
7712
8158
  const code = err.code;
@@ -7821,12 +8267,7 @@ async function bookSlot(req, env, ctx) {
7821
8267
  await db.transact([meetingOp, appOp]);
7822
8268
  const booked = { ...app, ...bookingAttrs };
7823
8269
  await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
7824
- if (typeof app.email === "string" && app.email) {
7825
- await sendTemplated(
7826
- { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL, now: () => Date.now(), newId: () => crypto.randomUUID() },
7827
- { group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
7828
- ).catch(() => void 0);
7829
- }
8270
+ await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
7830
8271
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
7831
8272
  }
7832
8273
  var handleSchedule = async (req, url, env, ctx) => {
@@ -7857,21 +8298,21 @@ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
7857
8298
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
7858
8299
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
7859
8300
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
7860
- function fail5(code = "refund_target_mismatch") {
8301
+ function fail6(code = "refund_target_mismatch") {
7861
8302
  throw Object.assign(new Error(code), { code });
7862
8303
  }
7863
8304
  async function refundSubscriptionPayment(secretKey, input) {
7864
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
8305
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
7865
8306
  const call = async (method, path, params, key3) => {
7866
8307
  const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7867
- if (!result.ok) fail5("stripe_error");
7868
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
8308
+ if (!result.ok) fail6("stripe_error");
8309
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
7869
8310
  return result.body;
7870
8311
  };
7871
8312
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
7872
- if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
8313
+ if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail6();
7873
8314
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
7874
- if (row(sub.metadata)[key3] !== value) fail5();
8315
+ if (row(sub.metadata)[key3] !== value) fail6();
7875
8316
  }
7876
8317
  let cursor;
7877
8318
  let earliest;
@@ -7884,55 +8325,55 @@ async function refundSubscriptionPayment(secretKey, input) {
7884
8325
  limit: 100,
7885
8326
  ...cursor ? { starting_after: cursor } : {}
7886
8327
  });
7887
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
8328
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail6("stripe_invalid_response");
7888
8329
  for (const value of list.data) {
7889
8330
  const invoice2 = row(value);
7890
8331
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
7891
- 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();
8332
+ 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();
7892
8333
  seen.add(id2(invoice2));
7893
8334
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
7894
8335
  }
7895
8336
  if (!list.has_more) break;
7896
- if (!list.data.length || page === 9) fail5("refund_history_incomplete");
8337
+ if (!list.data.length || page === 9) fail6("refund_history_incomplete");
7897
8338
  cursor = id2(list.data[list.data.length - 1]);
7898
8339
  }
7899
- if (!earliest) fail5("no_charge");
8340
+ if (!earliest) fail6("no_charge");
7900
8341
  const invoice = earliest;
7901
- if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
8342
+ if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail6();
7902
8343
  let chargeId = id2(invoice.charge);
7903
8344
  let paymentIntentId = id2(invoice.payment_intent);
7904
8345
  if (!chargeId && !paymentIntentId) {
7905
8346
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
7906
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
8347
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail6("refund_payment_ambiguous");
7907
8348
  const payment = row(payments.data[0]);
7908
- if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
8349
+ if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail6();
7909
8350
  paymentIntentId = id2(row(payment.payment).payment_intent);
7910
8351
  }
7911
8352
  if (!chargeId) {
7912
- if (!providerId2(paymentIntentId, "pi")) fail5();
8353
+ if (!providerId2(paymentIntentId, "pi")) fail6();
7913
8354
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
7914
- if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
8355
+ if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail6();
7915
8356
  chargeId = id2(payment.latest_charge);
7916
8357
  }
7917
- if (!providerId2(chargeId, "ch")) fail5();
8358
+ if (!providerId2(chargeId, "ch")) fail6();
7918
8359
  const charge = await call("GET", `/v1/charges/${chargeId}`);
7919
- 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();
7920
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
8360
+ 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();
8361
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail6();
7921
8362
  if (charge.amount_refunded !== charge.amount) {
7922
8363
  const refund = await call("POST", "/v1/refunds", {
7923
8364
  charge: chargeId,
7924
8365
  ...input.metadata ? { metadata: input.metadata } : {}
7925
8366
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
7926
- 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");
8367
+ 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");
7927
8368
  }
7928
8369
  let subscriptionCanceled = sub.status === "canceled";
7929
8370
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
7930
8371
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
7931
- if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
8372
+ if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail6("refund_cancel_incomplete");
7932
8373
  subscriptionCanceled = true;
7933
8374
  }
7934
8375
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
7935
- 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");
8376
+ 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");
7936
8377
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
7937
8378
  }
7938
8379
 
@@ -7998,7 +8439,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
7998
8439
  idempotencyKey ? { idempotencyKey } : {}
7999
8440
  );
8000
8441
  }
8001
- function fail6(op, r) {
8442
+ function fail7(op, r) {
8002
8443
  const err = new Error(`stripe ${op} failed: ${r.status}`);
8003
8444
  err.code = "stripe_error";
8004
8445
  throw err;
@@ -8048,7 +8489,7 @@ function createStripeProvider(config) {
8048
8489
  { email: input.email, name: input.name, metadata: meta },
8049
8490
  `cus:${input.applicationId}`
8050
8491
  );
8051
- if (!cust.ok) fail6("customer create", cust);
8492
+ if (!cust.ok) fail7("customer create", cust);
8052
8493
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
8053
8494
  customerId = cust.body.id;
8054
8495
  }
@@ -8080,7 +8521,7 @@ function createStripeProvider(config) {
8080
8521
  // retry between create and the db write can't orphan a second one.
8081
8522
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
8082
8523
  );
8083
- if (!sub.ok) fail6("subscription create", sub);
8524
+ if (!sub.ok) fail7("subscription create", sub);
8084
8525
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
8085
8526
  const subscriptionId = sub.body.id;
8086
8527
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -8090,14 +8531,14 @@ function createStripeProvider(config) {
8090
8531
  "expand[2]": "discounts",
8091
8532
  "expand[3]": "latest_invoice.discounts"
8092
8533
  });
8093
- if (!expanded.ok) fail6("subscription invoice evidence", expanded);
8534
+ if (!expanded.ok) fail7("subscription invoice evidence", expanded);
8094
8535
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
8095
8536
  sub = expanded;
8096
8537
  }
8097
8538
  verifySubscriptionResult(sub.body, input, customerId, meta);
8098
8539
  const invoice = sub.body.latest_invoice;
8099
8540
  const clientSecret2 = clientSecretOf(sub.body);
8100
- if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
8541
+ if (!clientSecret2) fail7("subscription confirmation-secret missing", sub);
8101
8542
  await tagSubscriptionPayment(
8102
8543
  sk,
8103
8544
  invoice,
@@ -8922,6 +9363,58 @@ var handleNamedSeat = async (req, url, env, ctx) => {
8922
9363
  }
8923
9364
  };
8924
9365
 
9366
+ // src/named-seat-accepted-template.ts
9367
+ var SEAT_ACCEPTED_TEMPLATE = {
9368
+ enabled: true,
9369
+ subject: "{{recipientName}} accepted your {{chapterName}} seat",
9370
+ 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}}"
9371
+ };
9372
+
9373
+ // src/named-seat-accepted-notice.ts
9374
+ var text5 = (value) => typeof value === "string" ? value : "";
9375
+ async function notifyNamedSeatAccepted(db, env, ctx, input) {
9376
+ const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
9377
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
9378
+ return { sent: false, reason: "purchaser-missing" };
9379
+ }
9380
+ const group = await membershipLoad(db, "groups", ctx.chapter.id);
9381
+ if (!group) return { sent: false, reason: "group-missing" };
9382
+ const config = emailGroupFrom(group);
9383
+ config.emailTemplates = { seatAccepted: SEAT_ACCEPTED_TEMPLATE, ...config.emailTemplates };
9384
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
9385
+ return sendTemplated({
9386
+ db,
9387
+ envName: env.ODLA_ENV,
9388
+ sender: env.SEND_EMAIL,
9389
+ from: env.EMAIL_FROM,
9390
+ debugEmail: env.ODLA_DEBUG_EMAIL,
9391
+ now: () => Date.now(),
9392
+ newId: () => crypto.randomUUID()
9393
+ }, {
9394
+ group: config,
9395
+ template: "seatAccepted",
9396
+ to: text5(purchaser.email),
9397
+ dedupeKey: `named-seat-accepted:${input.seatId}`,
9398
+ applicationId: input.primaryApplicationId,
9399
+ vars: {
9400
+ firstName: text5(purchaser.firstName),
9401
+ purchaserName,
9402
+ recipientName: input.recipientName,
9403
+ tierName: await tierName(db, group, input.skuId),
9404
+ chapterName: ctx.chapter.name,
9405
+ membersUrl: `${input.origin}/members/`
9406
+ }
9407
+ });
9408
+ }
9409
+ async function tierName(db, group, skuId) {
9410
+ try {
9411
+ const tier = (await loadTiers(db, group)).find((candidate) => candidate.id === skuId);
9412
+ if (tier) return tier.name;
9413
+ } catch {
9414
+ }
9415
+ return skuId;
9416
+ }
9417
+
8925
9418
  // src/worker-routes-named-seat-recipient.ts
8926
9419
  var handleNamedSeatRecipient = async (req, url, env, ctx) => {
8927
9420
  if (url.pathname === "/api/named-seat/invitation") {
@@ -9039,6 +9532,13 @@ var handleNamedSeatRecipient = async (req, url, env, ctx) => {
9039
9532
  const current = await membershipLoad(db, "applications", body.applicationId), group = await membershipLoad(db, "groups", ctx.chapter.id);
9040
9533
  if (!current || current.namedSeatClaimPending !== false || !current.namedSeatAcceptedAt || !group) throw new Error("seat application reconciliation pending");
9041
9534
  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");
9535
+ await notifyNamedSeatAccepted(db, env, ctx, {
9536
+ seatId: body.seatId,
9537
+ primaryApplicationId: invitation.primaryApplicationId,
9538
+ recipientName: invitation.recipientName,
9539
+ skuId: invitation.skuId,
9540
+ origin: url.origin
9541
+ }).catch(() => console.warn("named_seat_accepted_notice_incomplete", { seatId: body.seatId }));
9042
9542
  await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) }).catch(() => void 0);
9043
9543
  const state = await continueJoin(db, ctx, env, url.origin, current, group);
9044
9544
  if (!state) throw new Error("seat Journey reconciliation pending");
@@ -9399,8 +9899,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
9399
9899
  };
9400
9900
 
9401
9901
  // src/crm-account-repair.ts
9402
- async function repairAccountPeoplePage(deps2, after) {
9403
- const users = (await deps2.db.query({ $users: { $: {
9902
+ async function repairAccountPeoplePage(deps, after) {
9903
+ const users = (await deps.db.query({ $users: { $: {
9404
9904
  ...after ? { where: { id: { $gt: after } } } : {},
9405
9905
  order: { id: "asc" },
9406
9906
  limit: 50
@@ -9409,7 +9909,7 @@ async function repairAccountPeoplePage(deps2, after) {
9409
9909
  const errors = [];
9410
9910
  for (const user of users) {
9411
9911
  try {
9412
- if (await syncAccountToCrm(deps2, String(user.id))) synced++;
9912
+ if (await syncAccountToCrm(deps, String(user.id))) synced++;
9413
9913
  } catch (error) {
9414
9914
  const message = error instanceof Error ? error.message : "";
9415
9915
  errors.push({ userId: String(user.id), code: /^crm_projection_identity_(ambiguous|archived|conflict)$/.test(message) ? message : "crm_projection_unavailable" });
@@ -9421,8 +9921,8 @@ async function repairAccountPeoplePage(deps2, after) {
9421
9921
  // src/crm-application-repair.ts
9422
9922
  import { getRecord as getRecord4, updateRecord as updateRecord5 } from "@odla-ai/crm";
9423
9923
  async function repairApplicationLink(db, chapter, recordId, actorId) {
9424
- const deps2 = { crm: chapter.crm, db };
9425
- const record4 = await getRecord4(deps2, recordId);
9924
+ const deps = { crm: chapter.crm, db };
9925
+ const record4 = await getRecord4(deps, recordId);
9426
9926
  if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
9427
9927
  if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
9428
9928
  const fields = record4.fields;
@@ -9440,7 +9940,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
9440
9940
  }
9441
9941
  const app = [...applications.values()][0];
9442
9942
  if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
9443
- await updateRecord5(deps2, {
9943
+ await updateRecord5(deps, {
9444
9944
  id: recordId,
9445
9945
  input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
9446
9946
  expectedUpdatedAt: record4.updatedAt,
@@ -10138,15 +10638,6 @@ async function loadGroup2(db, id6) {
10138
10638
  return groups?.[0] ?? null;
10139
10639
  }
10140
10640
  var str2 = (v, d = "") => typeof v === "string" ? v : d;
10141
- var notifyDeps = (db, env) => ({
10142
- db,
10143
- debugEmail: env.ODLA_DEBUG_EMAIL,
10144
- envName: env.ODLA_ENV,
10145
- sender: env.SEND_EMAIL,
10146
- from: env.EMAIL_FROM,
10147
- now: () => Date.now(),
10148
- newId: () => crypto.randomUUID()
10149
- });
10150
10641
  var handleAdminGroupEmail = async (req, url, env, ctx) => {
10151
10642
  if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
10152
10643
  const got = await gate5(req, env, ctx);
@@ -10165,6 +10656,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
10165
10656
  groupId: group.id,
10166
10657
  name: group.name,
10167
10658
  replyTo: str2(group.replyTo),
10659
+ fromName: str2(group.fromName),
10168
10660
  notificationEmail: str2(group.notificationEmail),
10169
10661
  debugEmail: str2(group.debugEmail),
10170
10662
  emailTemplates,
@@ -10191,11 +10683,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
10191
10683
  for (const key3 of EMAIL_TEMPLATE_NAMES) {
10192
10684
  const t = templates[key3];
10193
10685
  const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
10194
- const text6 = typeof t?.text === "string" ? t.text : "";
10195
- if (!subject || !text6.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
10686
+ const text8 = typeof t?.text === "string" ? t.text : "";
10687
+ if (!subject || !text8.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
10196
10688
  if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
10197
- if (text6.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
10198
- clean[key3] = { subject, text: text6, enabled: t?.enabled !== false };
10689
+ if (text8.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
10690
+ clean[key3] = { subject, text: text8, enabled: t?.enabled !== false };
10199
10691
  }
10200
10692
  const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
10201
10693
  const notificationEmail = str2(body.notificationEmail).trim();
@@ -10204,13 +10696,15 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
10204
10696
  if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
10205
10697
  if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
10206
10698
  if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
10699
+ const fromName = str2(body.fromName).trim();
10700
+ 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);
10207
10701
  const commitmentText = str2(body.commitmentText);
10208
10702
  const normsText = str2(body.normsText);
10209
10703
  if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
10210
10704
  const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
10211
10705
  await db.transact([
10212
10706
  { t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
10213
- { t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail } }
10707
+ { t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
10214
10708
  ]);
10215
10709
  return json({ ok: true });
10216
10710
  };
@@ -10259,11 +10753,11 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
10259
10753
  }
10260
10754
  const group = await loadGroup2(db, ctx.chapter.id);
10261
10755
  if (!group) return json({ error: "not found" }, 404);
10262
- const res = await sendTemplated(notifyDeps(db, env), {
10756
+ const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
10263
10757
  group: emailGroupFrom(group),
10264
10758
  template,
10265
10759
  to: str2(group.notificationEmail),
10266
- vars: { firstName: "Sample", lastName: "Person", email: "sample@example.com", phone: "(555) 010-0100", state: "CA", adminUrl: `${url.origin}/admin/`, membersUrl: `${url.origin}/members/` },
10760
+ vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
10267
10761
  dedupeKey: `test:${template}:${Date.now()}`,
10268
10762
  force: true
10269
10763
  });
@@ -10290,7 +10784,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
10290
10784
  const app = data.applications?.[0] ?? null;
10291
10785
  if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
10292
10786
  const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
10293
- 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;
10787
+ 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;
10294
10788
  const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
10295
10789
  let mailBody = typeof r.body === "string" ? r.body : null;
10296
10790
  if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
@@ -10314,6 +10808,28 @@ var handleAdminComms = async (req, url, env, ctx) => {
10314
10808
  import { getRecord as getRecord5, typeSummary } from "@odla-ai/crm";
10315
10809
  import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
10316
10810
 
10811
+ // src/network-edge.ts
10812
+ var EDGE_UNPAIRED = "edge_unpaired";
10813
+ function pairingCommand(leaderId, targetId, env) {
10814
+ const scope2 = env ? ` --env ${env}` : "";
10815
+ return `odla-ai chapter edge pair --leader ${leaderId} --follower ${targetId}${scope2}`;
10816
+ }
10817
+ function edgeUnpaired(leaderId, target, env) {
10818
+ return {
10819
+ code: EDGE_UNPAIRED,
10820
+ target: target.id,
10821
+ secretName: target.secretName,
10822
+ message: `edge to "${target.id}" is unpaired: leader vault slot "${target.secretName}" is empty`,
10823
+ hint: pairingCommand(leaderId, target.id, env)
10824
+ };
10825
+ }
10826
+ function unpairedResponse(unpaired) {
10827
+ return new Response(JSON.stringify({ error: unpaired.message, ...unpaired }), {
10828
+ status: 502,
10829
+ headers: { "content-type": "application/json" }
10830
+ });
10831
+ }
10832
+
10317
10833
  // src/worker-network-push.ts
10318
10834
  import { recordDeliveryAttempt } from "@odla-ai/crm";
10319
10835
  import { signFederatedRequest as signFederatedRequest3 } from "@odla-ai/db";
@@ -10326,15 +10842,15 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
10326
10842
  newId: () => crypto.randomUUID()
10327
10843
  };
10328
10844
  if (!secret) {
10329
- const error = `vault secret "${target.secretName}" is missing`;
10845
+ const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
10330
10846
  await recordDeliveryAttempt(crmDeps3, {
10331
10847
  recordId: record4.id,
10332
10848
  targetId: target.id,
10333
10849
  status: "failed",
10334
10850
  payloadVersion: 2,
10335
- error
10851
+ error: unpaired.message
10336
10852
  });
10337
- return { id: target.id, name: target.name, ok: false, error };
10853
+ return { id: target.id, name: target.name, ok: false, error: unpaired.message, code: unpaired.code, hint: unpaired.hint };
10338
10854
  }
10339
10855
  let payload;
10340
10856
  try {
@@ -10475,17 +10991,20 @@ async function snapshotOne(db, env, ctx, target) {
10475
10991
  publicSite: { available: null }
10476
10992
  };
10477
10993
  const secret = await getVaultSecret(db, target.secretName);
10478
- if (!secret) return {
10479
- ...base,
10480
- available: false,
10481
- transport: {
10482
- kind: transportKind,
10483
- ...target.binding ? { binding: target.binding } : {},
10484
- reachable: null,
10485
- error: { code: "edge_secret_missing", message: "edge secret is missing" }
10486
- },
10487
- error: "edge secret is missing"
10488
- };
10994
+ if (!secret) {
10995
+ const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
10996
+ return {
10997
+ ...base,
10998
+ available: false,
10999
+ transport: {
11000
+ kind: transportKind,
11001
+ ...target.binding ? { binding: target.binding } : {},
11002
+ reachable: null,
11003
+ error: unpaired
11004
+ },
11005
+ error: unpaired.message
11006
+ };
11007
+ }
10489
11008
  const destination = new URL("/api/network/snapshot", target.url);
10490
11009
  try {
10491
11010
  const transport = resolveNetworkTargetTransport(env, target);
@@ -10965,7 +11484,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
10965
11484
  if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
10966
11485
  return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
10967
11486
  }
10968
- const deps2 = { crm: ctx.chapter.crm, db };
11487
+ const deps = { crm: ctx.chapter.crm, db };
10969
11488
  if (hasFields && ctx.chapter.mode === "chapter" && record4.type === "person") {
10970
11489
  const updated2 = await updateChapterPerson({
10971
11490
  db,
@@ -10984,7 +11503,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
10984
11503
  record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
10985
11504
  });
10986
11505
  }
10987
- const updated = hasFields ? await updateRecord6(deps2, {
11506
+ const updated = hasFields ? await updateRecord6(deps, {
10988
11507
  id: record4.id,
10989
11508
  input: fields,
10990
11509
  mutationId: activityId,
@@ -10995,7 +11514,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
10995
11514
  authorId: `network:${verified.sender}`,
10996
11515
  meta: auditMeta
10997
11516
  }
10998
- }) : await setStage3(deps2, {
11517
+ }) : await setStage3(deps, {
10999
11518
  id: record4.id,
11000
11519
  to: input.stage,
11001
11520
  authorId: `network:${verified.sender}`,
@@ -11095,7 +11614,10 @@ function targetFor2(ctx, id6) {
11095
11614
  }
11096
11615
  async function readTargetRecords(db, env, ctx, target, query) {
11097
11616
  const secret = await getVaultSecret(db, target.secretName);
11098
- if (!secret) return { ok: false, error: "edge secret is missing" };
11617
+ if (!secret) {
11618
+ const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
11619
+ return { ok: false, error: unpaired.message, unpaired };
11620
+ }
11099
11621
  const destination = new URL("/api/network/records", target.url);
11100
11622
  destination.search = query.toString();
11101
11623
  try {
@@ -11132,7 +11654,7 @@ async function mutateTargetRecord(db, env, ctx, target, input) {
11132
11654
  return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
11133
11655
  }
11134
11656
  const secret = await getVaultSecret(db, target.secretName);
11135
- if (!secret) return json({ error: "edge secret is missing", code: "edge_unavailable" }, 502);
11657
+ if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
11136
11658
  const destination = new URL("/api/network/records", target.url);
11137
11659
  const body = JSON.stringify(input);
11138
11660
  try {
@@ -11214,7 +11736,9 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
11214
11736
  if (window.search) query.set("search", window.search);
11215
11737
  if (window.stage) query.set("stage", window.stage);
11216
11738
  const result = await readTargetRecords(got.db, env, ctx, target, query);
11217
- if (!result.ok) return json({ error: result.error }, result.status ?? 502);
11739
+ if (!result.ok) {
11740
+ return result.unpaired ? unpairedResponse(result.unpaired) : json({ error: result.error }, result.status ?? 502);
11741
+ }
11218
11742
  if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
11219
11743
  return json({ error: "follower did not apply the requested record query" }, 502);
11220
11744
  }
@@ -11470,7 +11994,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
11470
11994
  body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
11471
11995
  }
11472
11996
  const secret = await getVaultSecret(got.db, target.secretName);
11473
- if (!secret) return json({ error: "edge secret is missing" }, 502);
11997
+ if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
11474
11998
  const destination = new URL("/api/network/shared-notes", target.url);
11475
11999
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
11476
12000
  try {
@@ -11524,11 +12048,11 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
11524
12048
  if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
11525
12049
  const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
11526
12050
  const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
11527
- const provenance = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
12051
+ const provenance2 = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
11528
12052
  const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
11529
12053
  if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
11530
12054
  if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
11531
- if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
12055
+ if (!provenance2 || provenance2.length > 500) errors.push("provenance is required and bounded");
11532
12056
  if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
11533
12057
  if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
11534
12058
  errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
@@ -11596,7 +12120,7 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
11596
12120
  sourceKind: "external_reimport",
11597
12121
  sourceSystem,
11598
12122
  sourceRecordId,
11599
- provenance,
12123
+ provenance: provenance2,
11600
12124
  approved: true,
11601
12125
  isRealPerson: true,
11602
12126
  displayName,
@@ -12301,7 +12825,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
12301
12825
  }
12302
12826
 
12303
12827
  // src/worker-routes-network-person-registry.ts
12304
- var PATH3 = "/api/admin/network-person-registry";
12828
+ var PATH5 = "/api/admin/network-person-registry";
12305
12829
  var MAX_BODY2 = 8 * 1024 * 1024;
12306
12830
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
12307
12831
  var positiveRevision = (value) => {
@@ -12326,7 +12850,7 @@ function registryError(error) {
12326
12850
  return null;
12327
12851
  }
12328
12852
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12329
- if (url.pathname !== PATH3) return null;
12853
+ if (url.pathname !== PATH5) return null;
12330
12854
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
12331
12855
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
12332
12856
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -12338,9 +12862,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12338
12862
  if (req.method === "POST") {
12339
12863
  let body;
12340
12864
  try {
12341
- const text6 = await req.text();
12342
- if (new TextEncoder().encode(text6).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
12343
- const parsed = JSON.parse(text6);
12865
+ const text8 = await req.text();
12866
+ if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
12867
+ const parsed = JSON.parse(text8);
12344
12868
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
12345
12869
  body = parsed;
12346
12870
  } catch {
@@ -12509,7 +13033,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
12509
13033
  // src/network-commercial-parity.ts
12510
13034
  var DIGEST = /^[0-9a-f]{64}$/;
12511
13035
  var MAX_RECORDS = 500;
12512
- var text4 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
13036
+ var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
12513
13037
  function parityMessage(partition, kind, value) {
12514
13038
  return [
12515
13039
  "chapter-commercial-parity",
@@ -12541,18 +13065,18 @@ async function digestWithKey(key3, partition, kind, value) {
12541
13065
  async function buildChapterCommercialParitySnapshot(input) {
12542
13066
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
12543
13067
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
12544
- const eligible = input.rows.filter((row2) => text4(row2.id) && (text4(row2.stripeCustomerId) || text4(row2.stripeSubscriptionId)));
13068
+ const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
12545
13069
  const selected2 = eligible.slice(0, maxRecords);
12546
13070
  const key3 = await parityKey(input.secret);
12547
13071
  const records = await Promise.all(selected2.map(async (row2) => {
12548
- const applicationId = text4(row2.id);
12549
- const customerId = text4(row2.stripeCustomerId);
12550
- const subscriptionId = text4(row2.stripeSubscriptionId);
13072
+ const applicationId = text6(row2.id);
13073
+ const customerId = text6(row2.stripeCustomerId);
13074
+ const subscriptionId = text6(row2.stripeSubscriptionId);
12551
13075
  return {
12552
13076
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
12553
13077
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
12554
13078
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
12555
- status: text4(row2.status, 64),
13079
+ status: text6(row2.status, 64),
12556
13080
  canceled: row2.canceled === true
12557
13081
  };
12558
13082
  }));
@@ -12578,7 +13102,7 @@ function validSnapshot(snapshot, source) {
12578
13102
  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;
12579
13103
  const seen = /* @__PURE__ */ new Set();
12580
13104
  for (const record4 of records) {
12581
- 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;
13105
+ 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;
12582
13106
  const key3 = recordKey(record4);
12583
13107
  if (seen.has(key3)) return false;
12584
13108
  seen.add(key3);
@@ -12589,16 +13113,16 @@ async function reconcileCommercialParitySnapshot(input) {
12589
13113
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
12590
13114
  const sourceKey = networkCommercialSourceKey(input.source);
12591
13115
  const key3 = await parityKey(input.secret);
12592
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text4(row2.applicationId) && text4(row2.objectId)).map(async (row2) => {
12593
- const customerId = text4(row2.customerId);
12594
- const subscriptionId = text4(row2.subscriptionId) ?? text4(row2.objectId);
13116
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
13117
+ const customerId = text6(row2.customerId);
13118
+ const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
12595
13119
  return {
12596
13120
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
12597
13121
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
12598
13122
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
12599
13123
  status: null,
12600
13124
  canceled: false,
12601
- stripeStatus: text4(row2.status, 64),
13125
+ stripeStatus: text6(row2.status, 64),
12602
13126
  stripeCanceled: row2.status === "canceled"
12603
13127
  };
12604
13128
  }));
@@ -12724,7 +13248,7 @@ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
12724
13248
  async function reconcileTarget(db, env, ctx, target, projection) {
12725
13249
  const base = { targetId: target.id, name: target.name, url: target.url };
12726
13250
  const secret = await getVaultSecret(db, target.secretName);
12727
- if (!secret) return { ...base, available: false, ok: false, error: { code: "edge_secret_missing" } };
13251
+ if (!secret) return { ...base, available: false, ok: false, error: edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV) };
12728
13252
  const destination = new URL(PARITY_PATH, target.url);
12729
13253
  try {
12730
13254
  const transport = resolveNetworkTargetTransport(env, target);
@@ -12880,11 +13404,23 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
12880
13404
  const event = await constructStripeEvent(body, signature, secret);
12881
13405
  const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
12882
13406
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
12883
- const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact, Date.now(), await stripePayloadDigest(body));
13407
+ const result = await ingestNetworkCommercialFact(
13408
+ db,
13409
+ ctx.chapter.networkCommercialReadModel.sources,
13410
+ fact,
13411
+ Date.now(),
13412
+ await stripePayloadDigest(body),
13413
+ networkCommercialAlerts(ctx, WEBHOOK_PATH)
13414
+ );
12884
13415
  if (result.status === "collision") {
12885
13416
  console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
12886
13417
  }
12887
- return json({ ok: true, status: result.status, eventId: fact.eventId });
13418
+ return json({
13419
+ ok: true,
13420
+ status: result.status,
13421
+ eventId: fact.eventId,
13422
+ ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
13423
+ });
12888
13424
  } catch (error) {
12889
13425
  if (error instanceof StripeError) return json({ error: error.message, code: error.code }, 400);
12890
13426
  throw error;
@@ -12908,18 +13444,39 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12908
13444
  } catch {
12909
13445
  return json({ error: "invalid JSON body" }, 400);
12910
13446
  }
13447
+ const alerts = networkCommercialAlerts(ctx, ADMIN_PATH2);
12911
13448
  if (parsed.command === "recover-event") {
12912
13449
  try {
12913
13450
  return json(await recoverNetworkCommercialEvent(
12914
13451
  db,
12915
13452
  ctx.chapter.networkCommercialReadModel.sources,
12916
13453
  String(parsed.eventId ?? ""),
12917
- got.user.userId
13454
+ got.user.userId,
13455
+ Date.now(),
13456
+ alerts
12918
13457
  ));
12919
13458
  } catch (error) {
12920
13459
  return json({ error: error instanceof Error ? error.message : "event recovery failed" }, 409);
12921
13460
  }
12922
13461
  }
13462
+ if (parsed.command === "resolve") {
13463
+ if (typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) return json({ error: "resolve requires a valid mutationId" }, 400);
13464
+ const reason = typeof parsed.reason === "string" ? parsed.reason.trim() : "";
13465
+ if (!reason || reason.length > 500) return json({ error: "resolve requires a reason of at most 500 characters" }, 400);
13466
+ try {
13467
+ const result = await resolveNetworkCommercialCollision(
13468
+ db,
13469
+ ctx.chapter.networkCommercialReadModel.sources,
13470
+ { eventId: String(parsed.eventId ?? ""), mutationId: parsed.mutationId, actorUserId: got.user.userId, reason },
13471
+ Date.now(),
13472
+ alerts
13473
+ );
13474
+ return json({ resolved: true, eventId: result.eventId, reviewId: result.reviewId, ...safeProjection(result.projection) });
13475
+ } catch (error) {
13476
+ const code = error.code;
13477
+ return json({ error: error instanceof Error ? error.message : "resolve failed", code }, code === "invalid_event" ? 400 : 409);
13478
+ }
13479
+ }
12923
13480
  if (parsed.command === "reconcile-chapters") {
12924
13481
  const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
12925
13482
  return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
@@ -12947,9 +13504,9 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12947
13504
  }
12948
13505
  }
12949
13506
  if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
12950
- return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also requires a valid mutationId" }, 400);
13507
+ return json({ error: "rebuild, resolve, recover-event, reconcile-stripe, or reconcile-chapters command is required; rebuild and resolve also require a valid mutationId" }, 400);
12951
13508
  }
12952
- const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
13509
+ const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId, Date.now(), alerts);
12953
13510
  return json({ rebuilt: true, ...safeProjection(projection) });
12954
13511
  }
12955
13512
  return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
@@ -13059,7 +13616,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
13059
13616
  function unknownKeys3(value, allowed2, path, errors) {
13060
13617
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
13061
13618
  }
13062
- function text5(value, path, errors, max = MAX_TEXT2, required = true) {
13619
+ function text7(value, path, errors, max = MAX_TEXT2, required = true) {
13063
13620
  if (value === void 0 && !required) return void 0;
13064
13621
  if (typeof value !== "string" || required && !value.trim()) {
13065
13622
  errors.push(`${path} must be a non-empty string`);
@@ -13085,7 +13642,7 @@ function scopeValue(value, path, errors) {
13085
13642
  return { kind: raw2.kind };
13086
13643
  }
13087
13644
  if (raw2.kind === "chapter") {
13088
- const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
13645
+ const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
13089
13646
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
13090
13647
  return { kind: "chapter", chapterId };
13091
13648
  }
@@ -13099,22 +13656,22 @@ function resourceValue(value, path, errors) {
13099
13656
  return null;
13100
13657
  }
13101
13658
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
13102
- const id6 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
13659
+ const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
13103
13660
  if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
13104
- const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
13661
+ const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
13105
13662
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
13106
- const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
13107
- const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
13108
- const status2 = text5(raw2.status, `${path}.status`, errors) ?? "";
13663
+ const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
13664
+ const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
13665
+ const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
13109
13666
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
13110
13667
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
13111
13668
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
13112
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
13669
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
13113
13670
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
13114
13671
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
13115
13672
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
13116
- const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
13117
- const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
13673
+ const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
13674
+ const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
13118
13675
  if (url) {
13119
13676
  try {
13120
13677
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -13126,9 +13683,9 @@ function resourceValue(value, path, errors) {
13126
13683
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
13127
13684
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
13128
13685
  const brandAsset = asset ? {
13129
- bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
13130
- assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
13131
- ...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
13686
+ bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
13687
+ assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
13688
+ ...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
13132
13689
  } : void 0;
13133
13690
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
13134
13691
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -13143,21 +13700,21 @@ function resourceValue(value, path, errors) {
13143
13700
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
13144
13701
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
13145
13702
  const promotedFrom = promotedRaw ? {
13146
- chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
13147
- resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
13703
+ chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
13704
+ resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
13148
13705
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
13149
13706
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
13150
- promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
13707
+ promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
13151
13708
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
13152
13709
  } : void 0;
13153
- const provenance = {
13154
- createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
13710
+ const provenance2 = {
13711
+ createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
13155
13712
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
13156
- updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
13713
+ updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
13157
13714
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
13158
13715
  ...promotedFrom ? { promotedFrom } : {}
13159
13716
  };
13160
- return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
13717
+ return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
13161
13718
  }
13162
13719
  function validateBuilderLibraryDocument(value, options = {}) {
13163
13720
  const errors = [];
@@ -13172,7 +13729,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
13172
13729
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
13173
13730
  const sourceType = sourceRaw?.type;
13174
13731
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
13175
- const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
13732
+ const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
13176
13733
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
13177
13734
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
13178
13735
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -13180,8 +13737,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
13180
13737
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
13181
13738
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
13182
13739
  const target = targetRaw ? {
13183
- appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
13184
- environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
13740
+ appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
13741
+ environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
13185
13742
  } : void 0;
13186
13743
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
13187
13744
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15193,7 +15750,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
15193
15750
  };
15194
15751
  signal.addEventListener("abort", cancel, { once: true });
15195
15752
  let size = 0;
15196
- let text6 = "";
15753
+ let text8 = "";
15197
15754
  const decoder = new TextDecoder();
15198
15755
  try {
15199
15756
  while (true) {
@@ -15203,10 +15760,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
15203
15760
  if (done) break;
15204
15761
  size += value.byteLength;
15205
15762
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
15206
- text6 += decoder.decode(value, { stream: true });
15763
+ text8 += decoder.decode(value, { stream: true });
15207
15764
  }
15208
15765
  try {
15209
- return JSON.parse(text6 + decoder.decode());
15766
+ return JSON.parse(text8 + decoder.decode());
15210
15767
  } catch {
15211
15768
  throw new TypeError(`invalid ${label}`);
15212
15769
  }
@@ -15868,6 +16425,7 @@ var BUILTIN_ROUTES = [
15868
16425
  handlePaymentRecovery,
15869
16426
  handleAdminNamedSeat,
15870
16427
  handleAdminMembershipSeats,
16428
+ handleAdminMembershipPolicyVersions,
15871
16429
  handleAdminMeetings,
15872
16430
  handleAdminScheduling,
15873
16431
  // Roster + identity