@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.
@@ -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: couponId2, percentOffBps, duration, policyVersionId } = terms2.discount;
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(couponId2)}`, expansion);
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: couponId2,
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:${couponId2}` }
567
+ { idempotencyKey: `chapter-policy-coupon:${couponId}` }
567
568
  );
568
569
  }
569
570
  const coupon = result.body;
570
- if (!result.ok || !coupon || coupon.id !== couponId2 || !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
+ 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 couponId(input, policyId) {
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 couponId(input, contract.discountPolicy?.id)
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 couponId(input, policy?.id)
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 couponId(input, policy?.id)
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 ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
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 (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
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 (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
4880
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
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 (!ID.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
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 ID2 = /^[a-z][a-z0-9-]{1,79}$/;
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 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
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 ID3 = /^[a-z][a-z0-9-]{0,79}$/;
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 && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
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 && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
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 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 PATH4 = "/api/admin/network/admissions";
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 (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
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 === PATH4 && req.method === "GET") {
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 import_stripe10 = require("@odla-ai/stripe");
8570
+ var import_stripe11 = require("@odla-ai/stripe");
8337
8571
 
8338
8572
  // src/payments-refund.ts
8339
- var import_stripe7 = require("@odla-ai/stripe");
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, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
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 import_stripe9 = require("@odla-ai/stripe");
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, import_stripe9.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
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, import_stripe9.stripeRequest)({ secretKey }, "GET", path);
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, import_stripe9.stripeRequest)(
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, import_stripe10.stripeRequest)(
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, import_stripe10.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
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 (sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length)) amountCents = null;
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 PATH5 = "/api/admin/network-person-registry";
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 !== PATH5) return null;
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 import_stripe11 = require("@odla-ai/stripe");
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, import_stripe11.constructStripeEvent)(body, signature, secret);
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 import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
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, import_stripe11.initStripe)({ secretKey });
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 import_stripe11.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
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 ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
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 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
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 import_stripe12 = require("@odla-ai/stripe");
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, import_stripe12.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
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, import_stripe12.stripeRequest)(
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 import_stripe13 = require("@odla-ai/stripe");
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, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
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 import_stripe14 = require("@odla-ai/stripe");
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, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", path, params);
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 couponId2 = typeof couponRef === "string" ? couponRef : couponRef?.id;
14669
- if (!couponId2 || typeof couponId2 !== "string") reject7();
14670
- const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId2)}`, { "expand[0]": "applies_to" });
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, import_stripe14.stripeRequest)(
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, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
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, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
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 SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
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) || !SAFE.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");
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]) : SAFE.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 import_stripe15 = require("@odla-ai/stripe");
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, import_stripe15.stripeRequest)(
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, import_stripe15.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
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