@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.
package/dist/index.js CHANGED
@@ -3286,8 +3286,8 @@ var object = (value) => {
3286
3286
  var id6 = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
3287
3287
  var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
3288
3288
  var time = (value) => {
3289
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
3290
- return integer2(at, 0, 864e13);
3289
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
3290
+ return integer2(at2, 0, 864e13);
3291
3291
  };
3292
3292
  var ids = (value, allowEmpty = false) => {
3293
3293
  if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
@@ -3331,7 +3331,7 @@ function membershipOfferingVersion(value) {
3331
3331
  term: normalizedTerm,
3332
3332
  autoRenew: true,
3333
3333
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
3334
- seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
3334
+ seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
3335
3335
  ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
3336
3336
  requiredDisclosures: ids(input.requiredDisclosures)
3337
3337
  };
@@ -4103,6 +4103,181 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4103
4103
  // src/membership-provider-lifecycle.ts
4104
4104
  import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4105
4105
 
4106
+ // src/membership-seat-reservation.ts
4107
+ async function recipientKey(input, email) {
4108
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4109
+ return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4110
+ }
4111
+ async function reserveNamedMembershipSeat(db, input, now) {
4112
+ membershipClock(now);
4113
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4114
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4115
+ const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4116
+ 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");
4117
+ const state = await loadMembershipAuthorityState(db, input);
4118
+ const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4119
+ requireMembershipScope(member, input);
4120
+ if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4121
+ const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4122
+ let seat = await membershipLoad(db, "networkSeat", id10);
4123
+ if (!seat) {
4124
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4125
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4126
+ 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) {
4127
+ membershipFail("named_seat_purchase_unavailable");
4128
+ }
4129
+ const attrs = {
4130
+ id: id10,
4131
+ ...membershipScope(input),
4132
+ primaryMembershipId: memberId,
4133
+ recipientName: name,
4134
+ recipientEmailNormalized: email,
4135
+ purchaserUserId: input.purchaserUserId,
4136
+ recipientClaimId: claimId,
4137
+ priceId: input.priceId,
4138
+ primaryPriceId: member.providerPriceId,
4139
+ amountCents: offering.seatPriceCents,
4140
+ currency: offering.currency,
4141
+ interval: offering.interval,
4142
+ providerCustomerId: member.providerCustomerId,
4143
+ providerSubscriptionId: member.providerSubscriptionId,
4144
+ status: "payment_pending",
4145
+ preparedAt: now,
4146
+ termEndsAt: member.termEndsAt,
4147
+ seatPurchaseEndsAt: purchaseEndsAt,
4148
+ offeringSnapshot: offering,
4149
+ revision: 1,
4150
+ createdAt: now,
4151
+ updatedAt: now
4152
+ };
4153
+ try {
4154
+ await db.transact([
4155
+ membershipUpdate("networkSeat", id10, attrs),
4156
+ membershipUpdate("networkSeatRecipient", claimId, {
4157
+ id: claimId,
4158
+ appId: input.appId,
4159
+ chapterId: input.chapterId,
4160
+ environment: input.environment,
4161
+ runtime: input.runtime,
4162
+ seatId: id10,
4163
+ recipientEmailNormalized: email,
4164
+ createdAt: now
4165
+ }),
4166
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
4167
+ ], { mutationId: `named-seat-reserve:${id10}`, guards: [
4168
+ { ns: "networkSeat", id: id10, exists: false },
4169
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
4170
+ { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4171
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4172
+ ] });
4173
+ } catch (error) {
4174
+ if (error.code !== "transact_guard_failed") throw error;
4175
+ }
4176
+ seat = await membershipLoad(db, "networkSeat", id10);
4177
+ if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4178
+ }
4179
+ requireMembershipScope(seat, input);
4180
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4181
+ return seat;
4182
+ }
4183
+
4184
+ // src/membership-seat-included.ts
4185
+ var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
4186
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
4187
+ function namedSeatRecipientState(seat) {
4188
+ switch (String(seat.status)) {
4189
+ case "renewal_pending":
4190
+ case "pending_acceptance":
4191
+ return "invited";
4192
+ case "pending_approval":
4193
+ case "pending_primary":
4194
+ return "accepted";
4195
+ case "active":
4196
+ return "joined";
4197
+ case "refund_pending":
4198
+ case "refunded":
4199
+ return seat.refundReason === "declined" ? "declined" : "refunded";
4200
+ case "canceled":
4201
+ return "canceled";
4202
+ default:
4203
+ return "payment_pending";
4204
+ }
4205
+ }
4206
+ var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4207
+ async function grantIncludedNamedSeat(db, input, now) {
4208
+ membershipClock(now);
4209
+ const seat = await reserveNamedMembershipSeat(db, input, now);
4210
+ if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4211
+ const state = await loadMembershipAuthorityState(db, input);
4212
+ const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4213
+ requireMembershipScope(member, input);
4214
+ 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)) {
4215
+ membershipFail("named_seat_purchase_unavailable");
4216
+ }
4217
+ if (seat.status === "payment_pending") {
4218
+ const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
4219
+ try {
4220
+ await db.transact([
4221
+ membershipUpdate("networkSeat", seat.id, {
4222
+ status: "pending_acceptance",
4223
+ purchasedAt: now,
4224
+ termStartedAt: now,
4225
+ termEndsAt: member.termEndsAt,
4226
+ acceptanceEndsAt: Number(member.termEndsAt),
4227
+ entitlementId,
4228
+ revision: Number(seat.revision) + 1,
4229
+ updatedAt: now
4230
+ }),
4231
+ membershipUpdate("networkEntitlement", entitlementId, {
4232
+ id: entitlementId,
4233
+ ...membershipScope(input),
4234
+ membershipId: member.id,
4235
+ seatId: seat.id,
4236
+ kind: "named_seat",
4237
+ status: "pending_acceptance",
4238
+ termStartedAt: now,
4239
+ termEndsAt: member.termEndsAt,
4240
+ offeringSnapshot: member.offeringSnapshot,
4241
+ sourceEventId: source,
4242
+ revision: 1,
4243
+ createdAt: now,
4244
+ updatedAt: now
4245
+ }),
4246
+ membershipUpdate("membershipEffect", inviteId, {
4247
+ id: inviteId,
4248
+ chapterId: input.chapterId,
4249
+ environment: input.environment,
4250
+ runtime: input.runtime,
4251
+ membershipId: member.id,
4252
+ seatId: seat.id,
4253
+ kind: "invite_named_recipient",
4254
+ status: "pending",
4255
+ attempts: 0,
4256
+ createdAt: now,
4257
+ updatedAt: now
4258
+ })
4259
+ ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4260
+ {
4261
+ ns: "networkSeat",
4262
+ id: seat.id,
4263
+ exists: true,
4264
+ equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4265
+ absent: ["purchasedAt", "providerPaymentIntentId"]
4266
+ },
4267
+ { ns: "networkEntitlement", id: entitlementId, exists: false },
4268
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4269
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4270
+ ] });
4271
+ } catch (error) {
4272
+ if (error.code !== "transact_guard_failed") throw error;
4273
+ }
4274
+ }
4275
+ const current = await membershipLoad(db, "networkSeat", seat.id);
4276
+ requireMembershipScope(current, input);
4277
+ if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4278
+ return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4279
+ }
4280
+
4106
4281
  // src/membership-renewal-authority.ts
4107
4282
  async function reconcileMembershipRenewal(db, input, now) {
4108
4283
  membershipClock(now);
@@ -4138,7 +4313,9 @@ async function reconcileMembershipRenewal(db, input, now) {
4138
4313
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
4139
4314
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4140
4315
  if (seat) requireMembershipScope(seat, input);
4141
- const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4316
+ const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
4317
+ const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
4318
+ const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
4142
4319
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
4143
4320
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
4144
4321
  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");
@@ -4178,19 +4355,19 @@ async function reconcileMembershipRenewal(db, input, now) {
4178
4355
  evidenceGuard
4179
4356
  ];
4180
4357
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4181
- if (billedSeat?.entitlementId) {
4182
- const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(billedSeat.entitlementId));
4358
+ if (carriedSeat?.entitlementId) {
4359
+ const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
4183
4360
  requireMembershipScope(seatEntitlement, input);
4184
4361
  guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
4185
4362
  ops.push(
4186
- membershipUpdate("networkSeat", String(billedSeat.id), {
4363
+ membershipUpdate("networkSeat", String(carriedSeat.id), {
4187
4364
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4188
- revision: Number(billedSeat.revision) + 1,
4365
+ revision: Number(carriedSeat.revision) + 1,
4189
4366
  updatedAt: now
4190
4367
  }),
4191
4368
  membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
4192
4369
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4193
- status: billedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
4370
+ status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
4194
4371
  revision: Number(seatEntitlement.revision) + 1,
4195
4372
  updatedAt: now
4196
4373
  })
@@ -4442,84 +4619,6 @@ async function verifyMembershipAccessReceipt(value, input, now) {
4442
4619
  return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
4443
4620
  }
4444
4621
 
4445
- // src/membership-seat-reservation.ts
4446
- async function recipientKey(input, email) {
4447
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4448
- return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4449
- }
4450
- async function reserveNamedMembershipSeat(db, input, now) {
4451
- membershipClock(now);
4452
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4453
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4454
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4455
- 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");
4456
- const state = await loadMembershipAuthorityState(db, input);
4457
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4458
- requireMembershipScope(member, input);
4459
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4460
- const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4461
- let seat = await membershipLoad(db, "networkSeat", id10);
4462
- if (!seat) {
4463
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4464
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4465
- 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) {
4466
- membershipFail("named_seat_purchase_unavailable");
4467
- }
4468
- const attrs = {
4469
- id: id10,
4470
- ...membershipScope(input),
4471
- primaryMembershipId: memberId,
4472
- recipientName: name,
4473
- recipientEmailNormalized: email,
4474
- purchaserUserId: input.purchaserUserId,
4475
- recipientClaimId: claimId,
4476
- priceId: input.priceId,
4477
- primaryPriceId: member.providerPriceId,
4478
- amountCents: offering.seatPriceCents,
4479
- currency: offering.currency,
4480
- interval: offering.interval,
4481
- providerCustomerId: member.providerCustomerId,
4482
- providerSubscriptionId: member.providerSubscriptionId,
4483
- status: "payment_pending",
4484
- preparedAt: now,
4485
- termEndsAt: member.termEndsAt,
4486
- seatPurchaseEndsAt: purchaseEndsAt,
4487
- offeringSnapshot: offering,
4488
- revision: 1,
4489
- createdAt: now,
4490
- updatedAt: now
4491
- };
4492
- try {
4493
- await db.transact([
4494
- membershipUpdate("networkSeat", id10, attrs),
4495
- membershipUpdate("networkSeatRecipient", claimId, {
4496
- id: claimId,
4497
- appId: input.appId,
4498
- chapterId: input.chapterId,
4499
- environment: input.environment,
4500
- runtime: input.runtime,
4501
- seatId: id10,
4502
- recipientEmailNormalized: email,
4503
- createdAt: now
4504
- }),
4505
- membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
4506
- ], { mutationId: `named-seat-reserve:${id10}`, guards: [
4507
- { ns: "networkSeat", id: id10, exists: false },
4508
- { ns: "networkSeatRecipient", id: claimId, exists: false },
4509
- { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4510
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4511
- ] });
4512
- } catch (error) {
4513
- if (error.code !== "transact_guard_failed") throw error;
4514
- }
4515
- seat = await membershipLoad(db, "networkSeat", id10);
4516
- if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4517
- }
4518
- requireMembershipScope(seat, input);
4519
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4520
- return seat;
4521
- }
4522
-
4523
4622
  // src/membership-seat-stripe.ts
4524
4623
  import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
4525
4624
 
@@ -4622,6 +4721,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4622
4721
  // src/membership-seat-checkout.ts
4623
4722
  async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
4624
4723
  let seat = await reserveNamedMembershipSeat(db, input, now);
4724
+ if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
4625
4725
  const state = await loadMembershipAuthorityState(db, input);
4626
4726
  const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4627
4727
  requireMembershipScope(member, input);
@@ -4828,7 +4928,7 @@ async function recordRecipient(db, input, now, approve) {
4828
4928
  requireMembershipScope(member, input);
4829
4929
  const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
4830
4930
  requireMembershipScope(entitlement, input);
4831
- 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");
4931
+ 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");
4832
4932
  if (seat.acceptedAt === void 0) {
4833
4933
  if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
4834
4934
  membershipFail("named_seat_recipient_verification_required");
@@ -4943,7 +5043,7 @@ async function requestNamedSeatRefund(db, input, now) {
4943
5043
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4944
5044
  requireMembershipScope(member, input);
4945
5045
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
4946
- 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");
5046
+ 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");
4947
5047
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
4948
5048
  await db.transact([
4949
5049
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -4988,7 +5088,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
4988
5088
  await loadMembershipAuthorityState(db, seat);
4989
5089
  if (seat.status === "refunded") return;
4990
5090
  if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
4991
- const result = await refundNamedSeatStripePayment(secretKey, seat);
5091
+ const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
4992
5092
  await db.transact([
4993
5093
  membershipUpdate("networkSeat", seatId, {
4994
5094
  status: "refunded",
@@ -5022,7 +5122,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
5022
5122
  await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
5023
5123
  await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
5024
5124
  const refunded = await membershipLoad(db, "networkSeat", input.seatId);
5025
- if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
5125
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
5026
5126
  throw new Error("named seat refund unconfirmed");
5027
5127
  }
5028
5128
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
@@ -5044,6 +5144,7 @@ async function namedSeatOfferDigest(offer) {
5044
5144
  ])));
5045
5145
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5046
5146
  }
5147
+ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
5047
5148
  async function readNamedSeatOffer(db, input, now) {
5048
5149
  membershipClock(now);
5049
5150
  await loadMembershipAuthorityState(db, input);
@@ -5066,10 +5167,19 @@ async function readNamedSeatOffer(db, input, now) {
5066
5167
  seatId: String(seat.id),
5067
5168
  status: String(seat.status),
5068
5169
  recipientName: String(seat.recipientName),
5069
- recipientEmail: String(seat.recipientEmailNormalized)
5170
+ recipientEmail: String(seat.recipientEmailNormalized),
5171
+ recipientState: namedSeatRecipientState(seat),
5172
+ ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
5173
+ ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
5174
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
5070
5175
  } } : {}
5071
5176
  };
5072
- return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
5177
+ return {
5178
+ ...offer,
5179
+ digest: await namedSeatOfferDigest(offer),
5180
+ included: offering.seatPriceCents === 0,
5181
+ purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
5182
+ };
5073
5183
  }
5074
5184
 
5075
5185
  // src/membership-seat-invitation.ts
@@ -5195,6 +5305,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5195
5305
  };
5196
5306
  if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
5197
5307
  if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
5308
+ const checkout = {
5309
+ ...target,
5310
+ priceId: peer.seatPriceId,
5311
+ recipientName: String(input.recipientName),
5312
+ recipientEmail: String(input.recipientEmail),
5313
+ purchaserEmail: String(input.purchaserEmail),
5314
+ purchaserUserId: String(input.purchaserUserId)
5315
+ };
5316
+ if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
5198
5317
  const secret = await getVaultSecret(db, stripeSecretName);
5199
5318
  if (!secret) throw new Error("network Stripe provider is unavailable");
5200
5319
  if (action === "seat-decline") {
@@ -5210,14 +5329,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5210
5329
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
5211
5330
  }
5212
5331
  if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
5213
- if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
5214
- ...target,
5215
- priceId: peer.seatPriceId,
5216
- recipientName: String(input.recipientName),
5217
- recipientEmail: String(input.recipientEmail),
5218
- purchaserEmail: String(input.purchaserEmail),
5219
- purchaserUserId: String(input.purchaserUserId)
5220
- }, secret, now);
5332
+ if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
5221
5333
  const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
5222
5334
  if (action === "seat-payments") {
5223
5335
  const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
@@ -5528,13 +5640,13 @@ async function requestNamedSeatDecline(options) {
5528
5640
  verifiedRecipientEmail: options.verifiedRecipientEmail
5529
5641
  });
5530
5642
  const seat = result.seat;
5531
- 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");
5643
+ 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");
5532
5644
  return seat;
5533
5645
  }
5534
5646
  async function requestNamedSeatOperatorRefund(options) {
5535
5647
  const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
5536
5648
  const seat = result.seat;
5537
- 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");
5649
+ 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");
5538
5650
  return seat;
5539
5651
  }
5540
5652
  async function requestNamedSeatReservation(options) {
@@ -5548,13 +5660,13 @@ async function requestNamedSeatReservation(options) {
5548
5660
  });
5549
5661
  const seat = result.seat;
5550
5662
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
5551
- 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");
5663
+ 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");
5552
5664
  return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
5553
5665
  }
5554
5666
  async function requestNamedSeatOffer(options) {
5555
5667
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
5556
5668
  const offer = result.seat;
5557
- 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");
5669
+ 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");
5558
5670
  return offer;
5559
5671
  }
5560
5672
  async function requestNamedSeatInvitation(options) {
@@ -5590,7 +5702,8 @@ async function requestNamedSeatCheckout(options) {
5590
5702
  });
5591
5703
  const seat = result.seat;
5592
5704
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
5593
- 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)) {
5705
+ const included = seat?.amountCents === 0;
5706
+ 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))) {
5594
5707
  throw new Error("invalid named seat checkout receipt");
5595
5708
  }
5596
5709
  return seat;