@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.js
CHANGED
|
@@ -497,30 +497,30 @@ async function checkBillingTerms(secretKey, input, resume, now, createMissing) {
|
|
|
497
497
|
const terms2 = subscriptionBillingTerms(input.billingTerms);
|
|
498
498
|
if (!resume && terms2.initialTermEndsAt !== void 0 && (terms2.initialTermEndsAt <= now || terms2.initialTermEndsAt - now > 730 * 864e5)) fail();
|
|
499
499
|
if (!terms2.discount) return;
|
|
500
|
-
const { couponId
|
|
500
|
+
const { couponId, percentOffBps, duration, policyVersionId } = terms2.discount;
|
|
501
501
|
const productId = input.discountProductId;
|
|
502
502
|
if (productId !== void 0 && !/^prod_[A-Za-z0-9_]+$/.test(productId)) fail();
|
|
503
503
|
const metadata = { ...stripePartitionMetadata(input), discountPolicyVersionId: policyVersionId };
|
|
504
504
|
const expansion = { "expand[0]": "applies_to" };
|
|
505
|
-
let result = await stripeRequest({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(
|
|
505
|
+
let result = await stripeRequest({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId)}`, expansion);
|
|
506
506
|
if (createMissing && result.status === 404 && result.body?.error?.code === "resource_missing") {
|
|
507
507
|
result = await stripeRequest(
|
|
508
508
|
{ secretKey },
|
|
509
509
|
"POST",
|
|
510
510
|
"/v1/coupons",
|
|
511
511
|
{
|
|
512
|
-
id:
|
|
512
|
+
id: couponId,
|
|
513
513
|
percent_off: percentOffBps / 100,
|
|
514
514
|
duration,
|
|
515
515
|
metadata,
|
|
516
516
|
...expansion,
|
|
517
517
|
...productId ? { "applies_to[products][0]": productId } : {}
|
|
518
518
|
},
|
|
519
|
-
{ idempotencyKey: `chapter-policy-coupon:${
|
|
519
|
+
{ idempotencyKey: `chapter-policy-coupon:${couponId}` }
|
|
520
520
|
);
|
|
521
521
|
}
|
|
522
522
|
const coupon = result.body;
|
|
523
|
-
if (!result.ok || !coupon || coupon.id !==
|
|
523
|
+
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();
|
|
524
524
|
}
|
|
525
525
|
function subscriptionBillingParams(input, productId) {
|
|
526
526
|
if (!input.billingTerms) return {};
|
|
@@ -1890,7 +1890,7 @@ async function hash(value) {
|
|
|
1890
1890
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
|
|
1891
1891
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1892
1892
|
}
|
|
1893
|
-
async function
|
|
1893
|
+
async function membershipDiscountCouponId(input, policyId) {
|
|
1894
1894
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
1895
1895
|
}
|
|
1896
1896
|
async function load(db, ns, id6) {
|
|
@@ -1916,7 +1916,7 @@ async function restore(row2, input) {
|
|
|
1916
1916
|
discountPolicy: contract.discountPolicy,
|
|
1917
1917
|
approvedFoundingCount: Number(row2.approvedFoundingCount),
|
|
1918
1918
|
now: Number(row2.issuedAt),
|
|
1919
|
-
couponId: await
|
|
1919
|
+
couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
|
|
1920
1920
|
});
|
|
1921
1921
|
if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
|
|
1922
1922
|
const result = {
|
|
@@ -1947,7 +1947,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
1947
1947
|
discountPolicy: policy,
|
|
1948
1948
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1949
1949
|
now: input.now,
|
|
1950
|
-
couponId: await
|
|
1950
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
1951
1951
|
});
|
|
1952
1952
|
const discount = contract.billingTerms.discount;
|
|
1953
1953
|
return {
|
|
@@ -1997,7 +1997,7 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
1997
1997
|
discountPolicy: policy,
|
|
1998
1998
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1999
1999
|
now: input.now,
|
|
2000
|
-
couponId: await
|
|
2000
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
2001
2001
|
});
|
|
2002
2002
|
const value = {
|
|
2003
2003
|
id: input.offerId,
|
|
@@ -4150,6 +4150,239 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4150
4150
|
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4151
4151
|
};
|
|
4152
4152
|
|
|
4153
|
+
// src/membership-discount-reterm.ts
|
|
4154
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4155
|
+
|
|
4156
|
+
// src/subscription-provider-contract.ts
|
|
4157
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4158
|
+
function subscriptionContractMismatch() {
|
|
4159
|
+
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4160
|
+
}
|
|
4161
|
+
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4162
|
+
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();
|
|
4163
|
+
const result = await stripeRequest5({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4164
|
+
const price = result.body;
|
|
4165
|
+
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();
|
|
4166
|
+
return typeof price.product === "string" ? price.product : void 0;
|
|
4167
|
+
}
|
|
4168
|
+
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4169
|
+
const items = sub?.items;
|
|
4170
|
+
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();
|
|
4171
|
+
const invoice = sub.latest_invoice;
|
|
4172
|
+
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4173
|
+
verifySubscriptionBillingResult(sub, input);
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// src/membership-discount-reterm.ts
|
|
4177
|
+
var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4178
|
+
var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
|
|
4179
|
+
var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
|
|
4180
|
+
var couponOf = (discount) => {
|
|
4181
|
+
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4182
|
+
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4183
|
+
};
|
|
4184
|
+
async function retermPolicy(db, scope2, id6) {
|
|
4185
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
|
|
4186
|
+
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4187
|
+
let policy;
|
|
4188
|
+
try {
|
|
4189
|
+
policy = membershipDiscountPolicyVersion(row2.snapshot);
|
|
4190
|
+
} catch {
|
|
4191
|
+
return membershipFail("membership_reterm_policy_ineligible");
|
|
4192
|
+
}
|
|
4193
|
+
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");
|
|
4194
|
+
return policy;
|
|
4195
|
+
}
|
|
4196
|
+
async function attachProviderDiscount(sk, scope2, member, policy, couponId, productId) {
|
|
4197
|
+
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4198
|
+
const read2 = await stripeRequest6({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4199
|
+
const sub = read2.body, items = sub?.items?.data;
|
|
4200
|
+
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4201
|
+
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) {
|
|
4202
|
+
throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
|
|
4203
|
+
}
|
|
4204
|
+
const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
|
|
4205
|
+
if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
|
|
4206
|
+
const present = discounts.map(couponOf);
|
|
4207
|
+
if (present.length === 1 && present[0] === couponId) return false;
|
|
4208
|
+
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4209
|
+
const written = await stripeRequest6({ secretKey: sk }, "POST", path, {
|
|
4210
|
+
"discounts[0][coupon]": couponId,
|
|
4211
|
+
"expand[0]": "discounts",
|
|
4212
|
+
"metadata[discountPolicyVersionId]": policy.id
|
|
4213
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy.id}` });
|
|
4214
|
+
const after = written.body?.discounts;
|
|
4215
|
+
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4216
|
+
membershipFail("membership_reterm_provider_unconfirmed");
|
|
4217
|
+
}
|
|
4218
|
+
return true;
|
|
4219
|
+
}
|
|
4220
|
+
async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
|
|
4221
|
+
membershipClock(now);
|
|
4222
|
+
const limit = input.limit ?? 100;
|
|
4223
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4224
|
+
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4225
|
+
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");
|
|
4226
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4227
|
+
const policy = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4228
|
+
const receipt = { policyVersionId: policy.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
4229
|
+
const skip = (membershipId, reason) => {
|
|
4230
|
+
receipt.skipped.push({ membershipId, reason });
|
|
4231
|
+
};
|
|
4232
|
+
let rows5;
|
|
4233
|
+
if (input.membershipIds) {
|
|
4234
|
+
rows5 = [];
|
|
4235
|
+
for (const id6 of input.membershipIds) {
|
|
4236
|
+
const member = await membershipLoad(db, "networkMembership", id6);
|
|
4237
|
+
if (!member) skip(id6, "not_found");
|
|
4238
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id6, "scope_mismatch");
|
|
4239
|
+
else rows5.push(member);
|
|
4240
|
+
}
|
|
4241
|
+
} else {
|
|
4242
|
+
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4243
|
+
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
4244
|
+
receipt.more = found.length > limit;
|
|
4245
|
+
rows5 = found.slice(0, limit);
|
|
4246
|
+
}
|
|
4247
|
+
for (const member of rows5) {
|
|
4248
|
+
const id6 = String(member.id);
|
|
4249
|
+
if (member.discountPolicyVersionId === policy.id) {
|
|
4250
|
+
receipt.unchanged.push(id6);
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
if (!RETERMABLE.has(String(member.status))) {
|
|
4254
|
+
skip(id6, "status");
|
|
4255
|
+
continue;
|
|
4256
|
+
}
|
|
4257
|
+
if (member.founding !== true) {
|
|
4258
|
+
skip(id6, "not_founding");
|
|
4259
|
+
continue;
|
|
4260
|
+
}
|
|
4261
|
+
if (member.cohort !== policy.cohortLabel) {
|
|
4262
|
+
skip(id6, "cohort_mismatch");
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
const skuId = String(member.baseSkuId);
|
|
4266
|
+
if (!policy.discountEligibleSkuIds.includes(skuId)) {
|
|
4267
|
+
skip(id6, "sku_not_discounted");
|
|
4268
|
+
continue;
|
|
4269
|
+
}
|
|
4270
|
+
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4271
|
+
const renewal = Math.round(base * (1e4 - policy.percentOffBps) / 1e4);
|
|
4272
|
+
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4273
|
+
skip(id6, "would_not_widen");
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
4276
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy.id);
|
|
4277
|
+
let providerUpdated;
|
|
4278
|
+
try {
|
|
4279
|
+
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
4280
|
+
priceId: String(member.providerPriceId),
|
|
4281
|
+
priceCents: base,
|
|
4282
|
+
currency: String(member.currency),
|
|
4283
|
+
interval: member.interval === "month" ? "month" : "year",
|
|
4284
|
+
environment: scope2.environment
|
|
4285
|
+
}, true);
|
|
4286
|
+
if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
|
|
4287
|
+
await prepareSubscriptionBillingTerms(stripeSecretKey, {
|
|
4288
|
+
...scope2,
|
|
4289
|
+
priceCents: base,
|
|
4290
|
+
currency: String(member.currency),
|
|
4291
|
+
discountProductId: productId,
|
|
4292
|
+
billingTerms: {
|
|
4293
|
+
offeringVersionId: String(member.offeringVersionId),
|
|
4294
|
+
discount: { policyVersionId: policy.id, couponId, percentOffBps: policy.percentOffBps, duration: "forever" }
|
|
4295
|
+
}
|
|
4296
|
+
}, false);
|
|
4297
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy, couponId, productId);
|
|
4298
|
+
} catch (error) {
|
|
4299
|
+
const code = error.code;
|
|
4300
|
+
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4301
|
+
skip(id6, "provider_mismatch");
|
|
4302
|
+
continue;
|
|
4303
|
+
}
|
|
4304
|
+
throw error;
|
|
4305
|
+
}
|
|
4306
|
+
try {
|
|
4307
|
+
await db.transact([membershipUpdate("networkMembership", id6, {
|
|
4308
|
+
discountPolicyVersionId: policy.id,
|
|
4309
|
+
discountSnapshot: policy,
|
|
4310
|
+
renewalAmountCents: renewal,
|
|
4311
|
+
revision: Number(member.revision) + 1,
|
|
4312
|
+
updatedAt: now
|
|
4313
|
+
})], {
|
|
4314
|
+
mutationId: `membership-reterm:${id6}:${policy.id}:${member.revision}`,
|
|
4315
|
+
guards: [
|
|
4316
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4317
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4318
|
+
{ ns: "discountPolicyVersion", id: policy.id, exists: true }
|
|
4319
|
+
]
|
|
4320
|
+
});
|
|
4321
|
+
} catch (error) {
|
|
4322
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4323
|
+
return membershipFail("membership_reterm_retry_required");
|
|
4324
|
+
}
|
|
4325
|
+
receipt.retermed.push({
|
|
4326
|
+
membershipId: id6,
|
|
4327
|
+
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4328
|
+
previousRenewalAmountCents: current,
|
|
4329
|
+
renewalAmountCents: renewal,
|
|
4330
|
+
providerUpdated
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
return receipt;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
// src/worker-routes-admin-membership-reterm.ts
|
|
4337
|
+
var PATH4 = "/api/admin/membership-discount/reterm";
|
|
4338
|
+
var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4339
|
+
var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4340
|
+
var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
|
|
4341
|
+
var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
4342
|
+
if (url.pathname !== PATH4) return null;
|
|
4343
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
4344
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
4345
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4346
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4347
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4348
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4349
|
+
let db;
|
|
4350
|
+
try {
|
|
4351
|
+
db = operator.makeDb(env);
|
|
4352
|
+
} catch {
|
|
4353
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4354
|
+
}
|
|
4355
|
+
let body;
|
|
4356
|
+
try {
|
|
4357
|
+
body = await req.json();
|
|
4358
|
+
} catch {
|
|
4359
|
+
return json({ error: BAD }, 400);
|
|
4360
|
+
}
|
|
4361
|
+
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
4362
|
+
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)) {
|
|
4363
|
+
return json({ error: BAD }, 400);
|
|
4364
|
+
}
|
|
4365
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
4366
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
4367
|
+
try {
|
|
4368
|
+
const receipt = await retermMembershipDiscount(
|
|
4369
|
+
db,
|
|
4370
|
+
secret,
|
|
4371
|
+
{ appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
|
|
4372
|
+
{
|
|
4373
|
+
policyVersionId: body.policyVersionId,
|
|
4374
|
+
...body.membershipIds ? { membershipIds: body.membershipIds } : {},
|
|
4375
|
+
...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
|
|
4376
|
+
},
|
|
4377
|
+
Date.now()
|
|
4378
|
+
);
|
|
4379
|
+
return json({ ok: true, ...receipt });
|
|
4380
|
+
} catch (error) {
|
|
4381
|
+
const code = error.code;
|
|
4382
|
+
return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4383
|
+
}
|
|
4384
|
+
};
|
|
4385
|
+
|
|
4153
4386
|
// src/worker-routes-membership-cancel.ts
|
|
4154
4387
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
4155
4388
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4810,7 +5043,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
|
|
|
4810
5043
|
}
|
|
4811
5044
|
|
|
4812
5045
|
// src/admission-grants.ts
|
|
4813
|
-
var
|
|
5046
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
4814
5047
|
var MAX_GRANTS = 2e3;
|
|
4815
5048
|
var MAX_NOTE = 2e3;
|
|
4816
5049
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -4832,10 +5065,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4832
5065
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
4833
5066
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
4834
5067
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
4835
|
-
if (!
|
|
5068
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
4836
5069
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
4837
|
-
if (!
|
|
4838
|
-
if (!
|
|
5070
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
5071
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
4839
5072
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
4840
5073
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
4841
5074
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -4850,7 +5083,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4850
5083
|
const decision = grant.decision;
|
|
4851
5084
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
4852
5085
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
4853
|
-
if (!
|
|
5086
|
+
if (!ID2.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
4854
5087
|
if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
|
|
4855
5088
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
4856
5089
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -5066,7 +5299,7 @@ function chapterFlowFieldContract(chapter, flow, application = chapter.applicati
|
|
|
5066
5299
|
// src/gift-offers.ts
|
|
5067
5300
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
5068
5301
|
var MAX_GIFT_OFFERS = 20;
|
|
5069
|
-
var
|
|
5302
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
5070
5303
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
5071
5304
|
var CURRENCY = /^[a-z]{3}$/;
|
|
5072
5305
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -5099,7 +5332,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5099
5332
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5100
5333
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
5101
5334
|
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
5102
|
-
if (id6 && !
|
|
5335
|
+
if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5103
5336
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5104
5337
|
ids2.add(id6);
|
|
5105
5338
|
const targetRaw = record(item.target);
|
|
@@ -5110,7 +5343,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5110
5343
|
target = { type: "tier", tierId };
|
|
5111
5344
|
} else if (targetRaw?.type === "entitlement") {
|
|
5112
5345
|
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
5113
|
-
if (entitlementKey && !
|
|
5346
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
5114
5347
|
target = { type: "entitlement", entitlementKey };
|
|
5115
5348
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
5116
5349
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -6896,7 +7129,7 @@ async function memberContentDigest(document) {
|
|
|
6896
7129
|
}
|
|
6897
7130
|
|
|
6898
7131
|
// src/member-content-validation-helpers.ts
|
|
6899
|
-
var
|
|
7132
|
+
var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6900
7133
|
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6901
7134
|
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
6902
7135
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
@@ -6936,7 +7169,7 @@ function offerValue(value, path, errors, tiers, audience) {
|
|
|
6936
7169
|
}
|
|
6937
7170
|
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
6938
7171
|
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
6939
|
-
if (tierId && !
|
|
7172
|
+
if (tierId && !ID4.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
6940
7173
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
6941
7174
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
6942
7175
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
@@ -6969,7 +7202,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
6969
7202
|
const type = sourceRaw.type;
|
|
6970
7203
|
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
6971
7204
|
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
6972
|
-
if (appId && !
|
|
7205
|
+
if (appId && !ID4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6973
7206
|
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
6974
7207
|
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
6975
7208
|
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
@@ -7006,7 +7239,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7006
7239
|
const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7007
7240
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7008
7241
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7009
|
-
if (id6 && !
|
|
7242
|
+
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7010
7243
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7011
7244
|
if (ids2.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
|
|
7012
7245
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
@@ -7933,7 +8166,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7933
8166
|
}
|
|
7934
8167
|
|
|
7935
8168
|
// src/worker-routes-network-admission.ts
|
|
7936
|
-
var
|
|
8169
|
+
var PATH5 = "/api/admin/network/admissions";
|
|
7937
8170
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7938
8171
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7939
8172
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -8013,11 +8246,11 @@ async function parseBody(req) {
|
|
|
8013
8246
|
}
|
|
8014
8247
|
}
|
|
8015
8248
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
8016
|
-
if (![
|
|
8249
|
+
if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
8017
8250
|
const auth = await gate2(req, env, ctx);
|
|
8018
8251
|
if ("response" in auth) return auth.response;
|
|
8019
8252
|
const db = auth.db;
|
|
8020
|
-
if (url.pathname ===
|
|
8253
|
+
if (url.pathname === PATH5 && req.method === "GET") {
|
|
8021
8254
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
8022
8255
|
const target2 = targetFor(ctx, targetId2);
|
|
8023
8256
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -8291,10 +8524,10 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8291
8524
|
};
|
|
8292
8525
|
|
|
8293
8526
|
// src/payments-stripe.ts
|
|
8294
|
-
import { stripeRequest as
|
|
8527
|
+
import { stripeRequest as stripeRequest9, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
8295
8528
|
|
|
8296
8529
|
// src/payments-refund.ts
|
|
8297
|
-
import { stripeRequest as
|
|
8530
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
8298
8531
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8299
8532
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8300
8533
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -8304,7 +8537,7 @@ function fail6(code = "refund_target_mismatch") {
|
|
|
8304
8537
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8305
8538
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
8306
8539
|
const call = async (method, path, params, key3) => {
|
|
8307
|
-
const result = await
|
|
8540
|
+
const result = await stripeRequest7({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
8308
8541
|
if (!result.ok) fail6("stripe_error");
|
|
8309
8542
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
|
|
8310
8543
|
return result.body;
|
|
@@ -8378,35 +8611,13 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8378
8611
|
}
|
|
8379
8612
|
|
|
8380
8613
|
// src/subscription-payment-identity.ts
|
|
8381
|
-
import { stripeRequest as
|
|
8382
|
-
|
|
8383
|
-
// src/subscription-provider-contract.ts
|
|
8384
|
-
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
8385
|
-
function subscriptionContractMismatch() {
|
|
8386
|
-
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
8387
|
-
}
|
|
8388
|
-
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
8389
|
-
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();
|
|
8390
|
-
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
8391
|
-
const price = result.body;
|
|
8392
|
-
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();
|
|
8393
|
-
return typeof price.product === "string" ? price.product : void 0;
|
|
8394
|
-
}
|
|
8395
|
-
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
8396
|
-
const items = sub?.items;
|
|
8397
|
-
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();
|
|
8398
|
-
const invoice = sub.latest_invoice;
|
|
8399
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
8400
|
-
verifySubscriptionBillingResult(sub, input);
|
|
8401
|
-
}
|
|
8402
|
-
|
|
8403
|
-
// src/subscription-payment-identity.ts
|
|
8614
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
8404
8615
|
var id3 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8405
8616
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8406
8617
|
let paymentId = id3(invoice.payment_intent);
|
|
8407
8618
|
if (!paymentId) {
|
|
8408
8619
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
8409
|
-
const result2 = await
|
|
8620
|
+
const result2 = await stripeRequest8({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
8410
8621
|
const list = result2.body;
|
|
8411
8622
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8412
8623
|
const link = list.data[0];
|
|
@@ -8415,11 +8626,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8415
8626
|
}
|
|
8416
8627
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8417
8628
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8418
|
-
const result = await
|
|
8629
|
+
const result = await stripeRequest8({ secretKey }, "GET", path);
|
|
8419
8630
|
const payment = result.body;
|
|
8420
8631
|
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();
|
|
8421
8632
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8422
|
-
const tagged = await
|
|
8633
|
+
const tagged = await stripeRequest8(
|
|
8423
8634
|
{ secretKey },
|
|
8424
8635
|
"POST",
|
|
8425
8636
|
path,
|
|
@@ -8431,7 +8642,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8431
8642
|
|
|
8432
8643
|
// src/payments-stripe.ts
|
|
8433
8644
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8434
|
-
return
|
|
8645
|
+
return stripeRequest9(
|
|
8435
8646
|
{ secretKey: sk },
|
|
8436
8647
|
method,
|
|
8437
8648
|
path,
|
|
@@ -10218,6 +10429,7 @@ function dashboardMetricData(points, now) {
|
|
|
10218
10429
|
}
|
|
10219
10430
|
|
|
10220
10431
|
// src/chapter-billing-amount.ts
|
|
10432
|
+
var hasProviderDiscount = (sub, items) => Boolean(sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length));
|
|
10221
10433
|
function chapterBillingAmount(sub, intent, env) {
|
|
10222
10434
|
const unknown = { amountCents: null, listAmountCents: null, interval: "year", annualCents: null };
|
|
10223
10435
|
const items = sub?.items?.data;
|
|
@@ -10234,10 +10446,11 @@ function chapterBillingAmount(sub, intent, env) {
|
|
|
10234
10446
|
const primary = items.filter((it) => it.price?.id === intent.priceId);
|
|
10235
10447
|
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 };
|
|
10236
10448
|
if (terms2?.discount?.duration === "forever") amountCents = listAmountCents - intent.priceCents + Math.round(intent.priceCents * (1e4 - terms2.discount.percentOffBps) / 1e4);
|
|
10449
|
+
else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10237
10450
|
} catch {
|
|
10238
10451
|
amountCents = null;
|
|
10239
10452
|
}
|
|
10240
|
-
} else if (
|
|
10453
|
+
} else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10241
10454
|
return { amountCents, listAmountCents, interval, annualCents: amountCents === null ? null : amountCents * (interval === "month" ? 12 : 1) };
|
|
10242
10455
|
}
|
|
10243
10456
|
function chapterBillingActive(sub, app) {
|
|
@@ -12825,7 +13038,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12825
13038
|
}
|
|
12826
13039
|
|
|
12827
13040
|
// src/worker-routes-network-person-registry.ts
|
|
12828
|
-
var
|
|
13041
|
+
var PATH6 = "/api/admin/network-person-registry";
|
|
12829
13042
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12830
13043
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12831
13044
|
var positiveRevision = (value) => {
|
|
@@ -12850,7 +13063,7 @@ function registryError(error) {
|
|
|
12850
13063
|
return null;
|
|
12851
13064
|
}
|
|
12852
13065
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12853
|
-
if (url.pathname !==
|
|
13066
|
+
if (url.pathname !== PATH6) return null;
|
|
12854
13067
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12855
13068
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12856
13069
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -13596,7 +13809,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
13596
13809
|
import { verifyFederatedRequest as verifyFederatedRequest9 } from "@odla-ai/db";
|
|
13597
13810
|
|
|
13598
13811
|
// src/builder-library-validation.ts
|
|
13599
|
-
var
|
|
13812
|
+
var ID5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13600
13813
|
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
13601
13814
|
var MAX_RESOURCES = 200;
|
|
13602
13815
|
var MAX_MARKDOWN3 = 65536;
|
|
@@ -13657,7 +13870,7 @@ function resourceValue(value, path, errors) {
|
|
|
13657
13870
|
}
|
|
13658
13871
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13659
13872
|
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13660
|
-
if (id6 && !
|
|
13873
|
+
if (id6 && !ID5.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13661
13874
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13662
13875
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13663
13876
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14079,7 +14292,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14079
14292
|
};
|
|
14080
14293
|
|
|
14081
14294
|
// src/membership-cancel-authority.ts
|
|
14082
|
-
import { stripeRequest as
|
|
14295
|
+
import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
14083
14296
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14084
14297
|
membershipClock(now);
|
|
14085
14298
|
const id6 = await networkMembershipId(target);
|
|
@@ -14090,14 +14303,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14090
14303
|
membershipFail("membership_cancellation_unavailable");
|
|
14091
14304
|
}
|
|
14092
14305
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14093
|
-
const read2 = await
|
|
14306
|
+
const read2 = await stripeRequest10({ secretKey: stripeSecret }, "GET", path);
|
|
14094
14307
|
const sub = read2.body;
|
|
14095
14308
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14096
14309
|
...stripePartitionMetadata(target),
|
|
14097
14310
|
applicationId: target.applicationId
|
|
14098
14311
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14099
14312
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14100
|
-
const write = await
|
|
14313
|
+
const write = await stripeRequest10(
|
|
14101
14314
|
{ secretKey: stripeSecret },
|
|
14102
14315
|
"POST",
|
|
14103
14316
|
path,
|
|
@@ -14319,7 +14532,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14319
14532
|
}
|
|
14320
14533
|
|
|
14321
14534
|
// src/membership-provider-lifecycle.ts
|
|
14322
|
-
import { stripeRequest as
|
|
14535
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
14323
14536
|
|
|
14324
14537
|
// src/membership-renewal-authority.ts
|
|
14325
14538
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -14481,7 +14694,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14481
14694
|
}
|
|
14482
14695
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
14483
14696
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
14484
|
-
const result = await
|
|
14697
|
+
const result = await stripeRequest11({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
14485
14698
|
const sub = result.body;
|
|
14486
14699
|
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)) {
|
|
14487
14700
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -14597,7 +14810,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
14597
14810
|
}
|
|
14598
14811
|
|
|
14599
14812
|
// src/membership-seat-stripe.ts
|
|
14600
|
-
import { stripeRequest as
|
|
14813
|
+
import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
|
|
14601
14814
|
var reject7 = () => {
|
|
14602
14815
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
14603
14816
|
};
|
|
@@ -14609,7 +14822,7 @@ var metadataFor = (seat) => ({
|
|
|
14609
14822
|
});
|
|
14610
14823
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
14611
14824
|
async function read(sk, path, params) {
|
|
14612
|
-
const response = await
|
|
14825
|
+
const response = await stripeRequest12({ secretKey: sk }, "GET", path, params);
|
|
14613
14826
|
if (!response.ok) return reject7();
|
|
14614
14827
|
return response.body;
|
|
14615
14828
|
}
|
|
@@ -14632,9 +14845,9 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
14632
14845
|
if (!Array.isArray(discounts) || discounts.length > 5) reject7();
|
|
14633
14846
|
for (const discount of discounts) {
|
|
14634
14847
|
const couponRef = discount.source?.coupon ?? discount.coupon;
|
|
14635
|
-
const
|
|
14636
|
-
if (!
|
|
14637
|
-
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(
|
|
14848
|
+
const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
|
|
14849
|
+
if (!couponId || typeof couponId !== "string") reject7();
|
|
14850
|
+
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
|
|
14638
14851
|
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();
|
|
14639
14852
|
}
|
|
14640
14853
|
return sub;
|
|
@@ -14646,11 +14859,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
14646
14859
|
}
|
|
14647
14860
|
await subscriptionForSeat(sk, seat);
|
|
14648
14861
|
const metadata = metadataFor(seat);
|
|
14649
|
-
const result = seat.providerPaymentIntentId ? await
|
|
14862
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest12(
|
|
14650
14863
|
{ secretKey: sk },
|
|
14651
14864
|
"GET",
|
|
14652
14865
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
14653
|
-
) : await
|
|
14866
|
+
) : await stripeRequest12({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
14654
14867
|
amount: seat.amountCents,
|
|
14655
14868
|
currency: seat.currency,
|
|
14656
14869
|
customer: seat.providerCustomerId,
|
|
@@ -14668,7 +14881,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14668
14881
|
const sub = await subscriptionForSeat(sk, seat);
|
|
14669
14882
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
14670
14883
|
if (existing) return String(existing.id);
|
|
14671
|
-
const result = await
|
|
14884
|
+
const result = await stripeRequest12({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
14672
14885
|
subscription: seat.providerSubscriptionId,
|
|
14673
14886
|
price: seat.priceId,
|
|
14674
14887
|
quantity: 1,
|
|
@@ -15120,7 +15333,7 @@ var respond = (body, status2 = 200) => new Response(JSON.stringify(body), {
|
|
|
15120
15333
|
status: status2,
|
|
15121
15334
|
headers: { "content-type": "application/json", "cache-control": "no-store" }
|
|
15122
15335
|
});
|
|
15123
|
-
var
|
|
15336
|
+
var SAFE2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
15124
15337
|
var SAFE_ERROR = /* @__PURE__ */ new Set([
|
|
15125
15338
|
"offer_identity_conflict",
|
|
15126
15339
|
"membership_authority_unavailable",
|
|
@@ -15168,7 +15381,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15168
15381
|
if (options.stripeSecretName !== void 0 && !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(options.stripeSecretName)) throw new TypeError("invalid Stripe secret slot");
|
|
15169
15382
|
const identities = /* @__PURE__ */ new Set();
|
|
15170
15383
|
for (const peer of peers) {
|
|
15171
|
-
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !
|
|
15384
|
+
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");
|
|
15172
15385
|
const identity = `${peer.appId}:${peer.runtime}`;
|
|
15173
15386
|
if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
|
|
15174
15387
|
identities.add(identity);
|
|
@@ -15202,7 +15415,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15202
15415
|
try {
|
|
15203
15416
|
input = JSON.parse(verified.body);
|
|
15204
15417
|
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] ?? []];
|
|
15205
|
-
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]) :
|
|
15418
|
+
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])))) {
|
|
15206
15419
|
throw new TypeError("invalid request");
|
|
15207
15420
|
}
|
|
15208
15421
|
} catch {
|
|
@@ -15539,7 +15752,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
15539
15752
|
}
|
|
15540
15753
|
|
|
15541
15754
|
// src/membership-seat-cancel.ts
|
|
15542
|
-
import { initStripe as initStripe3, stripeRequest as
|
|
15755
|
+
import { initStripe as initStripe3, stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
|
|
15543
15756
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
15544
15757
|
membershipClock(now);
|
|
15545
15758
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -15567,7 +15780,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
15567
15780
|
}
|
|
15568
15781
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
15569
15782
|
const request = async (method, path, params) => {
|
|
15570
|
-
const result = await
|
|
15783
|
+
const result = await stripeRequest13(
|
|
15571
15784
|
{ secretKey },
|
|
15572
15785
|
method,
|
|
15573
15786
|
path,
|
|
@@ -16426,6 +16639,7 @@ var BUILTIN_ROUTES = [
|
|
|
16426
16639
|
handleAdminNamedSeat,
|
|
16427
16640
|
handleAdminMembershipSeats,
|
|
16428
16641
|
handleAdminMembershipPolicyVersions,
|
|
16642
|
+
handleAdminMembershipReterm,
|
|
16429
16643
|
handleAdminMeetings,
|
|
16430
16644
|
handleAdminScheduling,
|
|
16431
16645
|
// Roster + identity
|
|
@@ -16509,6 +16723,7 @@ export {
|
|
|
16509
16723
|
refundNamedSeatByOperator,
|
|
16510
16724
|
renderActiveSignupControlHtml,
|
|
16511
16725
|
resolveNetworkTargetTransport,
|
|
16726
|
+
retermMembershipDiscount,
|
|
16512
16727
|
retryChapterApprovalEffects,
|
|
16513
16728
|
retryLeaderAdmissionDeliveries,
|
|
16514
16729
|
withActiveSignupControlTheme
|