@odla-ai/chapter 0.50.0 → 0.51.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/ui/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  timeLabel,
27
27
  tzShort,
28
28
  useTierSelection
29
- } from "../chunk-OS46ZE46.js";
29
+ } from "../chunk-UAXWA5BL.js";
30
30
  import {
31
31
  APPLICATION_STAGES,
32
32
  AdminNote,
@@ -26,7 +26,7 @@ import {
26
26
  timeLabel,
27
27
  tzShort,
28
28
  useTierSelection
29
- } from "../../chunk-OS46ZE46.js";
29
+ } from "../../chunk-UAXWA5BL.js";
30
30
  import {
31
31
  BrandStyle,
32
32
  ChapterCopyProvider,
@@ -1702,6 +1702,11 @@ function canApprove(status2, p) {
1702
1702
  }
1703
1703
 
1704
1704
  // src/membership-policy.ts
1705
+ function seatPurchaseDeadline(offering, member) {
1706
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
1707
+ if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
1708
+ return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
1709
+ }
1705
1710
  var fail2 = (field) => {
1706
1711
  throw new TypeError(`invalid membership policy: ${field}`);
1707
1712
  };
@@ -1758,7 +1763,7 @@ function membershipOfferingVersion(value) {
1758
1763
  autoRenew: true,
1759
1764
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1760
1765
  seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1761
- seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
1766
+ ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1762
1767
  requiredDisclosures: ids(input.requiredDisclosures)
1763
1768
  };
1764
1769
  }
@@ -2149,7 +2154,7 @@ async function confirmMembershipPayment(db, input, now) {
2149
2154
  initialTermEndsAt: endsAt,
2150
2155
  termStartedAt: paidAt,
2151
2156
  termEndsAt: endsAt,
2152
- seatPurchaseEndsAt: addMembershipCalendarMonths(Number(paidAt), offering.seatPurchaseWindowCalendarMonths),
2157
+ seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
2153
2158
  autoRenew: true,
2154
2159
  refundedCents: 0,
2155
2160
  revision: 1,
@@ -3684,9 +3689,37 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3684
3689
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
3685
3690
  }
3686
3691
 
3692
+ // src/membership-seat-window.ts
3693
+ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
3694
+ membershipClock(now);
3695
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
3696
+ const state = await loadMembershipAuthorityState(db, scope2);
3697
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
3698
+ let lifted = 0;
3699
+ for (const member of rows5.slice(0, limit)) {
3700
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
3701
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
3702
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
3703
+ const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
3704
+ await db.transact([membershipUpdate("networkMembership", String(member.id), {
3705
+ offeringSnapshot: windowless,
3706
+ seatPurchaseEndsAt: Number(member.termEndsAt),
3707
+ revision: Number(member.revision) + 1,
3708
+ updatedAt: now
3709
+ })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
3710
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3711
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
3712
+ ] });
3713
+ lifted++;
3714
+ }
3715
+ return { lifted, more: rows5.length > limit };
3716
+ }
3717
+
3687
3718
  // src/worker-routes-admin-membership-seats.ts
3688
3719
  var LIST = "/api/admin/membership-seats";
3720
+ var LIFT = "/api/admin/membership-seats/lift-purchase-window";
3689
3721
  var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
3722
+ var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
3690
3723
  var PUBLIC = [
3691
3724
  "id",
3692
3725
  "appId",
@@ -3711,11 +3744,11 @@ var PUBLIC = [
3711
3744
  ];
3712
3745
  var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
3713
3746
  var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3714
- const one = url.pathname.match(ONE);
3715
- if (url.pathname !== LIST && !one) return null;
3747
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
3748
+ if (url.pathname !== LIST && !one && !lift) return null;
3716
3749
  const operator = ctx.membershipAuthorityOperator;
3717
3750
  if (!operator) return json({ error: "not found" }, 404);
3718
- if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3751
+ if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3719
3752
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3720
3753
  if (!user) return json({ error: "unauthorized" }, 401);
3721
3754
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
@@ -3725,6 +3758,24 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3725
3758
  } catch {
3726
3759
  return json({ error: "the membership authority is not hosted by this deployment" }, 404);
3727
3760
  }
3761
+ if (lift) {
3762
+ let body;
3763
+ try {
3764
+ body = await req.json();
3765
+ } catch {
3766
+ return json({ error: "chapterId and runtime are required" }, 400);
3767
+ }
3768
+ const appId = body.appId ?? body.chapterId;
3769
+ 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)) {
3770
+ return json({ error: "chapterId and runtime are required" }, 400);
3771
+ }
3772
+ try {
3773
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
3774
+ } catch (error) {
3775
+ const code = error.code;
3776
+ return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3777
+ }
3778
+ }
3728
3779
  if (!one) {
3729
3780
  const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
3730
3781
  return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
@@ -8772,15 +8823,16 @@ async function readNamedSeatOffer(db, input, now) {
8772
8823
  const offering = membershipOfferingVersion(member.offeringSnapshot);
8773
8824
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
8774
8825
  if (seat) requireMembershipScope(seat, input);
8826
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
8775
8827
  const offer = {
8776
8828
  membershipId: id6,
8777
8829
  amountCents: offering.seatPriceCents,
8778
8830
  currency: offering.currency,
8779
8831
  interval: offering.interval,
8780
8832
  termEndsAt: Number(member.termEndsAt),
8781
- purchaseEndsAt: Number(member.seatPurchaseEndsAt),
8833
+ purchaseEndsAt,
8782
8834
  autoRenew: member.autoRenew === true,
8783
- eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= Number(member.seatPurchaseEndsAt) && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
8835
+ eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
8784
8836
  ...seat ? { seat: {
8785
8837
  seatId: String(seat.id),
8786
8838
  status: String(seat.status),
@@ -8788,7 +8840,7 @@ async function readNamedSeatOffer(db, input, now) {
8788
8840
  recipientEmail: String(seat.recipientEmailNormalized)
8789
8841
  } } : {}
8790
8842
  };
8791
- return { ...offer, digest: await namedSeatOfferDigest(offer) };
8843
+ return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
8792
8844
  }
8793
8845
 
8794
8846
  // src/named-seat-request.ts
@@ -13796,7 +13848,11 @@ async function reconcileMembershipRenewal(db, input, now) {
13796
13848
  membershipUpdate("networkMembership", id6, {
13797
13849
  status: status2,
13798
13850
  graceEndsAt,
13799
- ...paid ? { termStartedAt: start, termEndsAt: end } : {},
13851
+ ...paid ? {
13852
+ termStartedAt: start,
13853
+ termEndsAt: end,
13854
+ ...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
13855
+ } : {},
13800
13856
  revision: Number(member.revision) + 1,
13801
13857
  updatedAt: now
13802
13858
  }),
@@ -13956,10 +14012,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
13956
14012
  const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
13957
14013
  let seat = await membershipLoad(db, "networkSeat", id6);
13958
14014
  if (!seat) {
13959
- 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 > Number(member.seatPurchaseEndsAt) || !Number.isSafeInteger(member.seatPurchaseEndsAt) || member.namedSeatId !== void 0) {
14015
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
14016
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
14017
+ 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) {
13960
14018
  membershipFail("named_seat_purchase_unavailable");
13961
14019
  }
13962
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
13963
14020
  const attrs = {
13964
14021
  id: id6,
13965
14022
  ...membershipScope(input),
@@ -13978,7 +14035,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13978
14035
  status: "payment_pending",
13979
14036
  preparedAt: now,
13980
14037
  termEndsAt: member.termEndsAt,
13981
- seatPurchaseEndsAt: member.seatPurchaseEndsAt,
14038
+ seatPurchaseEndsAt: purchaseEndsAt,
13982
14039
  offeringSnapshot: offering,
13983
14040
  revision: 1,
13984
14041
  createdAt: now,
@@ -14180,7 +14237,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
14180
14237
  }
14181
14238
  if (seat.purchasedAt === void 0) {
14182
14239
  if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
14183
- const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= Number(member.seatPurchaseEndsAt) && now < Number(member.termEndsAt);
14240
+ const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
14184
14241
  const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
14185
14242
  try {
14186
14243
  await db.transact([
@@ -15399,7 +15456,7 @@ async function requestNamedSeatReservation(options) {
15399
15456
  async function requestNamedSeatOffer(options) {
15400
15457
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
15401
15458
  const offer = result.seat;
15402
- 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.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
15459
+ 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");
15403
15460
  return offer;
15404
15461
  }
15405
15462
  async function requestNamedSeatInvitation(options) {