@odla-ai/chapter 0.58.1 → 0.59.0

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.cjs CHANGED
@@ -254,7 +254,7 @@ __export(src_exports, {
254
254
  slotWindow: () => slotWindow,
255
255
  stageIndex: () => stageIndex,
256
256
  stripeCall: () => stripeCall,
257
- stripeForm: () => import_stripe11.stripeForm,
257
+ stripeForm: () => import_stripe12.stripeForm,
258
258
  subAnnualCents: () => subAnnualCents,
259
259
  submitApplication: () => submitApplication,
260
260
  subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
@@ -276,7 +276,7 @@ __export(src_exports, {
276
276
  verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
277
277
  verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
278
278
  verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
279
- verifyStripeSignature: () => import_stripe11.verifyStripeSignature,
279
+ verifyStripeSignature: () => import_stripe12.verifyStripeSignature,
280
280
  webhookMutationId: () => webhookMutationId
281
281
  });
282
282
  module.exports = __toCommonJS(src_exports);
@@ -1843,7 +1843,9 @@ var DEFAULT_JOIN_COPY = {
1843
1843
  failed: "The booking could not be completed.",
1844
1844
  loading: "Loading available times\u2026",
1845
1845
  book: "Book this time",
1846
- booking: "Booking\u2026"
1846
+ booking: "Booking\u2026",
1847
+ pickDate: "Pick a date",
1848
+ pickTime: "Pick a time"
1847
1849
  },
1848
1850
  payment: {
1849
1851
  setupFailed: "Payment could not be set up. Please try again.",
@@ -1995,8 +1997,8 @@ var APPLICATION_LABELS = {
1995
1997
  state: "State"
1996
1998
  };
1997
1999
  function humanize(id11) {
1998
- const text6 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1999
- return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id11;
2000
+ const text7 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
2001
+ return text7 ? `${text7[0].toUpperCase()}${text7.slice(1)}` : id11;
2000
2002
  }
2001
2003
  function applicationDefs(chapter) {
2002
2004
  try {
@@ -2795,6 +2797,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2795
2797
  else if (eventType.startsWith("refund.")) objectKind = "refund";
2796
2798
  else if (eventType === "charge.refunded") objectKind = "refund";
2797
2799
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
2800
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
2798
2801
  const customerId = refId(object2.customer, "cus_");
2799
2802
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
2800
2803
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -2804,6 +2807,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2804
2807
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
2805
2808
  const currency = text(object2.currency)?.toLowerCase();
2806
2809
  const status2 = text(object2.status);
2810
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
2807
2811
  return {
2808
2812
  eventId,
2809
2813
  digest,
@@ -2822,6 +2826,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2822
2826
  ...invoiceId ? { invoiceId } : {},
2823
2827
  ...chargeId ? { chargeId } : {},
2824
2828
  ...paymentIntentId ? { paymentIntentId } : {},
2829
+ ...disputeReason ? { disputeReason } : {},
2825
2830
  ...price,
2826
2831
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2827
2832
  ...status2 ? { status: status2 } : {},
@@ -2864,6 +2869,7 @@ function ownRef(fact) {
2864
2869
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
2865
2870
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
2866
2871
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
2872
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
2867
2873
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
2868
2874
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
2869
2875
  }
@@ -2919,6 +2925,7 @@ function projectNetworkCommercialEvents(input, sources) {
2919
2925
  continue;
2920
2926
  }
2921
2927
  const source = assignments.get(fact.eventId);
2928
+ if (!source && fact.objectKind === "dispute") continue;
2922
2929
  if (!source) {
2923
2930
  const direct = directSource(fact, sources);
2924
2931
  const keys2 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -2935,6 +2942,7 @@ function projectNetworkCommercialEvents(input, sources) {
2935
2942
  const subscriptions = rows3.filter((row2) => row2.objectKind === "subscription");
2936
2943
  const invoices = rows3.filter((row2) => row2.objectKind === "invoice");
2937
2944
  const seatPayments = rows3.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
2945
+ const disputes = rows3.filter((row2) => row2.objectKind === "dispute");
2938
2946
  const refunds = rows3.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
2939
2947
  for (const charge of rows3.filter((row2) => row2.eventType === "charge.refunded")) {
2940
2948
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -2955,6 +2963,7 @@ function projectNetworkCommercialEvents(input, sources) {
2955
2963
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2956
2964
  return {
2957
2965
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2966
+ ...disputes.length ? { disputes } : {},
2958
2967
  customers,
2959
2968
  subscriptions,
2960
2969
  invoices,
@@ -4558,7 +4567,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4558
4567
  }
4559
4568
 
4560
4569
  // src/membership-provider-lifecycle.ts
4561
- var import_stripe4 = require("@odla-ai/stripe");
4570
+ var import_stripe5 = require("@odla-ai/stripe");
4562
4571
 
4563
4572
  // src/membership-seat-reservation.ts
4564
4573
  async function namedSeatRecipientClaimId(input, email) {
@@ -4849,12 +4858,113 @@ async function reconcileMembershipRenewal(db, input, now) {
4849
4858
  return membershipFail("membership_renewal_retry_required");
4850
4859
  }
4851
4860
 
4861
+ // src/membership-dispute-authority.ts
4862
+ var import_stripe4 = require("@odla-ai/stripe");
4863
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
4864
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
4865
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
4866
+ var TERMINAL = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
4867
+ var text3 = (value) => typeof value === "string" && value ? value : void 0;
4868
+ var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
4869
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
4870
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
4871
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
4872
+ const charge = await (0, import_stripe4.stripeRequest)(config, "GET", `/v1/charges/${chargeId}`);
4873
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
4874
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
4875
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
4876
+ let subscriptionId;
4877
+ if (invoiceId) {
4878
+ const invoice = await (0, import_stripe4.stripeRequest)(config, "GET", `/v1/invoices/${invoiceId}`);
4879
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
4880
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
4881
+ }
4882
+ const seatId = text3(charge.body.metadata?.seatId);
4883
+ return {
4884
+ chargeId,
4885
+ customerId,
4886
+ ...invoiceId ? { invoiceId } : {},
4887
+ ...subscriptionId ? { subscriptionId } : {},
4888
+ ...paymentIntentId ? { paymentIntentId } : {},
4889
+ ...seatId ? { seatId } : {}
4890
+ };
4891
+ }
4892
+ function disputedChargeCoversMembership(charge, member) {
4893
+ return charge.customerId === member.providerCustomerId && !charge.seatId && (charge.invoiceId !== void 0 && charge.invoiceId === member.providerInitialInvoiceId || charge.subscriptionId !== void 0 && charge.subscriptionId === member.providerSubscriptionId || charge.paymentIntentId !== void 0 && charge.paymentIntentId === member.rechargePaymentIntentId);
4894
+ }
4895
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
4896
+ membershipClock(now);
4897
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4898
+ const fact = row2?.fact;
4899
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4900
+ if (!fact || fact.objectKind !== "dispute" || fact.eventId !== input.eventId || fact.digest !== row2.digest || !fact.chargeId || !fact.objectId || fact.livemode !== (input.environment === "prod")) membershipFail("membership_billing_evidence_pending");
4901
+ const id11 = await networkMembershipId(input);
4902
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
4903
+ membershipId: id11,
4904
+ status: String(value.status),
4905
+ termEndsAt: Number(value.termEndsAt),
4906
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
4907
+ autoRenew: value.autoRenew === true,
4908
+ ...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
4909
+ applied
4910
+ });
4911
+ const known = await membershipLoad(db, "networkMembership", id11);
4912
+ requireMembershipScope(known, input);
4913
+ if (known.disputeEventId === input.eventId) return receipt(known);
4914
+ const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
4915
+ for (let attempt = 0; attempt < 8; attempt++) {
4916
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4917
+ requireMembershipScope(member, input);
4918
+ if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
4919
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
4920
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
4921
+ const sameDispute = member.disputeId === fact.objectId;
4922
+ if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
4923
+ const won = DISPUTE_WON_STATUSES.includes(status2);
4924
+ const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
4925
+ const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
4926
+ const attrs = {
4927
+ status: next,
4928
+ disputeId: fact.objectId,
4929
+ disputeStatus: status2,
4930
+ disputeEventId: input.eventId,
4931
+ disputeClosedAt: closed ? now : 0,
4932
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
4933
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
4934
+ ...sameDispute ? {} : { disputedAt: now },
4935
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
4936
+ revision: Number(member.revision) + 1,
4937
+ updatedAt: now
4938
+ };
4939
+ try {
4940
+ await db.transact([membershipUpdate("networkMembership", id11, attrs)], { mutationId: `membership-dispute:${id11}:${input.eventId}`, guards: [
4941
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
4942
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4943
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4944
+ evidenceGuard
4945
+ ] });
4946
+ } catch (error) {
4947
+ if (error.code !== "transact_guard_failed") throw error;
4948
+ continue;
4949
+ }
4950
+ const saved = await membershipLoad(db, "networkMembership", id11);
4951
+ requireMembershipScope(saved, input);
4952
+ return receipt(saved, true);
4953
+ }
4954
+ return membershipFail("membership_dispute_retry_required");
4955
+ }
4956
+
4852
4957
  // src/membership-provider-lifecycle.ts
4853
4958
  async function reconcileMembershipBilling(db, input, now, stripeSecretName = "stripe_secret_key") {
4854
4959
  membershipClock(now);
4855
4960
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4856
4961
  const fact = row2?.fact;
4857
4962
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
4963
+ if (fact?.objectKind === "dispute") {
4964
+ const secret = await getVaultSecret(db, stripeSecretName);
4965
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
4966
+ return reconcileMembershipDispute(db, input, now, secret);
4967
+ }
4858
4968
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4859
4969
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
4860
4970
  appId: input.appId,
@@ -4897,7 +5007,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4897
5007
  }
4898
5008
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
4899
5009
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4900
- const result = await (0, import_stripe4.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
5010
+ const result = await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4901
5011
  const sub = result.body;
4902
5012
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
4903
5013
  membershipFail("membership_billing_provider_mismatch");
@@ -4954,7 +5064,7 @@ async function expireRestartedMembershipSeat(db, target, member, now) {
4954
5064
  }
4955
5065
 
4956
5066
  // src/membership-restart-authority.ts
4957
- var import_stripe5 = require("@odla-ai/stripe");
5067
+ var import_stripe6 = require("@odla-ai/stripe");
4958
5068
 
4959
5069
  // src/stripe-event-digest.ts
4960
5070
  async function stripePayloadDigest(value) {
@@ -4969,14 +5079,14 @@ function membershipCanRestart(member, now) {
4969
5079
  }
4970
5080
  async function stopPreviousSubscription(member, target, secret) {
4971
5081
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4972
- const result = await (0, import_stripe5.stripeRequest)({ secretKey: secret }, "GET", path);
5082
+ const result = await (0, import_stripe6.stripeRequest)({ secretKey: secret }, "GET", path);
4973
5083
  const sub = result.body;
4974
5084
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4975
5085
  ...stripePartitionMetadata(target),
4976
5086
  applicationId: target.applicationId
4977
5087
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4978
5088
  if (sub.status === "canceled") return;
4979
- const canceled = await (0, import_stripe5.stripeRequest)(
5089
+ const canceled = await (0, import_stripe6.stripeRequest)(
4980
5090
  { secretKey: secret },
4981
5091
  "DELETE",
4982
5092
  path,
@@ -5077,16 +5187,16 @@ async function verifyMembershipAccessReceipt(value, input, now) {
5077
5187
  }
5078
5188
 
5079
5189
  // src/membership-seat-stripe.ts
5080
- var import_stripe7 = require("@odla-ai/stripe");
5190
+ var import_stripe8 = require("@odla-ai/stripe");
5081
5191
 
5082
5192
  // src/subscription-provider-contract.ts
5083
- var import_stripe6 = require("@odla-ai/stripe");
5193
+ var import_stripe7 = require("@odla-ai/stripe");
5084
5194
  function subscriptionContractMismatch() {
5085
5195
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5086
5196
  }
5087
5197
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
5088
5198
  if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
5089
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5199
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5090
5200
  const price = result.body;
5091
5201
  if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
5092
5202
  return typeof price.product === "string" ? price.product : void 0;
@@ -5104,7 +5214,7 @@ var metadataFor = (seat) => ({
5104
5214
  });
5105
5215
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
5106
5216
  async function read(sk, path, params) {
5107
- const response = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "GET", path, params);
5217
+ const response = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "GET", path, params);
5108
5218
  if (!response.ok) return reject3();
5109
5219
  return response.body;
5110
5220
  }
@@ -5141,11 +5251,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
5141
5251
  }
5142
5252
  await subscriptionForSeat(sk, seat);
5143
5253
  const metadata = metadataFor(seat);
5144
- const result = seat.providerPaymentIntentId ? await (0, import_stripe7.stripeRequest)(
5254
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe8.stripeRequest)(
5145
5255
  { secretKey: sk },
5146
5256
  "GET",
5147
5257
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
5148
- ) : await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
5258
+ ) : await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
5149
5259
  amount: seat.amountCents,
5150
5260
  currency: seat.currency,
5151
5261
  customer: seat.providerCustomerId,
@@ -5167,7 +5277,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
5167
5277
  return String(existing.id);
5168
5278
  }
5169
5279
  if (seat.combinedInvoice) reject3();
5170
- const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
5280
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
5171
5281
  subscription: seat.providerSubscriptionId,
5172
5282
  price: seat.priceId,
5173
5283
  quantity: 1,
@@ -5442,10 +5552,10 @@ async function recordRecipient(db, input, now, approve) {
5442
5552
  }
5443
5553
 
5444
5554
  // src/membership-seat-refund-stripe.ts
5445
- var import_stripe9 = require("@odla-ai/stripe");
5555
+ var import_stripe10 = require("@odla-ai/stripe");
5446
5556
 
5447
5557
  // src/membership-checkout-seat-refund.ts
5448
- var import_stripe8 = require("@odla-ai/stripe");
5558
+ var import_stripe9 = require("@odla-ai/stripe");
5449
5559
  var fail4 = (code = "named_seat_refund_mismatch") => {
5450
5560
  throw Object.assign(new Error(code), { code });
5451
5561
  };
@@ -5454,7 +5564,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
5454
5564
  const invoiceRef = seat.combinedInvoice;
5455
5565
  if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
5456
5566
  const call = async (method, path, params, key3) => {
5457
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5567
+ const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5458
5568
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
5459
5569
  return result.body;
5460
5570
  };
@@ -5527,7 +5637,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
5527
5637
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
5528
5638
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
5529
5639
  const call = async (method, path, params, idempotencyKey) => {
5530
- const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5640
+ const result = await (0, import_stripe10.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5531
5641
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
5532
5642
  return result.body;
5533
5643
  };
@@ -6229,7 +6339,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
6229
6339
  };
6230
6340
  signal.addEventListener("abort", cancel, { once: true });
6231
6341
  let size = 0;
6232
- let text6 = "";
6342
+ let text7 = "";
6233
6343
  const decoder = new TextDecoder();
6234
6344
  try {
6235
6345
  while (true) {
@@ -6239,10 +6349,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
6239
6349
  if (done) break;
6240
6350
  size += value.byteLength;
6241
6351
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
6242
- text6 += decoder.decode(value, { stream: true });
6352
+ text7 += decoder.decode(value, { stream: true });
6243
6353
  }
6244
6354
  try {
6245
- return JSON.parse(text6 + decoder.decode());
6355
+ return JSON.parse(text7 + decoder.decode());
6246
6356
  } catch {
6247
6357
  throw new TypeError(`invalid ${label}`);
6248
6358
  }
@@ -6456,7 +6566,7 @@ async function requestMembershipRefund(options) {
6456
6566
  async function requestMembershipRenewal(options) {
6457
6567
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
6458
6568
  const row2 = result.renewal;
6459
- if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
6569
+ if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
6460
6570
  throw new Error("invalid membership renewal receipt");
6461
6571
  }
6462
6572
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -6516,7 +6626,7 @@ async function requestMembershipCancellation(options) {
6516
6626
  }
6517
6627
 
6518
6628
  // src/membership-seat-cancel.ts
6519
- var import_stripe10 = require("@odla-ai/stripe");
6629
+ var import_stripe11 = require("@odla-ai/stripe");
6520
6630
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6521
6631
  membershipClock(now);
6522
6632
  const state = await loadMembershipAuthorityState(db, target);
@@ -6544,7 +6654,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6544
6654
  }
6545
6655
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
6546
6656
  const request = async (method, path, params) => {
6547
- const result = await (0, import_stripe10.stripeRequest)(
6657
+ const result = await (0, import_stripe11.stripeRequest)(
6548
6658
  { secretKey },
6549
6659
  method,
6550
6660
  path,
@@ -6557,7 +6667,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6557
6667
  let payment;
6558
6668
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
6559
6669
  else {
6560
- const snapshot = await listStripeObjects((0, import_stripe10.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
6670
+ const snapshot = await listStripeObjects((0, import_stripe11.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
6561
6671
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
6562
6672
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
6563
6673
  payment = matches2[0];
@@ -6768,7 +6878,20 @@ var membershipAuthorityEntities = {
6768
6878
  graceEndsAt: attr9("number", true),
6769
6879
  ...times(),
6770
6880
  refundRequestedAt: attr9("number", true),
6771
- restartId: key2(true)
6881
+ restartId: key2(true),
6882
+ // A card-network dispute on this membership's payment: Stripe's dispute id and status, the reason the cardholder gave,
6883
+ // the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
6884
+ disputeId: key2(true),
6885
+ disputeStatus: attr9("string", true),
6886
+ disputeReason: attr9("string", true),
6887
+ disputeAmountCents: attr9("number", true),
6888
+ disputedAt: attr9("number", true),
6889
+ disputeClosedAt: attr9("number", true),
6890
+ disputePriorStatus: attr9("string", true),
6891
+ disputeEventId: key2(true),
6892
+ rechargePaymentIntentId: key2(true),
6893
+ rechargedAt: attr9("number", true),
6894
+ rechargeAttempt: attr9("json", true)
6772
6895
  } },
6773
6896
  membershipRestart: { attrs: {
6774
6897
  id: id10(),
@@ -7204,7 +7327,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
7204
7327
  function unknownKeys2(value, allowed2, path, errors) {
7205
7328
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7206
7329
  }
7207
- function text3(value, path, errors, max = MAX_TEXT2, required = true) {
7330
+ function text4(value, path, errors, max = MAX_TEXT2, required = true) {
7208
7331
  if (value === void 0 && !required) return void 0;
7209
7332
  if (typeof value !== "string" || required && !value.trim()) {
7210
7333
  errors.push(`${path} must be a non-empty string`);
@@ -7230,7 +7353,7 @@ function scopeValue(value, path, errors) {
7230
7353
  return { kind: raw2.kind };
7231
7354
  }
7232
7355
  if (raw2.kind === "chapter") {
7233
- const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7356
+ const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7234
7357
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
7235
7358
  return { kind: "chapter", chapterId };
7236
7359
  }
@@ -7244,22 +7367,22 @@ function resourceValue(value, path, errors) {
7244
7367
  return null;
7245
7368
  }
7246
7369
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
7247
- const id11 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
7370
+ const id11 = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
7248
7371
  if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
7249
- const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
7372
+ const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
7250
7373
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
7251
- const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
7252
- const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
7253
- const status2 = text3(raw2.status, `${path}.status`, errors) ?? "";
7374
+ const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
7375
+ const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
7376
+ const status2 = text4(raw2.status, `${path}.status`, errors) ?? "";
7254
7377
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
7255
7378
  const revision = integer3(raw2.revision, `${path}.revision`, errors);
7256
7379
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
7257
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
7380
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
7258
7381
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
7259
7382
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
7260
7383
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
7261
- const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
7262
- const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
7384
+ const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
7385
+ const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
7263
7386
  if (url) {
7264
7387
  try {
7265
7388
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -7271,9 +7394,9 @@ function resourceValue(value, path, errors) {
7271
7394
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
7272
7395
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
7273
7396
  const brandAsset = asset ? {
7274
- bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
7275
- assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
7276
- ...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
7397
+ bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
7398
+ assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
7399
+ ...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
7277
7400
  } : void 0;
7278
7401
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
7279
7402
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -7288,17 +7411,17 @@ function resourceValue(value, path, errors) {
7288
7411
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
7289
7412
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
7290
7413
  const promotedFrom = promotedRaw ? {
7291
- chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7292
- resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
7414
+ chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7415
+ resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
7293
7416
  resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
7294
7417
  documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
7295
- promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7418
+ promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7296
7419
  promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
7297
7420
  } : void 0;
7298
7421
  const provenance = {
7299
- createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7422
+ createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7300
7423
  createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
7301
- updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7424
+ updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7302
7425
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
7303
7426
  ...promotedFrom ? { promotedFrom } : {}
7304
7427
  };
@@ -7317,7 +7440,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
7317
7440
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
7318
7441
  const sourceType = sourceRaw?.type;
7319
7442
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
7320
- const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7443
+ const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7321
7444
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
7322
7445
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
7323
7446
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -7325,8 +7448,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
7325
7448
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
7326
7449
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
7327
7450
  const target = targetRaw ? {
7328
- appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7329
- environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
7451
+ appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7452
+ environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
7330
7453
  } : void 0;
7331
7454
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
7332
7455
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -7421,9 +7544,9 @@ async function createBuilderLibraryEnvelope(document) {
7421
7544
  }
7422
7545
 
7423
7546
  // src/email-retirement.ts
7424
- function isRetiredEmailCopy(subject, text6) {
7547
+ function isRetiredEmailCopy(subject, text7) {
7425
7548
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
7426
- const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7549
+ const bodyCopy = text7.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7427
7550
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
7428
7551
  }
7429
7552
 
@@ -7501,10 +7624,10 @@ function planDelivery(input) {
7501
7624
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
7502
7625
  if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7503
7626
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
7504
- const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
7627
+ const text7 = redirect ? `(dev redirect; original recipient: ${input.to})
7505
7628
 
7506
7629
  ` + renderedText : renderedText;
7507
- return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
7630
+ return { deliver: true, transport, to, subject, text: text7, redirected: redirect };
7508
7631
  }
7509
7632
 
7510
7633
  // src/email-readiness.ts
@@ -7778,7 +7901,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
7778
7901
  }
7779
7902
 
7780
7903
  // src/payments.ts
7781
- var import_stripe11 = require("@odla-ai/stripe");
7904
+ var import_stripe12 = require("@odla-ai/stripe");
7782
7905
  function paymentsReady(group, hasSecretKey) {
7783
7906
  return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
7784
7907
  }
@@ -7839,6 +7962,17 @@ function findGiftRef(obj) {
7839
7962
  }
7840
7963
  function normalizeWebhookEvent(event2, options = {}) {
7841
7964
  const obj = event2.data?.object ?? {};
7965
+ if (event2.type.startsWith("charge.dispute.")) {
7966
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
7967
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
7968
+ return {
7969
+ kind: "disputed",
7970
+ chargeId,
7971
+ disputeId: obj.id,
7972
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
7973
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
7974
+ };
7975
+ }
7842
7976
  if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
7843
7977
  const gift = findGiftRef(obj);
7844
7978
  if (gift.giftIntentId) {
@@ -8149,7 +8283,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
8149
8283
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
8150
8284
  var CURRENCY = /^[a-z]{3}$/;
8151
8285
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
8152
- var text4 = (value, path, errors, max) => {
8286
+ var text5 = (value, path, errors, max) => {
8153
8287
  if (typeof value !== "string" || !value.trim()) {
8154
8288
  errors.push(`${path} is required`);
8155
8289
  return "";
@@ -8177,23 +8311,23 @@ function validateGiftOfferCatalog(input, tierIds) {
8177
8311
  }
8178
8312
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
8179
8313
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8180
- const id11 = text4(item.id, `${path}.id`, errors, 80);
8314
+ const id11 = text5(item.id, `${path}.id`, errors, 80);
8181
8315
  if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8182
8316
  if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
8183
8317
  ids2.add(id11);
8184
8318
  const targetRaw = record3(item.target);
8185
8319
  let target = { type: "entitlement", entitlementKey: "invalid" };
8186
8320
  if (targetRaw?.type === "tier") {
8187
- const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8321
+ const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8188
8322
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
8189
8323
  target = { type: "tier", tierId };
8190
8324
  } else if (targetRaw?.type === "entitlement") {
8191
- const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8325
+ const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8192
8326
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
8193
8327
  target = { type: "entitlement", entitlementKey };
8194
8328
  } else errors.push(`${path}.target.type must be tier or entitlement`);
8195
8329
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
8196
- const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8330
+ const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8197
8331
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
8198
8332
  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`);
8199
8333
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -8215,9 +8349,9 @@ function validateGiftOfferCatalog(input, tierIds) {
8215
8349
  redemptionLifetimeHours: lifetime,
8216
8350
  ...activeFrom !== void 0 ? { activeFrom } : {},
8217
8351
  ...activeUntil !== void 0 ? { activeUntil } : {},
8218
- title: text4(item.title, `${path}.title`, errors, 160),
8219
- description: text4(item.description, `${path}.description`, errors, 2e3),
8220
- ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
8352
+ title: text5(item.title, `${path}.title`, errors, 160),
8353
+ description: text5(item.description, `${path}.description`, errors, 2e3),
8354
+ ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
8221
8355
  });
8222
8356
  }
8223
8357
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -8676,9 +8810,9 @@ function subAnnualCents(sub) {
8676
8810
  }
8677
8811
 
8678
8812
  // src/payments-stripe.ts
8679
- var import_stripe12 = require("@odla-ai/stripe");
8813
+ var import_stripe13 = require("@odla-ai/stripe");
8680
8814
  async function stripeCall(sk, method, path, params, idempotencyKey) {
8681
- return (0, import_stripe12.stripeRequest)(
8815
+ return (0, import_stripe13.stripeRequest)(
8682
8816
  { secretKey: sk },
8683
8817
  method,
8684
8818
  path,
@@ -9300,10 +9434,10 @@ function boundedString(value, path, errors, max, required = false) {
9300
9434
  errors.push(`${path} must be a string`);
9301
9435
  return void 0;
9302
9436
  }
9303
- const text6 = value.trim();
9304
- if (required && !text6) errors.push(`${path} is required`);
9305
- if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9306
- return text6;
9437
+ const text7 = value.trim();
9438
+ if (required && !text7) errors.push(`${path} is required`);
9439
+ if (text7.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9440
+ return text7;
9307
9441
  }
9308
9442
  function validateMembershipPageDocument(input, options = {}) {
9309
9443
  const errors = [];
@@ -9352,8 +9486,8 @@ function validateMembershipPageDocument(input, options = {}) {
9352
9486
  else {
9353
9487
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
9354
9488
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
9355
- const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9356
- return text6 ? [text6] : [];
9489
+ const text7 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9490
+ return text7 ? [text7] : [];
9357
9491
  });
9358
9492
  }
9359
9493
  }
@@ -9452,7 +9586,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
9452
9586
  function unknownKeys3(value, allowed2, path, errors) {
9453
9587
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
9454
9588
  }
9455
- function text5(value, path, errors, max = 65536) {
9589
+ function text6(value, path, errors, max = 65536) {
9456
9590
  if (typeof value !== "string" || !value.trim()) {
9457
9591
  errors.push(`${path} is required`);
9458
9592
  return "";
@@ -9475,11 +9609,11 @@ function tiersOf(value, errors) {
9475
9609
  return [];
9476
9610
  }
9477
9611
  unknownKeys3(item, TIER_KEYS2, path, errors);
9478
- const id11 = text5(item.id, `${path}.id`, errors, 80);
9612
+ const id11 = text6(item.id, `${path}.id`, errors, 80);
9479
9613
  if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9480
9614
  if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
9481
9615
  seen.add(id11);
9482
- const name = text5(item.name, `${path}.name`, errors, 160);
9616
+ const name = text6(item.name, `${path}.name`, errors, 160);
9483
9617
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
9484
9618
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
9485
9619
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -9497,7 +9631,7 @@ function tiersOf(value, errors) {
9497
9631
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
9498
9632
  errors.push(`${path}.interval must be year or month`);
9499
9633
  }
9500
- const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
9634
+ const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
9501
9635
  return [{
9502
9636
  id: id11,
9503
9637
  name,
@@ -9555,7 +9689,7 @@ function policyOf(value, errors, tiers2) {
9555
9689
  const result = {};
9556
9690
  for (const key3 of allowed2) {
9557
9691
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
9558
- result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
9692
+ result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
9559
9693
  }
9560
9694
  const tierPolicies = {};
9561
9695
  if (input.tierPolicies !== void 0) {
@@ -9570,8 +9704,8 @@ function policyOf(value, errors, tiers2) {
9570
9704
  }
9571
9705
  unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
9572
9706
  tierPolicies[id11] = {
9573
- refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9574
- ...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9707
+ refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9708
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9575
9709
  };
9576
9710
  }
9577
9711
  }
@@ -9611,9 +9745,9 @@ function validateSignupControlDocument(input) {
9611
9745
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
9612
9746
  const source = recordOf(root.source);
9613
9747
  const target = recordOf(root.target);
9614
- const appId = text5(source?.appId, "document.source.appId", errors, 63);
9615
- const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
9616
- const environment = text5(target?.environment, "document.target.environment", errors, 63);
9748
+ const appId = text6(source?.appId, "document.source.appId", errors, 63);
9749
+ const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
9750
+ const environment = text6(target?.environment, "document.target.environment", errors, 63);
9617
9751
  if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
9618
9752
  if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
9619
9753
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9790,9 +9924,9 @@ function markdownBenefits(lines, path, errors) {
9790
9924
  errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
9791
9925
  continue;
9792
9926
  }
9793
- const text6 = (match[1] ?? "").trim();
9794
- if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9795
- if (text6) benefits.push(text6);
9927
+ const text7 = (match[1] ?? "").trim();
9928
+ if (text7.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9929
+ if (text7) benefits.push(text7);
9796
9930
  }
9797
9931
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
9798
9932
  return benefits.slice(0, MAX_BENEFITS2);