@odla-ai/chapter 0.49.0 → 0.50.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/README.md +29 -2
- package/dist/{chunk-KXIGZWG5.js → chunk-4EA5V4JE.js} +855 -614
- package/dist/chunk-4EA5V4JE.js.map +1 -0
- package/dist/{chunk-N2JIKINY.js → chunk-JXX4BFDP.js} +5 -2
- package/dist/chunk-JXX4BFDP.js.map +1 -0
- package/dist/{chunk-KHDO2QFA.js → chunk-OS46ZE46.js} +40 -24
- package/dist/chunk-OS46ZE46.js.map +1 -0
- package/dist/{copy-context-By4Nq7yB.d.ts → copy-context-Ddat__H5.d.ts} +10 -2
- package/dist/index.cjs +106 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -6
- package/dist/index.d.ts +52 -6
- package/dist/index.js +106 -43
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +54 -6
- package/dist/ui/admin/index.js +10 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +11 -3
- package/dist/ui/member/index.d.ts +10 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +520 -261
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +81 -5
- package/dist/worker/index.d.ts +81 -5
- package/dist/worker/index.js +514 -255
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-KHDO2QFA.js.map +0 -1
- package/dist/chunk-KXIGZWG5.js.map +0 -1
- package/dist/chunk-N2JIKINY.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -176,6 +176,7 @@ __export(src_exports, {
|
|
|
176
176
|
planMemberSystemEventReplay: () => planMemberSystemEventReplay,
|
|
177
177
|
planMemberSystemMigration: () => planMemberSystemMigration,
|
|
178
178
|
planPrivateRoleMigration: () => planPrivateRoleMigration,
|
|
179
|
+
previewMembershipCommercialOffer: () => previewMembershipCommercialOffer,
|
|
179
180
|
projectApplicant: () => projectApplicant,
|
|
180
181
|
projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
|
|
181
182
|
projectMemberContent: () => projectMemberContent,
|
|
@@ -208,6 +209,7 @@ __export(src_exports, {
|
|
|
208
209
|
requestMembershipChapterEffect: () => requestMembershipChapterEffect,
|
|
209
210
|
requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
|
|
210
211
|
requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
|
|
212
|
+
requestMembershipOfferPreview: () => requestMembershipOfferPreview,
|
|
211
213
|
requestMembershipPayment: () => requestMembershipPayment,
|
|
212
214
|
requestMembershipRefund: () => requestMembershipRefund,
|
|
213
215
|
requestMembershipRenewal: () => requestMembershipRenewal,
|
|
@@ -219,6 +221,7 @@ __export(src_exports, {
|
|
|
219
221
|
requestNamedSeatDecline: () => requestNamedSeatDecline,
|
|
220
222
|
requestNamedSeatInvitation: () => requestNamedSeatInvitation,
|
|
221
223
|
requestNamedSeatOffer: () => requestNamedSeatOffer,
|
|
224
|
+
requestNamedSeatOperatorRefund: () => requestNamedSeatOperatorRefund,
|
|
222
225
|
requestNamedSeatPayment: () => requestNamedSeatPayment,
|
|
223
226
|
requestNamedSeatRefund: () => requestNamedSeatRefund2,
|
|
224
227
|
requestNamedSeatReservation: () => requestNamedSeatReservation,
|
|
@@ -1713,6 +1716,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1713
1716
|
},
|
|
1714
1717
|
records: {
|
|
1715
1718
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
1719
|
+
invitation: "Invitation",
|
|
1716
1720
|
comms: "Comms",
|
|
1717
1721
|
commsHistory: "Comms history",
|
|
1718
1722
|
scheduling: "Scheduling",
|
|
@@ -1797,7 +1801,9 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1797
1801
|
processing: "Processing\u2026",
|
|
1798
1802
|
payAndContinue: "Pay and continue",
|
|
1799
1803
|
incomplete: "The payment did not complete. Please try again.",
|
|
1800
|
-
consent: "I have read and agree to the pricing and refund policy above."
|
|
1804
|
+
consent: "I have read and agree to the pricing and refund policy above.",
|
|
1805
|
+
foundingMember: "Founding member rate",
|
|
1806
|
+
foundingDiscount: "Founding discount"
|
|
1801
1807
|
},
|
|
1802
1808
|
done: {
|
|
1803
1809
|
label: "You're booked",
|
|
@@ -3354,7 +3360,7 @@ function findTier(tiers2, tierId) {
|
|
|
3354
3360
|
if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
|
|
3355
3361
|
return tiers2.find((tier) => tier.id === tierId) ?? null;
|
|
3356
3362
|
}
|
|
3357
|
-
function joinConfigTiers(tiers2, defaults = {}) {
|
|
3363
|
+
function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
3358
3364
|
return tiers2.map((tier) => ({
|
|
3359
3365
|
id: tier.id,
|
|
3360
3366
|
name: tier.name,
|
|
@@ -3362,7 +3368,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
|
|
|
3362
3368
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
3363
3369
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
3364
3370
|
blurb: tier.blurb ?? "",
|
|
3365
|
-
free: tierIsFree(tier)
|
|
3371
|
+
free: tierIsFree(tier),
|
|
3372
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
3366
3373
|
}));
|
|
3367
3374
|
}
|
|
3368
3375
|
|
|
@@ -3648,24 +3655,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
3648
3655
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
3649
3656
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
3650
3657
|
}
|
|
3651
|
-
async function
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3658
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
3659
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
3660
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
3661
|
+
}
|
|
3662
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3663
|
+
const state = await authoritativeState(db, input);
|
|
3664
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3665
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3666
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3667
|
+
const contract = createMembershipCommercialOffer({
|
|
3668
|
+
offering,
|
|
3669
|
+
discountPolicy: policy,
|
|
3670
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3671
|
+
now: input.now,
|
|
3672
|
+
couponId: await couponId(input, policy?.id)
|
|
3673
|
+
});
|
|
3674
|
+
const discount = contract.billingTerms.discount;
|
|
3675
|
+
return {
|
|
3676
|
+
skuId: input.skuId,
|
|
3677
|
+
offeringVersionId: offering.id,
|
|
3678
|
+
basePriceCents: offering.basePriceCents,
|
|
3679
|
+
currency: offering.currency,
|
|
3680
|
+
interval: offering.interval,
|
|
3681
|
+
founding: contract.founding,
|
|
3682
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
3683
|
+
dueTodayCents: contract.dueTodayCents,
|
|
3684
|
+
renewalCents: contract.renewalCents,
|
|
3685
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
3686
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
async function authoritativeState(db, input) {
|
|
3690
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3655
3691
|
if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
|
|
3656
|
-
|
|
3657
|
-
|
|
3692
|
+
return state;
|
|
3693
|
+
}
|
|
3694
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
3658
3695
|
const rows3 = await db.query({
|
|
3659
3696
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
3660
3697
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
3661
3698
|
});
|
|
3662
3699
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3663
|
-
const offerings = rows3.membershipOfferingVersion.map((
|
|
3664
|
-
const policies = rows3.discountPolicyVersion.map((
|
|
3700
|
+
const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3701
|
+
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
|
|
3665
3702
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3666
|
-
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
3703
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3667
3704
|
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3668
3705
|
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy && policies.filter((item) => item.version === policy.version).length !== 1) reject("ambiguous_membership_policy");
|
|
3706
|
+
return { offering, policy };
|
|
3707
|
+
}
|
|
3708
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
3709
|
+
validate(input);
|
|
3710
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3711
|
+
const state = await authoritativeState(db, input);
|
|
3712
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3713
|
+
if (existing) return restore(existing, input);
|
|
3714
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3669
3715
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3670
3716
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3671
3717
|
const contract = createMembershipCommercialOffer({
|
|
@@ -4852,7 +4898,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4852
4898
|
membershipUpdate("networkSeat", input.seatId, {
|
|
4853
4899
|
status: status2,
|
|
4854
4900
|
purchasedAt: paidAt,
|
|
4855
|
-
acceptanceEndsAt:
|
|
4901
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4856
4902
|
termStartedAt: paidAt,
|
|
4857
4903
|
termEndsAt: member.termEndsAt,
|
|
4858
4904
|
paymentEventId: input.eventId,
|
|
@@ -4900,7 +4946,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
4900
4946
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
4901
4947
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
4902
4948
|
requireMembershipScope(member, target);
|
|
4903
|
-
if (now >= Number(
|
|
4949
|
+
if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
|
|
4904
4950
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
4905
4951
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
4906
4952
|
try {
|
|
@@ -4975,7 +5021,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4975
5021
|
requireMembershipScope(entitlement, input);
|
|
4976
5022
|
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");
|
|
4977
5023
|
if (seat.acceptedAt === void 0) {
|
|
4978
|
-
if (approve ||
|
|
5024
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
4979
5025
|
membershipFail("named_seat_recipient_verification_required");
|
|
4980
5026
|
}
|
|
4981
5027
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -5088,7 +5134,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
5088
5134
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5089
5135
|
requireMembershipScope(member, input);
|
|
5090
5136
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
5091
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "
|
|
5137
|
+
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");
|
|
5092
5138
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
5093
5139
|
await db.transact([
|
|
5094
5140
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -5160,6 +5206,19 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
5160
5206
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
5161
5207
|
}
|
|
5162
5208
|
|
|
5209
|
+
// src/membership-seat-operator-refund.ts
|
|
5210
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
5211
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5212
|
+
requireMembershipScope(seat, input);
|
|
5213
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
5214
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
5215
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5216
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
5217
|
+
throw new Error("named seat refund unconfirmed");
|
|
5218
|
+
}
|
|
5219
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
5220
|
+
}
|
|
5221
|
+
|
|
5163
5222
|
// src/membership-seat-offer.ts
|
|
5164
5223
|
async function namedSeatOfferDigest(offer) {
|
|
5165
5224
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -5172,7 +5231,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
5172
5231
|
"named-only",
|
|
5173
5232
|
"full-upfront",
|
|
5174
5233
|
"auto-renew-coterm",
|
|
5175
|
-
"
|
|
5234
|
+
"no-automatic-refund"
|
|
5176
5235
|
])));
|
|
5177
5236
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5178
5237
|
}
|
|
@@ -5212,7 +5271,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
5212
5271
|
await loadMembershipAuthorityState(db, target);
|
|
5213
5272
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5214
5273
|
requireMembershipScope(member, target);
|
|
5215
|
-
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)
|
|
5274
|
+
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
|
|
5216
5275
|
return {
|
|
5217
5276
|
seatId: input.seatId,
|
|
5218
5277
|
primaryApplicationId: target.applicationId,
|
|
@@ -5242,7 +5301,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
5242
5301
|
requireMembershipScope(member, input);
|
|
5243
5302
|
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");
|
|
5244
5303
|
let applicable;
|
|
5245
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
5304
|
+
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));
|
|
5246
5305
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
5247
5306
|
else applicable = (await readNamedSeatAccess(db, {
|
|
5248
5307
|
...input,
|
|
@@ -5271,6 +5330,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
5271
5330
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
5272
5331
|
"seat-payments": ["seatId", "eventId"],
|
|
5273
5332
|
"seat-refunds": ["seatId", "eventId"],
|
|
5333
|
+
"seat-operator-refund": ["seatId"],
|
|
5274
5334
|
"seat-accept": recipient,
|
|
5275
5335
|
"seat-approve": recipient,
|
|
5276
5336
|
"seat-decline": recipient,
|
|
@@ -5339,6 +5399,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5339
5399
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
5340
5400
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5341
5401
|
}
|
|
5402
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5342
5403
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
5343
5404
|
...target,
|
|
5344
5405
|
priceId: peer.seatPriceId,
|
|
@@ -5427,7 +5488,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5427
5488
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
5428
5489
|
const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
|
|
5429
5490
|
const action = url.pathname.split("/").at(-1);
|
|
5430
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5491
|
+
if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5431
5492
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
5432
5493
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
5433
5494
|
let body;
|
|
@@ -5449,13 +5510,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5449
5510
|
let input;
|
|
5450
5511
|
try {
|
|
5451
5512
|
input = JSON.parse(verified.body);
|
|
5452
|
-
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
5513
|
+
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
5453
5514
|
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
|
|
5454
5515
|
throw new TypeError("invalid request");
|
|
5455
5516
|
}
|
|
5456
5517
|
} catch {
|
|
5457
5518
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
5458
5519
|
}
|
|
5520
|
+
if (action === "offer-preview") {
|
|
5521
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
5522
|
+
appId: peer.appId,
|
|
5523
|
+
chapterId: peer.chapterId,
|
|
5524
|
+
environment: peer.environment,
|
|
5525
|
+
runtime: peer.runtime,
|
|
5526
|
+
skuId: String(input.skuId),
|
|
5527
|
+
now: options.now?.() ?? Date.now()
|
|
5528
|
+
}) });
|
|
5529
|
+
}
|
|
5459
5530
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
5460
5531
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
5461
5532
|
action,
|
|
@@ -5606,6 +5677,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
5606
5677
|
lifetimeMs: 6e4
|
|
5607
5678
|
});
|
|
5608
5679
|
}
|
|
5680
|
+
async function requestMembershipOfferPreview(options) {
|
|
5681
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
5682
|
+
const preview = result.preview;
|
|
5683
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
5684
|
+
return preview;
|
|
5685
|
+
}
|
|
5609
5686
|
async function requestMembershipCheckoutBinding(options) {
|
|
5610
5687
|
const result = await requestAuthority(options, "checkouts", {
|
|
5611
5688
|
applicationId: options.applicationId,
|
|
@@ -5644,6 +5721,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
5644
5721
|
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");
|
|
5645
5722
|
return seat;
|
|
5646
5723
|
}
|
|
5724
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
5725
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5726
|
+
const seat = result.seat;
|
|
5727
|
+
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");
|
|
5728
|
+
return seat;
|
|
5729
|
+
}
|
|
5647
5730
|
async function requestNamedSeatReservation(options) {
|
|
5648
5731
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
5649
5732
|
applicationId: options.applicationId,
|
|
@@ -5888,7 +5971,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5888
5971
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
5889
5972
|
await loadMembershipAuthorityState(db, scope2);
|
|
5890
5973
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
5891
|
-
let
|
|
5974
|
+
let pending = 0, complete = 0, more = false;
|
|
5892
5975
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
5893
5976
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5894
5977
|
more ||= seats.length > limit;
|
|
@@ -5906,26 +5989,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5906
5989
|
}
|
|
5907
5990
|
}
|
|
5908
5991
|
}
|
|
5909
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
5910
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5911
|
-
more ||= seats.length > limit;
|
|
5912
|
-
for (const seat of seats.slice(0, limit)) {
|
|
5913
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
5914
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
5915
|
-
try {
|
|
5916
|
-
await requestNamedSeatRefund(db, {
|
|
5917
|
-
...scope2,
|
|
5918
|
-
applicationId: String(seat.applicationId),
|
|
5919
|
-
skuId: String(seat.skuId),
|
|
5920
|
-
seatId: String(seat.id),
|
|
5921
|
-
reason: "expired"
|
|
5922
|
-
}, now);
|
|
5923
|
-
expired++;
|
|
5924
|
-
} catch {
|
|
5925
|
-
pending++;
|
|
5926
|
-
}
|
|
5927
|
-
}
|
|
5928
|
-
}
|
|
5929
5992
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
5930
5993
|
chapterId: scope2.chapterId,
|
|
5931
5994
|
environment: scope2.environment,
|
|
@@ -5987,7 +6050,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5987
6050
|
pending++;
|
|
5988
6051
|
}
|
|
5989
6052
|
}
|
|
5990
|
-
return { complete, pending,
|
|
6053
|
+
return { complete, pending, more };
|
|
5991
6054
|
}
|
|
5992
6055
|
|
|
5993
6056
|
// src/schema-membership-offers.ts
|