@odla-ai/chapter 0.52.1 → 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.
@@ -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: couponId2, percentOffBps, duration, policyVersionId } = terms2.discount;
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(couponId2)}`, expansion);
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: couponId2,
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:${couponId2}` }
519
+ { idempotencyKey: `chapter-policy-coupon:${couponId}` }
520
520
  );
521
521
  }
522
522
  const coupon = result.body;
523
- 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();
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 couponId(input, policyId) {
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 couponId(input, contract.discountPolicy?.id)
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 couponId(input, policy?.id)
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 couponId(input, policy?.id)
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 ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
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 (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
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 (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
4838
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
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 (!ID.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
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 ID2 = /^[a-z][a-z0-9-]{1,79}$/;
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 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
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 ID3 = /^[a-z][a-z0-9-]{0,79}$/;
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 && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
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 && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
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 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 PATH4 = "/api/admin/network/admissions";
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 (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
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 === PATH4 && req.method === "GET") {
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 stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8527
+ import { stripeRequest as stripeRequest9, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8295
8528
 
8296
8529
  // src/payments-refund.ts
8297
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
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 stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
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 stripeRequest7 } from "@odla-ai/stripe";
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 stripeRequest7({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
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 stripeRequest7({ secretKey }, "GET", path);
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 stripeRequest7(
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 stripeRequest8(
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 (sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length)) amountCents = null;
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 PATH5 = "/api/admin/network-person-registry";
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 !== PATH5) return null;
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 ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
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 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
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 stripeRequest9 } from "@odla-ai/stripe";
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 stripeRequest9({ secretKey: stripeSecret }, "GET", path);
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 stripeRequest9(
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 stripeRequest10 } from "@odla-ai/stripe";
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 stripeRequest10({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
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 stripeRequest11 } from "@odla-ai/stripe";
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 stripeRequest11({ secretKey: sk }, "GET", path, params);
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 couponId2 = typeof couponRef === "string" ? couponRef : couponRef?.id;
14636
- if (!couponId2 || typeof couponId2 !== "string") reject7();
14637
- const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId2)}`, { "expand[0]": "applies_to" });
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 stripeRequest11(
14862
+ const result = seat.providerPaymentIntentId ? await stripeRequest12(
14650
14863
  { secretKey: sk },
14651
14864
  "GET",
14652
14865
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
14653
- ) : await stripeRequest11({ secretKey: sk }, "POST", "/v1/payment_intents", {
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 stripeRequest11({ secretKey: sk }, "POST", "/v1/subscription_items", {
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 SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
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) || !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");
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]) : SAFE.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 stripeRequest12 } from "@odla-ai/stripe";
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 stripeRequest12(
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