@odla-ai/chapter 0.53.0 → 0.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,12 @@ __export(worker_exports, {
36
36
  deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
37
37
  deliverSignupControl: () => deliverSignupControl,
38
38
  drainMembershipEffects: () => drainMembershipEffects,
39
+ grantIncludedNamedSeat: () => grantIncludedNamedSeat,
40
+ liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
41
+ lowerSeatPrice: () => lowerSeatPrice,
42
+ namedSeatIncluded: () => namedSeatIncluded,
43
+ namedSeatRecipientState: () => namedSeatRecipientState,
44
+ readNamedSeatOffer: () => readNamedSeatOffer,
39
45
  refundNamedSeatByOperator: () => refundNamedSeatByOperator,
40
46
  renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
41
47
  resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
@@ -1803,8 +1809,8 @@ var object = (value) => {
1803
1809
  var id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
1804
1810
  var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
1805
1811
  var time = (value) => {
1806
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
1807
- return integer2(at, 0, 864e13);
1812
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
1813
+ return integer2(at2, 0, 864e13);
1808
1814
  };
1809
1815
  var ids = (value, allowEmpty = false) => {
1810
1816
  if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
@@ -1848,7 +1854,7 @@ function membershipOfferingVersion(value) {
1848
1854
  term: normalizedTerm,
1849
1855
  autoRenew: true,
1850
1856
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1851
- seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1857
+ seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
1852
1858
  ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1853
1859
  requiredDisclosures: ids(input.requiredDisclosures)
1854
1860
  };
@@ -2936,7 +2942,7 @@ async function checkoutVariables(db, group, app) {
2936
2942
  const frozen = await frozenCheckoutVariables(db, app);
2937
2943
  return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
2938
2944
  }
2939
- function formatInstant(at, timeZone) {
2945
+ function formatInstant(at2, timeZone) {
2940
2946
  return new Intl.DateTimeFormat("en-US", {
2941
2947
  weekday: "long",
2942
2948
  month: "long",
@@ -2946,7 +2952,7 @@ function formatInstant(at, timeZone) {
2946
2952
  minute: "2-digit",
2947
2953
  timeZoneName: "short",
2948
2954
  timeZone
2949
- }).format(new Date(at));
2955
+ }).format(new Date(at2));
2950
2956
  }
2951
2957
  var adminUrlFor = (origin) => `${origin}/admin/`;
2952
2958
  var membersUrlFor = (origin) => `${origin}/members/`;
@@ -3894,6 +3900,181 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
3894
3900
  return { refundedCents, refundIds };
3895
3901
  }
3896
3902
 
3903
+ // src/membership-seat-reservation.ts
3904
+ async function recipientKey(input, email) {
3905
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
3906
+ return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
3907
+ }
3908
+ async function reserveNamedMembershipSeat(db, input, now) {
3909
+ membershipClock(now);
3910
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
3911
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
3912
+ const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
3913
+ 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");
3914
+ const state = await loadMembershipAuthorityState(db, input);
3915
+ const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
3916
+ requireMembershipScope(member, input);
3917
+ if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
3918
+ const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
3919
+ let seat = await membershipLoad(db, "networkSeat", id6);
3920
+ if (!seat) {
3921
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
3922
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
3923
+ 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) {
3924
+ membershipFail("named_seat_purchase_unavailable");
3925
+ }
3926
+ const attrs = {
3927
+ id: id6,
3928
+ ...membershipScope(input),
3929
+ primaryMembershipId: memberId,
3930
+ recipientName: name,
3931
+ recipientEmailNormalized: email,
3932
+ purchaserUserId: input.purchaserUserId,
3933
+ recipientClaimId: claimId,
3934
+ priceId: input.priceId,
3935
+ primaryPriceId: member.providerPriceId,
3936
+ amountCents: offering.seatPriceCents,
3937
+ currency: offering.currency,
3938
+ interval: offering.interval,
3939
+ providerCustomerId: member.providerCustomerId,
3940
+ providerSubscriptionId: member.providerSubscriptionId,
3941
+ status: "payment_pending",
3942
+ preparedAt: now,
3943
+ termEndsAt: member.termEndsAt,
3944
+ seatPurchaseEndsAt: purchaseEndsAt,
3945
+ offeringSnapshot: offering,
3946
+ revision: 1,
3947
+ createdAt: now,
3948
+ updatedAt: now
3949
+ };
3950
+ try {
3951
+ await db.transact([
3952
+ membershipUpdate("networkSeat", id6, attrs),
3953
+ membershipUpdate("networkSeatRecipient", claimId, {
3954
+ id: claimId,
3955
+ appId: input.appId,
3956
+ chapterId: input.chapterId,
3957
+ environment: input.environment,
3958
+ runtime: input.runtime,
3959
+ seatId: id6,
3960
+ recipientEmailNormalized: email,
3961
+ createdAt: now
3962
+ }),
3963
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
3964
+ ], { mutationId: `named-seat-reserve:${id6}`, guards: [
3965
+ { ns: "networkSeat", id: id6, exists: false },
3966
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
3967
+ { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
3968
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
3969
+ ] });
3970
+ } catch (error) {
3971
+ if (error.code !== "transact_guard_failed") throw error;
3972
+ }
3973
+ seat = await membershipLoad(db, "networkSeat", id6);
3974
+ if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
3975
+ }
3976
+ requireMembershipScope(seat, input);
3977
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
3978
+ return seat;
3979
+ }
3980
+
3981
+ // src/membership-seat-included.ts
3982
+ var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
3983
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
3984
+ function namedSeatRecipientState(seat) {
3985
+ switch (String(seat.status)) {
3986
+ case "renewal_pending":
3987
+ case "pending_acceptance":
3988
+ return "invited";
3989
+ case "pending_approval":
3990
+ case "pending_primary":
3991
+ return "accepted";
3992
+ case "active":
3993
+ return "joined";
3994
+ case "refund_pending":
3995
+ case "refunded":
3996
+ return seat.refundReason === "declined" ? "declined" : "refunded";
3997
+ case "canceled":
3998
+ return "canceled";
3999
+ default:
4000
+ return "payment_pending";
4001
+ }
4002
+ }
4003
+ var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4004
+ async function grantIncludedNamedSeat(db, input, now) {
4005
+ membershipClock(now);
4006
+ const seat = await reserveNamedMembershipSeat(db, input, now);
4007
+ if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4008
+ const state = await loadMembershipAuthorityState(db, input);
4009
+ const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4010
+ requireMembershipScope(member, input);
4011
+ 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)) {
4012
+ membershipFail("named_seat_purchase_unavailable");
4013
+ }
4014
+ if (seat.status === "payment_pending") {
4015
+ const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
4016
+ try {
4017
+ await db.transact([
4018
+ membershipUpdate("networkSeat", seat.id, {
4019
+ status: "pending_acceptance",
4020
+ purchasedAt: now,
4021
+ termStartedAt: now,
4022
+ termEndsAt: member.termEndsAt,
4023
+ acceptanceEndsAt: Number(member.termEndsAt),
4024
+ entitlementId,
4025
+ revision: Number(seat.revision) + 1,
4026
+ updatedAt: now
4027
+ }),
4028
+ membershipUpdate("networkEntitlement", entitlementId, {
4029
+ id: entitlementId,
4030
+ ...membershipScope(input),
4031
+ membershipId: member.id,
4032
+ seatId: seat.id,
4033
+ kind: "named_seat",
4034
+ status: "pending_acceptance",
4035
+ termStartedAt: now,
4036
+ termEndsAt: member.termEndsAt,
4037
+ offeringSnapshot: member.offeringSnapshot,
4038
+ sourceEventId: source,
4039
+ revision: 1,
4040
+ createdAt: now,
4041
+ updatedAt: now
4042
+ }),
4043
+ membershipUpdate("membershipEffect", inviteId, {
4044
+ id: inviteId,
4045
+ chapterId: input.chapterId,
4046
+ environment: input.environment,
4047
+ runtime: input.runtime,
4048
+ membershipId: member.id,
4049
+ seatId: seat.id,
4050
+ kind: "invite_named_recipient",
4051
+ status: "pending",
4052
+ attempts: 0,
4053
+ createdAt: now,
4054
+ updatedAt: now
4055
+ })
4056
+ ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4057
+ {
4058
+ ns: "networkSeat",
4059
+ id: seat.id,
4060
+ exists: true,
4061
+ equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4062
+ absent: ["purchasedAt", "providerPaymentIntentId"]
4063
+ },
4064
+ { ns: "networkEntitlement", id: entitlementId, exists: false },
4065
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4066
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4067
+ ] });
4068
+ } catch (error) {
4069
+ if (error.code !== "transact_guard_failed") throw error;
4070
+ }
4071
+ }
4072
+ const current = await membershipLoad(db, "networkSeat", seat.id);
4073
+ requireMembershipScope(current, input);
4074
+ if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4075
+ return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4076
+ }
4077
+
3897
4078
  // src/membership-seat-refund.ts
3898
4079
  async function requestNamedSeatRefund(db, input, now) {
3899
4080
  membershipClock(now);
@@ -3903,7 +4084,7 @@ async function requestNamedSeatRefund(db, input, now) {
3903
4084
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
3904
4085
  requireMembershipScope(member, input);
3905
4086
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
3906
- 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");
4087
+ 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");
3907
4088
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
3908
4089
  await db.transact([
3909
4090
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -3948,7 +4129,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
3948
4129
  await loadMembershipAuthorityState(db, seat);
3949
4130
  if (seat.status === "refunded") return;
3950
4131
  if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
3951
- const result = await refundNamedSeatStripePayment(secretKey, seat);
4132
+ const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
3952
4133
  await db.transact([
3953
4134
  membershipUpdate("networkSeat", seatId, {
3954
4135
  status: "refunded",
@@ -3970,7 +4151,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3970
4151
  await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
3971
4152
  await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
3972
4153
  const refunded = await membershipLoad(db, "networkSeat", input.seatId);
3973
- if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
4154
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
3974
4155
  throw new Error("named seat refund unconfirmed");
3975
4156
  }
3976
4157
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
@@ -4002,9 +4183,56 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
4002
4183
  return { lifted, more: rows5.length > limit };
4003
4184
  }
4004
4185
 
4186
+ // src/membership-seat-reprice.ts
4187
+ async function lowerSeatPrice(db, scope2, now, limit = 100) {
4188
+ membershipClock(now);
4189
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
4190
+ const state = await loadMembershipAuthorityState(db, scope2);
4191
+ const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
4192
+ if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
4193
+ 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];
4194
+ if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
4195
+ const target = current.seatPriceCents;
4196
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4197
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
4198
+ for (const member of rows5.slice(0, limit)) {
4199
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
4200
+ const id6 = String(member.id), skip = (reason) => {
4201
+ receipt.skipped.push({ membershipId: id6, reason });
4202
+ };
4203
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
4204
+ if (offering.seatPriceCents <= target) {
4205
+ skip("seat price already at or below the current offering");
4206
+ continue;
4207
+ }
4208
+ if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
4209
+ skip(`membership is ${String(member.status)}`);
4210
+ continue;
4211
+ }
4212
+ const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4213
+ if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
4214
+ skip(`seat is ${String(seat.status)}`);
4215
+ continue;
4216
+ }
4217
+ const ops = [membershipUpdate("networkMembership", id6, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
4218
+ const guards = [
4219
+ { ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision } },
4220
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4221
+ ];
4222
+ if (seat) {
4223
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
4224
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
4225
+ }
4226
+ await db.transact(ops, { mutationId: `seat-reprice:${id6}:${member.revision}`, guards });
4227
+ receipt.repriced++;
4228
+ }
4229
+ return receipt;
4230
+ }
4231
+
4005
4232
  // src/worker-routes-admin-membership-seats.ts
4006
4233
  var LIST = "/api/admin/membership-seats";
4007
4234
  var LIFT = "/api/admin/membership-seats/lift-purchase-window";
4235
+ var REPRICE = "/api/admin/membership-seats/reprice";
4008
4236
  var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
4009
4237
  var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
4010
4238
  var PUBLIC = [
@@ -4031,11 +4259,11 @@ var PUBLIC = [
4031
4259
  ];
4032
4260
  var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
4033
4261
  var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4034
- const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
4035
- if (url.pathname !== LIST && !one && !lift) return null;
4262
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
4263
+ if (url.pathname !== LIST && !one && !lift && !reprice) return null;
4036
4264
  const operator = ctx.membershipAuthorityOperator;
4037
4265
  if (!operator) return json({ error: "not found" }, 404);
4038
- if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4266
+ if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4039
4267
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4040
4268
  if (!user) return json({ error: "unauthorized" }, 401);
4041
4269
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
@@ -4045,7 +4273,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4045
4273
  } catch {
4046
4274
  return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4047
4275
  }
4048
- if (lift) {
4276
+ if (lift || reprice) {
4049
4277
  let body;
4050
4278
  try {
4051
4279
  body = await req.json();
@@ -4053,14 +4281,16 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4053
4281
  return json({ error: "chapterId and runtime are required" }, 400);
4054
4282
  }
4055
4283
  const appId = body.appId ?? body.chapterId;
4056
- 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)) {
4057
- return json({ error: "chapterId and runtime are required" }, 400);
4284
+ 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))) {
4285
+ return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
4058
4286
  }
4287
+ const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
4059
4288
  try {
4060
- return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
4289
+ if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
4290
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
4061
4291
  } catch (error) {
4062
4292
  const code = error.code;
4063
- return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4293
+ return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
4064
4294
  }
4065
4295
  }
4066
4296
  if (!one) {
@@ -4096,8 +4326,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4096
4326
  var LIMIT = 200;
4097
4327
  var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4098
4328
  var instant = (value) => {
4099
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4100
- return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
4329
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4330
+ return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4101
4331
  };
4102
4332
  function provenance(row2) {
4103
4333
  const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
@@ -6470,7 +6700,7 @@ var handleMe = async (req, url, env, ctx) => {
6470
6700
  } catch {
6471
6701
  }
6472
6702
  }
6473
- return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(memberAccess && ctx.membershipAuthority?.namedSeats && member?.source.stripeSubscriptionId && member.source.status !== "submitted") });
6703
+ return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
6474
6704
  };
6475
6705
  var handleCrm = async (req, url, env, ctx) => {
6476
6706
  const crmBase = ctx.crmBase;
@@ -9468,6 +9698,7 @@ async function namedSeatOfferDigest(offer) {
9468
9698
  ])));
9469
9699
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
9470
9700
  }
9701
+ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
9471
9702
  async function readNamedSeatOffer(db, input, now) {
9472
9703
  membershipClock(now);
9473
9704
  await loadMembershipAuthorityState(db, input);
@@ -9490,10 +9721,19 @@ async function readNamedSeatOffer(db, input, now) {
9490
9721
  seatId: String(seat.id),
9491
9722
  status: String(seat.status),
9492
9723
  recipientName: String(seat.recipientName),
9493
- recipientEmail: String(seat.recipientEmailNormalized)
9724
+ recipientEmail: String(seat.recipientEmailNormalized),
9725
+ recipientState: namedSeatRecipientState(seat),
9726
+ ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
9727
+ ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
9728
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
9494
9729
  } } : {}
9495
9730
  };
9496
- return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
9731
+ return {
9732
+ ...offer,
9733
+ digest: await namedSeatOfferDigest(offer),
9734
+ included: offering.seatPriceCents === 0,
9735
+ purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
9736
+ };
9497
9737
  }
9498
9738
 
9499
9739
  // src/named-seat-request.ts
@@ -9569,8 +9809,9 @@ var handleNamedSeat = async (req, url, env, ctx) => {
9569
9809
  if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || recipientEmail === normalizeChapterEmail(user.email)) {
9570
9810
  return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
9571
9811
  }
9572
- if (!offer.eligible || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
9573
- if (!group?.stripePublishableKey || !group.refundPolicyText || !group.merchantDisclosureText) return json({ error: "seat checkout disclosures unavailable" }, 503);
9812
+ 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);
9813
+ if (!offer.eligible && !granted || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
9814
+ if (!group?.refundPolicyText || !group.merchantDisclosureText || !offer.included && !group.stripePublishableKey) return json({ error: "seat checkout disclosures unavailable" }, 503);
9574
9815
  const consentId = `${offer.membershipId}:named-seat-consent`;
9575
9816
  const prior = await membershipLoad(db, "namedSeatConsents", consentId);
9576
9817
  const attrs = {
@@ -9609,7 +9850,7 @@ var handleNamedSeat = async (req, url, env, ctx) => {
9609
9850
  }
9610
9851
  return json({
9611
9852
  ...receipt,
9612
- publishableKey: String(group.stripePublishableKey),
9853
+ ...group.stripePublishableKey ? { publishableKey: String(group.stripePublishableKey) } : {},
9613
9854
  lineItems: { standardCents: offer.amountCents, discountCents: 0, dueTodayCents: offer.amountCents }
9614
9855
  });
9615
9856
  } catch {
@@ -11406,7 +11647,7 @@ async function networkApplicationAction(opts) {
11406
11647
  if (app.status !== input.expectedStage || !applicationActionStages(app, env, ctx).includes(input.stage)) {
11407
11648
  return json({ error: "application action is no longer available; reload", code: "invalid_application_action" }, 409);
11408
11649
  }
11409
- const at = Date.now();
11650
+ const at2 = Date.now();
11410
11651
  const meta = { [ACTION_META]: {
11411
11652
  version: 1,
11412
11653
  state: "pending",
@@ -11426,7 +11667,7 @@ async function networkApplicationAction(opts) {
11426
11667
  recordId: record4.id,
11427
11668
  kind: "note",
11428
11669
  authorId: `network:${sourceId}`,
11429
- createdAt: at,
11670
+ createdAt: at2,
11430
11671
  body: `Requested ${input.expectedStage} \u2192 ${input.stage} from ${sourceId}`,
11431
11672
  meta
11432
11673
  } },
@@ -11435,7 +11676,7 @@ async function networkApplicationAction(opts) {
11435
11676
  recordId: record4.id,
11436
11677
  kind: "note",
11437
11678
  authorId: `network:${sourceId}`,
11438
- createdAt: at,
11679
+ createdAt: at2,
11439
11680
  body: "Application operation reconciliation",
11440
11681
  meta: { state: "pending", activityId }
11441
11682
  } }
@@ -12282,9 +12523,9 @@ function normalizeNetworkIdentity(kind, value) {
12282
12523
  return trimmed;
12283
12524
  }
12284
12525
  function emailLooksReal(value) {
12285
- const at = value.lastIndexOf("@");
12286
- if (at <= 0 || at === value.length - 1) return false;
12287
- const domain = value.slice(at + 1).toLowerCase();
12526
+ const at2 = value.lastIndexOf("@");
12527
+ if (at2 <= 0 || at2 === value.length - 1) return false;
12528
+ const domain = value.slice(at2 + 1).toLowerCase();
12288
12529
  return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
12289
12530
  }
12290
12531
  function validateNetworkPersonImport(input, allowedChapterIds) {
@@ -14603,7 +14844,9 @@ async function reconcileMembershipRenewal(db, input, now) {
14603
14844
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
14604
14845
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
14605
14846
  if (seat) requireMembershipScope(seat, input);
14606
- const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
14847
+ const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
14848
+ const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
14849
+ const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
14607
14850
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
14608
14851
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
14609
14852
  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");
@@ -14643,19 +14886,19 @@ async function reconcileMembershipRenewal(db, input, now) {
14643
14886
  evidenceGuard
14644
14887
  ];
14645
14888
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
14646
- if (billedSeat?.entitlementId) {
14647
- const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(billedSeat.entitlementId));
14889
+ if (carriedSeat?.entitlementId) {
14890
+ const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
14648
14891
  requireMembershipScope(seatEntitlement, input);
14649
14892
  guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
14650
14893
  ops.push(
14651
- membershipUpdate("networkSeat", String(billedSeat.id), {
14894
+ membershipUpdate("networkSeat", String(carriedSeat.id), {
14652
14895
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
14653
- revision: Number(billedSeat.revision) + 1,
14896
+ revision: Number(carriedSeat.revision) + 1,
14654
14897
  updatedAt: now
14655
14898
  }),
14656
14899
  membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
14657
14900
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
14658
- status: billedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
14901
+ status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
14659
14902
  revision: Number(seatEntitlement.revision) + 1,
14660
14903
  updatedAt: now
14661
14904
  })
@@ -14765,84 +15008,6 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
14765
15008
  return membershipFail("membership_billing_retry_required");
14766
15009
  }
14767
15010
 
14768
- // src/membership-seat-reservation.ts
14769
- async function recipientKey(input, email) {
14770
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
14771
- return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
14772
- }
14773
- async function reserveNamedMembershipSeat(db, input, now) {
14774
- membershipClock(now);
14775
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
14776
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
14777
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
14778
- 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");
14779
- const state = await loadMembershipAuthorityState(db, input);
14780
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
14781
- requireMembershipScope(member, input);
14782
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
14783
- const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
14784
- let seat = await membershipLoad(db, "networkSeat", id6);
14785
- if (!seat) {
14786
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
14787
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
14788
- 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) {
14789
- membershipFail("named_seat_purchase_unavailable");
14790
- }
14791
- const attrs = {
14792
- id: id6,
14793
- ...membershipScope(input),
14794
- primaryMembershipId: memberId,
14795
- recipientName: name,
14796
- recipientEmailNormalized: email,
14797
- purchaserUserId: input.purchaserUserId,
14798
- recipientClaimId: claimId,
14799
- priceId: input.priceId,
14800
- primaryPriceId: member.providerPriceId,
14801
- amountCents: offering.seatPriceCents,
14802
- currency: offering.currency,
14803
- interval: offering.interval,
14804
- providerCustomerId: member.providerCustomerId,
14805
- providerSubscriptionId: member.providerSubscriptionId,
14806
- status: "payment_pending",
14807
- preparedAt: now,
14808
- termEndsAt: member.termEndsAt,
14809
- seatPurchaseEndsAt: purchaseEndsAt,
14810
- offeringSnapshot: offering,
14811
- revision: 1,
14812
- createdAt: now,
14813
- updatedAt: now
14814
- };
14815
- try {
14816
- await db.transact([
14817
- membershipUpdate("networkSeat", id6, attrs),
14818
- membershipUpdate("networkSeatRecipient", claimId, {
14819
- id: claimId,
14820
- appId: input.appId,
14821
- chapterId: input.chapterId,
14822
- environment: input.environment,
14823
- runtime: input.runtime,
14824
- seatId: id6,
14825
- recipientEmailNormalized: email,
14826
- createdAt: now
14827
- }),
14828
- membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
14829
- ], { mutationId: `named-seat-reserve:${id6}`, guards: [
14830
- { ns: "networkSeat", id: id6, exists: false },
14831
- { ns: "networkSeatRecipient", id: claimId, exists: false },
14832
- { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
14833
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
14834
- ] });
14835
- } catch (error) {
14836
- if (error.code !== "transact_guard_failed") throw error;
14837
- }
14838
- seat = await membershipLoad(db, "networkSeat", id6);
14839
- if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
14840
- }
14841
- requireMembershipScope(seat, input);
14842
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
14843
- return seat;
14844
- }
14845
-
14846
15011
  // src/membership-seat-stripe.ts
14847
15012
  var import_stripe15 = require("@odla-ai/stripe");
14848
15013
  var reject7 = () => {
@@ -14930,6 +15095,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
14930
15095
  // src/membership-seat-checkout.ts
14931
15096
  async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
14932
15097
  let seat = await reserveNamedMembershipSeat(db, input, now);
15098
+ if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
14933
15099
  const state = await loadMembershipAuthorityState(db, input);
14934
15100
  const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
14935
15101
  requireMembershipScope(member, input);
@@ -15136,7 +15302,7 @@ async function recordRecipient(db, input, now, approve) {
15136
15302
  requireMembershipScope(member, input);
15137
15303
  const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
15138
15304
  requireMembershipScope(entitlement, input);
15139
- 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");
15305
+ 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");
15140
15306
  if (seat.acceptedAt === void 0) {
15141
15307
  if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
15142
15308
  membershipFail("named_seat_recipient_verification_required");
@@ -15323,6 +15489,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
15323
15489
  };
15324
15490
  if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
15325
15491
  if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
15492
+ const checkout = {
15493
+ ...target,
15494
+ priceId: peer.seatPriceId,
15495
+ recipientName: String(input.recipientName),
15496
+ recipientEmail: String(input.recipientEmail),
15497
+ purchaserEmail: String(input.purchaserEmail),
15498
+ purchaserUserId: String(input.purchaserUserId)
15499
+ };
15500
+ if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
15326
15501
  const secret = await getVaultSecret(db, stripeSecretName);
15327
15502
  if (!secret) throw new Error("network Stripe provider is unavailable");
15328
15503
  if (action === "seat-decline") {
@@ -15338,14 +15513,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
15338
15513
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
15339
15514
  }
15340
15515
  if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
15341
- if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
15342
- ...target,
15343
- priceId: peer.seatPriceId,
15344
- recipientName: String(input.recipientName),
15345
- recipientEmail: String(input.recipientEmail),
15346
- purchaserEmail: String(input.purchaserEmail),
15347
- purchaserUserId: String(input.purchaserUserId)
15348
- }, secret, now);
15516
+ if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
15349
15517
  const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
15350
15518
  if (action === "seat-payments") {
15351
15519
  const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
@@ -16201,13 +16369,13 @@ async function requestNamedSeatDecline(options) {
16201
16369
  verifiedRecipientEmail: options.verifiedRecipientEmail
16202
16370
  });
16203
16371
  const seat = result.seat;
16204
- 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");
16372
+ 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");
16205
16373
  return seat;
16206
16374
  }
16207
16375
  async function requestNamedSeatOperatorRefund(options) {
16208
16376
  const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
16209
16377
  const seat = result.seat;
16210
- 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");
16378
+ 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");
16211
16379
  return seat;
16212
16380
  }
16213
16381
  async function requestNamedSeatReservation(options) {
@@ -16221,13 +16389,13 @@ async function requestNamedSeatReservation(options) {
16221
16389
  });
16222
16390
  const seat = result.seat;
16223
16391
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
16224
- 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");
16392
+ 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");
16225
16393
  return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
16226
16394
  }
16227
16395
  async function requestNamedSeatOffer(options) {
16228
16396
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
16229
16397
  const offer = result.seat;
16230
- 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");
16398
+ 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");
16231
16399
  return offer;
16232
16400
  }
16233
16401
  async function requestNamedSeatInvitation(options) {
@@ -16263,7 +16431,8 @@ async function requestNamedSeatCheckout(options) {
16263
16431
  });
16264
16432
  const seat = result.seat;
16265
16433
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
16266
- 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)) {
16434
+ const included = seat?.amountCents === 0;
16435
+ 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))) {
16267
16436
  throw new Error("invalid named seat checkout receipt");
16268
16437
  }
16269
16438
  return seat;