@odla-ai/chapter 0.52.1 → 0.54.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.
@@ -497,30 +497,30 @@ async function checkBillingTerms(secretKey, input, resume, now, createMissing) {
497
497
  const terms2 = subscriptionBillingTerms(input.billingTerms);
498
498
  if (!resume && terms2.initialTermEndsAt !== void 0 && (terms2.initialTermEndsAt <= now || terms2.initialTermEndsAt - now > 730 * 864e5)) fail();
499
499
  if (!terms2.discount) return;
500
- const { couponId: couponId2, percentOffBps, duration, policyVersionId } = terms2.discount;
500
+ const { couponId, percentOffBps, duration, policyVersionId } = terms2.discount;
501
501
  const productId = input.discountProductId;
502
502
  if (productId !== void 0 && !/^prod_[A-Za-z0-9_]+$/.test(productId)) fail();
503
503
  const metadata = { ...stripePartitionMetadata(input), discountPolicyVersionId: policyVersionId };
504
504
  const expansion = { "expand[0]": "applies_to" };
505
- let result = await stripeRequest({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId2)}`, expansion);
505
+ let result = await stripeRequest({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId)}`, expansion);
506
506
  if (createMissing && result.status === 404 && result.body?.error?.code === "resource_missing") {
507
507
  result = await stripeRequest(
508
508
  { secretKey },
509
509
  "POST",
510
510
  "/v1/coupons",
511
511
  {
512
- id: couponId2,
512
+ id: couponId,
513
513
  percent_off: percentOffBps / 100,
514
514
  duration,
515
515
  metadata,
516
516
  ...expansion,
517
517
  ...productId ? { "applies_to[products][0]": productId } : {}
518
518
  },
519
- { idempotencyKey: `chapter-policy-coupon:${couponId2}` }
519
+ { idempotencyKey: `chapter-policy-coupon:${couponId}` }
520
520
  );
521
521
  }
522
522
  const coupon = result.body;
523
- if (!result.ok || !coupon || coupon.id !== couponId2 || !resume && coupon.valid !== true || coupon.livemode !== (input.environment === "prod") || coupon.percent_off !== percentOffBps / 100 || coupon.duration !== duration || coupon.amount_off != null || (productId ? !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== productId : coupon.applies_to != null) || !Object.entries(metadata).every(([key3, value]) => coupon.metadata?.[key3] === value)) fail();
523
+ if (!result.ok || !coupon || coupon.id !== couponId || !resume && coupon.valid !== true || coupon.livemode !== (input.environment === "prod") || coupon.percent_off !== percentOffBps / 100 || coupon.duration !== duration || coupon.amount_off != null || (productId ? !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== productId : coupon.applies_to != null) || !Object.entries(metadata).every(([key3, value]) => coupon.metadata?.[key3] === value)) fail();
524
524
  }
525
525
  function subscriptionBillingParams(input, productId) {
526
526
  if (!input.billingTerms) return {};
@@ -1760,8 +1760,8 @@ var object = (value) => {
1760
1760
  var id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
1761
1761
  var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
1762
1762
  var time = (value) => {
1763
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
1764
- return integer2(at, 0, 864e13);
1763
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
1764
+ return integer2(at2, 0, 864e13);
1765
1765
  };
1766
1766
  var ids = (value, allowEmpty = false) => {
1767
1767
  if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
@@ -1805,7 +1805,7 @@ function membershipOfferingVersion(value) {
1805
1805
  term: normalizedTerm,
1806
1806
  autoRenew: true,
1807
1807
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1808
- seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1808
+ seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
1809
1809
  ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1810
1810
  requiredDisclosures: ids(input.requiredDisclosures)
1811
1811
  };
@@ -1890,7 +1890,7 @@ async function hash(value) {
1890
1890
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
1891
1891
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1892
1892
  }
1893
- async function couponId(input, policyId) {
1893
+ async function membershipDiscountCouponId(input, policyId) {
1894
1894
  return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
1895
1895
  }
1896
1896
  async function load(db, ns, id6) {
@@ -1916,7 +1916,7 @@ async function restore(row2, input) {
1916
1916
  discountPolicy: contract.discountPolicy,
1917
1917
  approvedFoundingCount: Number(row2.approvedFoundingCount),
1918
1918
  now: Number(row2.issuedAt),
1919
- couponId: await couponId(input, contract.discountPolicy?.id)
1919
+ couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
1920
1920
  });
1921
1921
  if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
1922
1922
  const result = {
@@ -1947,7 +1947,7 @@ async function previewMembershipCommercialOffer(db, input) {
1947
1947
  discountPolicy: policy,
1948
1948
  approvedFoundingCount: Number(approvedFoundingCount),
1949
1949
  now: input.now,
1950
- couponId: await couponId(input, policy?.id)
1950
+ couponId: await membershipDiscountCouponId(input, policy?.id)
1951
1951
  });
1952
1952
  const discount = contract.billingTerms.discount;
1953
1953
  return {
@@ -1997,7 +1997,7 @@ async function issueMembershipCommercialOffer(db, input) {
1997
1997
  discountPolicy: policy,
1998
1998
  approvedFoundingCount: Number(approvedFoundingCount),
1999
1999
  now: input.now,
2000
- couponId: await couponId(input, policy?.id)
2000
+ couponId: await membershipDiscountCouponId(input, policy?.id)
2001
2001
  });
2002
2002
  const value = {
2003
2003
  id: input.offerId,
@@ -2893,7 +2893,7 @@ async function checkoutVariables(db, group, app) {
2893
2893
  const frozen = await frozenCheckoutVariables(db, app);
2894
2894
  return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
2895
2895
  }
2896
- function formatInstant(at, timeZone) {
2896
+ function formatInstant(at2, timeZone) {
2897
2897
  return new Intl.DateTimeFormat("en-US", {
2898
2898
  weekday: "long",
2899
2899
  month: "long",
@@ -2903,7 +2903,7 @@ function formatInstant(at, timeZone) {
2903
2903
  minute: "2-digit",
2904
2904
  timeZoneName: "short",
2905
2905
  timeZone
2906
- }).format(new Date(at));
2906
+ }).format(new Date(at2));
2907
2907
  }
2908
2908
  var adminUrlFor = (origin) => `${origin}/admin/`;
2909
2909
  var membersUrlFor = (origin) => `${origin}/members/`;
@@ -3851,6 +3851,181 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
3851
3851
  return { refundedCents, refundIds };
3852
3852
  }
3853
3853
 
3854
+ // src/membership-seat-reservation.ts
3855
+ async function recipientKey(input, email) {
3856
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
3857
+ return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
3858
+ }
3859
+ async function reserveNamedMembershipSeat(db, input, now) {
3860
+ membershipClock(now);
3861
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
3862
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
3863
+ const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
3864
+ if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
3865
+ const state = await loadMembershipAuthorityState(db, input);
3866
+ const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
3867
+ requireMembershipScope(member, input);
3868
+ if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
3869
+ const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
3870
+ let seat = await membershipLoad(db, "networkSeat", id6);
3871
+ if (!seat) {
3872
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
3873
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
3874
+ if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
3875
+ membershipFail("named_seat_purchase_unavailable");
3876
+ }
3877
+ const attrs = {
3878
+ id: id6,
3879
+ ...membershipScope(input),
3880
+ primaryMembershipId: memberId,
3881
+ recipientName: name,
3882
+ recipientEmailNormalized: email,
3883
+ purchaserUserId: input.purchaserUserId,
3884
+ recipientClaimId: claimId,
3885
+ priceId: input.priceId,
3886
+ primaryPriceId: member.providerPriceId,
3887
+ amountCents: offering.seatPriceCents,
3888
+ currency: offering.currency,
3889
+ interval: offering.interval,
3890
+ providerCustomerId: member.providerCustomerId,
3891
+ providerSubscriptionId: member.providerSubscriptionId,
3892
+ status: "payment_pending",
3893
+ preparedAt: now,
3894
+ termEndsAt: member.termEndsAt,
3895
+ seatPurchaseEndsAt: purchaseEndsAt,
3896
+ offeringSnapshot: offering,
3897
+ revision: 1,
3898
+ createdAt: now,
3899
+ updatedAt: now
3900
+ };
3901
+ try {
3902
+ await db.transact([
3903
+ membershipUpdate("networkSeat", id6, attrs),
3904
+ membershipUpdate("networkSeatRecipient", claimId, {
3905
+ id: claimId,
3906
+ appId: input.appId,
3907
+ chapterId: input.chapterId,
3908
+ environment: input.environment,
3909
+ runtime: input.runtime,
3910
+ seatId: id6,
3911
+ recipientEmailNormalized: email,
3912
+ createdAt: now
3913
+ }),
3914
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
3915
+ ], { mutationId: `named-seat-reserve:${id6}`, guards: [
3916
+ { ns: "networkSeat", id: id6, exists: false },
3917
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
3918
+ { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
3919
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
3920
+ ] });
3921
+ } catch (error) {
3922
+ if (error.code !== "transact_guard_failed") throw error;
3923
+ }
3924
+ seat = await membershipLoad(db, "networkSeat", id6);
3925
+ if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
3926
+ }
3927
+ requireMembershipScope(seat, input);
3928
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
3929
+ return seat;
3930
+ }
3931
+
3932
+ // src/membership-seat-included.ts
3933
+ var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
3934
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
3935
+ function namedSeatRecipientState(seat) {
3936
+ switch (String(seat.status)) {
3937
+ case "renewal_pending":
3938
+ case "pending_acceptance":
3939
+ return "invited";
3940
+ case "pending_approval":
3941
+ case "pending_primary":
3942
+ return "accepted";
3943
+ case "active":
3944
+ return "joined";
3945
+ case "refund_pending":
3946
+ case "refunded":
3947
+ return seat.refundReason === "declined" ? "declined" : "refunded";
3948
+ case "canceled":
3949
+ return "canceled";
3950
+ default:
3951
+ return "payment_pending";
3952
+ }
3953
+ }
3954
+ var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
3955
+ async function grantIncludedNamedSeat(db, input, now) {
3956
+ membershipClock(now);
3957
+ const seat = await reserveNamedMembershipSeat(db, input, now);
3958
+ if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
3959
+ const state = await loadMembershipAuthorityState(db, input);
3960
+ const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
3961
+ requireMembershipScope(member, input);
3962
+ if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || member.namedSeatId !== seat.id || now >= Number(member.termEndsAt)) {
3963
+ membershipFail("named_seat_purchase_unavailable");
3964
+ }
3965
+ if (seat.status === "payment_pending") {
3966
+ const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
3967
+ try {
3968
+ await db.transact([
3969
+ membershipUpdate("networkSeat", seat.id, {
3970
+ status: "pending_acceptance",
3971
+ purchasedAt: now,
3972
+ termStartedAt: now,
3973
+ termEndsAt: member.termEndsAt,
3974
+ acceptanceEndsAt: Number(member.termEndsAt),
3975
+ entitlementId,
3976
+ revision: Number(seat.revision) + 1,
3977
+ updatedAt: now
3978
+ }),
3979
+ membershipUpdate("networkEntitlement", entitlementId, {
3980
+ id: entitlementId,
3981
+ ...membershipScope(input),
3982
+ membershipId: member.id,
3983
+ seatId: seat.id,
3984
+ kind: "named_seat",
3985
+ status: "pending_acceptance",
3986
+ termStartedAt: now,
3987
+ termEndsAt: member.termEndsAt,
3988
+ offeringSnapshot: member.offeringSnapshot,
3989
+ sourceEventId: source,
3990
+ revision: 1,
3991
+ createdAt: now,
3992
+ updatedAt: now
3993
+ }),
3994
+ membershipUpdate("membershipEffect", inviteId, {
3995
+ id: inviteId,
3996
+ chapterId: input.chapterId,
3997
+ environment: input.environment,
3998
+ runtime: input.runtime,
3999
+ membershipId: member.id,
4000
+ seatId: seat.id,
4001
+ kind: "invite_named_recipient",
4002
+ status: "pending",
4003
+ attempts: 0,
4004
+ createdAt: now,
4005
+ updatedAt: now
4006
+ })
4007
+ ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4008
+ {
4009
+ ns: "networkSeat",
4010
+ id: seat.id,
4011
+ exists: true,
4012
+ equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4013
+ absent: ["purchasedAt", "providerPaymentIntentId"]
4014
+ },
4015
+ { ns: "networkEntitlement", id: entitlementId, exists: false },
4016
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4017
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4018
+ ] });
4019
+ } catch (error) {
4020
+ if (error.code !== "transact_guard_failed") throw error;
4021
+ }
4022
+ }
4023
+ const current = await membershipLoad(db, "networkSeat", seat.id);
4024
+ requireMembershipScope(current, input);
4025
+ if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4026
+ return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4027
+ }
4028
+
3854
4029
  // src/membership-seat-refund.ts
3855
4030
  async function requestNamedSeatRefund(db, input, now) {
3856
4031
  membershipClock(now);
@@ -3860,7 +4035,7 @@ async function requestNamedSeatRefund(db, input, now) {
3860
4035
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
3861
4036
  requireMembershipScope(member, input);
3862
4037
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
3863
- if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
4038
+ if (!seat.purchasedAt || !seat.providerPaymentIntentId && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
3864
4039
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
3865
4040
  await db.transact([
3866
4041
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -3905,7 +4080,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
3905
4080
  await loadMembershipAuthorityState(db, seat);
3906
4081
  if (seat.status === "refunded") return;
3907
4082
  if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
3908
- const result = await refundNamedSeatStripePayment(secretKey, seat);
4083
+ const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
3909
4084
  await db.transact([
3910
4085
  membershipUpdate("networkSeat", seatId, {
3911
4086
  status: "refunded",
@@ -3927,7 +4102,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3927
4102
  await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
3928
4103
  await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
3929
4104
  const refunded = await membershipLoad(db, "networkSeat", input.seatId);
3930
- if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
4105
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
3931
4106
  throw new Error("named seat refund unconfirmed");
3932
4107
  }
3933
4108
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
@@ -3959,9 +4134,56 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
3959
4134
  return { lifted, more: rows5.length > limit };
3960
4135
  }
3961
4136
 
4137
+ // src/membership-seat-reprice.ts
4138
+ async function lowerSeatPrice(db, scope2, now, limit = 100) {
4139
+ membershipClock(now);
4140
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
4141
+ const state = await loadMembershipAuthorityState(db, scope2);
4142
+ const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
4143
+ if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
4144
+ const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
4145
+ if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
4146
+ const target = current.seatPriceCents;
4147
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4148
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
4149
+ for (const member of rows5.slice(0, limit)) {
4150
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
4151
+ const id6 = String(member.id), skip = (reason) => {
4152
+ receipt.skipped.push({ membershipId: id6, reason });
4153
+ };
4154
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
4155
+ if (offering.seatPriceCents <= target) {
4156
+ skip("seat price already at or below the current offering");
4157
+ continue;
4158
+ }
4159
+ if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
4160
+ skip(`membership is ${String(member.status)}`);
4161
+ continue;
4162
+ }
4163
+ const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4164
+ if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
4165
+ skip(`seat is ${String(seat.status)}`);
4166
+ continue;
4167
+ }
4168
+ const ops = [membershipUpdate("networkMembership", id6, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
4169
+ const guards = [
4170
+ { ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision } },
4171
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4172
+ ];
4173
+ if (seat) {
4174
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
4175
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
4176
+ }
4177
+ await db.transact(ops, { mutationId: `seat-reprice:${id6}:${member.revision}`, guards });
4178
+ receipt.repriced++;
4179
+ }
4180
+ return receipt;
4181
+ }
4182
+
3962
4183
  // src/worker-routes-admin-membership-seats.ts
3963
4184
  var LIST = "/api/admin/membership-seats";
3964
4185
  var LIFT = "/api/admin/membership-seats/lift-purchase-window";
4186
+ var REPRICE = "/api/admin/membership-seats/reprice";
3965
4187
  var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
3966
4188
  var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
3967
4189
  var PUBLIC = [
@@ -3988,11 +4210,11 @@ var PUBLIC = [
3988
4210
  ];
3989
4211
  var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
3990
4212
  var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3991
- const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
3992
- if (url.pathname !== LIST && !one && !lift) return null;
4213
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
4214
+ if (url.pathname !== LIST && !one && !lift && !reprice) return null;
3993
4215
  const operator = ctx.membershipAuthorityOperator;
3994
4216
  if (!operator) return json({ error: "not found" }, 404);
3995
- if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4217
+ if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3996
4218
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3997
4219
  if (!user) return json({ error: "unauthorized" }, 401);
3998
4220
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
@@ -4002,7 +4224,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4002
4224
  } catch {
4003
4225
  return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4004
4226
  }
4005
- if (lift) {
4227
+ if (lift || reprice) {
4006
4228
  let body;
4007
4229
  try {
4008
4230
  body = await req.json();
@@ -4010,14 +4232,16 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4010
4232
  return json({ error: "chapterId and runtime are required" }, 400);
4011
4233
  }
4012
4234
  const appId = body.appId ?? body.chapterId;
4013
- if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime)) {
4014
- return json({ error: "chapterId and runtime are required" }, 400);
4235
+ if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime) || reprice && (typeof body.skuId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(body.skuId))) {
4236
+ return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
4015
4237
  }
4238
+ const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
4016
4239
  try {
4017
- return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
4240
+ if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
4241
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
4018
4242
  } catch (error) {
4019
4243
  const code = error.code;
4020
- return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4244
+ return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
4021
4245
  }
4022
4246
  }
4023
4247
  if (!one) {
@@ -4053,8 +4277,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4053
4277
  var LIMIT = 200;
4054
4278
  var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4055
4279
  var instant = (value) => {
4056
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4057
- return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
4280
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4281
+ return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4058
4282
  };
4059
4283
  function provenance(row2) {
4060
4284
  const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
@@ -4150,6 +4374,239 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
4150
4374
  return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
4151
4375
  };
4152
4376
 
4377
+ // src/membership-discount-reterm.ts
4378
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4379
+
4380
+ // src/subscription-provider-contract.ts
4381
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4382
+ function subscriptionContractMismatch() {
4383
+ throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4384
+ }
4385
+ async function verifySubscriptionPrice(secretKey, input, resume = false) {
4386
+ 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();
4387
+ const result = await stripeRequest5({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4388
+ const price = result.body;
4389
+ 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();
4390
+ return typeof price.product === "string" ? price.product : void 0;
4391
+ }
4392
+ function verifySubscriptionResult(sub, input, customerId, metadata) {
4393
+ const items = sub?.items;
4394
+ if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
4395
+ const invoice = sub.latest_invoice;
4396
+ if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
4397
+ verifySubscriptionBillingResult(sub, input);
4398
+ }
4399
+
4400
+ // src/membership-discount-reterm.ts
4401
+ var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4402
+ var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
4403
+ var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
4404
+ var couponOf = (discount) => {
4405
+ const ref = discount?.source?.coupon ?? discount?.coupon;
4406
+ return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
4407
+ };
4408
+ async function retermPolicy(db, scope2, id6) {
4409
+ const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
4410
+ if (!row2) membershipFail("membership_reterm_policy_unknown");
4411
+ let policy;
4412
+ try {
4413
+ policy = membershipDiscountPolicyVersion(row2.snapshot);
4414
+ } catch {
4415
+ return membershipFail("membership_reterm_policy_ineligible");
4416
+ }
4417
+ if (policy.id !== id6 || policy.chapterId !== scope2.chapterId || policy.status !== "published" || policy.benefitDuration.kind !== "while_active" || policy.percentOffBps <= 0 || policy.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
4418
+ return policy;
4419
+ }
4420
+ async function attachProviderDiscount(sk, scope2, member, policy, couponId, productId) {
4421
+ const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
4422
+ const read2 = await stripeRequest6({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
4423
+ const sub = read2.body, items = sub?.items?.data;
4424
+ const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
4425
+ if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
4426
+ throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
4427
+ }
4428
+ const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
4429
+ if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
4430
+ const present = discounts.map(couponOf);
4431
+ if (present.length === 1 && present[0] === couponId) return false;
4432
+ if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
4433
+ const written = await stripeRequest6({ secretKey: sk }, "POST", path, {
4434
+ "discounts[0][coupon]": couponId,
4435
+ "expand[0]": "discounts",
4436
+ "metadata[discountPolicyVersionId]": policy.id
4437
+ }, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy.id}` });
4438
+ const after = written.body?.discounts;
4439
+ if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
4440
+ membershipFail("membership_reterm_provider_unconfirmed");
4441
+ }
4442
+ return true;
4443
+ }
4444
+ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
4445
+ membershipClock(now);
4446
+ const limit = input.limit ?? 100;
4447
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
4448
+ if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
4449
+ if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id6) => typeof id6 !== "string" || !SAFE.test(id6)))) throw new TypeError("invalid membership ids");
4450
+ const state = await loadMembershipAuthorityState(db, scope2);
4451
+ const policy = await retermPolicy(db, scope2, input.policyVersionId);
4452
+ const receipt = { policyVersionId: policy.id, retermed: [], unchanged: [], skipped: [], more: false };
4453
+ const skip = (membershipId, reason) => {
4454
+ receipt.skipped.push({ membershipId, reason });
4455
+ };
4456
+ let rows5;
4457
+ if (input.membershipIds) {
4458
+ rows5 = [];
4459
+ for (const id6 of input.membershipIds) {
4460
+ const member = await membershipLoad(db, "networkMembership", id6);
4461
+ if (!member) skip(id6, "not_found");
4462
+ else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id6, "scope_mismatch");
4463
+ else rows5.push(member);
4464
+ }
4465
+ } else {
4466
+ const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4467
+ if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
4468
+ receipt.more = found.length > limit;
4469
+ rows5 = found.slice(0, limit);
4470
+ }
4471
+ for (const member of rows5) {
4472
+ const id6 = String(member.id);
4473
+ if (member.discountPolicyVersionId === policy.id) {
4474
+ receipt.unchanged.push(id6);
4475
+ continue;
4476
+ }
4477
+ if (!RETERMABLE.has(String(member.status))) {
4478
+ skip(id6, "status");
4479
+ continue;
4480
+ }
4481
+ if (member.founding !== true) {
4482
+ skip(id6, "not_founding");
4483
+ continue;
4484
+ }
4485
+ if (member.cohort !== policy.cohortLabel) {
4486
+ skip(id6, "cohort_mismatch");
4487
+ continue;
4488
+ }
4489
+ const skuId = String(member.baseSkuId);
4490
+ if (!policy.discountEligibleSkuIds.includes(skuId)) {
4491
+ skip(id6, "sku_not_discounted");
4492
+ continue;
4493
+ }
4494
+ const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
4495
+ const renewal = Math.round(base * (1e4 - policy.percentOffBps) / 1e4);
4496
+ if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
4497
+ skip(id6, "would_not_widen");
4498
+ continue;
4499
+ }
4500
+ const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy.id);
4501
+ let providerUpdated;
4502
+ try {
4503
+ const productId = await verifySubscriptionPrice(stripeSecretKey, {
4504
+ priceId: String(member.providerPriceId),
4505
+ priceCents: base,
4506
+ currency: String(member.currency),
4507
+ interval: member.interval === "month" ? "month" : "year",
4508
+ environment: scope2.environment
4509
+ }, true);
4510
+ if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
4511
+ await prepareSubscriptionBillingTerms(stripeSecretKey, {
4512
+ ...scope2,
4513
+ priceCents: base,
4514
+ currency: String(member.currency),
4515
+ discountProductId: productId,
4516
+ billingTerms: {
4517
+ offeringVersionId: String(member.offeringVersionId),
4518
+ discount: { policyVersionId: policy.id, couponId, percentOffBps: policy.percentOffBps, duration: "forever" }
4519
+ }
4520
+ }, false);
4521
+ providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy, couponId, productId);
4522
+ } catch (error) {
4523
+ const code = error.code;
4524
+ if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
4525
+ skip(id6, "provider_mismatch");
4526
+ continue;
4527
+ }
4528
+ throw error;
4529
+ }
4530
+ try {
4531
+ await db.transact([membershipUpdate("networkMembership", id6, {
4532
+ discountPolicyVersionId: policy.id,
4533
+ discountSnapshot: policy,
4534
+ renewalAmountCents: renewal,
4535
+ revision: Number(member.revision) + 1,
4536
+ updatedAt: now
4537
+ })], {
4538
+ mutationId: `membership-reterm:${id6}:${policy.id}:${member.revision}`,
4539
+ guards: [
4540
+ { ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
4541
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
4542
+ { ns: "discountPolicyVersion", id: policy.id, exists: true }
4543
+ ]
4544
+ });
4545
+ } catch (error) {
4546
+ if (error.code !== "transact_guard_failed") throw error;
4547
+ return membershipFail("membership_reterm_retry_required");
4548
+ }
4549
+ receipt.retermed.push({
4550
+ membershipId: id6,
4551
+ ...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
4552
+ previousRenewalAmountCents: current,
4553
+ renewalAmountCents: renewal,
4554
+ providerUpdated
4555
+ });
4556
+ }
4557
+ return receipt;
4558
+ }
4559
+
4560
+ // src/worker-routes-admin-membership-reterm.ts
4561
+ var PATH4 = "/api/admin/membership-discount/reterm";
4562
+ var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4563
+ var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4564
+ var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
4565
+ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
4566
+ if (url.pathname !== PATH4) return null;
4567
+ const operator = ctx.membershipAuthorityOperator;
4568
+ if (!operator) return json({ error: "not found" }, 404);
4569
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
4570
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4571
+ if (!user) return json({ error: "unauthorized" }, 401);
4572
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4573
+ let db;
4574
+ try {
4575
+ db = operator.makeDb(env);
4576
+ } catch {
4577
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4578
+ }
4579
+ let body;
4580
+ try {
4581
+ body = await req.json();
4582
+ } catch {
4583
+ return json({ error: BAD }, 400);
4584
+ }
4585
+ const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
4586
+ if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((id6) => typeof id6 !== "string" || !ID.test(id6))) || body.limit !== void 0 && (!Number.isSafeInteger(body.limit) || Number(body.limit) < 1 || Number(body.limit) > 200)) {
4587
+ return json({ error: BAD }, 400);
4588
+ }
4589
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
4590
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
4591
+ try {
4592
+ const receipt = await retermMembershipDiscount(
4593
+ db,
4594
+ secret,
4595
+ { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
4596
+ {
4597
+ policyVersionId: body.policyVersionId,
4598
+ ...body.membershipIds ? { membershipIds: body.membershipIds } : {},
4599
+ ...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
4600
+ },
4601
+ Date.now()
4602
+ );
4603
+ return json({ ok: true, ...receipt });
4604
+ } catch (error) {
4605
+ const code = error.code;
4606
+ return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4607
+ }
4608
+ };
4609
+
4153
4610
  // src/worker-routes-membership-cancel.ts
4154
4611
  var handleMembershipCancellation = async (req, url, env, ctx) => {
4155
4612
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4810,7 +5267,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
4810
5267
  }
4811
5268
 
4812
5269
  // src/admission-grants.ts
4813
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
5270
+ var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
4814
5271
  var MAX_GRANTS = 2e3;
4815
5272
  var MAX_NOTE = 2e3;
4816
5273
  var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -4832,10 +5289,10 @@ function validateAdmissionGrantDocument(input) {
4832
5289
  const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
4833
5290
  const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
4834
5291
  const environment = typeof target.environment === "string" ? target.environment : "";
4835
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
5292
+ if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
4836
5293
  if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
4837
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
4838
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
5294
+ if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
5295
+ if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
4839
5296
  if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
4840
5297
  const rawGrants = Array.isArray(root.grants) ? root.grants : [];
4841
5298
  if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
@@ -4850,7 +5307,7 @@ function validateAdmissionGrantDocument(input) {
4850
5307
  const decision = grant.decision;
4851
5308
  const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
4852
5309
  const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
4853
- if (!ID.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
5310
+ if (!ID2.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
4854
5311
  if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
4855
5312
  if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
4856
5313
  if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
@@ -5066,7 +5523,7 @@ function chapterFlowFieldContract(chapter, flow, application = chapter.applicati
5066
5523
  // src/gift-offers.ts
5067
5524
  var GIFT_OFFER_CATALOG_VERSION = 1;
5068
5525
  var MAX_GIFT_OFFERS = 20;
5069
- var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
5526
+ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
5070
5527
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
5071
5528
  var CURRENCY = /^[a-z]{3}$/;
5072
5529
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -5099,7 +5556,7 @@ function validateGiftOfferCatalog(input, tierIds) {
5099
5556
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
5100
5557
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
5101
5558
  const id6 = text3(item.id, `${path}.id`, errors, 80);
5102
- if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
5559
+ if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
5103
5560
  if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
5104
5561
  ids2.add(id6);
5105
5562
  const targetRaw = record(item.target);
@@ -5110,7 +5567,7 @@ function validateGiftOfferCatalog(input, tierIds) {
5110
5567
  target = { type: "tier", tierId };
5111
5568
  } else if (targetRaw?.type === "entitlement") {
5112
5569
  const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
5113
- if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
5570
+ if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
5114
5571
  target = { type: "entitlement", entitlementKey };
5115
5572
  } else errors.push(`${path}.target.type must be tier or entitlement`);
5116
5573
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
@@ -6194,7 +6651,7 @@ var handleMe = async (req, url, env, ctx) => {
6194
6651
  } catch {
6195
6652
  }
6196
6653
  }
6197
- return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(memberAccess && ctx.membershipAuthority?.namedSeats && member?.source.stripeSubscriptionId && member.source.status !== "submitted") });
6654
+ return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
6198
6655
  };
6199
6656
  var handleCrm = async (req, url, env, ctx) => {
6200
6657
  const crmBase = ctx.crmBase;
@@ -6896,7 +7353,7 @@ async function memberContentDigest(document) {
6896
7353
  }
6897
7354
 
6898
7355
  // src/member-content-validation-helpers.ts
6899
- var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
7356
+ var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
6900
7357
  var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6901
7358
  var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
6902
7359
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
@@ -6936,7 +7393,7 @@ function offerValue(value, path, errors, tiers, audience) {
6936
7393
  }
6937
7394
  unknownKeys2(raw2, OFFER_KEYS, path, errors);
6938
7395
  const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
6939
- if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
7396
+ if (tierId && !ID4.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
6940
7397
  if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
6941
7398
  if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
6942
7399
  errors.push(`${path}.tierId must grant access to this page`);
@@ -6969,7 +7426,7 @@ function validateMemberContentDocument(input, options = {}) {
6969
7426
  const type = sourceRaw.type;
6970
7427
  const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
6971
7428
  if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
6972
- if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7429
+ if (appId && !ID4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
6973
7430
  if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
6974
7431
  if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
6975
7432
  if (type === "parent" || type === "chapter") source = { type, appId };
@@ -7006,7 +7463,7 @@ function validateMemberContentDocument(input, options = {}) {
7006
7463
  const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
7007
7464
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
7008
7465
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
7009
- if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7466
+ if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7010
7467
  if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
7011
7468
  if (ids2.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
7012
7469
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
@@ -7933,7 +8390,7 @@ async function deliverAdmissionGrants(options) {
7933
8390
  }
7934
8391
 
7935
8392
  // src/worker-routes-network-admission.ts
7936
- var PATH4 = "/api/admin/network/admissions";
8393
+ var PATH5 = "/api/admin/network/admissions";
7937
8394
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7938
8395
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7939
8396
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -8013,11 +8470,11 @@ async function parseBody(req) {
8013
8470
  }
8014
8471
  }
8015
8472
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
8016
- if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
8473
+ if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
8017
8474
  const auth = await gate2(req, env, ctx);
8018
8475
  if ("response" in auth) return auth.response;
8019
8476
  const db = auth.db;
8020
- if (url.pathname === PATH4 && req.method === "GET") {
8477
+ if (url.pathname === PATH5 && req.method === "GET") {
8021
8478
  const targetId2 = url.searchParams.get("targetId") ?? "";
8022
8479
  const target2 = targetFor(ctx, targetId2);
8023
8480
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -8291,10 +8748,10 @@ var handleSchedule = async (req, url, env, ctx) => {
8291
8748
  };
8292
8749
 
8293
8750
  // src/payments-stripe.ts
8294
- import { stripeRequest as stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8751
+ import { stripeRequest as stripeRequest9, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8295
8752
 
8296
8753
  // src/payments-refund.ts
8297
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
8754
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
8298
8755
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
8299
8756
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
8300
8757
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
@@ -8304,7 +8761,7 @@ function fail6(code = "refund_target_mismatch") {
8304
8761
  async function refundSubscriptionPayment(secretKey, input) {
8305
8762
  if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
8306
8763
  const call = async (method, path, params, key3) => {
8307
- const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
8764
+ const result = await stripeRequest7({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
8308
8765
  if (!result.ok) fail6("stripe_error");
8309
8766
  if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
8310
8767
  return result.body;
@@ -8378,35 +8835,13 @@ async function refundSubscriptionPayment(secretKey, input) {
8378
8835
  }
8379
8836
 
8380
8837
  // src/subscription-payment-identity.ts
8381
- import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
8382
-
8383
- // src/subscription-provider-contract.ts
8384
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
8385
- function subscriptionContractMismatch() {
8386
- throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
8387
- }
8388
- async function verifySubscriptionPrice(secretKey, input, resume = false) {
8389
- 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();
8390
- const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
8391
- const price = result.body;
8392
- 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();
8393
- return typeof price.product === "string" ? price.product : void 0;
8394
- }
8395
- function verifySubscriptionResult(sub, input, customerId, metadata) {
8396
- const items = sub?.items;
8397
- if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
8398
- const invoice = sub.latest_invoice;
8399
- if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
8400
- verifySubscriptionBillingResult(sub, input);
8401
- }
8402
-
8403
- // src/subscription-payment-identity.ts
8838
+ import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
8404
8839
  var id3 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
8405
8840
  async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
8406
8841
  let paymentId = id3(invoice.payment_intent);
8407
8842
  if (!paymentId) {
8408
8843
  if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
8409
- const result2 = await stripeRequest7({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
8844
+ const result2 = await stripeRequest8({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
8410
8845
  const list = result2.body;
8411
8846
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
8412
8847
  const link = list.data[0];
@@ -8415,11 +8850,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
8415
8850
  }
8416
8851
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
8417
8852
  const path = `/v1/payment_intents/${paymentId}`;
8418
- const result = await stripeRequest7({ secretKey }, "GET", path);
8853
+ const result = await stripeRequest8({ secretKey }, "GET", path);
8419
8854
  const payment = result.body;
8420
8855
  if (!result.ok || payment?.id !== paymentId || id3(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
8421
8856
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
8422
- const tagged = await stripeRequest7(
8857
+ const tagged = await stripeRequest8(
8423
8858
  { secretKey },
8424
8859
  "POST",
8425
8860
  path,
@@ -8431,7 +8866,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
8431
8866
 
8432
8867
  // src/payments-stripe.ts
8433
8868
  async function stripeCall(sk, method, path, params, idempotencyKey) {
8434
- return stripeRequest8(
8869
+ return stripeRequest9(
8435
8870
  { secretKey: sk },
8436
8871
  method,
8437
8872
  path,
@@ -9214,6 +9649,7 @@ async function namedSeatOfferDigest(offer) {
9214
9649
  ])));
9215
9650
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
9216
9651
  }
9652
+ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
9217
9653
  async function readNamedSeatOffer(db, input, now) {
9218
9654
  membershipClock(now);
9219
9655
  await loadMembershipAuthorityState(db, input);
@@ -9236,10 +9672,19 @@ async function readNamedSeatOffer(db, input, now) {
9236
9672
  seatId: String(seat.id),
9237
9673
  status: String(seat.status),
9238
9674
  recipientName: String(seat.recipientName),
9239
- recipientEmail: String(seat.recipientEmailNormalized)
9675
+ recipientEmail: String(seat.recipientEmailNormalized),
9676
+ recipientState: namedSeatRecipientState(seat),
9677
+ ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
9678
+ ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
9679
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
9240
9680
  } } : {}
9241
9681
  };
9242
- return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
9682
+ return {
9683
+ ...offer,
9684
+ digest: await namedSeatOfferDigest(offer),
9685
+ included: offering.seatPriceCents === 0,
9686
+ purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
9687
+ };
9243
9688
  }
9244
9689
 
9245
9690
  // src/named-seat-request.ts
@@ -9315,8 +9760,9 @@ var handleNamedSeat = async (req, url, env, ctx) => {
9315
9760
  if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || recipientEmail === normalizeChapterEmail(user.email)) {
9316
9761
  return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
9317
9762
  }
9318
- if (!offer.eligible || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
9319
- if (!group?.stripePublishableKey || !group.refundPolicyText || !group.merchantDisclosureText) return json({ error: "seat checkout disclosures unavailable" }, 503);
9763
+ const granted = offer.included && offer.seat !== void 0 && offer.seat.recipientEmail === recipientEmail && offer.seat.recipientName === recipientName && ["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(offer.seat.status);
9764
+ if (!offer.eligible && !granted || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
9765
+ if (!group?.refundPolicyText || !group.merchantDisclosureText || !offer.included && !group.stripePublishableKey) return json({ error: "seat checkout disclosures unavailable" }, 503);
9320
9766
  const consentId = `${offer.membershipId}:named-seat-consent`;
9321
9767
  const prior = await membershipLoad(db, "namedSeatConsents", consentId);
9322
9768
  const attrs = {
@@ -9355,7 +9801,7 @@ var handleNamedSeat = async (req, url, env, ctx) => {
9355
9801
  }
9356
9802
  return json({
9357
9803
  ...receipt,
9358
- publishableKey: String(group.stripePublishableKey),
9804
+ ...group.stripePublishableKey ? { publishableKey: String(group.stripePublishableKey) } : {},
9359
9805
  lineItems: { standardCents: offer.amountCents, discountCents: 0, dueTodayCents: offer.amountCents }
9360
9806
  });
9361
9807
  } catch {
@@ -10218,6 +10664,7 @@ function dashboardMetricData(points, now) {
10218
10664
  }
10219
10665
 
10220
10666
  // src/chapter-billing-amount.ts
10667
+ var hasProviderDiscount = (sub, items) => Boolean(sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length));
10221
10668
  function chapterBillingAmount(sub, intent, env) {
10222
10669
  const unknown = { amountCents: null, listAmountCents: null, interval: "year", annualCents: null };
10223
10670
  const items = sub?.items?.data;
@@ -10234,10 +10681,11 @@ function chapterBillingAmount(sub, intent, env) {
10234
10681
  const primary = items.filter((it) => it.price?.id === intent.priceId);
10235
10682
  if (primary.length !== 1 || primary[0].quantity !== 1 || primary[0].price.unit_amount !== intent.priceCents || intent.interval !== interval || intent.currency !== "usd") return { ...unknown, listAmountCents, interval };
10236
10683
  if (terms2?.discount?.duration === "forever") amountCents = listAmountCents - intent.priceCents + Math.round(intent.priceCents * (1e4 - terms2.discount.percentOffBps) / 1e4);
10684
+ else if (hasProviderDiscount(sub, items)) amountCents = null;
10237
10685
  } catch {
10238
10686
  amountCents = null;
10239
10687
  }
10240
- } else if (sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length)) amountCents = null;
10688
+ } else if (hasProviderDiscount(sub, items)) amountCents = null;
10241
10689
  return { amountCents, listAmountCents, interval, annualCents: amountCents === null ? null : amountCents * (interval === "month" ? 12 : 1) };
10242
10690
  }
10243
10691
  function chapterBillingActive(sub, app) {
@@ -11155,7 +11603,7 @@ async function networkApplicationAction(opts) {
11155
11603
  if (app.status !== input.expectedStage || !applicationActionStages(app, env, ctx).includes(input.stage)) {
11156
11604
  return json({ error: "application action is no longer available; reload", code: "invalid_application_action" }, 409);
11157
11605
  }
11158
- const at = Date.now();
11606
+ const at2 = Date.now();
11159
11607
  const meta = { [ACTION_META]: {
11160
11608
  version: 1,
11161
11609
  state: "pending",
@@ -11175,7 +11623,7 @@ async function networkApplicationAction(opts) {
11175
11623
  recordId: record4.id,
11176
11624
  kind: "note",
11177
11625
  authorId: `network:${sourceId}`,
11178
- createdAt: at,
11626
+ createdAt: at2,
11179
11627
  body: `Requested ${input.expectedStage} \u2192 ${input.stage} from ${sourceId}`,
11180
11628
  meta
11181
11629
  } },
@@ -11184,7 +11632,7 @@ async function networkApplicationAction(opts) {
11184
11632
  recordId: record4.id,
11185
11633
  kind: "note",
11186
11634
  authorId: `network:${sourceId}`,
11187
- createdAt: at,
11635
+ createdAt: at2,
11188
11636
  body: "Application operation reconciliation",
11189
11637
  meta: { state: "pending", activityId }
11190
11638
  } }
@@ -12035,9 +12483,9 @@ function normalizeNetworkIdentity(kind, value) {
12035
12483
  return trimmed;
12036
12484
  }
12037
12485
  function emailLooksReal(value) {
12038
- const at = value.lastIndexOf("@");
12039
- if (at <= 0 || at === value.length - 1) return false;
12040
- const domain = value.slice(at + 1).toLowerCase();
12486
+ const at2 = value.lastIndexOf("@");
12487
+ if (at2 <= 0 || at2 === value.length - 1) return false;
12488
+ const domain = value.slice(at2 + 1).toLowerCase();
12041
12489
  return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
12042
12490
  }
12043
12491
  function validateNetworkPersonImport(input, allowedChapterIds) {
@@ -12825,7 +13273,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
12825
13273
  }
12826
13274
 
12827
13275
  // src/worker-routes-network-person-registry.ts
12828
- var PATH5 = "/api/admin/network-person-registry";
13276
+ var PATH6 = "/api/admin/network-person-registry";
12829
13277
  var MAX_BODY2 = 8 * 1024 * 1024;
12830
13278
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
12831
13279
  var positiveRevision = (value) => {
@@ -12850,7 +13298,7 @@ function registryError(error) {
12850
13298
  return null;
12851
13299
  }
12852
13300
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12853
- if (url.pathname !== PATH5) return null;
13301
+ if (url.pathname !== PATH6) return null;
12854
13302
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
12855
13303
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
12856
13304
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -13596,7 +14044,7 @@ var handleFormation = async (req, url, env, ctx) => {
13596
14044
  import { verifyFederatedRequest as verifyFederatedRequest9 } from "@odla-ai/db";
13597
14045
 
13598
14046
  // src/builder-library-validation.ts
13599
- var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14047
+ var ID5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13600
14048
  var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
13601
14049
  var MAX_RESOURCES = 200;
13602
14050
  var MAX_MARKDOWN3 = 65536;
@@ -13657,7 +14105,7 @@ function resourceValue(value, path, errors) {
13657
14105
  }
13658
14106
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
13659
14107
  const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
13660
- if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
14108
+ if (id6 && !ID5.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
13661
14109
  const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
13662
14110
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
13663
14111
  const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
@@ -14079,7 +14527,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
14079
14527
  };
14080
14528
 
14081
14529
  // src/membership-cancel-authority.ts
14082
- import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
14530
+ import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
14083
14531
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
14084
14532
  membershipClock(now);
14085
14533
  const id6 = await networkMembershipId(target);
@@ -14090,14 +14538,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
14090
14538
  membershipFail("membership_cancellation_unavailable");
14091
14539
  }
14092
14540
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
14093
- const read2 = await stripeRequest9({ secretKey: stripeSecret }, "GET", path);
14541
+ const read2 = await stripeRequest10({ secretKey: stripeSecret }, "GET", path);
14094
14542
  const sub = read2.body;
14095
14543
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
14096
14544
  ...stripePartitionMetadata(target),
14097
14545
  applicationId: target.applicationId
14098
14546
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
14099
14547
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
14100
- const write = await stripeRequest9(
14548
+ const write = await stripeRequest10(
14101
14549
  { secretKey: stripeSecret },
14102
14550
  "POST",
14103
14551
  path,
@@ -14319,7 +14767,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
14319
14767
  }
14320
14768
 
14321
14769
  // src/membership-provider-lifecycle.ts
14322
- import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
14770
+ import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
14323
14771
 
14324
14772
  // src/membership-renewal-authority.ts
14325
14773
  async function reconcileMembershipRenewal(db, input, now) {
@@ -14356,7 +14804,9 @@ async function reconcileMembershipRenewal(db, input, now) {
14356
14804
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
14357
14805
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
14358
14806
  if (seat) requireMembershipScope(seat, input);
14359
- const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
14807
+ const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
14808
+ const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
14809
+ const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
14360
14810
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
14361
14811
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
14362
14812
  if (!Number.isSafeInteger(amount) || amount <= 0 || fact.invoiceTotalCents !== amount || JSON.stringify(lines.map((line) => line.priceId).sort()) !== JSON.stringify(prices)) membershipFail("membership_renewal_amount_mismatch");
@@ -14396,19 +14846,19 @@ async function reconcileMembershipRenewal(db, input, now) {
14396
14846
  evidenceGuard
14397
14847
  ];
14398
14848
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
14399
- if (billedSeat?.entitlementId) {
14400
- const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(billedSeat.entitlementId));
14849
+ if (carriedSeat?.entitlementId) {
14850
+ const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
14401
14851
  requireMembershipScope(seatEntitlement, input);
14402
14852
  guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
14403
14853
  ops.push(
14404
- membershipUpdate("networkSeat", String(billedSeat.id), {
14854
+ membershipUpdate("networkSeat", String(carriedSeat.id), {
14405
14855
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
14406
- revision: Number(billedSeat.revision) + 1,
14856
+ revision: Number(carriedSeat.revision) + 1,
14407
14857
  updatedAt: now
14408
14858
  }),
14409
14859
  membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
14410
14860
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
14411
- status: billedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
14861
+ status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
14412
14862
  revision: Number(seatEntitlement.revision) + 1,
14413
14863
  updatedAt: now
14414
14864
  })
@@ -14481,7 +14931,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
14481
14931
  }
14482
14932
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
14483
14933
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
14484
- const result = await stripeRequest10({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
14934
+ const result = await stripeRequest11({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
14485
14935
  const sub = result.body;
14486
14936
  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)) {
14487
14937
  membershipFail("membership_billing_provider_mismatch");
@@ -14518,86 +14968,8 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
14518
14968
  return membershipFail("membership_billing_retry_required");
14519
14969
  }
14520
14970
 
14521
- // src/membership-seat-reservation.ts
14522
- async function recipientKey(input, email) {
14523
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
14524
- return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
14525
- }
14526
- async function reserveNamedMembershipSeat(db, input, now) {
14527
- membershipClock(now);
14528
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
14529
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
14530
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
14531
- if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
14532
- const state = await loadMembershipAuthorityState(db, input);
14533
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
14534
- requireMembershipScope(member, input);
14535
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
14536
- const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
14537
- let seat = await membershipLoad(db, "networkSeat", id6);
14538
- if (!seat) {
14539
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
14540
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
14541
- if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
14542
- membershipFail("named_seat_purchase_unavailable");
14543
- }
14544
- const attrs = {
14545
- id: id6,
14546
- ...membershipScope(input),
14547
- primaryMembershipId: memberId,
14548
- recipientName: name,
14549
- recipientEmailNormalized: email,
14550
- purchaserUserId: input.purchaserUserId,
14551
- recipientClaimId: claimId,
14552
- priceId: input.priceId,
14553
- primaryPriceId: member.providerPriceId,
14554
- amountCents: offering.seatPriceCents,
14555
- currency: offering.currency,
14556
- interval: offering.interval,
14557
- providerCustomerId: member.providerCustomerId,
14558
- providerSubscriptionId: member.providerSubscriptionId,
14559
- status: "payment_pending",
14560
- preparedAt: now,
14561
- termEndsAt: member.termEndsAt,
14562
- seatPurchaseEndsAt: purchaseEndsAt,
14563
- offeringSnapshot: offering,
14564
- revision: 1,
14565
- createdAt: now,
14566
- updatedAt: now
14567
- };
14568
- try {
14569
- await db.transact([
14570
- membershipUpdate("networkSeat", id6, attrs),
14571
- membershipUpdate("networkSeatRecipient", claimId, {
14572
- id: claimId,
14573
- appId: input.appId,
14574
- chapterId: input.chapterId,
14575
- environment: input.environment,
14576
- runtime: input.runtime,
14577
- seatId: id6,
14578
- recipientEmailNormalized: email,
14579
- createdAt: now
14580
- }),
14581
- membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
14582
- ], { mutationId: `named-seat-reserve:${id6}`, guards: [
14583
- { ns: "networkSeat", id: id6, exists: false },
14584
- { ns: "networkSeatRecipient", id: claimId, exists: false },
14585
- { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
14586
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
14587
- ] });
14588
- } catch (error) {
14589
- if (error.code !== "transact_guard_failed") throw error;
14590
- }
14591
- seat = await membershipLoad(db, "networkSeat", id6);
14592
- if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
14593
- }
14594
- requireMembershipScope(seat, input);
14595
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
14596
- return seat;
14597
- }
14598
-
14599
14971
  // src/membership-seat-stripe.ts
14600
- import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
14972
+ import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
14601
14973
  var reject7 = () => {
14602
14974
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
14603
14975
  };
@@ -14609,7 +14981,7 @@ var metadataFor = (seat) => ({
14609
14981
  });
14610
14982
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
14611
14983
  async function read(sk, path, params) {
14612
- const response = await stripeRequest11({ secretKey: sk }, "GET", path, params);
14984
+ const response = await stripeRequest12({ secretKey: sk }, "GET", path, params);
14613
14985
  if (!response.ok) return reject7();
14614
14986
  return response.body;
14615
14987
  }
@@ -14632,9 +15004,9 @@ async function subscriptionForSeat(sk, seat) {
14632
15004
  if (!Array.isArray(discounts) || discounts.length > 5) reject7();
14633
15005
  for (const discount of discounts) {
14634
15006
  const couponRef = discount.source?.coupon ?? discount.coupon;
14635
- const couponId2 = typeof couponRef === "string" ? couponRef : couponRef?.id;
14636
- if (!couponId2 || typeof couponId2 !== "string") reject7();
14637
- const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId2)}`, { "expand[0]": "applies_to" });
15007
+ const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
15008
+ if (!couponId || typeof couponId !== "string") reject7();
15009
+ const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
14638
15010
  if (coupon.livemode !== (seat.environment === "prod") || !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== primary.price.product) reject7();
14639
15011
  }
14640
15012
  return sub;
@@ -14646,11 +15018,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
14646
15018
  }
14647
15019
  await subscriptionForSeat(sk, seat);
14648
15020
  const metadata = metadataFor(seat);
14649
- const result = seat.providerPaymentIntentId ? await stripeRequest11(
15021
+ const result = seat.providerPaymentIntentId ? await stripeRequest12(
14650
15022
  { secretKey: sk },
14651
15023
  "GET",
14652
15024
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
14653
- ) : await stripeRequest11({ secretKey: sk }, "POST", "/v1/payment_intents", {
15025
+ ) : await stripeRequest12({ secretKey: sk }, "POST", "/v1/payment_intents", {
14654
15026
  amount: seat.amountCents,
14655
15027
  currency: seat.currency,
14656
15028
  customer: seat.providerCustomerId,
@@ -14668,7 +15040,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
14668
15040
  const sub = await subscriptionForSeat(sk, seat);
14669
15041
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
14670
15042
  if (existing) return String(existing.id);
14671
- const result = await stripeRequest11({ secretKey: sk }, "POST", "/v1/subscription_items", {
15043
+ const result = await stripeRequest12({ secretKey: sk }, "POST", "/v1/subscription_items", {
14672
15044
  subscription: seat.providerSubscriptionId,
14673
15045
  price: seat.priceId,
14674
15046
  quantity: 1,
@@ -14683,6 +15055,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
14683
15055
  // src/membership-seat-checkout.ts
14684
15056
  async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
14685
15057
  let seat = await reserveNamedMembershipSeat(db, input, now);
15058
+ if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
14686
15059
  const state = await loadMembershipAuthorityState(db, input);
14687
15060
  const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
14688
15061
  requireMembershipScope(member, input);
@@ -14889,7 +15262,7 @@ async function recordRecipient(db, input, now, approve) {
14889
15262
  requireMembershipScope(member, input);
14890
15263
  const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
14891
15264
  requireMembershipScope(entitlement, input);
14892
- if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !seat.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
15265
+ if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !namedSeatSettled(seat)) membershipFail("named_seat_acceptance_unavailable");
14893
15266
  if (seat.acceptedAt === void 0) {
14894
15267
  if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
14895
15268
  membershipFail("named_seat_recipient_verification_required");
@@ -15076,6 +15449,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
15076
15449
  };
15077
15450
  if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
15078
15451
  if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
15452
+ const checkout = {
15453
+ ...target,
15454
+ priceId: peer.seatPriceId,
15455
+ recipientName: String(input.recipientName),
15456
+ recipientEmail: String(input.recipientEmail),
15457
+ purchaserEmail: String(input.purchaserEmail),
15458
+ purchaserUserId: String(input.purchaserUserId)
15459
+ };
15460
+ if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
15079
15461
  const secret = await getVaultSecret(db, stripeSecretName);
15080
15462
  if (!secret) throw new Error("network Stripe provider is unavailable");
15081
15463
  if (action === "seat-decline") {
@@ -15091,14 +15473,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
15091
15473
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
15092
15474
  }
15093
15475
  if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
15094
- if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
15095
- ...target,
15096
- priceId: peer.seatPriceId,
15097
- recipientName: String(input.recipientName),
15098
- recipientEmail: String(input.recipientEmail),
15099
- purchaserEmail: String(input.purchaserEmail),
15100
- purchaserUserId: String(input.purchaserUserId)
15101
- }, secret, now);
15476
+ if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
15102
15477
  const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
15103
15478
  if (action === "seat-payments") {
15104
15479
  const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
@@ -15120,7 +15495,7 @@ var respond = (body, status2 = 200) => new Response(JSON.stringify(body), {
15120
15495
  status: status2,
15121
15496
  headers: { "content-type": "application/json", "cache-control": "no-store" }
15122
15497
  });
15123
- var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
15498
+ var SAFE2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
15124
15499
  var SAFE_ERROR = /* @__PURE__ */ new Set([
15125
15500
  "offer_identity_conflict",
15126
15501
  "membership_authority_unavailable",
@@ -15168,7 +15543,7 @@ function createMembershipAuthorityHandler(options) {
15168
15543
  if (options.stripeSecretName !== void 0 && !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(options.stripeSecretName)) throw new TypeError("invalid Stripe secret slot");
15169
15544
  const identities = /* @__PURE__ */ new Set();
15170
15545
  for (const peer of peers) {
15171
- if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
15546
+ if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE2.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
15172
15547
  const identity = `${peer.appId}:${peer.runtime}`;
15173
15548
  if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
15174
15549
  identities.add(identity);
@@ -15202,7 +15577,7 @@ function createMembershipAuthorityHandler(options) {
15202
15577
  try {
15203
15578
  input = JSON.parse(verified.body);
15204
15579
  const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
15205
- if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
15580
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE2.test(input[key3])))) {
15206
15581
  throw new TypeError("invalid request");
15207
15582
  }
15208
15583
  } catch {
@@ -15539,7 +15914,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
15539
15914
  }
15540
15915
 
15541
15916
  // src/membership-seat-cancel.ts
15542
- import { initStripe as initStripe3, stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
15917
+ import { initStripe as initStripe3, stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
15543
15918
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
15544
15919
  membershipClock(now);
15545
15920
  const state = await loadMembershipAuthorityState(db, target);
@@ -15567,7 +15942,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
15567
15942
  }
15568
15943
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
15569
15944
  const request = async (method, path, params) => {
15570
- const result = await stripeRequest12(
15945
+ const result = await stripeRequest13(
15571
15946
  { secretKey },
15572
15947
  method,
15573
15948
  path,
@@ -15954,13 +16329,13 @@ async function requestNamedSeatDecline(options) {
15954
16329
  verifiedRecipientEmail: options.verifiedRecipientEmail
15955
16330
  });
15956
16331
  const seat = result.seat;
15957
- if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
16332
+ if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents < 0) throw new Error("invalid named seat refund receipt");
15958
16333
  return seat;
15959
16334
  }
15960
16335
  async function requestNamedSeatOperatorRefund(options) {
15961
16336
  const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
15962
16337
  const seat = result.seat;
15963
- if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
16338
+ if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents < 0) throw new Error("invalid named seat refund receipt");
15964
16339
  return seat;
15965
16340
  }
15966
16341
  async function requestNamedSeatReservation(options) {
@@ -15974,13 +16349,13 @@ async function requestNamedSeatReservation(options) {
15974
16349
  });
15975
16350
  const seat = result.seat;
15976
16351
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
15977
- if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents <= 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
16352
+ if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
15978
16353
  return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
15979
16354
  }
15980
16355
  async function requestNamedSeatOffer(options) {
15981
16356
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
15982
16357
  const offer = result.seat;
15983
- if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
16358
+ if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents < 0 || offer.included !== (offer.amountCents === 0) || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
15984
16359
  return offer;
15985
16360
  }
15986
16361
  async function requestNamedSeatInvitation(options) {
@@ -16016,7 +16391,8 @@ async function requestNamedSeatCheckout(options) {
16016
16391
  });
16017
16392
  const seat = result.seat;
16018
16393
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
16019
- if (!seat || seat.seatId !== expected || !/^pi_[A-Za-z0-9_]+$/.test(seat.paymentIntentId) || typeof seat.clientSecret !== "string" || !Number.isSafeInteger(seat.amountCents) || seat.amountCents <= 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) {
16394
+ const included = seat?.amountCents === 0;
16395
+ if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || (included ? seat.included !== true || seat.paymentIntentId !== void 0 || seat.clientSecret !== void 0 || !["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(seat.status) : seat.included !== void 0 || !/^pi_[A-Za-z0-9_]+$/.test(String(seat.paymentIntentId)) || typeof seat.clientSecret !== "string" || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status))) {
16020
16396
  throw new Error("invalid named seat checkout receipt");
16021
16397
  }
16022
16398
  return seat;
@@ -16426,6 +16802,7 @@ var BUILTIN_ROUTES = [
16426
16802
  handleAdminNamedSeat,
16427
16803
  handleAdminMembershipSeats,
16428
16804
  handleAdminMembershipPolicyVersions,
16805
+ handleAdminMembershipReterm,
16429
16806
  handleAdminMeetings,
16430
16807
  handleAdminScheduling,
16431
16808
  // Roster + identity
@@ -16506,9 +16883,15 @@ export {
16506
16883
  deliverMembershipChapterEffect,
16507
16884
  deliverSignupControl,
16508
16885
  drainMembershipEffects,
16886
+ grantIncludedNamedSeat,
16887
+ lowerSeatPrice,
16888
+ namedSeatIncluded,
16889
+ namedSeatRecipientState,
16890
+ readNamedSeatOffer,
16509
16891
  refundNamedSeatByOperator,
16510
16892
  renderActiveSignupControlHtml,
16511
16893
  resolveNetworkTargetTransport,
16894
+ retermMembershipDiscount,
16512
16895
  retryChapterApprovalEffects,
16513
16896
  retryLeaderAdmissionDeliveries,
16514
16897
  withActiveSignupControlTheme