@odla-ai/chapter 0.58.1 → 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.js CHANGED
@@ -1560,7 +1560,9 @@ var DEFAULT_JOIN_COPY = {
1560
1560
  failed: "The booking could not be completed.",
1561
1561
  loading: "Loading available times\u2026",
1562
1562
  book: "Book this time",
1563
- booking: "Booking\u2026"
1563
+ booking: "Booking\u2026",
1564
+ pickDate: "Pick a date",
1565
+ pickTime: "Pick a time"
1564
1566
  },
1565
1567
  payment: {
1566
1568
  setupFailed: "Payment could not be set up. Please try again.",
@@ -1712,8 +1714,8 @@ var APPLICATION_LABELS = {
1712
1714
  state: "State"
1713
1715
  };
1714
1716
  function humanize(id11) {
1715
- const text6 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1716
- return text6 ? `${text6[0].toUpperCase()}${text6.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;
1717
1719
  }
1718
1720
  function applicationDefs(chapter) {
1719
1721
  try {
@@ -2512,6 +2514,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2512
2514
  else if (eventType.startsWith("refund.")) objectKind = "refund";
2513
2515
  else if (eventType === "charge.refunded") objectKind = "refund";
2514
2516
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
2517
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
2515
2518
  const customerId = refId(object2.customer, "cus_");
2516
2519
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
2517
2520
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -2521,6 +2524,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2521
2524
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
2522
2525
  const currency = text(object2.currency)?.toLowerCase();
2523
2526
  const status2 = text(object2.status);
2527
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
2524
2528
  return {
2525
2529
  eventId,
2526
2530
  digest,
@@ -2539,6 +2543,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2539
2543
  ...invoiceId ? { invoiceId } : {},
2540
2544
  ...chargeId ? { chargeId } : {},
2541
2545
  ...paymentIntentId ? { paymentIntentId } : {},
2546
+ ...disputeReason ? { disputeReason } : {},
2542
2547
  ...price,
2543
2548
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2544
2549
  ...status2 ? { status: status2 } : {},
@@ -2581,6 +2586,7 @@ function ownRef(fact) {
2581
2586
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
2582
2587
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
2583
2588
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
2589
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
2584
2590
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
2585
2591
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
2586
2592
  }
@@ -2636,6 +2642,7 @@ function projectNetworkCommercialEvents(input, sources) {
2636
2642
  continue;
2637
2643
  }
2638
2644
  const source = assignments.get(fact.eventId);
2645
+ if (!source && fact.objectKind === "dispute") continue;
2639
2646
  if (!source) {
2640
2647
  const direct = directSource(fact, sources);
2641
2648
  const keys2 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -2652,6 +2659,7 @@ function projectNetworkCommercialEvents(input, sources) {
2652
2659
  const subscriptions = rows3.filter((row2) => row2.objectKind === "subscription");
2653
2660
  const invoices = rows3.filter((row2) => row2.objectKind === "invoice");
2654
2661
  const seatPayments = rows3.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
2662
+ const disputes = rows3.filter((row2) => row2.objectKind === "dispute");
2655
2663
  const refunds = rows3.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
2656
2664
  for (const charge of rows3.filter((row2) => row2.eventType === "charge.refunded")) {
2657
2665
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -2672,6 +2680,7 @@ function projectNetworkCommercialEvents(input, sources) {
2672
2680
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2673
2681
  return {
2674
2682
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2683
+ ...disputes.length ? { disputes } : {},
2675
2684
  customers,
2676
2685
  subscriptions,
2677
2686
  invoices,
@@ -4275,7 +4284,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4275
4284
  }
4276
4285
 
4277
4286
  // src/membership-provider-lifecycle.ts
4278
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4287
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4279
4288
 
4280
4289
  // src/membership-seat-reservation.ts
4281
4290
  async function namedSeatRecipientClaimId(input, email) {
@@ -4373,6 +4382,9 @@ function namedSeatRecipientState(seat) {
4373
4382
  return seat.refundReason === "declined" ? "declined" : "refunded";
4374
4383
  case "canceled":
4375
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";
4376
4388
  default:
4377
4389
  return "payment_pending";
4378
4390
  }
@@ -4566,12 +4578,174 @@ async function reconcileMembershipRenewal(db, input, now) {
4566
4578
  return membershipFail("membership_renewal_retry_required");
4567
4579
  }
4568
4580
 
4581
+ // src/membership-dispute-authority.ts
4582
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4583
+
4584
+ // src/membership-dispute-transition.ts
4585
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
4586
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
4587
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
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
4612
+ var text3 = (value) => typeof value === "string" && value ? value : void 0;
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);
4664
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
4665
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
4666
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
4667
+ const charge = await stripeRequest4(config, "GET", `/v1/charges/${chargeId}`);
4668
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
4669
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
4670
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
4671
+ let subscriptionId;
4672
+ if (invoiceId) {
4673
+ const invoice = await stripeRequest4(config, "GET", `/v1/invoices/${invoiceId}`);
4674
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
4675
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
4676
+ }
4677
+ const seatId = text4(charge.body.metadata?.seatId);
4678
+ return {
4679
+ chargeId,
4680
+ customerId,
4681
+ ...invoiceId ? { invoiceId } : {},
4682
+ ...subscriptionId ? { subscriptionId } : {},
4683
+ ...paymentIntentId ? { paymentIntentId } : {},
4684
+ ...seatId ? { seatId } : {}
4685
+ };
4686
+ }
4687
+ function disputedChargeCoversMembership(charge, member) {
4688
+ 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);
4689
+ }
4690
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
4691
+ membershipClock(now);
4692
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4693
+ const fact = row2?.fact;
4694
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4695
+ 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");
4696
+ const id11 = await networkMembershipId(input);
4697
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
4698
+ membershipId: id11,
4699
+ status: String(value.status),
4700
+ termEndsAt: Number(value.termEndsAt),
4701
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
4702
+ autoRenew: value.autoRenew === true,
4703
+ ...text4(value.disputeStatus) ? { disputeStatus: text4(value.disputeStatus) } : {},
4704
+ applied
4705
+ });
4706
+ const known = await membershipLoad(db, "networkMembership", id11);
4707
+ requireMembershipScope(known, input);
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) };
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 });
4714
+ for (let attempt = 0; attempt < 8; attempt++) {
4715
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4716
+ requireMembershipScope(member, input);
4717
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
4718
+ const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
4719
+ if (!attrs) return receipt(member, false);
4720
+ try {
4721
+ await db.transact([membershipUpdate("networkMembership", id11, attrs)], { mutationId: `membership-dispute:${id11}:${input.eventId}`, guards: [
4722
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
4723
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4724
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4725
+ evidenceGuard
4726
+ ] });
4727
+ } catch (error) {
4728
+ if (error.code !== "transact_guard_failed") throw error;
4729
+ continue;
4730
+ }
4731
+ const saved = await membershipLoad(db, "networkMembership", id11);
4732
+ requireMembershipScope(saved, input);
4733
+ return receipt(saved, true);
4734
+ }
4735
+ return membershipFail("membership_dispute_retry_required");
4736
+ }
4737
+
4569
4738
  // src/membership-provider-lifecycle.ts
4570
4739
  async function reconcileMembershipBilling(db, input, now, stripeSecretName = "stripe_secret_key") {
4571
4740
  membershipClock(now);
4572
4741
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4573
4742
  const fact = row2?.fact;
4574
4743
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
4744
+ if (fact?.objectKind === "dispute") {
4745
+ const secret = await getVaultSecret(db, stripeSecretName);
4746
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
4747
+ return reconcileMembershipDispute(db, input, now, secret);
4748
+ }
4575
4749
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4576
4750
  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({
4577
4751
  appId: input.appId,
@@ -4614,7 +4788,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4614
4788
  }
4615
4789
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
4616
4790
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4617
- const result = await stripeRequest4({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4791
+ const result = await stripeRequest5({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4618
4792
  const sub = result.body;
4619
4793
  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)) {
4620
4794
  membershipFail("membership_billing_provider_mismatch");
@@ -4671,7 +4845,7 @@ async function expireRestartedMembershipSeat(db, target, member, now) {
4671
4845
  }
4672
4846
 
4673
4847
  // src/membership-restart-authority.ts
4674
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4848
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4675
4849
 
4676
4850
  // src/stripe-event-digest.ts
4677
4851
  async function stripePayloadDigest(value) {
@@ -4686,14 +4860,14 @@ function membershipCanRestart(member, now) {
4686
4860
  }
4687
4861
  async function stopPreviousSubscription(member, target, secret) {
4688
4862
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4689
- const result = await stripeRequest5({ secretKey: secret }, "GET", path);
4863
+ const result = await stripeRequest6({ secretKey: secret }, "GET", path);
4690
4864
  const sub = result.body;
4691
4865
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4692
4866
  ...stripePartitionMetadata(target),
4693
4867
  applicationId: target.applicationId
4694
4868
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4695
4869
  if (sub.status === "canceled") return;
4696
- const canceled = await stripeRequest5(
4870
+ const canceled = await stripeRequest6(
4697
4871
  { secretKey: secret },
4698
4872
  "DELETE",
4699
4873
  path,
@@ -4794,16 +4968,16 @@ async function verifyMembershipAccessReceipt(value, input, now) {
4794
4968
  }
4795
4969
 
4796
4970
  // src/membership-seat-stripe.ts
4797
- import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
4971
+ import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
4798
4972
 
4799
4973
  // src/subscription-provider-contract.ts
4800
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4974
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
4801
4975
  function subscriptionContractMismatch() {
4802
4976
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4803
4977
  }
4804
4978
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
4805
4979
  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();
4806
- const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4980
+ const result = await stripeRequest7({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4807
4981
  const price = result.body;
4808
4982
  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();
4809
4983
  return typeof price.product === "string" ? price.product : void 0;
@@ -4821,7 +4995,7 @@ var metadataFor = (seat) => ({
4821
4995
  });
4822
4996
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
4823
4997
  async function read(sk, path, params) {
4824
- const response = await stripeRequest7({ secretKey: sk }, "GET", path, params);
4998
+ const response = await stripeRequest8({ secretKey: sk }, "GET", path, params);
4825
4999
  if (!response.ok) return reject3();
4826
5000
  return response.body;
4827
5001
  }
@@ -4858,11 +5032,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
4858
5032
  }
4859
5033
  await subscriptionForSeat(sk, seat);
4860
5034
  const metadata = metadataFor(seat);
4861
- const result = seat.providerPaymentIntentId ? await stripeRequest7(
5035
+ const result = seat.providerPaymentIntentId ? await stripeRequest8(
4862
5036
  { secretKey: sk },
4863
5037
  "GET",
4864
5038
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
4865
- ) : await stripeRequest7({ secretKey: sk }, "POST", "/v1/payment_intents", {
5039
+ ) : await stripeRequest8({ secretKey: sk }, "POST", "/v1/payment_intents", {
4866
5040
  amount: seat.amountCents,
4867
5041
  currency: seat.currency,
4868
5042
  customer: seat.providerCustomerId,
@@ -4884,7 +5058,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4884
5058
  return String(existing.id);
4885
5059
  }
4886
5060
  if (seat.combinedInvoice) reject3();
4887
- const result = await stripeRequest7({ secretKey: sk }, "POST", "/v1/subscription_items", {
5061
+ const result = await stripeRequest8({ secretKey: sk }, "POST", "/v1/subscription_items", {
4888
5062
  subscription: seat.providerSubscriptionId,
4889
5063
  price: seat.priceId,
4890
5064
  quantity: 1,
@@ -5159,10 +5333,10 @@ async function recordRecipient(db, input, now, approve) {
5159
5333
  }
5160
5334
 
5161
5335
  // src/membership-seat-refund-stripe.ts
5162
- import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
5336
+ import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
5163
5337
 
5164
5338
  // src/membership-checkout-seat-refund.ts
5165
- import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
5339
+ import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
5166
5340
  var fail4 = (code = "named_seat_refund_mismatch") => {
5167
5341
  throw Object.assign(new Error(code), { code });
5168
5342
  };
@@ -5171,7 +5345,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
5171
5345
  const invoiceRef = seat.combinedInvoice;
5172
5346
  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();
5173
5347
  const call = async (method, path, params, key3) => {
5174
- const result = await stripeRequest8({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5348
+ const result = await stripeRequest9({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5175
5349
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
5176
5350
  return result.body;
5177
5351
  };
@@ -5244,7 +5418,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
5244
5418
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
5245
5419
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
5246
5420
  const call = async (method, path, params, idempotencyKey) => {
5247
- const result = await stripeRequest9({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5421
+ const result = await stripeRequest10({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5248
5422
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
5249
5423
  return result.body;
5250
5424
  };
@@ -5946,7 +6120,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
5946
6120
  };
5947
6121
  signal.addEventListener("abort", cancel, { once: true });
5948
6122
  let size = 0;
5949
- let text6 = "";
6123
+ let text8 = "";
5950
6124
  const decoder = new TextDecoder();
5951
6125
  try {
5952
6126
  while (true) {
@@ -5956,10 +6130,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
5956
6130
  if (done) break;
5957
6131
  size += value.byteLength;
5958
6132
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
5959
- text6 += decoder.decode(value, { stream: true });
6133
+ text8 += decoder.decode(value, { stream: true });
5960
6134
  }
5961
6135
  try {
5962
- return JSON.parse(text6 + decoder.decode());
6136
+ return JSON.parse(text8 + decoder.decode());
5963
6137
  } catch {
5964
6138
  throw new TypeError(`invalid ${label}`);
5965
6139
  }
@@ -6173,7 +6347,7 @@ async function requestMembershipRefund(options) {
6173
6347
  async function requestMembershipRenewal(options) {
6174
6348
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
6175
6349
  const row2 = result.renewal;
6176
- 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)) {
6350
+ 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)) {
6177
6351
  throw new Error("invalid membership renewal receipt");
6178
6352
  }
6179
6353
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -6233,7 +6407,7 @@ async function requestMembershipCancellation(options) {
6233
6407
  }
6234
6408
 
6235
6409
  // src/membership-seat-cancel.ts
6236
- import { initStripe, stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
6410
+ import { initStripe, stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
6237
6411
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6238
6412
  membershipClock(now);
6239
6413
  const state = await loadMembershipAuthorityState(db, target);
@@ -6261,7 +6435,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6261
6435
  }
6262
6436
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
6263
6437
  const request = async (method, path, params) => {
6264
- const result = await stripeRequest10(
6438
+ const result = await stripeRequest11(
6265
6439
  { secretKey },
6266
6440
  method,
6267
6441
  path,
@@ -6446,6 +6620,19 @@ var id10 = () => attr9("string", false, true, true);
6446
6620
  var key2 = (optional = false) => attr9("string", optional, false, true);
6447
6621
  var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
6448
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
+ });
6449
6636
  var membershipAuthorityEntities = {
6450
6637
  networkMembership: { attrs: {
6451
6638
  id: id10(),
@@ -6485,7 +6672,8 @@ var membershipAuthorityEntities = {
6485
6672
  graceEndsAt: attr9("number", true),
6486
6673
  ...times(),
6487
6674
  refundRequestedAt: attr9("number", true),
6488
- restartId: key2(true)
6675
+ restartId: key2(true),
6676
+ ...dispute()
6489
6677
  } },
6490
6678
  membershipRestart: { attrs: {
6491
6679
  id: id10(),
@@ -6561,7 +6749,8 @@ var membershipAuthorityEntities = {
6561
6749
  refundedCents: attr9("number", true),
6562
6750
  providerRefundIds: attr9("json", true),
6563
6751
  regiftedAt: attr9("number", true),
6564
- formerRecipients: attr9("json", true)
6752
+ formerRecipients: attr9("json", true),
6753
+ ...dispute()
6565
6754
  } },
6566
6755
  networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
6567
6756
  membershipEffect: { attrs: {
@@ -6921,7 +7110,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
6921
7110
  function unknownKeys2(value, allowed2, path, errors) {
6922
7111
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6923
7112
  }
6924
- function text3(value, path, errors, max = MAX_TEXT2, required = true) {
7113
+ function text5(value, path, errors, max = MAX_TEXT2, required = true) {
6925
7114
  if (value === void 0 && !required) return void 0;
6926
7115
  if (typeof value !== "string" || required && !value.trim()) {
6927
7116
  errors.push(`${path} must be a non-empty string`);
@@ -6947,7 +7136,7 @@ function scopeValue(value, path, errors) {
6947
7136
  return { kind: raw2.kind };
6948
7137
  }
6949
7138
  if (raw2.kind === "chapter") {
6950
- const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7139
+ const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
6951
7140
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
6952
7141
  return { kind: "chapter", chapterId };
6953
7142
  }
@@ -6961,22 +7150,22 @@ function resourceValue(value, path, errors) {
6961
7150
  return null;
6962
7151
  }
6963
7152
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
6964
- const id11 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
7153
+ const id11 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
6965
7154
  if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
6966
- const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
7155
+ const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
6967
7156
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
6968
- const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
6969
- const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
6970
- const status2 = text3(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) ?? "";
6971
7160
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
6972
7161
  const revision = integer3(raw2.revision, `${path}.revision`, errors);
6973
7162
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
6974
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(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) ?? "") : [];
6975
7164
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
6976
7165
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
6977
7166
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
6978
- const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
6979
- const url = text3(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);
6980
7169
  if (url) {
6981
7170
  try {
6982
7171
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -6988,9 +7177,9 @@ function resourceValue(value, path, errors) {
6988
7177
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
6989
7178
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
6990
7179
  const brandAsset = asset ? {
6991
- bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
6992
- assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
6993
- ...asset.filename === void 0 ? {} : { filename: text3(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) ?? "" }
6994
7183
  } : void 0;
6995
7184
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
6996
7185
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -7005,17 +7194,17 @@ function resourceValue(value, path, errors) {
7005
7194
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
7006
7195
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
7007
7196
  const promotedFrom = promotedRaw ? {
7008
- chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7009
- resourceId: text3(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) ?? "",
7010
7199
  resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
7011
7200
  documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
7012
- promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7201
+ promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7013
7202
  promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
7014
7203
  } : void 0;
7015
7204
  const provenance = {
7016
- createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7205
+ createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7017
7206
  createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
7018
- updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7207
+ updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7019
7208
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
7020
7209
  ...promotedFrom ? { promotedFrom } : {}
7021
7210
  };
@@ -7034,7 +7223,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
7034
7223
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
7035
7224
  const sourceType = sourceRaw?.type;
7036
7225
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
7037
- const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7226
+ const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7038
7227
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
7039
7228
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
7040
7229
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -7042,8 +7231,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
7042
7231
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
7043
7232
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
7044
7233
  const target = targetRaw ? {
7045
- appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7046
- environment: text3(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) ?? ""
7047
7236
  } : void 0;
7048
7237
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
7049
7238
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -7138,9 +7327,9 @@ async function createBuilderLibraryEnvelope(document) {
7138
7327
  }
7139
7328
 
7140
7329
  // src/email-retirement.ts
7141
- function isRetiredEmailCopy(subject, text6) {
7330
+ function isRetiredEmailCopy(subject, text8) {
7142
7331
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
7143
- const bodyCopy = text6.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/, "");
7144
7333
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
7145
7334
  }
7146
7335
 
@@ -7218,10 +7407,10 @@ function planDelivery(input) {
7218
7407
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
7219
7408
  if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7220
7409
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
7221
- const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
7410
+ const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
7222
7411
 
7223
7412
  ` + renderedText : renderedText;
7224
- return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
7413
+ return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
7225
7414
  }
7226
7415
 
7227
7416
  // src/email-readiness.ts
@@ -7556,6 +7745,17 @@ function findGiftRef(obj) {
7556
7745
  }
7557
7746
  function normalizeWebhookEvent(event2, options = {}) {
7558
7747
  const obj = event2.data?.object ?? {};
7748
+ if (event2.type.startsWith("charge.dispute.")) {
7749
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
7750
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
7751
+ return {
7752
+ kind: "disputed",
7753
+ chargeId,
7754
+ disputeId: obj.id,
7755
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
7756
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
7757
+ };
7758
+ }
7559
7759
  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 };
7560
7760
  const gift = findGiftRef(obj);
7561
7761
  if (gift.giftIntentId) {
@@ -7866,7 +8066,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
7866
8066
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
7867
8067
  var CURRENCY = /^[a-z]{3}$/;
7868
8068
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
7869
- var text4 = (value, path, errors, max) => {
8069
+ var text6 = (value, path, errors, max) => {
7870
8070
  if (typeof value !== "string" || !value.trim()) {
7871
8071
  errors.push(`${path} is required`);
7872
8072
  return "";
@@ -7894,23 +8094,23 @@ function validateGiftOfferCatalog(input, tierIds) {
7894
8094
  }
7895
8095
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
7896
8096
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7897
- const id11 = text4(item.id, `${path}.id`, errors, 80);
8097
+ const id11 = text6(item.id, `${path}.id`, errors, 80);
7898
8098
  if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7899
8099
  if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
7900
8100
  ids2.add(id11);
7901
8101
  const targetRaw = record3(item.target);
7902
8102
  let target = { type: "entitlement", entitlementKey: "invalid" };
7903
8103
  if (targetRaw?.type === "tier") {
7904
- const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8104
+ const tierId = text6(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
7905
8105
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
7906
8106
  target = { type: "tier", tierId };
7907
8107
  } else if (targetRaw?.type === "entitlement") {
7908
- const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8108
+ const entitlementKey = text6(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
7909
8109
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
7910
8110
  target = { type: "entitlement", entitlementKey };
7911
8111
  } else errors.push(`${path}.target.type must be tier or entitlement`);
7912
8112
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
7913
- const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8113
+ const stripePriceId = text6(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
7914
8114
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
7915
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`);
7916
8116
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -7932,9 +8132,9 @@ function validateGiftOfferCatalog(input, tierIds) {
7932
8132
  redemptionLifetimeHours: lifetime,
7933
8133
  ...activeFrom !== void 0 ? { activeFrom } : {},
7934
8134
  ...activeUntil !== void 0 ? { activeUntil } : {},
7935
- title: text4(item.title, `${path}.title`, errors, 160),
7936
- description: text4(item.description, `${path}.description`, errors, 2e3),
7937
- ctaLabel: text4(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)
7938
8138
  });
7939
8139
  }
7940
8140
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -8398,9 +8598,9 @@ function subAnnualCents(sub) {
8398
8598
  }
8399
8599
 
8400
8600
  // src/payments-stripe.ts
8401
- import { stripeRequest as stripeRequest11, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8601
+ import { stripeRequest as stripeRequest12, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8402
8602
  async function stripeCall(sk, method, path, params, idempotencyKey) {
8403
- return stripeRequest11(
8603
+ return stripeRequest12(
8404
8604
  { secretKey: sk },
8405
8605
  method,
8406
8606
  path,
@@ -9022,10 +9222,10 @@ function boundedString(value, path, errors, max, required = false) {
9022
9222
  errors.push(`${path} must be a string`);
9023
9223
  return void 0;
9024
9224
  }
9025
- const text6 = value.trim();
9026
- if (required && !text6) errors.push(`${path} is required`);
9027
- if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9028
- return text6;
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;
9029
9229
  }
9030
9230
  function validateMembershipPageDocument(input, options = {}) {
9031
9231
  const errors = [];
@@ -9074,8 +9274,8 @@ function validateMembershipPageDocument(input, options = {}) {
9074
9274
  else {
9075
9275
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
9076
9276
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
9077
- const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9078
- return text6 ? [text6] : [];
9277
+ const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9278
+ return text8 ? [text8] : [];
9079
9279
  });
9080
9280
  }
9081
9281
  }
@@ -9174,7 +9374,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
9174
9374
  function unknownKeys3(value, allowed2, path, errors) {
9175
9375
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
9176
9376
  }
9177
- function text5(value, path, errors, max = 65536) {
9377
+ function text7(value, path, errors, max = 65536) {
9178
9378
  if (typeof value !== "string" || !value.trim()) {
9179
9379
  errors.push(`${path} is required`);
9180
9380
  return "";
@@ -9197,11 +9397,11 @@ function tiersOf(value, errors) {
9197
9397
  return [];
9198
9398
  }
9199
9399
  unknownKeys3(item, TIER_KEYS2, path, errors);
9200
- const id11 = text5(item.id, `${path}.id`, errors, 80);
9400
+ const id11 = text7(item.id, `${path}.id`, errors, 80);
9201
9401
  if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9202
9402
  if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
9203
9403
  seen.add(id11);
9204
- const name = text5(item.name, `${path}.name`, errors, 160);
9404
+ const name = text7(item.name, `${path}.name`, errors, 160);
9205
9405
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
9206
9406
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
9207
9407
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -9219,7 +9419,7 @@ function tiersOf(value, errors) {
9219
9419
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
9220
9420
  errors.push(`${path}.interval must be year or month`);
9221
9421
  }
9222
- const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
9422
+ const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
9223
9423
  return [{
9224
9424
  id: id11,
9225
9425
  name,
@@ -9277,7 +9477,7 @@ function policyOf(value, errors, tiers2) {
9277
9477
  const result = {};
9278
9478
  for (const key3 of allowed2) {
9279
9479
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
9280
- result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
9480
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
9281
9481
  }
9282
9482
  const tierPolicies = {};
9283
9483
  if (input.tierPolicies !== void 0) {
@@ -9292,8 +9492,8 @@ function policyOf(value, errors, tiers2) {
9292
9492
  }
9293
9493
  unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
9294
9494
  tierPolicies[id11] = {
9295
- refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9296
- ...policy2.trustCopy !== void 0 ? { trustCopy: text5(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) } : {}
9297
9497
  };
9298
9498
  }
9299
9499
  }
@@ -9333,9 +9533,9 @@ function validateSignupControlDocument(input) {
9333
9533
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
9334
9534
  const source = recordOf(root.source);
9335
9535
  const target = recordOf(root.target);
9336
- const appId = text5(source?.appId, "document.source.appId", errors, 63);
9337
- const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
9338
- const environment = text5(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);
9339
9539
  if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
9340
9540
  if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
9341
9541
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9512,9 +9712,9 @@ function markdownBenefits(lines, path, errors) {
9512
9712
  errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
9513
9713
  continue;
9514
9714
  }
9515
- const text6 = (match[1] ?? "").trim();
9516
- if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9517
- if (text6) benefits.push(text6);
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);
9518
9718
  }
9519
9719
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
9520
9720
  return benefits.slice(0, MAX_BENEFITS2);