@odla-ai/chapter 0.53.0 → 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.
package/dist/index.cjs CHANGED
@@ -3565,8 +3565,8 @@ var object = (value) => {
3565
3565
  var id6 = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
3566
3566
  var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
3567
3567
  var time = (value) => {
3568
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
3569
- return integer2(at, 0, 864e13);
3568
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
3569
+ return integer2(at2, 0, 864e13);
3570
3570
  };
3571
3571
  var ids = (value, allowEmpty = false) => {
3572
3572
  if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
@@ -3610,7 +3610,7 @@ function membershipOfferingVersion(value) {
3610
3610
  term: normalizedTerm,
3611
3611
  autoRenew: true,
3612
3612
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
3613
- seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
3613
+ seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
3614
3614
  ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
3615
3615
  requiredDisclosures: ids(input.requiredDisclosures)
3616
3616
  };
@@ -4382,6 +4382,181 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4382
4382
  // src/membership-provider-lifecycle.ts
4383
4383
  var import_stripe4 = require("@odla-ai/stripe");
4384
4384
 
4385
+ // src/membership-seat-reservation.ts
4386
+ async function recipientKey(input, email) {
4387
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4388
+ return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4389
+ }
4390
+ async function reserveNamedMembershipSeat(db, input, now) {
4391
+ membershipClock(now);
4392
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4393
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4394
+ const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4395
+ 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");
4396
+ const state = await loadMembershipAuthorityState(db, input);
4397
+ const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4398
+ requireMembershipScope(member, input);
4399
+ if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4400
+ const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4401
+ let seat = await membershipLoad(db, "networkSeat", id10);
4402
+ if (!seat) {
4403
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4404
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4405
+ 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) {
4406
+ membershipFail("named_seat_purchase_unavailable");
4407
+ }
4408
+ const attrs = {
4409
+ id: id10,
4410
+ ...membershipScope(input),
4411
+ primaryMembershipId: memberId,
4412
+ recipientName: name,
4413
+ recipientEmailNormalized: email,
4414
+ purchaserUserId: input.purchaserUserId,
4415
+ recipientClaimId: claimId,
4416
+ priceId: input.priceId,
4417
+ primaryPriceId: member.providerPriceId,
4418
+ amountCents: offering.seatPriceCents,
4419
+ currency: offering.currency,
4420
+ interval: offering.interval,
4421
+ providerCustomerId: member.providerCustomerId,
4422
+ providerSubscriptionId: member.providerSubscriptionId,
4423
+ status: "payment_pending",
4424
+ preparedAt: now,
4425
+ termEndsAt: member.termEndsAt,
4426
+ seatPurchaseEndsAt: purchaseEndsAt,
4427
+ offeringSnapshot: offering,
4428
+ revision: 1,
4429
+ createdAt: now,
4430
+ updatedAt: now
4431
+ };
4432
+ try {
4433
+ await db.transact([
4434
+ membershipUpdate("networkSeat", id10, attrs),
4435
+ membershipUpdate("networkSeatRecipient", claimId, {
4436
+ id: claimId,
4437
+ appId: input.appId,
4438
+ chapterId: input.chapterId,
4439
+ environment: input.environment,
4440
+ runtime: input.runtime,
4441
+ seatId: id10,
4442
+ recipientEmailNormalized: email,
4443
+ createdAt: now
4444
+ }),
4445
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
4446
+ ], { mutationId: `named-seat-reserve:${id10}`, guards: [
4447
+ { ns: "networkSeat", id: id10, exists: false },
4448
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
4449
+ { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4450
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4451
+ ] });
4452
+ } catch (error) {
4453
+ if (error.code !== "transact_guard_failed") throw error;
4454
+ }
4455
+ seat = await membershipLoad(db, "networkSeat", id10);
4456
+ if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4457
+ }
4458
+ requireMembershipScope(seat, input);
4459
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4460
+ return seat;
4461
+ }
4462
+
4463
+ // src/membership-seat-included.ts
4464
+ var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
4465
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
4466
+ function namedSeatRecipientState(seat) {
4467
+ switch (String(seat.status)) {
4468
+ case "renewal_pending":
4469
+ case "pending_acceptance":
4470
+ return "invited";
4471
+ case "pending_approval":
4472
+ case "pending_primary":
4473
+ return "accepted";
4474
+ case "active":
4475
+ return "joined";
4476
+ case "refund_pending":
4477
+ case "refunded":
4478
+ return seat.refundReason === "declined" ? "declined" : "refunded";
4479
+ case "canceled":
4480
+ return "canceled";
4481
+ default:
4482
+ return "payment_pending";
4483
+ }
4484
+ }
4485
+ var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4486
+ async function grantIncludedNamedSeat(db, input, now) {
4487
+ membershipClock(now);
4488
+ const seat = await reserveNamedMembershipSeat(db, input, now);
4489
+ if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4490
+ const state = await loadMembershipAuthorityState(db, input);
4491
+ const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4492
+ requireMembershipScope(member, input);
4493
+ 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)) {
4494
+ membershipFail("named_seat_purchase_unavailable");
4495
+ }
4496
+ if (seat.status === "payment_pending") {
4497
+ const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
4498
+ try {
4499
+ await db.transact([
4500
+ membershipUpdate("networkSeat", seat.id, {
4501
+ status: "pending_acceptance",
4502
+ purchasedAt: now,
4503
+ termStartedAt: now,
4504
+ termEndsAt: member.termEndsAt,
4505
+ acceptanceEndsAt: Number(member.termEndsAt),
4506
+ entitlementId,
4507
+ revision: Number(seat.revision) + 1,
4508
+ updatedAt: now
4509
+ }),
4510
+ membershipUpdate("networkEntitlement", entitlementId, {
4511
+ id: entitlementId,
4512
+ ...membershipScope(input),
4513
+ membershipId: member.id,
4514
+ seatId: seat.id,
4515
+ kind: "named_seat",
4516
+ status: "pending_acceptance",
4517
+ termStartedAt: now,
4518
+ termEndsAt: member.termEndsAt,
4519
+ offeringSnapshot: member.offeringSnapshot,
4520
+ sourceEventId: source,
4521
+ revision: 1,
4522
+ createdAt: now,
4523
+ updatedAt: now
4524
+ }),
4525
+ membershipUpdate("membershipEffect", inviteId, {
4526
+ id: inviteId,
4527
+ chapterId: input.chapterId,
4528
+ environment: input.environment,
4529
+ runtime: input.runtime,
4530
+ membershipId: member.id,
4531
+ seatId: seat.id,
4532
+ kind: "invite_named_recipient",
4533
+ status: "pending",
4534
+ attempts: 0,
4535
+ createdAt: now,
4536
+ updatedAt: now
4537
+ })
4538
+ ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4539
+ {
4540
+ ns: "networkSeat",
4541
+ id: seat.id,
4542
+ exists: true,
4543
+ equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4544
+ absent: ["purchasedAt", "providerPaymentIntentId"]
4545
+ },
4546
+ { ns: "networkEntitlement", id: entitlementId, exists: false },
4547
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4548
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4549
+ ] });
4550
+ } catch (error) {
4551
+ if (error.code !== "transact_guard_failed") throw error;
4552
+ }
4553
+ }
4554
+ const current = await membershipLoad(db, "networkSeat", seat.id);
4555
+ requireMembershipScope(current, input);
4556
+ if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4557
+ return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4558
+ }
4559
+
4385
4560
  // src/membership-renewal-authority.ts
4386
4561
  async function reconcileMembershipRenewal(db, input, now) {
4387
4562
  membershipClock(now);
@@ -4417,7 +4592,9 @@ async function reconcileMembershipRenewal(db, input, now) {
4417
4592
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
4418
4593
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4419
4594
  if (seat) requireMembershipScope(seat, input);
4420
- const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4595
+ const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
4596
+ const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
4597
+ const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
4421
4598
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
4422
4599
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
4423
4600
  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");
@@ -4457,19 +4634,19 @@ async function reconcileMembershipRenewal(db, input, now) {
4457
4634
  evidenceGuard
4458
4635
  ];
4459
4636
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4460
- if (billedSeat?.entitlementId) {
4461
- const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(billedSeat.entitlementId));
4637
+ if (carriedSeat?.entitlementId) {
4638
+ const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
4462
4639
  requireMembershipScope(seatEntitlement, input);
4463
4640
  guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
4464
4641
  ops.push(
4465
- membershipUpdate("networkSeat", String(billedSeat.id), {
4642
+ membershipUpdate("networkSeat", String(carriedSeat.id), {
4466
4643
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4467
- revision: Number(billedSeat.revision) + 1,
4644
+ revision: Number(carriedSeat.revision) + 1,
4468
4645
  updatedAt: now
4469
4646
  }),
4470
4647
  membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
4471
4648
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4472
- status: billedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
4649
+ status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
4473
4650
  revision: Number(seatEntitlement.revision) + 1,
4474
4651
  updatedAt: now
4475
4652
  })
@@ -4721,84 +4898,6 @@ async function verifyMembershipAccessReceipt(value, input, now) {
4721
4898
  return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
4722
4899
  }
4723
4900
 
4724
- // src/membership-seat-reservation.ts
4725
- async function recipientKey(input, email) {
4726
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4727
- return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4728
- }
4729
- async function reserveNamedMembershipSeat(db, input, now) {
4730
- membershipClock(now);
4731
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4732
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4733
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4734
- 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");
4735
- const state = await loadMembershipAuthorityState(db, input);
4736
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4737
- requireMembershipScope(member, input);
4738
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4739
- const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4740
- let seat = await membershipLoad(db, "networkSeat", id10);
4741
- if (!seat) {
4742
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4743
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4744
- 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) {
4745
- membershipFail("named_seat_purchase_unavailable");
4746
- }
4747
- const attrs = {
4748
- id: id10,
4749
- ...membershipScope(input),
4750
- primaryMembershipId: memberId,
4751
- recipientName: name,
4752
- recipientEmailNormalized: email,
4753
- purchaserUserId: input.purchaserUserId,
4754
- recipientClaimId: claimId,
4755
- priceId: input.priceId,
4756
- primaryPriceId: member.providerPriceId,
4757
- amountCents: offering.seatPriceCents,
4758
- currency: offering.currency,
4759
- interval: offering.interval,
4760
- providerCustomerId: member.providerCustomerId,
4761
- providerSubscriptionId: member.providerSubscriptionId,
4762
- status: "payment_pending",
4763
- preparedAt: now,
4764
- termEndsAt: member.termEndsAt,
4765
- seatPurchaseEndsAt: purchaseEndsAt,
4766
- offeringSnapshot: offering,
4767
- revision: 1,
4768
- createdAt: now,
4769
- updatedAt: now
4770
- };
4771
- try {
4772
- await db.transact([
4773
- membershipUpdate("networkSeat", id10, attrs),
4774
- membershipUpdate("networkSeatRecipient", claimId, {
4775
- id: claimId,
4776
- appId: input.appId,
4777
- chapterId: input.chapterId,
4778
- environment: input.environment,
4779
- runtime: input.runtime,
4780
- seatId: id10,
4781
- recipientEmailNormalized: email,
4782
- createdAt: now
4783
- }),
4784
- membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
4785
- ], { mutationId: `named-seat-reserve:${id10}`, guards: [
4786
- { ns: "networkSeat", id: id10, exists: false },
4787
- { ns: "networkSeatRecipient", id: claimId, exists: false },
4788
- { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4789
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4790
- ] });
4791
- } catch (error) {
4792
- if (error.code !== "transact_guard_failed") throw error;
4793
- }
4794
- seat = await membershipLoad(db, "networkSeat", id10);
4795
- if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4796
- }
4797
- requireMembershipScope(seat, input);
4798
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4799
- return seat;
4800
- }
4801
-
4802
4901
  // src/membership-seat-stripe.ts
4803
4902
  var import_stripe7 = require("@odla-ai/stripe");
4804
4903
 
@@ -4901,6 +5000,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4901
5000
  // src/membership-seat-checkout.ts
4902
5001
  async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
4903
5002
  let seat = await reserveNamedMembershipSeat(db, input, now);
5003
+ if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
4904
5004
  const state = await loadMembershipAuthorityState(db, input);
4905
5005
  const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4906
5006
  requireMembershipScope(member, input);
@@ -5107,7 +5207,7 @@ async function recordRecipient(db, input, now, approve) {
5107
5207
  requireMembershipScope(member, input);
5108
5208
  const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
5109
5209
  requireMembershipScope(entitlement, input);
5110
- 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");
5210
+ 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");
5111
5211
  if (seat.acceptedAt === void 0) {
5112
5212
  if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
5113
5213
  membershipFail("named_seat_recipient_verification_required");
@@ -5222,7 +5322,7 @@ async function requestNamedSeatRefund(db, input, now) {
5222
5322
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5223
5323
  requireMembershipScope(member, input);
5224
5324
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
5225
- 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");
5325
+ 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");
5226
5326
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
5227
5327
  await db.transact([
5228
5328
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -5267,7 +5367,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
5267
5367
  await loadMembershipAuthorityState(db, seat);
5268
5368
  if (seat.status === "refunded") return;
5269
5369
  if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
5270
- const result = await refundNamedSeatStripePayment(secretKey, seat);
5370
+ const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
5271
5371
  await db.transact([
5272
5372
  membershipUpdate("networkSeat", seatId, {
5273
5373
  status: "refunded",
@@ -5301,7 +5401,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
5301
5401
  await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
5302
5402
  await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
5303
5403
  const refunded = await membershipLoad(db, "networkSeat", input.seatId);
5304
- if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
5404
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
5305
5405
  throw new Error("named seat refund unconfirmed");
5306
5406
  }
5307
5407
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
@@ -5323,6 +5423,7 @@ async function namedSeatOfferDigest(offer) {
5323
5423
  ])));
5324
5424
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5325
5425
  }
5426
+ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
5326
5427
  async function readNamedSeatOffer(db, input, now) {
5327
5428
  membershipClock(now);
5328
5429
  await loadMembershipAuthorityState(db, input);
@@ -5345,10 +5446,19 @@ async function readNamedSeatOffer(db, input, now) {
5345
5446
  seatId: String(seat.id),
5346
5447
  status: String(seat.status),
5347
5448
  recipientName: String(seat.recipientName),
5348
- recipientEmail: String(seat.recipientEmailNormalized)
5449
+ recipientEmail: String(seat.recipientEmailNormalized),
5450
+ recipientState: namedSeatRecipientState(seat),
5451
+ ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
5452
+ ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
5453
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
5349
5454
  } } : {}
5350
5455
  };
5351
- return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
5456
+ return {
5457
+ ...offer,
5458
+ digest: await namedSeatOfferDigest(offer),
5459
+ included: offering.seatPriceCents === 0,
5460
+ purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
5461
+ };
5352
5462
  }
5353
5463
 
5354
5464
  // src/membership-seat-invitation.ts
@@ -5474,6 +5584,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5474
5584
  };
5475
5585
  if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
5476
5586
  if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
5587
+ const checkout = {
5588
+ ...target,
5589
+ priceId: peer.seatPriceId,
5590
+ recipientName: String(input.recipientName),
5591
+ recipientEmail: String(input.recipientEmail),
5592
+ purchaserEmail: String(input.purchaserEmail),
5593
+ purchaserUserId: String(input.purchaserUserId)
5594
+ };
5595
+ if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
5477
5596
  const secret = await getVaultSecret(db, stripeSecretName);
5478
5597
  if (!secret) throw new Error("network Stripe provider is unavailable");
5479
5598
  if (action === "seat-decline") {
@@ -5489,14 +5608,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5489
5608
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
5490
5609
  }
5491
5610
  if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
5492
- if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
5493
- ...target,
5494
- priceId: peer.seatPriceId,
5495
- recipientName: String(input.recipientName),
5496
- recipientEmail: String(input.recipientEmail),
5497
- purchaserEmail: String(input.purchaserEmail),
5498
- purchaserUserId: String(input.purchaserUserId)
5499
- }, secret, now);
5611
+ if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
5500
5612
  const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
5501
5613
  if (action === "seat-payments") {
5502
5614
  const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
@@ -5807,13 +5919,13 @@ async function requestNamedSeatDecline(options) {
5807
5919
  verifiedRecipientEmail: options.verifiedRecipientEmail
5808
5920
  });
5809
5921
  const seat = result.seat;
5810
- 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");
5922
+ 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");
5811
5923
  return seat;
5812
5924
  }
5813
5925
  async function requestNamedSeatOperatorRefund(options) {
5814
5926
  const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
5815
5927
  const seat = result.seat;
5816
- 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");
5928
+ 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");
5817
5929
  return seat;
5818
5930
  }
5819
5931
  async function requestNamedSeatReservation(options) {
@@ -5827,13 +5939,13 @@ async function requestNamedSeatReservation(options) {
5827
5939
  });
5828
5940
  const seat = result.seat;
5829
5941
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
5830
- 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");
5942
+ 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");
5831
5943
  return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
5832
5944
  }
5833
5945
  async function requestNamedSeatOffer(options) {
5834
5946
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
5835
5947
  const offer = result.seat;
5836
- 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");
5948
+ 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");
5837
5949
  return offer;
5838
5950
  }
5839
5951
  async function requestNamedSeatInvitation(options) {
@@ -5869,7 +5981,8 @@ async function requestNamedSeatCheckout(options) {
5869
5981
  });
5870
5982
  const seat = result.seat;
5871
5983
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
5872
- 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)) {
5984
+ const included = seat?.amountCents === 0;
5985
+ 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))) {
5873
5986
  throw new Error("invalid named seat checkout receipt");
5874
5987
  }
5875
5988
  return seat;