@odla-ai/chapter 0.56.2 → 0.57.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.
@@ -45,7 +45,7 @@ type AdminRecordsCopy = {
45
45
  [Field in AdminRecordsCopyKey]: string;
46
46
  };
47
47
  /** Every key a host may override under `admin.records`. */
48
- type AdminRecordsCopyKey = "workflowMissing" | "invitation" | "comms" | "commsHistory" | "scheduling" | "billing" | "notes" | "access" | "sharing" | "lifecycle" | "role" | "superAdminHint" | "noAccount" | "approve" | "refund" | "approved" | "refunded" | "roleSet" | "messageSent" | "messageNotSent" | "loadingTemplates" | "template" | "chooseTemplate" | "sending" | "send" | "noMessages" | "message" | "newStartPrompt" | "parseFailed" | "cancelConfirm" | "callCancelled" | "callRescheduled" | "noApplication" | "loadingMeetings" | "noMeetings" | "joinMeeting" | "openCalendar" | "reschedule" | "cancel";
48
+ type AdminRecordsCopyKey = "workflowMissing" | "invitation" | "comms" | "commsHistory" | "scheduling" | "billing" | "notes" | "access" | "sharing" | "lifecycle" | "role" | "superAdminHint" | "noAccount" | "approve" | "decline" | "refund" | "approved" | "declined" | "refunded" | "roleSet" | "messageSent" | "messageNotSent" | "loadingTemplates" | "template" | "chooseTemplate" | "sending" | "send" | "noMessages" | "message" | "newStartPrompt" | "parseFailed" | "cancelConfirm" | "callCancelled" | "callRescheduled" | "noApplication" | "loadingMeetings" | "noMeetings" | "joinMeeting" | "openCalendar" | "reschedule" | "cancel";
49
49
 
50
50
  type TextFields<Key extends string> = {
51
51
  [Field in Key]: string;
package/dist/index.cjs CHANGED
@@ -230,6 +230,7 @@ __export(src_exports, {
230
230
  requestNamedSeatOperatorRefund: () => requestNamedSeatOperatorRefund,
231
231
  requestNamedSeatPayment: () => requestNamedSeatPayment,
232
232
  requestNamedSeatRefund: () => requestNamedSeatRefund2,
233
+ requestNamedSeatRegift: () => requestNamedSeatRegift,
233
234
  requestNamedSeatReservation: () => requestNamedSeatReservation,
234
235
  resolveApplication: () => resolveApplication,
235
236
  resolveAuth: () => resolveAuth,
@@ -421,6 +422,7 @@ async function submitApplication(db, chapter, fields, opts) {
421
422
  clerkUserId: opts.namedSeat.userId,
422
423
  clerkAccountPending: false
423
424
  });
425
+ if (opts.predecessor?.clerkUserId && !opts.namedSeat) Object.assign(row2, { clerkUserId: opts.predecessor.clerkUserId, clerkAccountPending: false });
424
426
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
425
427
  row2.emailNormalized = normalizeChapterEmail(fields.email);
426
428
  row2.signupEmailKey = row2.emailNormalized;
@@ -439,6 +441,8 @@ async function submitApplication(db, chapter, fields, opts) {
439
441
  }
440
442
  if (acked) row2.disclaimerAckAt = opts.now;
441
443
  const ops = [
444
+ // The unique signup key leaves the closed predecessor before the new attempt takes it.
445
+ ...opts.predecessor && opts.predecessor.id !== id11 ? [{ t: "retract", ns: "applications", id: opts.predecessor.id, attrs: ["signupEmailKey"] }] : [],
442
446
  { t: "update", ns: "applications", id: id11, attrs: row2 }
443
447
  ];
444
448
  if (opts.admissionGrant?.decision === "waive_interview") {
@@ -1774,8 +1778,10 @@ var DEFAULT_ADMIN_COPY = {
1774
1778
  superAdminHint: "Super-admin, managed in odla Studio.",
1775
1779
  noAccount: "No linked account yet. The role is set once they sign in.",
1776
1780
  approve: "Approve",
1781
+ decline: "Decline",
1777
1782
  refund: "Refund",
1778
1783
  approved: "Approved.",
1784
+ declined: "Declined.",
1779
1785
  refunded: "Refund issued.",
1780
1786
  roleSet: "Role set to {role}.",
1781
1787
  messageSent: "Message sent.",
@@ -4555,7 +4561,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4555
4561
  var import_stripe4 = require("@odla-ai/stripe");
4556
4562
 
4557
4563
  // src/membership-seat-reservation.ts
4558
- async function recipientKey(input, email) {
4564
+ async function namedSeatRecipientClaimId(input, email) {
4559
4565
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4560
4566
  return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4561
4567
  }
@@ -4572,7 +4578,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4572
4578
  const id11 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4573
4579
  let seat = await membershipLoad(db, "networkSeat", id11);
4574
4580
  if (!seat) {
4575
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4581
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await namedSeatRecipientClaimId(input, email);
4576
4582
  const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4577
4583
  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) {
4578
4584
  membershipFail("named_seat_purchase_unavailable");
@@ -5661,6 +5667,128 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
5661
5667
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
5662
5668
  }
5663
5669
 
5670
+ // src/membership-effect-kinds.ts
5671
+ var MEMBERSHIP_CHAPTER_EFFECT_KINDS = [
5672
+ "invite_named_recipient",
5673
+ "grant_named_recipient_access",
5674
+ "revoke_named_recipient_access",
5675
+ "release_named_recipient"
5676
+ ];
5677
+ var namedSeatInvitationOpen = (member) => ["active", "cancel_at_term_end"].includes(String(member.status)) && Number.isSafeInteger(member.journeyApprovedAt);
5678
+
5679
+ // src/membership-seat-regift.ts
5680
+ var REGIFTABLE_SEAT_STATUSES = ["pending_acceptance", "pending_approval", "pending_primary"];
5681
+ var namedSeatRegiftable = (seat) => REGIFTABLE_SEAT_STATUSES.includes(String(seat.status));
5682
+ var receiptFor = (seat, released) => ({
5683
+ seatId: String(seat.id),
5684
+ status: "pending_acceptance",
5685
+ recipientName: String(seat.recipientName),
5686
+ recipientEmail: String(seat.recipientEmailNormalized),
5687
+ regiftedAt: Number(seat.regiftedAt),
5688
+ releasedRecipientApplicationId: released
5689
+ });
5690
+ async function regiftNamedSeat(db, input, now) {
5691
+ membershipClock(now);
5692
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
5693
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
5694
+ if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
5695
+ const state = await loadMembershipAuthorityState(db, input);
5696
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
5697
+ requireMembershipScope(seat, input);
5698
+ const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5699
+ requireMembershipScope(member, input);
5700
+ if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
5701
+ if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
5702
+ const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
5703
+ if (seat.recipientEmailNormalized === email && seat.recipientName === name && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
5704
+ return receiptFor(seat, history.at(-1)?.recipientApplicationId ?? null);
5705
+ }
5706
+ if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
5707
+ if (seat.recipientEmailNormalized === email) membershipFail("named_seat_recipient_unchanged");
5708
+ const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
5709
+ requireMembershipScope(entitlement, input);
5710
+ const claimId = await namedSeatRecipientClaimId(input, email);
5711
+ const former = {
5712
+ recipientName: String(seat.recipientName),
5713
+ recipientEmail: String(seat.recipientEmailNormalized),
5714
+ recipientClaimId: String(seat.recipientClaimId),
5715
+ releasedAt: now,
5716
+ ...typeof seat.recipientUserId === "string" ? { recipientUserId: seat.recipientUserId } : {},
5717
+ ...typeof seat.recipientApplicationId === "string" ? { recipientApplicationId: seat.recipientApplicationId } : {},
5718
+ ...Number.isSafeInteger(seat.acceptedAt) ? { acceptedAt: Number(seat.acceptedAt) } : {},
5719
+ ...Number.isSafeInteger(seat.journeyApprovedAt) ? { journeyApprovedAt: Number(seat.journeyApprovedAt) } : {}
5720
+ };
5721
+ const generation = history.length + 1, inviteId = `${seat.id}:invite:${generation}`, releaseId = `${seat.id}:release:${generation}`;
5722
+ const bound = Boolean(former.recipientUserId && former.recipientApplicationId);
5723
+ const stale = ((await db.query({ membershipEffect: { $: { where: { seatId: seat.id, kind: "invite_named_recipient", status: "pending" }, limit: 10 } } })).membershipEffect ?? []).filter((row2) => row2.chapterId === input.chapterId && row2.environment === input.environment && row2.runtime === input.runtime);
5724
+ const cleared = ["recipientUserId", "recipientApplicationId", "acceptedAt", "journeyApprovedAt"];
5725
+ const effect = (id11, kind, extra = {}) => membershipUpdate("membershipEffect", id11, {
5726
+ id: id11,
5727
+ chapterId: input.chapterId,
5728
+ environment: input.environment,
5729
+ runtime: input.runtime,
5730
+ membershipId: member.id,
5731
+ seatId: seat.id,
5732
+ kind,
5733
+ status: "pending",
5734
+ attempts: 0,
5735
+ createdAt: now,
5736
+ updatedAt: now,
5737
+ ...extra
5738
+ });
5739
+ const ops = [
5740
+ membershipUpdate("networkSeat", String(seat.id), {
5741
+ recipientName: name,
5742
+ recipientEmailNormalized: email,
5743
+ recipientClaimId: claimId,
5744
+ status: "pending_acceptance",
5745
+ regiftedAt: now,
5746
+ formerRecipients: [...history, former],
5747
+ revision: Number(seat.revision) + 1,
5748
+ updatedAt: now
5749
+ }),
5750
+ { t: "retract", ns: "networkSeat", id: String(seat.id), attrs: cleared },
5751
+ membershipUpdate("networkEntitlement", String(entitlement.id), { status: "pending_acceptance", revision: Number(entitlement.revision) + 1, updatedAt: now }),
5752
+ { t: "retract", ns: "networkEntitlement", id: String(entitlement.id), attrs: cleared },
5753
+ { t: "delete", ns: "networkSeatRecipient", id: String(seat.recipientClaimId) },
5754
+ membershipUpdate("networkSeatRecipient", claimId, {
5755
+ id: claimId,
5756
+ appId: input.appId,
5757
+ chapterId: input.chapterId,
5758
+ environment: input.environment,
5759
+ runtime: input.runtime,
5760
+ seatId: seat.id,
5761
+ recipientEmailNormalized: email,
5762
+ createdAt: now
5763
+ }),
5764
+ ...stale.map((row2) => membershipUpdate("membershipEffect", String(row2.id), { status: "complete", updatedAt: now })),
5765
+ effect(inviteId, "invite_named_recipient"),
5766
+ ...bound ? [effect(releaseId, "release_named_recipient", { recipientUserId: former.recipientUserId, recipientApplicationId: former.recipientApplicationId })] : []
5767
+ ];
5768
+ try {
5769
+ await db.transact(ops, { mutationId: `named-seat-regift:${seat.id}:${generation}`, guards: [
5770
+ { ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: seat.status } },
5771
+ { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
5772
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
5773
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
5774
+ ...stale.map((row2) => ({ ns: "membershipEffect", id: String(row2.id), exists: true, equals: { status: "pending" } })),
5775
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
5776
+ ] });
5777
+ } catch (error) {
5778
+ if (error.code !== "transact_guard_failed") throw error;
5779
+ const current = await membershipLoad(db, "networkSeat", input.seatId);
5780
+ requireMembershipScope(current, input);
5781
+ if (current.recipientEmailNormalized === email && current.status === "pending_acceptance") return receiptFor(current, former.recipientApplicationId ?? null);
5782
+ const claim = await membershipLoad(db, "networkSeatRecipient", claimId);
5783
+ if (claim && claim.seatId !== seat.id) membershipFail("named_seat_recipient_unavailable");
5784
+ membershipFail("named_seat_regift_retry_required");
5785
+ }
5786
+ const saved = await membershipLoad(db, "networkSeat", input.seatId);
5787
+ requireMembershipScope(saved, input);
5788
+ if (saved.recipientEmailNormalized !== email || saved.status !== "pending_acceptance") membershipFail("named_seat_regift_retry_required");
5789
+ return receiptFor(saved, former.recipientApplicationId ?? null);
5790
+ }
5791
+
5664
5792
  // src/membership-seat-offer.ts
5665
5793
  async function namedSeatOfferDigest(offer) {
5666
5794
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -5704,7 +5832,10 @@ async function readNamedSeatOffer(db, input, now) {
5704
5832
  recipientState: namedSeatRecipientState(seat),
5705
5833
  ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
5706
5834
  ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
5707
- ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
5835
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {},
5836
+ ...seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && !namedSeatInvitationOpen(member) ? { invitationHeld: true } : {},
5837
+ ...namedSeatRegiftable(seat) ? { regiftable: true } : {},
5838
+ ...at(seat.regiftedAt) !== void 0 ? { regiftedAt: at(seat.regiftedAt) } : {}
5708
5839
  } } : {}
5709
5840
  };
5710
5841
  return {
@@ -5752,10 +5883,14 @@ async function readMembershipChapterEffect(db, input, now) {
5752
5883
  requireMembershipScope(seat, input);
5753
5884
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5754
5885
  requireMembershipScope(member, input);
5755
- if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
5886
+ if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !MEMBERSHIP_CHAPTER_EFFECT_KINDS.includes(input.kind)) throw new Error("invalid chapter effect");
5887
+ const released = input.kind === "release_named_recipient";
5888
+ const recipientUserId = released ? effect.recipientUserId : seat.recipientUserId;
5889
+ const recipientApplicationId = released ? effect.recipientApplicationId : seat.recipientApplicationId;
5756
5890
  let applicable;
5757
- if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
5891
+ if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && namedSeatInvitationOpen(member);
5758
5892
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
5893
+ else if (released) applicable = typeof recipientApplicationId === "string" && seat.recipientApplicationId !== recipientApplicationId;
5759
5894
  else applicable = (await readNamedSeatAccess(db, {
5760
5895
  ...input,
5761
5896
  recipientUserId: String(seat.recipientUserId),
@@ -5765,8 +5900,8 @@ async function readMembershipChapterEffect(db, input, now) {
5765
5900
  ...input,
5766
5901
  recipientName: String(seat.recipientName),
5767
5902
  recipientEmail: String(seat.recipientEmailNormalized),
5768
- recipientUserId: typeof seat.recipientUserId === "string" ? seat.recipientUserId : null,
5769
- recipientApplicationId: typeof seat.recipientApplicationId === "string" ? seat.recipientApplicationId : null,
5903
+ recipientUserId: typeof recipientUserId === "string" ? recipientUserId : null,
5904
+ recipientApplicationId: typeof recipientApplicationId === "string" ? recipientApplicationId : null,
5770
5905
  acceptanceEndsAt: Number(seat.acceptanceEndsAt),
5771
5906
  termEndsAt: Number(seat.termEndsAt),
5772
5907
  applicable
@@ -5787,7 +5922,8 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
5787
5922
  "seat-accept": recipient,
5788
5923
  "seat-approve": recipient,
5789
5924
  "seat-decline": recipient,
5790
- "seat-effect": ["seatId", "effectId", "kind"]
5925
+ "seat-effect": ["seatId", "effectId", "kind"],
5926
+ "seat-regift": ["seatId", "recipientName", "recipientEmail"]
5791
5927
  };
5792
5928
  async function handleMembershipSeatAction(action, db, peer, input, now, stripeSecretName) {
5793
5929
  if (!peer.seatPriceId) throw new Error("named seats are not configured");
@@ -5812,6 +5948,12 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5812
5948
  kind: String(input.kind)
5813
5949
  }, now);
5814
5950
  if (action === "seat-offer") return readNamedSeatOffer(db, target, now);
5951
+ if (action === "seat-regift") return regiftNamedSeat(db, {
5952
+ ...target,
5953
+ seatId: String(input.seatId),
5954
+ recipientName: String(input.recipientName),
5955
+ recipientEmail: String(input.recipientEmail)
5956
+ }, now);
5815
5957
  if (action === "seat-reservations") {
5816
5958
  const seat = await reserveNamedMembershipSeat(db, {
5817
5959
  ...target,
@@ -6189,6 +6331,18 @@ async function requestNamedSeatOperatorRefund(options) {
6189
6331
  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");
6190
6332
  return seat;
6191
6333
  }
6334
+ async function requestNamedSeatRegift(options) {
6335
+ const result = await requestAuthority(options, "seat-regift", {
6336
+ applicationId: options.applicationId,
6337
+ skuId: options.skuId,
6338
+ seatId: options.seatId,
6339
+ recipientName: options.recipientName,
6340
+ recipientEmail: options.recipientEmail
6341
+ });
6342
+ const seat = result.seat;
6343
+ if (!seat || seat.seatId !== options.seatId || seat.status !== "pending_acceptance" || typeof seat.recipientName !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(seat.recipientEmail)) || !Number.isSafeInteger(seat.regiftedAt) || seat.releasedRecipientApplicationId !== null && typeof seat.releasedRecipientApplicationId !== "string") throw new Error("invalid named seat regift receipt");
6344
+ return seat;
6345
+ }
6192
6346
  async function requestNamedSeatReservation(options) {
6193
6347
  const result = await requestAuthority(options, "seat-reservations", {
6194
6348
  applicationId: options.applicationId,
@@ -6434,7 +6588,7 @@ async function drainMembershipEffects(db, options, now) {
6434
6588
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
6435
6589
  await loadMembershipAuthorityState(db, scope2);
6436
6590
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
6437
- let pending = 0, complete = 0, more = false;
6591
+ let pending = 0, complete = 0, held = 0, more = false;
6438
6592
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
6439
6593
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
6440
6594
  more ||= seats.length > limit;
@@ -6470,6 +6624,10 @@ async function drainMembershipEffects(db, options, now) {
6470
6624
  requireMembershipScope(seat, target);
6471
6625
  if (seat.primaryMembershipId !== member.id) throw new Error("membership seat scope mismatch");
6472
6626
  }
6627
+ if (effect.kind === "invite_named_recipient" && seat?.acceptedAt === void 0 && !namedSeatInvitationOpen(member) && !["refund_pending", "refunded"].includes(String(seat?.status))) {
6628
+ held++;
6629
+ continue;
6630
+ }
6473
6631
  try {
6474
6632
  const attempts = Number(effect.attempts);
6475
6633
  if (!Number.isSafeInteger(attempts) || attempts < 0) throw new Error("invalid membership effect attempt");
@@ -6493,9 +6651,10 @@ async function drainMembershipEffects(db, options, now) {
6493
6651
  break;
6494
6652
  case "invite_named_recipient":
6495
6653
  case "grant_named_recipient_access":
6496
- case "revoke_named_recipient_access": {
6654
+ case "revoke_named_recipient_access":
6655
+ case "release_named_recipient": {
6497
6656
  if (!seat) throw new Error("named seat effect missing seat");
6498
- if (effect.kind !== "revoke_named_recipient_access" && ["refund_pending", "refunded"].includes(String(seat.status))) break;
6657
+ if (["invite_named_recipient", "grant_named_recipient_access"].includes(String(effect.kind)) && ["refund_pending", "refunded"].includes(String(seat.status))) break;
6499
6658
  if (!options.deliverChapterEffect) throw new Error("chapter effect receiver unavailable");
6500
6659
  const receipt = await options.deliverChapterEffect({ ...target, effectId: String(effect.id), seatId: String(effect.seatId), kind: effect.kind });
6501
6660
  if (receipt.effectId !== effect.id || receipt.complete !== true) throw new Error("chapter effect receipt unconfirmed");
@@ -6513,7 +6672,7 @@ async function drainMembershipEffects(db, options, now) {
6513
6672
  pending++;
6514
6673
  }
6515
6674
  }
6516
- return { complete, pending, more };
6675
+ return { complete, pending, held, more };
6517
6676
  }
6518
6677
 
6519
6678
  // src/schema-membership-offers.ts
@@ -6683,7 +6842,9 @@ var membershipAuthorityEntities = {
6683
6842
  ...times(),
6684
6843
  refundReason: attr9("string", true),
6685
6844
  refundedCents: attr9("number", true),
6686
- providerRefundIds: attr9("json", true)
6845
+ providerRefundIds: attr9("json", true),
6846
+ regiftedAt: attr9("number", true),
6847
+ formerRecipients: attr9("json", true)
6687
6848
  } },
6688
6849
  networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
6689
6850
  membershipEffect: { attrs: {
@@ -6698,6 +6859,8 @@ var membershipAuthorityEntities = {
6698
6859
  attempts: attr9("number"),
6699
6860
  providerObjectId: key2(true),
6700
6861
  error: attr9("string", true),
6862
+ recipientUserId: key2(true),
6863
+ recipientApplicationId: key2(true),
6701
6864
  createdAt: attr9("number"),
6702
6865
  updatedAt: attr9("number", false, false, true)
6703
6866
  } }