@odla-ai/chapter 0.52.0 → 0.53.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 +25 -0
- package/dist/{chunk-A6W53GVM.js → chunk-4M2PN5K3.js} +4 -2
- package/dist/chunk-4M2PN5K3.js.map +1 -0
- package/dist/{chunk-7N4VDWM5.js → chunk-PTC43J7F.js} +40 -6
- package/dist/{chunk-7N4VDWM5.js.map → chunk-PTC43J7F.js.map} +1 -1
- package/dist/{chunk-7RIT2UN5.js → chunk-TEHCORNC.js} +31 -12
- package/dist/chunk-TEHCORNC.js.map +1 -0
- package/dist/{copy-context-CYLlPzAc.d.ts → copy-context-qvFo6eA7.d.ts} +1 -1
- package/dist/index.cjs +11 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -8
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +6 -5
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +20 -14
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +299 -84
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +34 -2
- package/dist/worker/index.d.ts +34 -2
- package/dist/worker/index.js +292 -77
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7RIT2UN5.js.map +0 -1
- package/dist/chunk-A6W53GVM.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(worker_exports, {
|
|
|
39
39
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
40
40
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
41
41
|
resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
|
|
42
|
+
retermMembershipDiscount: () => retermMembershipDiscount,
|
|
42
43
|
retryChapterApprovalEffects: () => retryChapterApprovalEffects,
|
|
43
44
|
retryLeaderAdmissionDeliveries: () => retryLeaderAdmissionDeliveries,
|
|
44
45
|
withActiveSignupControlTheme: () => withActiveSignupControlTheme
|
|
@@ -544,30 +545,30 @@ async function checkBillingTerms(secretKey, input, resume, now, createMissing) {
|
|
|
544
545
|
const terms2 = subscriptionBillingTerms(input.billingTerms);
|
|
545
546
|
if (!resume && terms2.initialTermEndsAt !== void 0 && (terms2.initialTermEndsAt <= now || terms2.initialTermEndsAt - now > 730 * 864e5)) fail();
|
|
546
547
|
if (!terms2.discount) return;
|
|
547
|
-
const { couponId
|
|
548
|
+
const { couponId, percentOffBps, duration, policyVersionId } = terms2.discount;
|
|
548
549
|
const productId = input.discountProductId;
|
|
549
550
|
if (productId !== void 0 && !/^prod_[A-Za-z0-9_]+$/.test(productId)) fail();
|
|
550
551
|
const metadata = { ...stripePartitionMetadata(input), discountPolicyVersionId: policyVersionId };
|
|
551
552
|
const expansion = { "expand[0]": "applies_to" };
|
|
552
|
-
let result = await (0, import_stripe.stripeRequest)({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(
|
|
553
|
+
let result = await (0, import_stripe.stripeRequest)({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId)}`, expansion);
|
|
553
554
|
if (createMissing && result.status === 404 && result.body?.error?.code === "resource_missing") {
|
|
554
555
|
result = await (0, import_stripe.stripeRequest)(
|
|
555
556
|
{ secretKey },
|
|
556
557
|
"POST",
|
|
557
558
|
"/v1/coupons",
|
|
558
559
|
{
|
|
559
|
-
id:
|
|
560
|
+
id: couponId,
|
|
560
561
|
percent_off: percentOffBps / 100,
|
|
561
562
|
duration,
|
|
562
563
|
metadata,
|
|
563
564
|
...expansion,
|
|
564
565
|
...productId ? { "applies_to[products][0]": productId } : {}
|
|
565
566
|
},
|
|
566
|
-
{ idempotencyKey: `chapter-policy-coupon:${
|
|
567
|
+
{ idempotencyKey: `chapter-policy-coupon:${couponId}` }
|
|
567
568
|
);
|
|
568
569
|
}
|
|
569
570
|
const coupon = result.body;
|
|
570
|
-
if (!result.ok || !coupon || coupon.id !==
|
|
571
|
+
if (!result.ok || !coupon || coupon.id !== couponId || !resume && coupon.valid !== true || coupon.livemode !== (input.environment === "prod") || coupon.percent_off !== percentOffBps / 100 || coupon.duration !== duration || coupon.amount_off != null || (productId ? !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== productId : coupon.applies_to != null) || !Object.entries(metadata).every(([key3, value]) => coupon.metadata?.[key3] === value)) fail();
|
|
571
572
|
}
|
|
572
573
|
function subscriptionBillingParams(input, productId) {
|
|
573
574
|
if (!input.billingTerms) return {};
|
|
@@ -1932,7 +1933,7 @@ async function hash(value) {
|
|
|
1932
1933
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
|
|
1933
1934
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1934
1935
|
}
|
|
1935
|
-
async function
|
|
1936
|
+
async function membershipDiscountCouponId(input, policyId) {
|
|
1936
1937
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
1937
1938
|
}
|
|
1938
1939
|
async function load(db, ns, id6) {
|
|
@@ -1958,7 +1959,7 @@ async function restore(row2, input) {
|
|
|
1958
1959
|
discountPolicy: contract.discountPolicy,
|
|
1959
1960
|
approvedFoundingCount: Number(row2.approvedFoundingCount),
|
|
1960
1961
|
now: Number(row2.issuedAt),
|
|
1961
|
-
couponId: await
|
|
1962
|
+
couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
|
|
1962
1963
|
});
|
|
1963
1964
|
if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
|
|
1964
1965
|
const result = {
|
|
@@ -1989,7 +1990,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
1989
1990
|
discountPolicy: policy,
|
|
1990
1991
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1991
1992
|
now: input.now,
|
|
1992
|
-
couponId: await
|
|
1993
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
1993
1994
|
});
|
|
1994
1995
|
const discount = contract.billingTerms.discount;
|
|
1995
1996
|
return {
|
|
@@ -2039,7 +2040,7 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2039
2040
|
discountPolicy: policy,
|
|
2040
2041
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
2041
2042
|
now: input.now,
|
|
2042
|
-
couponId: await
|
|
2043
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
2043
2044
|
});
|
|
2044
2045
|
const value = {
|
|
2045
2046
|
id: input.offerId,
|
|
@@ -4192,6 +4193,239 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4192
4193
|
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4193
4194
|
};
|
|
4194
4195
|
|
|
4196
|
+
// src/membership-discount-reterm.ts
|
|
4197
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4198
|
+
|
|
4199
|
+
// src/subscription-provider-contract.ts
|
|
4200
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4201
|
+
function subscriptionContractMismatch() {
|
|
4202
|
+
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4203
|
+
}
|
|
4204
|
+
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4205
|
+
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
4206
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4207
|
+
const price = result.body;
|
|
4208
|
+
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
4209
|
+
return typeof price.product === "string" ? price.product : void 0;
|
|
4210
|
+
}
|
|
4211
|
+
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4212
|
+
const items = sub?.items;
|
|
4213
|
+
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
|
|
4214
|
+
const invoice = sub.latest_invoice;
|
|
4215
|
+
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4216
|
+
verifySubscriptionBillingResult(sub, input);
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
// src/membership-discount-reterm.ts
|
|
4220
|
+
var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4221
|
+
var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
|
|
4222
|
+
var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
|
|
4223
|
+
var couponOf = (discount) => {
|
|
4224
|
+
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4225
|
+
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4226
|
+
};
|
|
4227
|
+
async function retermPolicy(db, scope2, id6) {
|
|
4228
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
|
|
4229
|
+
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4230
|
+
let policy;
|
|
4231
|
+
try {
|
|
4232
|
+
policy = membershipDiscountPolicyVersion(row2.snapshot);
|
|
4233
|
+
} catch {
|
|
4234
|
+
return membershipFail("membership_reterm_policy_ineligible");
|
|
4235
|
+
}
|
|
4236
|
+
if (policy.id !== id6 || policy.chapterId !== scope2.chapterId || policy.status !== "published" || policy.benefitDuration.kind !== "while_active" || policy.percentOffBps <= 0 || policy.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
|
|
4237
|
+
return policy;
|
|
4238
|
+
}
|
|
4239
|
+
async function attachProviderDiscount(sk, scope2, member, policy, couponId, productId) {
|
|
4240
|
+
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4241
|
+
const read2 = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4242
|
+
const sub = read2.body, items = sub?.items?.data;
|
|
4243
|
+
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4244
|
+
if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
|
|
4245
|
+
throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
|
|
4246
|
+
}
|
|
4247
|
+
const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
|
|
4248
|
+
if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
|
|
4249
|
+
const present = discounts.map(couponOf);
|
|
4250
|
+
if (present.length === 1 && present[0] === couponId) return false;
|
|
4251
|
+
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4252
|
+
const written = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
4253
|
+
"discounts[0][coupon]": couponId,
|
|
4254
|
+
"expand[0]": "discounts",
|
|
4255
|
+
"metadata[discountPolicyVersionId]": policy.id
|
|
4256
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy.id}` });
|
|
4257
|
+
const after = written.body?.discounts;
|
|
4258
|
+
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4259
|
+
membershipFail("membership_reterm_provider_unconfirmed");
|
|
4260
|
+
}
|
|
4261
|
+
return true;
|
|
4262
|
+
}
|
|
4263
|
+
async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
|
|
4264
|
+
membershipClock(now);
|
|
4265
|
+
const limit = input.limit ?? 100;
|
|
4266
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4267
|
+
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4268
|
+
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id6) => typeof id6 !== "string" || !SAFE.test(id6)))) throw new TypeError("invalid membership ids");
|
|
4269
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4270
|
+
const policy = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4271
|
+
const receipt = { policyVersionId: policy.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
4272
|
+
const skip = (membershipId, reason) => {
|
|
4273
|
+
receipt.skipped.push({ membershipId, reason });
|
|
4274
|
+
};
|
|
4275
|
+
let rows5;
|
|
4276
|
+
if (input.membershipIds) {
|
|
4277
|
+
rows5 = [];
|
|
4278
|
+
for (const id6 of input.membershipIds) {
|
|
4279
|
+
const member = await membershipLoad(db, "networkMembership", id6);
|
|
4280
|
+
if (!member) skip(id6, "not_found");
|
|
4281
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id6, "scope_mismatch");
|
|
4282
|
+
else rows5.push(member);
|
|
4283
|
+
}
|
|
4284
|
+
} else {
|
|
4285
|
+
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4286
|
+
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
4287
|
+
receipt.more = found.length > limit;
|
|
4288
|
+
rows5 = found.slice(0, limit);
|
|
4289
|
+
}
|
|
4290
|
+
for (const member of rows5) {
|
|
4291
|
+
const id6 = String(member.id);
|
|
4292
|
+
if (member.discountPolicyVersionId === policy.id) {
|
|
4293
|
+
receipt.unchanged.push(id6);
|
|
4294
|
+
continue;
|
|
4295
|
+
}
|
|
4296
|
+
if (!RETERMABLE.has(String(member.status))) {
|
|
4297
|
+
skip(id6, "status");
|
|
4298
|
+
continue;
|
|
4299
|
+
}
|
|
4300
|
+
if (member.founding !== true) {
|
|
4301
|
+
skip(id6, "not_founding");
|
|
4302
|
+
continue;
|
|
4303
|
+
}
|
|
4304
|
+
if (member.cohort !== policy.cohortLabel) {
|
|
4305
|
+
skip(id6, "cohort_mismatch");
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
const skuId = String(member.baseSkuId);
|
|
4309
|
+
if (!policy.discountEligibleSkuIds.includes(skuId)) {
|
|
4310
|
+
skip(id6, "sku_not_discounted");
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4314
|
+
const renewal = Math.round(base * (1e4 - policy.percentOffBps) / 1e4);
|
|
4315
|
+
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4316
|
+
skip(id6, "would_not_widen");
|
|
4317
|
+
continue;
|
|
4318
|
+
}
|
|
4319
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy.id);
|
|
4320
|
+
let providerUpdated;
|
|
4321
|
+
try {
|
|
4322
|
+
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
4323
|
+
priceId: String(member.providerPriceId),
|
|
4324
|
+
priceCents: base,
|
|
4325
|
+
currency: String(member.currency),
|
|
4326
|
+
interval: member.interval === "month" ? "month" : "year",
|
|
4327
|
+
environment: scope2.environment
|
|
4328
|
+
}, true);
|
|
4329
|
+
if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
|
|
4330
|
+
await prepareSubscriptionBillingTerms(stripeSecretKey, {
|
|
4331
|
+
...scope2,
|
|
4332
|
+
priceCents: base,
|
|
4333
|
+
currency: String(member.currency),
|
|
4334
|
+
discountProductId: productId,
|
|
4335
|
+
billingTerms: {
|
|
4336
|
+
offeringVersionId: String(member.offeringVersionId),
|
|
4337
|
+
discount: { policyVersionId: policy.id, couponId, percentOffBps: policy.percentOffBps, duration: "forever" }
|
|
4338
|
+
}
|
|
4339
|
+
}, false);
|
|
4340
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy, couponId, productId);
|
|
4341
|
+
} catch (error) {
|
|
4342
|
+
const code = error.code;
|
|
4343
|
+
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4344
|
+
skip(id6, "provider_mismatch");
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
throw error;
|
|
4348
|
+
}
|
|
4349
|
+
try {
|
|
4350
|
+
await db.transact([membershipUpdate("networkMembership", id6, {
|
|
4351
|
+
discountPolicyVersionId: policy.id,
|
|
4352
|
+
discountSnapshot: policy,
|
|
4353
|
+
renewalAmountCents: renewal,
|
|
4354
|
+
revision: Number(member.revision) + 1,
|
|
4355
|
+
updatedAt: now
|
|
4356
|
+
})], {
|
|
4357
|
+
mutationId: `membership-reterm:${id6}:${policy.id}:${member.revision}`,
|
|
4358
|
+
guards: [
|
|
4359
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4360
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4361
|
+
{ ns: "discountPolicyVersion", id: policy.id, exists: true }
|
|
4362
|
+
]
|
|
4363
|
+
});
|
|
4364
|
+
} catch (error) {
|
|
4365
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4366
|
+
return membershipFail("membership_reterm_retry_required");
|
|
4367
|
+
}
|
|
4368
|
+
receipt.retermed.push({
|
|
4369
|
+
membershipId: id6,
|
|
4370
|
+
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4371
|
+
previousRenewalAmountCents: current,
|
|
4372
|
+
renewalAmountCents: renewal,
|
|
4373
|
+
providerUpdated
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
return receipt;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
// src/worker-routes-admin-membership-reterm.ts
|
|
4380
|
+
var PATH4 = "/api/admin/membership-discount/reterm";
|
|
4381
|
+
var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4382
|
+
var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4383
|
+
var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
|
|
4384
|
+
var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
4385
|
+
if (url.pathname !== PATH4) return null;
|
|
4386
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
4387
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
4388
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4389
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4390
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4391
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4392
|
+
let db;
|
|
4393
|
+
try {
|
|
4394
|
+
db = operator.makeDb(env);
|
|
4395
|
+
} catch {
|
|
4396
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4397
|
+
}
|
|
4398
|
+
let body;
|
|
4399
|
+
try {
|
|
4400
|
+
body = await req.json();
|
|
4401
|
+
} catch {
|
|
4402
|
+
return json({ error: BAD }, 400);
|
|
4403
|
+
}
|
|
4404
|
+
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
4405
|
+
if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((id6) => typeof id6 !== "string" || !ID.test(id6))) || body.limit !== void 0 && (!Number.isSafeInteger(body.limit) || Number(body.limit) < 1 || Number(body.limit) > 200)) {
|
|
4406
|
+
return json({ error: BAD }, 400);
|
|
4407
|
+
}
|
|
4408
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
4409
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
4410
|
+
try {
|
|
4411
|
+
const receipt = await retermMembershipDiscount(
|
|
4412
|
+
db,
|
|
4413
|
+
secret,
|
|
4414
|
+
{ appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
|
|
4415
|
+
{
|
|
4416
|
+
policyVersionId: body.policyVersionId,
|
|
4417
|
+
...body.membershipIds ? { membershipIds: body.membershipIds } : {},
|
|
4418
|
+
...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
|
|
4419
|
+
},
|
|
4420
|
+
Date.now()
|
|
4421
|
+
);
|
|
4422
|
+
return json({ ok: true, ...receipt });
|
|
4423
|
+
} catch (error) {
|
|
4424
|
+
const code = error.code;
|
|
4425
|
+
return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4426
|
+
}
|
|
4427
|
+
};
|
|
4428
|
+
|
|
4195
4429
|
// src/worker-routes-membership-cancel.ts
|
|
4196
4430
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
4197
4431
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4852,7 +5086,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
|
|
|
4852
5086
|
}
|
|
4853
5087
|
|
|
4854
5088
|
// src/admission-grants.ts
|
|
4855
|
-
var
|
|
5089
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
4856
5090
|
var MAX_GRANTS = 2e3;
|
|
4857
5091
|
var MAX_NOTE = 2e3;
|
|
4858
5092
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -4874,10 +5108,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4874
5108
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
4875
5109
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
4876
5110
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
4877
|
-
if (!
|
|
5111
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
4878
5112
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
4879
|
-
if (!
|
|
4880
|
-
if (!
|
|
5113
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
5114
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
4881
5115
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
4882
5116
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
4883
5117
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -4892,7 +5126,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4892
5126
|
const decision = grant.decision;
|
|
4893
5127
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
4894
5128
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
4895
|
-
if (!
|
|
5129
|
+
if (!ID2.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
4896
5130
|
if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
|
|
4897
5131
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
4898
5132
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -5108,7 +5342,7 @@ function chapterFlowFieldContract(chapter, flow, application = chapter.applicati
|
|
|
5108
5342
|
// src/gift-offers.ts
|
|
5109
5343
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
5110
5344
|
var MAX_GIFT_OFFERS = 20;
|
|
5111
|
-
var
|
|
5345
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
5112
5346
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
5113
5347
|
var CURRENCY = /^[a-z]{3}$/;
|
|
5114
5348
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -5141,7 +5375,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5141
5375
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5142
5376
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
5143
5377
|
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
5144
|
-
if (id6 && !
|
|
5378
|
+
if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5145
5379
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5146
5380
|
ids2.add(id6);
|
|
5147
5381
|
const targetRaw = record(item.target);
|
|
@@ -5152,7 +5386,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5152
5386
|
target = { type: "tier", tierId };
|
|
5153
5387
|
} else if (targetRaw?.type === "entitlement") {
|
|
5154
5388
|
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
5155
|
-
if (entitlementKey && !
|
|
5389
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
5156
5390
|
target = { type: "entitlement", entitlementKey };
|
|
5157
5391
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
5158
5392
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -6938,7 +7172,7 @@ async function memberContentDigest(document) {
|
|
|
6938
7172
|
}
|
|
6939
7173
|
|
|
6940
7174
|
// src/member-content-validation-helpers.ts
|
|
6941
|
-
var
|
|
7175
|
+
var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6942
7176
|
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6943
7177
|
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
6944
7178
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
@@ -6978,7 +7212,7 @@ function offerValue(value, path, errors, tiers, audience) {
|
|
|
6978
7212
|
}
|
|
6979
7213
|
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
6980
7214
|
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
6981
|
-
if (tierId && !
|
|
7215
|
+
if (tierId && !ID4.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
6982
7216
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
6983
7217
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
6984
7218
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
@@ -7011,7 +7245,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7011
7245
|
const type = sourceRaw.type;
|
|
7012
7246
|
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
7013
7247
|
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
7014
|
-
if (appId && !
|
|
7248
|
+
if (appId && !ID4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7015
7249
|
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
7016
7250
|
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
7017
7251
|
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
@@ -7048,7 +7282,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7048
7282
|
const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7049
7283
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7050
7284
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7051
|
-
if (id6 && !
|
|
7285
|
+
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7052
7286
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7053
7287
|
if (ids2.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
|
|
7054
7288
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
@@ -7975,7 +8209,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7975
8209
|
}
|
|
7976
8210
|
|
|
7977
8211
|
// src/worker-routes-network-admission.ts
|
|
7978
|
-
var
|
|
8212
|
+
var PATH5 = "/api/admin/network/admissions";
|
|
7979
8213
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7980
8214
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7981
8215
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -8055,11 +8289,11 @@ async function parseBody(req) {
|
|
|
8055
8289
|
}
|
|
8056
8290
|
}
|
|
8057
8291
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
8058
|
-
if (![
|
|
8292
|
+
if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
8059
8293
|
const auth = await gate2(req, env, ctx);
|
|
8060
8294
|
if ("response" in auth) return auth.response;
|
|
8061
8295
|
const db = auth.db;
|
|
8062
|
-
if (url.pathname ===
|
|
8296
|
+
if (url.pathname === PATH5 && req.method === "GET") {
|
|
8063
8297
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
8064
8298
|
const target2 = targetFor(ctx, targetId2);
|
|
8065
8299
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -8333,10 +8567,10 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8333
8567
|
};
|
|
8334
8568
|
|
|
8335
8569
|
// src/payments-stripe.ts
|
|
8336
|
-
var
|
|
8570
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
8337
8571
|
|
|
8338
8572
|
// src/payments-refund.ts
|
|
8339
|
-
var
|
|
8573
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
8340
8574
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8341
8575
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8342
8576
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -8346,7 +8580,7 @@ function fail6(code = "refund_target_mismatch") {
|
|
|
8346
8580
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8347
8581
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
8348
8582
|
const call = async (method, path, params, key3) => {
|
|
8349
|
-
const result = await (0,
|
|
8583
|
+
const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
8350
8584
|
if (!result.ok) fail6("stripe_error");
|
|
8351
8585
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
|
|
8352
8586
|
return result.body;
|
|
@@ -8420,35 +8654,13 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8420
8654
|
}
|
|
8421
8655
|
|
|
8422
8656
|
// src/subscription-payment-identity.ts
|
|
8423
|
-
var
|
|
8424
|
-
|
|
8425
|
-
// src/subscription-provider-contract.ts
|
|
8426
|
-
var import_stripe8 = require("@odla-ai/stripe");
|
|
8427
|
-
function subscriptionContractMismatch() {
|
|
8428
|
-
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
8429
|
-
}
|
|
8430
|
-
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
8431
|
-
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
8432
|
-
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
8433
|
-
const price = result.body;
|
|
8434
|
-
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
8435
|
-
return typeof price.product === "string" ? price.product : void 0;
|
|
8436
|
-
}
|
|
8437
|
-
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
8438
|
-
const items = sub?.items;
|
|
8439
|
-
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
|
|
8440
|
-
const invoice = sub.latest_invoice;
|
|
8441
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
8442
|
-
verifySubscriptionBillingResult(sub, input);
|
|
8443
|
-
}
|
|
8444
|
-
|
|
8445
|
-
// src/subscription-payment-identity.ts
|
|
8657
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
8446
8658
|
var id3 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8447
8659
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8448
8660
|
let paymentId = id3(invoice.payment_intent);
|
|
8449
8661
|
if (!paymentId) {
|
|
8450
8662
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
8451
|
-
const result2 = await (0,
|
|
8663
|
+
const result2 = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
8452
8664
|
const list = result2.body;
|
|
8453
8665
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8454
8666
|
const link = list.data[0];
|
|
@@ -8457,11 +8669,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8457
8669
|
}
|
|
8458
8670
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8459
8671
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8460
|
-
const result = await (0,
|
|
8672
|
+
const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", path);
|
|
8461
8673
|
const payment = result.body;
|
|
8462
8674
|
if (!result.ok || payment?.id !== paymentId || id3(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
|
|
8463
8675
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8464
|
-
const tagged = await (0,
|
|
8676
|
+
const tagged = await (0, import_stripe10.stripeRequest)(
|
|
8465
8677
|
{ secretKey },
|
|
8466
8678
|
"POST",
|
|
8467
8679
|
path,
|
|
@@ -8473,7 +8685,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8473
8685
|
|
|
8474
8686
|
// src/payments-stripe.ts
|
|
8475
8687
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8476
|
-
return (0,
|
|
8688
|
+
return (0, import_stripe11.stripeRequest)(
|
|
8477
8689
|
{ secretKey: sk },
|
|
8478
8690
|
method,
|
|
8479
8691
|
path,
|
|
@@ -8592,7 +8804,7 @@ function createStripeProvider(config) {
|
|
|
8592
8804
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
8593
8805
|
},
|
|
8594
8806
|
async ingestWebhook(rawBody, sigHeader) {
|
|
8595
|
-
if (!webhookSecret || !await (0,
|
|
8807
|
+
if (!webhookSecret || !await (0, import_stripe11.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
8596
8808
|
return { ok: false, reason: "bad_signature" };
|
|
8597
8809
|
}
|
|
8598
8810
|
const event = JSON.parse(rawBody);
|
|
@@ -10260,6 +10472,7 @@ function dashboardMetricData(points, now) {
|
|
|
10260
10472
|
}
|
|
10261
10473
|
|
|
10262
10474
|
// src/chapter-billing-amount.ts
|
|
10475
|
+
var hasProviderDiscount = (sub, items) => Boolean(sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length));
|
|
10263
10476
|
function chapterBillingAmount(sub, intent, env) {
|
|
10264
10477
|
const unknown = { amountCents: null, listAmountCents: null, interval: "year", annualCents: null };
|
|
10265
10478
|
const items = sub?.items?.data;
|
|
@@ -10276,10 +10489,11 @@ function chapterBillingAmount(sub, intent, env) {
|
|
|
10276
10489
|
const primary = items.filter((it) => it.price?.id === intent.priceId);
|
|
10277
10490
|
if (primary.length !== 1 || primary[0].quantity !== 1 || primary[0].price.unit_amount !== intent.priceCents || intent.interval !== interval || intent.currency !== "usd") return { ...unknown, listAmountCents, interval };
|
|
10278
10491
|
if (terms2?.discount?.duration === "forever") amountCents = listAmountCents - intent.priceCents + Math.round(intent.priceCents * (1e4 - terms2.discount.percentOffBps) / 1e4);
|
|
10492
|
+
else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10279
10493
|
} catch {
|
|
10280
10494
|
amountCents = null;
|
|
10281
10495
|
}
|
|
10282
|
-
} else if (
|
|
10496
|
+
} else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10283
10497
|
return { amountCents, listAmountCents, interval, annualCents: amountCents === null ? null : amountCents * (interval === "month" ? 12 : 1) };
|
|
10284
10498
|
}
|
|
10285
10499
|
function chapterBillingActive(sub, app) {
|
|
@@ -12858,7 +13072,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12858
13072
|
}
|
|
12859
13073
|
|
|
12860
13074
|
// src/worker-routes-network-person-registry.ts
|
|
12861
|
-
var
|
|
13075
|
+
var PATH6 = "/api/admin/network-person-registry";
|
|
12862
13076
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12863
13077
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12864
13078
|
var positiveRevision = (value) => {
|
|
@@ -12883,7 +13097,7 @@ function registryError(error) {
|
|
|
12883
13097
|
return null;
|
|
12884
13098
|
}
|
|
12885
13099
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12886
|
-
if (url.pathname !==
|
|
13100
|
+
if (url.pathname !== PATH6) return null;
|
|
12887
13101
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12888
13102
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12889
13103
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -13058,7 +13272,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
13058
13272
|
};
|
|
13059
13273
|
|
|
13060
13274
|
// src/worker-routes-network-commercial.ts
|
|
13061
|
-
var
|
|
13275
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
13062
13276
|
|
|
13063
13277
|
// src/worker-routes-network-commercial-parity.ts
|
|
13064
13278
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -13434,7 +13648,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
13434
13648
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
13435
13649
|
}
|
|
13436
13650
|
try {
|
|
13437
|
-
const event = await (0,
|
|
13651
|
+
const event = await (0, import_stripe12.constructStripeEvent)(body, signature, secret);
|
|
13438
13652
|
const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
|
|
13439
13653
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
13440
13654
|
const result = await ingestNetworkCommercialFact(
|
|
@@ -13455,7 +13669,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
13455
13669
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
13456
13670
|
});
|
|
13457
13671
|
} catch (error) {
|
|
13458
|
-
if (error instanceof
|
|
13672
|
+
if (error instanceof import_stripe12.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
13459
13673
|
throw error;
|
|
13460
13674
|
}
|
|
13461
13675
|
};
|
|
@@ -13518,7 +13732,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
13518
13732
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
13519
13733
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
13520
13734
|
try {
|
|
13521
|
-
const stripe = (0,
|
|
13735
|
+
const stripe = (0, import_stripe12.initStripe)({ secretKey });
|
|
13522
13736
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
13523
13737
|
listStripeObjects(stripe, "/v1/customers"),
|
|
13524
13738
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -13532,7 +13746,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
13532
13746
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
13533
13747
|
});
|
|
13534
13748
|
} catch (error) {
|
|
13535
|
-
if (error instanceof
|
|
13749
|
+
if (error instanceof import_stripe12.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
13536
13750
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
13537
13751
|
}
|
|
13538
13752
|
}
|
|
@@ -13629,7 +13843,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
13629
13843
|
var import_db14 = require("@odla-ai/db");
|
|
13630
13844
|
|
|
13631
13845
|
// src/builder-library-validation.ts
|
|
13632
|
-
var
|
|
13846
|
+
var ID5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13633
13847
|
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
13634
13848
|
var MAX_RESOURCES = 200;
|
|
13635
13849
|
var MAX_MARKDOWN3 = 65536;
|
|
@@ -13690,7 +13904,7 @@ function resourceValue(value, path, errors) {
|
|
|
13690
13904
|
}
|
|
13691
13905
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13692
13906
|
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13693
|
-
if (id6 && !
|
|
13907
|
+
if (id6 && !ID5.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13694
13908
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13695
13909
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13696
13910
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14112,7 +14326,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14112
14326
|
};
|
|
14113
14327
|
|
|
14114
14328
|
// src/membership-cancel-authority.ts
|
|
14115
|
-
var
|
|
14329
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
14116
14330
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14117
14331
|
membershipClock(now);
|
|
14118
14332
|
const id6 = await networkMembershipId(target);
|
|
@@ -14123,14 +14337,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14123
14337
|
membershipFail("membership_cancellation_unavailable");
|
|
14124
14338
|
}
|
|
14125
14339
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14126
|
-
const read2 = await (0,
|
|
14340
|
+
const read2 = await (0, import_stripe13.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
14127
14341
|
const sub = read2.body;
|
|
14128
14342
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14129
14343
|
...stripePartitionMetadata(target),
|
|
14130
14344
|
applicationId: target.applicationId
|
|
14131
14345
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14132
14346
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14133
|
-
const write = await (0,
|
|
14347
|
+
const write = await (0, import_stripe13.stripeRequest)(
|
|
14134
14348
|
{ secretKey: stripeSecret },
|
|
14135
14349
|
"POST",
|
|
14136
14350
|
path,
|
|
@@ -14352,7 +14566,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14352
14566
|
}
|
|
14353
14567
|
|
|
14354
14568
|
// src/membership-provider-lifecycle.ts
|
|
14355
|
-
var
|
|
14569
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
14356
14570
|
|
|
14357
14571
|
// src/membership-renewal-authority.ts
|
|
14358
14572
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -14514,7 +14728,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14514
14728
|
}
|
|
14515
14729
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
14516
14730
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
14517
|
-
const result = await (0,
|
|
14731
|
+
const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
14518
14732
|
const sub = result.body;
|
|
14519
14733
|
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
|
|
14520
14734
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -14630,7 +14844,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
14630
14844
|
}
|
|
14631
14845
|
|
|
14632
14846
|
// src/membership-seat-stripe.ts
|
|
14633
|
-
var
|
|
14847
|
+
var import_stripe15 = require("@odla-ai/stripe");
|
|
14634
14848
|
var reject7 = () => {
|
|
14635
14849
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
14636
14850
|
};
|
|
@@ -14642,7 +14856,7 @@ var metadataFor = (seat) => ({
|
|
|
14642
14856
|
});
|
|
14643
14857
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
14644
14858
|
async function read(sk, path, params) {
|
|
14645
|
-
const response = await (0,
|
|
14859
|
+
const response = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
14646
14860
|
if (!response.ok) return reject7();
|
|
14647
14861
|
return response.body;
|
|
14648
14862
|
}
|
|
@@ -14665,9 +14879,9 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
14665
14879
|
if (!Array.isArray(discounts) || discounts.length > 5) reject7();
|
|
14666
14880
|
for (const discount of discounts) {
|
|
14667
14881
|
const couponRef = discount.source?.coupon ?? discount.coupon;
|
|
14668
|
-
const
|
|
14669
|
-
if (!
|
|
14670
|
-
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(
|
|
14882
|
+
const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
|
|
14883
|
+
if (!couponId || typeof couponId !== "string") reject7();
|
|
14884
|
+
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
|
|
14671
14885
|
if (coupon.livemode !== (seat.environment === "prod") || !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== primary.price.product) reject7();
|
|
14672
14886
|
}
|
|
14673
14887
|
return sub;
|
|
@@ -14679,11 +14893,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
14679
14893
|
}
|
|
14680
14894
|
await subscriptionForSeat(sk, seat);
|
|
14681
14895
|
const metadata = metadataFor(seat);
|
|
14682
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
14896
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe15.stripeRequest)(
|
|
14683
14897
|
{ secretKey: sk },
|
|
14684
14898
|
"GET",
|
|
14685
14899
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
14686
|
-
) : await (0,
|
|
14900
|
+
) : await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
14687
14901
|
amount: seat.amountCents,
|
|
14688
14902
|
currency: seat.currency,
|
|
14689
14903
|
customer: seat.providerCustomerId,
|
|
@@ -14701,7 +14915,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14701
14915
|
const sub = await subscriptionForSeat(sk, seat);
|
|
14702
14916
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
14703
14917
|
if (existing) return String(existing.id);
|
|
14704
|
-
const result = await (0,
|
|
14918
|
+
const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
14705
14919
|
subscription: seat.providerSubscriptionId,
|
|
14706
14920
|
price: seat.priceId,
|
|
14707
14921
|
quantity: 1,
|
|
@@ -15153,7 +15367,7 @@ var respond = (body, status2 = 200) => new Response(JSON.stringify(body), {
|
|
|
15153
15367
|
status: status2,
|
|
15154
15368
|
headers: { "content-type": "application/json", "cache-control": "no-store" }
|
|
15155
15369
|
});
|
|
15156
|
-
var
|
|
15370
|
+
var SAFE2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
15157
15371
|
var SAFE_ERROR = /* @__PURE__ */ new Set([
|
|
15158
15372
|
"offer_identity_conflict",
|
|
15159
15373
|
"membership_authority_unavailable",
|
|
@@ -15201,7 +15415,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15201
15415
|
if (options.stripeSecretName !== void 0 && !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(options.stripeSecretName)) throw new TypeError("invalid Stripe secret slot");
|
|
15202
15416
|
const identities = /* @__PURE__ */ new Set();
|
|
15203
15417
|
for (const peer of peers) {
|
|
15204
|
-
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !
|
|
15418
|
+
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE2.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
|
|
15205
15419
|
const identity = `${peer.appId}:${peer.runtime}`;
|
|
15206
15420
|
if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
|
|
15207
15421
|
identities.add(identity);
|
|
@@ -15235,7 +15449,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15235
15449
|
try {
|
|
15236
15450
|
input = JSON.parse(verified.body);
|
|
15237
15451
|
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] ?? []];
|
|
15238
|
-
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]) :
|
|
15452
|
+
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]) : SAFE2.test(input[key3])))) {
|
|
15239
15453
|
throw new TypeError("invalid request");
|
|
15240
15454
|
}
|
|
15241
15455
|
} catch {
|
|
@@ -15572,7 +15786,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
15572
15786
|
}
|
|
15573
15787
|
|
|
15574
15788
|
// src/membership-seat-cancel.ts
|
|
15575
|
-
var
|
|
15789
|
+
var import_stripe16 = require("@odla-ai/stripe");
|
|
15576
15790
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
15577
15791
|
membershipClock(now);
|
|
15578
15792
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -15600,7 +15814,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
15600
15814
|
}
|
|
15601
15815
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
15602
15816
|
const request = async (method, path, params) => {
|
|
15603
|
-
const result = await (0,
|
|
15817
|
+
const result = await (0, import_stripe16.stripeRequest)(
|
|
15604
15818
|
{ secretKey },
|
|
15605
15819
|
method,
|
|
15606
15820
|
path,
|
|
@@ -15613,7 +15827,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
15613
15827
|
let payment;
|
|
15614
15828
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
15615
15829
|
else {
|
|
15616
|
-
const snapshot = await listStripeObjects((0,
|
|
15830
|
+
const snapshot = await listStripeObjects((0, import_stripe16.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
15617
15831
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
15618
15832
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
15619
15833
|
payment = matches2[0];
|
|
@@ -16459,6 +16673,7 @@ var BUILTIN_ROUTES = [
|
|
|
16459
16673
|
handleAdminNamedSeat,
|
|
16460
16674
|
handleAdminMembershipSeats,
|
|
16461
16675
|
handleAdminMembershipPolicyVersions,
|
|
16676
|
+
handleAdminMembershipReterm,
|
|
16462
16677
|
handleAdminMeetings,
|
|
16463
16678
|
handleAdminScheduling,
|
|
16464
16679
|
// Roster + identity
|