@odla-ai/chapter 0.47.8 → 0.47.10

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.
@@ -1525,12 +1525,12 @@ function validateControlledApplication(chapter, document) {
1525
1525
  return [...document.application.required, ...document.application.optional].filter((field) => !declared.has(field)).map((field) => `document.application field "${field}" is not declared by this chapter schema`);
1526
1526
  }
1527
1527
  function groupAttrs(document) {
1528
- const first4 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1528
+ const first5 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1529
1529
  const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1530
1530
  return {
1531
- ...first4 ? { standardPriceCents: first4.priceCents } : {},
1532
- currency: first4?.currency ?? "usd",
1533
- interval: first4?.interval ?? "year",
1531
+ ...first5 ? { standardPriceCents: first5.priceCents } : {},
1532
+ currency: first5?.currency ?? "usd",
1533
+ interval: first5?.interval ?? "year",
1534
1534
  foundingDiscountCents: 0,
1535
1535
  ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1536
1536
  ...document.policy ?? {},
@@ -1877,6 +1877,22 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1877
1877
  }
1878
1878
  }
1879
1879
 
1880
+ // src/worker-routes-health.ts
1881
+ var handleHealth = async (_req, url, env, ctx) => {
1882
+ if (url.pathname !== "/api/health") return null;
1883
+ const response = json({ ok: true });
1884
+ response.headers.set("cache-control", "no-store");
1885
+ for (const [name, value] of Object.entries({
1886
+ "x-odla-app-id": env.ODLA_APP_ID,
1887
+ "x-odla-chapter-id": ctx.chapter.id,
1888
+ "x-odla-data-environment": env.ODLA_ENV,
1889
+ "x-odla-runtime": env.ODLA_RUNTIME || "default"
1890
+ })) {
1891
+ if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name, value);
1892
+ }
1893
+ return response;
1894
+ };
1895
+
1880
1896
  // src/worker-routes.ts
1881
1897
  async function memberSessionApplication(db, chapterId, identity) {
1882
1898
  let apps = (await db.query({ applications: { $: {
@@ -1900,7 +1916,6 @@ async function memberSessionApplication(db, chapterId, identity) {
1900
1916
  source: app
1901
1917
  };
1902
1918
  }
1903
- var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
1904
1919
  var handleConfig = async (_req, url, env, ctx) => {
1905
1920
  if (url.pathname !== "/api/config") return null;
1906
1921
  try {
@@ -4498,12 +4513,130 @@ async function resolveStripeWebhookSecret(db, runtime) {
4498
4513
 
4499
4514
  // src/payments-route-support.ts
4500
4515
  var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
4501
- function paymentLineItems(group) {
4516
+ function paymentLineItems(group, tier) {
4502
4517
  const standard = Number(group.standardPriceCents ?? 0);
4503
4518
  const discount = Number(group.foundingDiscountCents ?? 0);
4519
+ const legacyTier = !tier || tier.id === "standard" && tier.priceCents === standard && tier.stripePriceId === group.stripePriceId;
4520
+ if (!legacyTier) {
4521
+ return { standardCents: tier.priceCents, discountCents: 0, dueTodayCents: tier.priceCents };
4522
+ }
4504
4523
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
4505
4524
  }
4506
4525
 
4526
+ // src/subscription-checkout.ts
4527
+ var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
4528
+ function mismatch() {
4529
+ const error = new Error("subscription checkout intent does not match this checkout");
4530
+ error.code = "checkout_intent_mismatch";
4531
+ throw error;
4532
+ }
4533
+ function intentAttrs(input) {
4534
+ return {
4535
+ id: input.applicationId,
4536
+ applicationId: input.applicationId,
4537
+ appId: input.appId,
4538
+ chapterId: input.chapterId,
4539
+ environment: input.environment,
4540
+ ...input.runtime ? { runtime: input.runtime } : {},
4541
+ groupId: input.groupId,
4542
+ tierId: input.tierId,
4543
+ priceId: input.priceId,
4544
+ priceCents: input.priceCents,
4545
+ currency: input.currency,
4546
+ interval: input.interval,
4547
+ purchaserEmailNormalized: input.emailNormalized,
4548
+ ...input.existingCustomerId ? { requestedCustomerId: input.existingCustomerId } : {},
4549
+ status: "created",
4550
+ createdAt: input.now,
4551
+ updatedAt: input.now
4552
+ };
4553
+ }
4554
+ function matchesIntent(stored, expected) {
4555
+ return [
4556
+ "id",
4557
+ "applicationId",
4558
+ "appId",
4559
+ "chapterId",
4560
+ "environment",
4561
+ "runtime",
4562
+ "groupId",
4563
+ "tierId",
4564
+ "priceId",
4565
+ "priceCents",
4566
+ "currency",
4567
+ "interval",
4568
+ "purchaserEmailNormalized",
4569
+ "requestedCustomerId"
4570
+ ].every((key) => stored[key] === expected[key]);
4571
+ }
4572
+ async function loadIntent(db, id) {
4573
+ return first3((await db.query({
4574
+ subscriptionCheckoutIntents: { $: { where: { id }, limit: 1 } }
4575
+ })).subscriptionCheckoutIntents);
4576
+ }
4577
+ async function startSubscriptionCheckout(db, provider, input) {
4578
+ const expected = intentAttrs(input);
4579
+ let stored = await loadIntent(db, input.applicationId);
4580
+ if (!stored) {
4581
+ const write = await db.transact([{
4582
+ t: "update",
4583
+ ns: "subscriptionCheckoutIntents",
4584
+ id: input.applicationId,
4585
+ attrs: expected
4586
+ }], {
4587
+ mutationId: `subscription-checkout-intent:${input.applicationId}`,
4588
+ guards: [{ ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: false }]
4589
+ });
4590
+ stored = write.duplicate ? await loadIntent(db, input.applicationId) : expected;
4591
+ }
4592
+ if (!stored || !matchesIntent(stored, expected)) mismatch();
4593
+ if (stored.status !== "created" && stored.status !== "checkout_ready") mismatch();
4594
+ const result = await provider.createSubscription({
4595
+ appId: input.appId,
4596
+ chapterId: input.chapterId,
4597
+ environment: input.environment,
4598
+ ...input.runtime ? { runtime: input.runtime } : {},
4599
+ applicationId: input.applicationId,
4600
+ groupId: input.groupId,
4601
+ email: input.email,
4602
+ name: input.name,
4603
+ priceId: input.priceId,
4604
+ existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
4605
+ });
4606
+ if (stored.providerCustomerId && stored.providerCustomerId !== result.customerId || stored.providerSubscriptionId && stored.providerSubscriptionId !== result.subscriptionId) mismatch();
4607
+ await db.transact([
4608
+ {
4609
+ t: "update",
4610
+ ns: "subscriptionCheckoutIntents",
4611
+ id: input.applicationId,
4612
+ attrs: {
4613
+ status: "checkout_ready",
4614
+ providerCustomerId: result.customerId,
4615
+ providerSubscriptionId: result.subscriptionId,
4616
+ updatedAt: input.now
4617
+ }
4618
+ },
4619
+ {
4620
+ t: "update",
4621
+ ns: "applications",
4622
+ id: input.applicationId,
4623
+ attrs: {
4624
+ stripeCustomerId: result.customerId,
4625
+ stripeSubscriptionId: result.subscriptionId,
4626
+ ...input.runtime ? { stripeRuntime: input.runtime } : {},
4627
+ refundPolicyAckAt: input.now
4628
+ }
4629
+ }
4630
+ ], {
4631
+ mutationId: `subscription-checkout-ready:${input.applicationId}:${result.subscriptionId}`,
4632
+ guards: [
4633
+ { ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: true, equals: { status: stored.status } },
4634
+ { ns: "applications", id: input.applicationId, exists: true, equals: { status: "submitted" } }
4635
+ ]
4636
+ });
4637
+ return result;
4638
+ }
4639
+
4507
4640
  // src/member-system-contract.ts
4508
4641
  function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
4509
4642
  const keys = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -4672,35 +4805,29 @@ async function startSubscription(req, env, ctx) {
4672
4805
  const provider = createStripeProvider({ secretKey });
4673
4806
  let result;
4674
4807
  try {
4675
- result = await provider.createSubscription({
4808
+ result = await startSubscriptionCheckout(db, provider, {
4676
4809
  appId: env.ODLA_APP_ID,
4677
4810
  chapterId: ctx.chapter.id,
4678
4811
  environment: env.ODLA_ENV,
4679
4812
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4680
4813
  applicationId,
4681
4814
  groupId: String(group.id),
4815
+ tierId: tier.id,
4816
+ priceCents: tier.priceCents,
4817
+ currency: tier.currency ?? String(group.currency ?? "usd"),
4818
+ interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
4682
4819
  email: String(app.email ?? ""),
4820
+ emailNormalized: normalizeChapterEmail(String(app.email ?? "")),
4683
4821
  name: `${app.firstName ?? ""} ${app.lastName ?? ""}`.trim(),
4684
4822
  priceId: String(priceId),
4685
- existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0
4823
+ existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0,
4824
+ now: Date.now()
4686
4825
  });
4687
4826
  } catch (err) {
4688
- return json({ error: "payment setup failed", code: paymentErrorCode(err) }, 502);
4827
+ const code = paymentErrorCode(err);
4828
+ return json({ error: "payment setup failed", code }, code === "checkout_intent_mismatch" ? 409 : 502);
4689
4829
  }
4690
- await db.transact([
4691
- {
4692
- t: "update",
4693
- ns: "applications",
4694
- id: applicationId,
4695
- attrs: {
4696
- stripeCustomerId: result.customerId,
4697
- stripeSubscriptionId: result.subscriptionId,
4698
- stripeRuntime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4699
- refundPolicyAckAt: Date.now()
4700
- }
4701
- }
4702
- ]);
4703
- return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group) });
4830
+ return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group, tier) });
4704
4831
  }
4705
4832
  async function ingestWebhook(req, env, ctx) {
4706
4833
  const db = ctx.makeDb(env);
@@ -4812,18 +4939,18 @@ var handlePayments = async (req, url, env, ctx) => {
4812
4939
  };
4813
4940
 
4814
4941
  // src/worker-routes-gifts.ts
4815
- var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
4942
+ var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
4816
4943
  var rows2 = (value) => Array.isArray(value) ? value : [];
4817
4944
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
4818
4945
  var userFor = async (req, env, ctx) => {
4819
4946
  const user = await ctx.verifyUser(req, env);
4820
4947
  return user?.email ? user : null;
4821
4948
  };
4822
- var loadClaim = async (db, id) => first3((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
4949
+ var loadClaim = async (db, id) => first4((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
4823
4950
  async function consumeRateLimit(db, chapterId, requester, now) {
4824
4951
  const bucket = await giftRequesterBucket(chapterId, requester);
4825
4952
  const id = `gift-rate:${chapterId}:${bucket}`;
4826
- const current = first3((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
4953
+ const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
4827
4954
  const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
4828
4955
  const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
4829
4956
  if (attemptCount > 10) return false;
@@ -4848,7 +4975,7 @@ var handleGifts = async (req, url, env, ctx) => {
4848
4975
  purchaserEmailNormalized: normalizeChapterEmail(user.email),
4849
4976
  chapterId: ctx.chapter.id
4850
4977
  }, limit: 200 } } });
4851
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4978
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4852
4979
  return json({
4853
4980
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
4854
4981
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
@@ -4870,7 +4997,7 @@ var handleGifts = async (req, url, env, ctx) => {
4870
4997
  const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4871
4998
  const offer = findGiftOffer(control3?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
4872
4999
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
4873
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5000
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4874
5001
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
4875
5002
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
4876
5003
  const emailNormalized = normalizeChapterEmail(user.email);
@@ -4939,16 +5066,16 @@ var handleGifts = async (req, url, env, ctx) => {
4939
5066
  return invalid();
4940
5067
  }
4941
5068
  if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code) || !applicationId) return invalid();
4942
- const app = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
5069
+ const app = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4943
5070
  const emailNormalized = normalizeChapterEmail(user.email);
4944
5071
  if (!app || String(app.groupId ?? "") !== ctx.chapter.id || String(app.emailNormalized ?? "") !== emailNormalized || typeof app.clerkUserId === "string" && app.clerkUserId !== user.userId) return invalid();
4945
5072
  const codeHash = await hashGiftCode(ctx.chapter.id, code);
4946
- const claim = first3((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
5073
+ const claim = first4((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
4947
5074
  if (!claim || claim.chapterId !== ctx.chapter.id) return invalid();
4948
5075
  const sameRedemption = (candidate) => candidate.status === "redeemed" && candidate.applicationId === applicationId && candidate.beneficiaryEmailNormalized === emailNormalized && candidate.beneficiaryUserId === user.userId;
4949
5076
  const success = async () => {
4950
- const current = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4951
- const group2 = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5077
+ const current = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
5078
+ const group2 = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4952
5079
  const state = current && group2 ? await continueJoin(db, ctx, env, url.origin, current, group2) : null;
4953
5080
  return json({
4954
5081
  ok: true,
@@ -4967,7 +5094,7 @@ var handleGifts = async (req, url, env, ctx) => {
4967
5094
  return invalid();
4968
5095
  }
4969
5096
  const target = claim.target;
4970
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5097
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4971
5098
  const tier = group ? findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null) : void 0;
4972
5099
  if (String(app.status ?? "") !== "submitted" || typeof app.stripeSubscriptionId === "string" || typeof app.giftClaimId === "string" || !tier || tierIsFree(tier) || target?.type !== "tier" || target.tierId !== app.tierId) return invalid();
4973
5100
  const redeemed = await redeemGiftClaim(db, claim, {