@odla-ai/chapter 0.59.0 → 0.59.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.
package/dist/index.d.cts CHANGED
@@ -1714,7 +1714,7 @@ declare function readMembershipAccess(db: ChapterDb, input: MembershipQuoteTarge
1714
1714
  declare function verifyMembershipAccessReceipt(value: unknown, input: MembershipQuoteTarget, now: number): Promise<MembershipAccessReceipt>;
1715
1715
 
1716
1716
  /** Where the named recipient stands, from the purchaser's side, folded from the seat's lifecycle status. */
1717
- type NamedSeatRecipientState = "payment_pending" | "invited" | "accepted" | "joined" | "declined" | "refunded" | "canceled";
1717
+ type NamedSeatRecipientState = "payment_pending" | "invited" | "accepted" | "joined" | "declined" | "refunded" | "canceled" | "disputed";
1718
1718
 
1719
1719
  /** The purchaser's one named seat as the members page shows it: who was named, and whether they accepted or joined. */
1720
1720
  interface NamedSeatOfferSeat {
package/dist/index.d.ts CHANGED
@@ -1714,7 +1714,7 @@ declare function readMembershipAccess(db: ChapterDb, input: MembershipQuoteTarge
1714
1714
  declare function verifyMembershipAccessReceipt(value: unknown, input: MembershipQuoteTarget, now: number): Promise<MembershipAccessReceipt>;
1715
1715
 
1716
1716
  /** Where the named recipient stands, from the purchaser's side, folded from the seat's lifecycle status. */
1717
- type NamedSeatRecipientState = "payment_pending" | "invited" | "accepted" | "joined" | "declined" | "refunded" | "canceled";
1717
+ type NamedSeatRecipientState = "payment_pending" | "invited" | "accepted" | "joined" | "declined" | "refunded" | "canceled" | "disputed";
1718
1718
 
1719
1719
  /** The purchaser's one named seat as the members page shows it: who was named, and whether they accepted or joined. */
1720
1720
  interface NamedSeatOfferSeat {
package/dist/index.js CHANGED
@@ -1714,8 +1714,8 @@ var APPLICATION_LABELS = {
1714
1714
  state: "State"
1715
1715
  };
1716
1716
  function humanize(id11) {
1717
- const text7 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1718
- return text7 ? `${text7[0].toUpperCase()}${text7.slice(1)}` : id11;
1717
+ const text8 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1718
+ return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id11;
1719
1719
  }
1720
1720
  function applicationDefs(chapter) {
1721
1721
  try {
@@ -4382,6 +4382,9 @@ function namedSeatRecipientState(seat) {
4382
4382
  return seat.refundReason === "declined" ? "declined" : "refunded";
4383
4383
  case "canceled":
4384
4384
  return "canceled";
4385
+ // A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
4386
+ case "payment_disputed":
4387
+ return "disputed";
4385
4388
  default:
4386
4389
  return "payment_pending";
4387
4390
  }
@@ -4577,12 +4580,87 @@ async function reconcileMembershipRenewal(db, input, now) {
4577
4580
 
4578
4581
  // src/membership-dispute-authority.ts
4579
4582
  import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4583
+
4584
+ // src/membership-dispute-transition.ts
4580
4585
  var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
4581
4586
  var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
4582
4587
  var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
4583
- var TERMINAL = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
4588
+ function disputeTransition(row2, fact, eventId, now, terminal) {
4589
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
4590
+ const sameDispute = row2.disputeId === fact.objectId;
4591
+ if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
4592
+ const won = DISPUTE_WON_STATUSES.includes(status2);
4593
+ const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
4594
+ const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
4595
+ const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
4596
+ return {
4597
+ status: next,
4598
+ disputeId: fact.objectId,
4599
+ disputeStatus: status2,
4600
+ disputeEventId: eventId,
4601
+ disputeClosedAt: closed ? now : 0,
4602
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
4603
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
4604
+ ...sameDispute ? {} : { disputedAt: now },
4605
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
4606
+ revision: Number(row2.revision) + 1,
4607
+ updatedAt: now
4608
+ };
4609
+ }
4610
+
4611
+ // src/membership-seat-dispute.ts
4584
4612
  var text3 = (value) => typeof value === "string" && value ? value : void 0;
4585
- var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
4613
+ var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
4614
+ var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text3(seat.disputeStatus) ? { disputeStatus: text3(seat.disputeStatus) } : {}, applied });
4615
+ function disputedChargeCoversSeat(charge, seat) {
4616
+ return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
4617
+ }
4618
+ async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
4619
+ if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
4620
+ const membership = async (applied, seat) => {
4621
+ const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
4622
+ requireMembershipScope(member, input);
4623
+ return {
4624
+ membershipId: id11,
4625
+ status: String(member.status),
4626
+ termEndsAt: Number(member.termEndsAt),
4627
+ graceEndsAt: Number(member.graceEndsAt ?? 0),
4628
+ autoRenew: member.autoRenew === true,
4629
+ applied,
4630
+ ...text3(member.disputeStatus) ? { disputeStatus: text3(member.disputeStatus) } : {},
4631
+ ...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
4632
+ };
4633
+ };
4634
+ for (let attempt = 0; attempt < 8; attempt++) {
4635
+ const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
4636
+ if (!seat) return membership(false);
4637
+ requireMembershipScope(seat, input);
4638
+ if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
4639
+ if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
4640
+ const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
4641
+ if (!attrs) return membership(false, seat);
4642
+ try {
4643
+ await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
4644
+ { ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
4645
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4646
+ { ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
4647
+ evidence.guard
4648
+ ] });
4649
+ } catch (error) {
4650
+ if (error.code !== "transact_guard_failed") throw error;
4651
+ continue;
4652
+ }
4653
+ const saved = await membershipLoad(db, "networkSeat", String(seat.id));
4654
+ requireMembershipScope(saved, input);
4655
+ return membership(true, saved);
4656
+ }
4657
+ return membershipFail("membership_dispute_retry_required");
4658
+ }
4659
+
4660
+ // src/membership-dispute-authority.ts
4661
+ var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
4662
+ var text4 = (value) => typeof value === "string" && value ? value : void 0;
4663
+ var refOf = (value) => typeof value === "string" ? text4(value) : text4(value?.id);
4586
4664
  async function readDisputedCharge(secretKey, chargeId, fetch2) {
4587
4665
  if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
4588
4666
  const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
@@ -4596,7 +4674,7 @@ async function readDisputedCharge(secretKey, chargeId, fetch2) {
4596
4674
  if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
4597
4675
  subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
4598
4676
  }
4599
- const seatId = text3(charge.body.metadata?.seatId);
4677
+ const seatId = text4(charge.body.metadata?.seatId);
4600
4678
  return {
4601
4679
  chargeId,
4602
4680
  customerId,
@@ -4622,37 +4700,23 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
4622
4700
  termEndsAt: Number(value.termEndsAt),
4623
4701
  graceEndsAt: Number(value.graceEndsAt ?? 0),
4624
4702
  autoRenew: value.autoRenew === true,
4625
- ...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
4703
+ ...text4(value.disputeStatus) ? { disputeStatus: text4(value.disputeStatus) } : {},
4626
4704
  applied
4627
4705
  });
4628
4706
  const known = await membershipLoad(db, "networkMembership", id11);
4629
4707
  requireMembershipScope(known, input);
4630
4708
  if (known.disputeEventId === input.eventId) return receipt(known);
4709
+ const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
4710
+ if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
4631
4711
  const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
4712
+ if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
4713
+ if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
4632
4714
  for (let attempt = 0; attempt < 8; attempt++) {
4633
4715
  const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4634
4716
  requireMembershipScope(member, input);
4635
- if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
4636
4717
  if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
4637
- const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
4638
- const sameDispute = member.disputeId === fact.objectId;
4639
- if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
4640
- const won = DISPUTE_WON_STATUSES.includes(status2);
4641
- const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
4642
- const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
4643
- const attrs = {
4644
- status: next,
4645
- disputeId: fact.objectId,
4646
- disputeStatus: status2,
4647
- disputeEventId: input.eventId,
4648
- disputeClosedAt: closed ? now : 0,
4649
- ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
4650
- ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
4651
- ...sameDispute ? {} : { disputedAt: now },
4652
- ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
4653
- revision: Number(member.revision) + 1,
4654
- updatedAt: now
4655
- };
4718
+ const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
4719
+ if (!attrs) return receipt(member, false);
4656
4720
  try {
4657
4721
  await db.transact([membershipUpdate("networkMembership", id11, attrs)], { mutationId: `membership-dispute:${id11}:${input.eventId}`, guards: [
4658
4722
  { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
@@ -6056,7 +6120,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
6056
6120
  };
6057
6121
  signal.addEventListener("abort", cancel, { once: true });
6058
6122
  let size = 0;
6059
- let text7 = "";
6123
+ let text8 = "";
6060
6124
  const decoder = new TextDecoder();
6061
6125
  try {
6062
6126
  while (true) {
@@ -6066,10 +6130,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
6066
6130
  if (done) break;
6067
6131
  size += value.byteLength;
6068
6132
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
6069
- text7 += decoder.decode(value, { stream: true });
6133
+ text8 += decoder.decode(value, { stream: true });
6070
6134
  }
6071
6135
  try {
6072
- return JSON.parse(text7 + decoder.decode());
6136
+ return JSON.parse(text8 + decoder.decode());
6073
6137
  } catch {
6074
6138
  throw new TypeError(`invalid ${label}`);
6075
6139
  }
@@ -6556,6 +6620,19 @@ var id10 = () => attr9("string", false, true, true);
6556
6620
  var key2 = (optional = false) => attr9("string", optional, false, true);
6557
6621
  var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
6558
6622
  var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
6623
+ var dispute = () => ({
6624
+ disputeId: key2(true),
6625
+ disputeStatus: attr9("string", true),
6626
+ disputeReason: attr9("string", true),
6627
+ disputeAmountCents: attr9("number", true),
6628
+ disputedAt: attr9("number", true),
6629
+ disputeClosedAt: attr9("number", true),
6630
+ disputePriorStatus: attr9("string", true),
6631
+ disputeEventId: key2(true),
6632
+ rechargePaymentIntentId: key2(true),
6633
+ rechargedAt: attr9("number", true),
6634
+ rechargeAttempt: attr9("json", true)
6635
+ });
6559
6636
  var membershipAuthorityEntities = {
6560
6637
  networkMembership: { attrs: {
6561
6638
  id: id10(),
@@ -6596,19 +6673,7 @@ var membershipAuthorityEntities = {
6596
6673
  ...times(),
6597
6674
  refundRequestedAt: attr9("number", true),
6598
6675
  restartId: key2(true),
6599
- // A card-network dispute on this membership's payment: Stripe's dispute id and status, the reason the cardholder gave,
6600
- // the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
6601
- disputeId: key2(true),
6602
- disputeStatus: attr9("string", true),
6603
- disputeReason: attr9("string", true),
6604
- disputeAmountCents: attr9("number", true),
6605
- disputedAt: attr9("number", true),
6606
- disputeClosedAt: attr9("number", true),
6607
- disputePriorStatus: attr9("string", true),
6608
- disputeEventId: key2(true),
6609
- rechargePaymentIntentId: key2(true),
6610
- rechargedAt: attr9("number", true),
6611
- rechargeAttempt: attr9("json", true)
6676
+ ...dispute()
6612
6677
  } },
6613
6678
  membershipRestart: { attrs: {
6614
6679
  id: id10(),
@@ -6684,7 +6749,8 @@ var membershipAuthorityEntities = {
6684
6749
  refundedCents: attr9("number", true),
6685
6750
  providerRefundIds: attr9("json", true),
6686
6751
  regiftedAt: attr9("number", true),
6687
- formerRecipients: attr9("json", true)
6752
+ formerRecipients: attr9("json", true),
6753
+ ...dispute()
6688
6754
  } },
6689
6755
  networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
6690
6756
  membershipEffect: { attrs: {
@@ -7044,7 +7110,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
7044
7110
  function unknownKeys2(value, allowed2, path, errors) {
7045
7111
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7046
7112
  }
7047
- function text4(value, path, errors, max = MAX_TEXT2, required = true) {
7113
+ function text5(value, path, errors, max = MAX_TEXT2, required = true) {
7048
7114
  if (value === void 0 && !required) return void 0;
7049
7115
  if (typeof value !== "string" || required && !value.trim()) {
7050
7116
  errors.push(`${path} must be a non-empty string`);
@@ -7070,7 +7136,7 @@ function scopeValue(value, path, errors) {
7070
7136
  return { kind: raw2.kind };
7071
7137
  }
7072
7138
  if (raw2.kind === "chapter") {
7073
- const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7139
+ const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7074
7140
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
7075
7141
  return { kind: "chapter", chapterId };
7076
7142
  }
@@ -7084,22 +7150,22 @@ function resourceValue(value, path, errors) {
7084
7150
  return null;
7085
7151
  }
7086
7152
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
7087
- const id11 = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
7153
+ const id11 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
7088
7154
  if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
7089
- const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
7155
+ const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
7090
7156
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
7091
- const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
7092
- const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
7093
- const status2 = text4(raw2.status, `${path}.status`, errors) ?? "";
7157
+ const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
7158
+ const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
7159
+ const status2 = text5(raw2.status, `${path}.status`, errors) ?? "";
7094
7160
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
7095
7161
  const revision = integer3(raw2.revision, `${path}.revision`, errors);
7096
7162
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
7097
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
7163
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
7098
7164
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
7099
7165
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
7100
7166
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
7101
- const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
7102
- const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
7167
+ const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
7168
+ const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
7103
7169
  if (url) {
7104
7170
  try {
7105
7171
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -7111,9 +7177,9 @@ function resourceValue(value, path, errors) {
7111
7177
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
7112
7178
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
7113
7179
  const brandAsset = asset ? {
7114
- bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
7115
- assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
7116
- ...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
7180
+ bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
7181
+ assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
7182
+ ...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
7117
7183
  } : void 0;
7118
7184
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
7119
7185
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -7128,17 +7194,17 @@ function resourceValue(value, path, errors) {
7128
7194
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
7129
7195
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
7130
7196
  const promotedFrom = promotedRaw ? {
7131
- chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7132
- resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
7197
+ chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7198
+ resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
7133
7199
  resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
7134
7200
  documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
7135
- promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7201
+ promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7136
7202
  promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
7137
7203
  } : void 0;
7138
7204
  const provenance = {
7139
- createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7205
+ createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7140
7206
  createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
7141
- updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7207
+ updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7142
7208
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
7143
7209
  ...promotedFrom ? { promotedFrom } : {}
7144
7210
  };
@@ -7157,7 +7223,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
7157
7223
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
7158
7224
  const sourceType = sourceRaw?.type;
7159
7225
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
7160
- const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7226
+ const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7161
7227
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
7162
7228
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
7163
7229
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -7165,8 +7231,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
7165
7231
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
7166
7232
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
7167
7233
  const target = targetRaw ? {
7168
- appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7169
- environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
7234
+ appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7235
+ environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
7170
7236
  } : void 0;
7171
7237
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
7172
7238
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -7261,9 +7327,9 @@ async function createBuilderLibraryEnvelope(document) {
7261
7327
  }
7262
7328
 
7263
7329
  // src/email-retirement.ts
7264
- function isRetiredEmailCopy(subject, text7) {
7330
+ function isRetiredEmailCopy(subject, text8) {
7265
7331
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
7266
- const bodyCopy = text7.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7332
+ const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7267
7333
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
7268
7334
  }
7269
7335
 
@@ -7341,10 +7407,10 @@ function planDelivery(input) {
7341
7407
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
7342
7408
  if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7343
7409
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
7344
- const text7 = redirect ? `(dev redirect; original recipient: ${input.to})
7410
+ const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
7345
7411
 
7346
7412
  ` + renderedText : renderedText;
7347
- return { deliver: true, transport, to, subject, text: text7, redirected: redirect };
7413
+ return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
7348
7414
  }
7349
7415
 
7350
7416
  // src/email-readiness.ts
@@ -8000,7 +8066,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
8000
8066
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
8001
8067
  var CURRENCY = /^[a-z]{3}$/;
8002
8068
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
8003
- var text5 = (value, path, errors, max) => {
8069
+ var text6 = (value, path, errors, max) => {
8004
8070
  if (typeof value !== "string" || !value.trim()) {
8005
8071
  errors.push(`${path} is required`);
8006
8072
  return "";
@@ -8028,23 +8094,23 @@ function validateGiftOfferCatalog(input, tierIds) {
8028
8094
  }
8029
8095
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
8030
8096
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8031
- const id11 = text5(item.id, `${path}.id`, errors, 80);
8097
+ const id11 = text6(item.id, `${path}.id`, errors, 80);
8032
8098
  if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8033
8099
  if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
8034
8100
  ids2.add(id11);
8035
8101
  const targetRaw = record3(item.target);
8036
8102
  let target = { type: "entitlement", entitlementKey: "invalid" };
8037
8103
  if (targetRaw?.type === "tier") {
8038
- const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8104
+ const tierId = text6(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8039
8105
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
8040
8106
  target = { type: "tier", tierId };
8041
8107
  } else if (targetRaw?.type === "entitlement") {
8042
- const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8108
+ const entitlementKey = text6(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8043
8109
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
8044
8110
  target = { type: "entitlement", entitlementKey };
8045
8111
  } else errors.push(`${path}.target.type must be tier or entitlement`);
8046
8112
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
8047
- const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8113
+ const stripePriceId = text6(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8048
8114
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
8049
8115
  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`);
8050
8116
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -8066,9 +8132,9 @@ function validateGiftOfferCatalog(input, tierIds) {
8066
8132
  redemptionLifetimeHours: lifetime,
8067
8133
  ...activeFrom !== void 0 ? { activeFrom } : {},
8068
8134
  ...activeUntil !== void 0 ? { activeUntil } : {},
8069
- title: text5(item.title, `${path}.title`, errors, 160),
8070
- description: text5(item.description, `${path}.description`, errors, 2e3),
8071
- ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
8135
+ title: text6(item.title, `${path}.title`, errors, 160),
8136
+ description: text6(item.description, `${path}.description`, errors, 2e3),
8137
+ ctaLabel: text6(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
8072
8138
  });
8073
8139
  }
8074
8140
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -9156,10 +9222,10 @@ function boundedString(value, path, errors, max, required = false) {
9156
9222
  errors.push(`${path} must be a string`);
9157
9223
  return void 0;
9158
9224
  }
9159
- const text7 = value.trim();
9160
- if (required && !text7) errors.push(`${path} is required`);
9161
- if (text7.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9162
- return text7;
9225
+ const text8 = value.trim();
9226
+ if (required && !text8) errors.push(`${path} is required`);
9227
+ if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9228
+ return text8;
9163
9229
  }
9164
9230
  function validateMembershipPageDocument(input, options = {}) {
9165
9231
  const errors = [];
@@ -9208,8 +9274,8 @@ function validateMembershipPageDocument(input, options = {}) {
9208
9274
  else {
9209
9275
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
9210
9276
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
9211
- const text7 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9212
- return text7 ? [text7] : [];
9277
+ const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9278
+ return text8 ? [text8] : [];
9213
9279
  });
9214
9280
  }
9215
9281
  }
@@ -9308,7 +9374,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
9308
9374
  function unknownKeys3(value, allowed2, path, errors) {
9309
9375
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
9310
9376
  }
9311
- function text6(value, path, errors, max = 65536) {
9377
+ function text7(value, path, errors, max = 65536) {
9312
9378
  if (typeof value !== "string" || !value.trim()) {
9313
9379
  errors.push(`${path} is required`);
9314
9380
  return "";
@@ -9331,11 +9397,11 @@ function tiersOf(value, errors) {
9331
9397
  return [];
9332
9398
  }
9333
9399
  unknownKeys3(item, TIER_KEYS2, path, errors);
9334
- const id11 = text6(item.id, `${path}.id`, errors, 80);
9400
+ const id11 = text7(item.id, `${path}.id`, errors, 80);
9335
9401
  if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9336
9402
  if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
9337
9403
  seen.add(id11);
9338
- const name = text6(item.name, `${path}.name`, errors, 160);
9404
+ const name = text7(item.name, `${path}.name`, errors, 160);
9339
9405
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
9340
9406
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
9341
9407
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -9353,7 +9419,7 @@ function tiersOf(value, errors) {
9353
9419
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
9354
9420
  errors.push(`${path}.interval must be year or month`);
9355
9421
  }
9356
- const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
9422
+ const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
9357
9423
  return [{
9358
9424
  id: id11,
9359
9425
  name,
@@ -9411,7 +9477,7 @@ function policyOf(value, errors, tiers2) {
9411
9477
  const result = {};
9412
9478
  for (const key3 of allowed2) {
9413
9479
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
9414
- result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
9480
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
9415
9481
  }
9416
9482
  const tierPolicies = {};
9417
9483
  if (input.tierPolicies !== void 0) {
@@ -9426,8 +9492,8 @@ function policyOf(value, errors, tiers2) {
9426
9492
  }
9427
9493
  unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
9428
9494
  tierPolicies[id11] = {
9429
- refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9430
- ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9495
+ refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9496
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9431
9497
  };
9432
9498
  }
9433
9499
  }
@@ -9467,9 +9533,9 @@ function validateSignupControlDocument(input) {
9467
9533
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
9468
9534
  const source = recordOf(root.source);
9469
9535
  const target = recordOf(root.target);
9470
- const appId = text6(source?.appId, "document.source.appId", errors, 63);
9471
- const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
9472
- const environment = text6(target?.environment, "document.target.environment", errors, 63);
9536
+ const appId = text7(source?.appId, "document.source.appId", errors, 63);
9537
+ const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
9538
+ const environment = text7(target?.environment, "document.target.environment", errors, 63);
9473
9539
  if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
9474
9540
  if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
9475
9541
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9646,9 +9712,9 @@ function markdownBenefits(lines, path, errors) {
9646
9712
  errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
9647
9713
  continue;
9648
9714
  }
9649
- const text7 = (match[1] ?? "").trim();
9650
- if (text7.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9651
- if (text7) benefits.push(text7);
9715
+ const text8 = (match[1] ?? "").trim();
9716
+ if (text8.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9717
+ if (text8) benefits.push(text8);
9652
9718
  }
9653
9719
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
9654
9720
  return benefits.slice(0, MAX_BENEFITS2);