@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.
@@ -1495,12 +1495,12 @@ function validateControlledApplication(chapter, document) {
1495
1495
  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`);
1496
1496
  }
1497
1497
  function groupAttrs(document) {
1498
- const first4 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1498
+ const first5 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1499
1499
  const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1500
1500
  return {
1501
- ...first4 ? { standardPriceCents: first4.priceCents } : {},
1502
- currency: first4?.currency ?? "usd",
1503
- interval: first4?.interval ?? "year",
1501
+ ...first5 ? { standardPriceCents: first5.priceCents } : {},
1502
+ currency: first5?.currency ?? "usd",
1503
+ interval: first5?.interval ?? "year",
1504
1504
  foundingDiscountCents: 0,
1505
1505
  ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1506
1506
  ...document.policy ?? {},
@@ -1847,6 +1847,22 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1847
1847
  }
1848
1848
  }
1849
1849
 
1850
+ // src/worker-routes-health.ts
1851
+ var handleHealth = async (_req, url, env, ctx) => {
1852
+ if (url.pathname !== "/api/health") return null;
1853
+ const response = json({ ok: true });
1854
+ response.headers.set("cache-control", "no-store");
1855
+ for (const [name, value] of Object.entries({
1856
+ "x-odla-app-id": env.ODLA_APP_ID,
1857
+ "x-odla-chapter-id": ctx.chapter.id,
1858
+ "x-odla-data-environment": env.ODLA_ENV,
1859
+ "x-odla-runtime": env.ODLA_RUNTIME || "default"
1860
+ })) {
1861
+ if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name, value);
1862
+ }
1863
+ return response;
1864
+ };
1865
+
1850
1866
  // src/worker-routes.ts
1851
1867
  async function memberSessionApplication(db, chapterId, identity) {
1852
1868
  let apps = (await db.query({ applications: { $: {
@@ -1870,7 +1886,6 @@ async function memberSessionApplication(db, chapterId, identity) {
1870
1886
  source: app
1871
1887
  };
1872
1888
  }
1873
- var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
1874
1889
  var handleConfig = async (_req, url, env, ctx) => {
1875
1890
  if (url.pathname !== "/api/config") return null;
1876
1891
  try {
@@ -4468,12 +4483,130 @@ async function resolveStripeWebhookSecret(db, runtime) {
4468
4483
 
4469
4484
  // src/payments-route-support.ts
4470
4485
  var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
4471
- function paymentLineItems(group) {
4486
+ function paymentLineItems(group, tier) {
4472
4487
  const standard = Number(group.standardPriceCents ?? 0);
4473
4488
  const discount = Number(group.foundingDiscountCents ?? 0);
4489
+ const legacyTier = !tier || tier.id === "standard" && tier.priceCents === standard && tier.stripePriceId === group.stripePriceId;
4490
+ if (!legacyTier) {
4491
+ return { standardCents: tier.priceCents, discountCents: 0, dueTodayCents: tier.priceCents };
4492
+ }
4474
4493
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
4475
4494
  }
4476
4495
 
4496
+ // src/subscription-checkout.ts
4497
+ var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
4498
+ function mismatch() {
4499
+ const error = new Error("subscription checkout intent does not match this checkout");
4500
+ error.code = "checkout_intent_mismatch";
4501
+ throw error;
4502
+ }
4503
+ function intentAttrs(input) {
4504
+ return {
4505
+ id: input.applicationId,
4506
+ applicationId: input.applicationId,
4507
+ appId: input.appId,
4508
+ chapterId: input.chapterId,
4509
+ environment: input.environment,
4510
+ ...input.runtime ? { runtime: input.runtime } : {},
4511
+ groupId: input.groupId,
4512
+ tierId: input.tierId,
4513
+ priceId: input.priceId,
4514
+ priceCents: input.priceCents,
4515
+ currency: input.currency,
4516
+ interval: input.interval,
4517
+ purchaserEmailNormalized: input.emailNormalized,
4518
+ ...input.existingCustomerId ? { requestedCustomerId: input.existingCustomerId } : {},
4519
+ status: "created",
4520
+ createdAt: input.now,
4521
+ updatedAt: input.now
4522
+ };
4523
+ }
4524
+ function matchesIntent(stored, expected) {
4525
+ return [
4526
+ "id",
4527
+ "applicationId",
4528
+ "appId",
4529
+ "chapterId",
4530
+ "environment",
4531
+ "runtime",
4532
+ "groupId",
4533
+ "tierId",
4534
+ "priceId",
4535
+ "priceCents",
4536
+ "currency",
4537
+ "interval",
4538
+ "purchaserEmailNormalized",
4539
+ "requestedCustomerId"
4540
+ ].every((key) => stored[key] === expected[key]);
4541
+ }
4542
+ async function loadIntent(db, id) {
4543
+ return first3((await db.query({
4544
+ subscriptionCheckoutIntents: { $: { where: { id }, limit: 1 } }
4545
+ })).subscriptionCheckoutIntents);
4546
+ }
4547
+ async function startSubscriptionCheckout(db, provider, input) {
4548
+ const expected = intentAttrs(input);
4549
+ let stored = await loadIntent(db, input.applicationId);
4550
+ if (!stored) {
4551
+ const write = await db.transact([{
4552
+ t: "update",
4553
+ ns: "subscriptionCheckoutIntents",
4554
+ id: input.applicationId,
4555
+ attrs: expected
4556
+ }], {
4557
+ mutationId: `subscription-checkout-intent:${input.applicationId}`,
4558
+ guards: [{ ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: false }]
4559
+ });
4560
+ stored = write.duplicate ? await loadIntent(db, input.applicationId) : expected;
4561
+ }
4562
+ if (!stored || !matchesIntent(stored, expected)) mismatch();
4563
+ if (stored.status !== "created" && stored.status !== "checkout_ready") mismatch();
4564
+ const result = await provider.createSubscription({
4565
+ appId: input.appId,
4566
+ chapterId: input.chapterId,
4567
+ environment: input.environment,
4568
+ ...input.runtime ? { runtime: input.runtime } : {},
4569
+ applicationId: input.applicationId,
4570
+ groupId: input.groupId,
4571
+ email: input.email,
4572
+ name: input.name,
4573
+ priceId: input.priceId,
4574
+ existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
4575
+ });
4576
+ if (stored.providerCustomerId && stored.providerCustomerId !== result.customerId || stored.providerSubscriptionId && stored.providerSubscriptionId !== result.subscriptionId) mismatch();
4577
+ await db.transact([
4578
+ {
4579
+ t: "update",
4580
+ ns: "subscriptionCheckoutIntents",
4581
+ id: input.applicationId,
4582
+ attrs: {
4583
+ status: "checkout_ready",
4584
+ providerCustomerId: result.customerId,
4585
+ providerSubscriptionId: result.subscriptionId,
4586
+ updatedAt: input.now
4587
+ }
4588
+ },
4589
+ {
4590
+ t: "update",
4591
+ ns: "applications",
4592
+ id: input.applicationId,
4593
+ attrs: {
4594
+ stripeCustomerId: result.customerId,
4595
+ stripeSubscriptionId: result.subscriptionId,
4596
+ ...input.runtime ? { stripeRuntime: input.runtime } : {},
4597
+ refundPolicyAckAt: input.now
4598
+ }
4599
+ }
4600
+ ], {
4601
+ mutationId: `subscription-checkout-ready:${input.applicationId}:${result.subscriptionId}`,
4602
+ guards: [
4603
+ { ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: true, equals: { status: stored.status } },
4604
+ { ns: "applications", id: input.applicationId, exists: true, equals: { status: "submitted" } }
4605
+ ]
4606
+ });
4607
+ return result;
4608
+ }
4609
+
4477
4610
  // src/member-system-contract.ts
4478
4611
  function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
4479
4612
  const keys = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -4642,35 +4775,29 @@ async function startSubscription(req, env, ctx) {
4642
4775
  const provider = createStripeProvider({ secretKey });
4643
4776
  let result;
4644
4777
  try {
4645
- result = await provider.createSubscription({
4778
+ result = await startSubscriptionCheckout(db, provider, {
4646
4779
  appId: env.ODLA_APP_ID,
4647
4780
  chapterId: ctx.chapter.id,
4648
4781
  environment: env.ODLA_ENV,
4649
4782
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4650
4783
  applicationId,
4651
4784
  groupId: String(group.id),
4785
+ tierId: tier.id,
4786
+ priceCents: tier.priceCents,
4787
+ currency: tier.currency ?? String(group.currency ?? "usd"),
4788
+ interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
4652
4789
  email: String(app.email ?? ""),
4790
+ emailNormalized: normalizeChapterEmail(String(app.email ?? "")),
4653
4791
  name: `${app.firstName ?? ""} ${app.lastName ?? ""}`.trim(),
4654
4792
  priceId: String(priceId),
4655
- existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0
4793
+ existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0,
4794
+ now: Date.now()
4656
4795
  });
4657
4796
  } catch (err) {
4658
- return json({ error: "payment setup failed", code: paymentErrorCode(err) }, 502);
4797
+ const code = paymentErrorCode(err);
4798
+ return json({ error: "payment setup failed", code }, code === "checkout_intent_mismatch" ? 409 : 502);
4659
4799
  }
4660
- await db.transact([
4661
- {
4662
- t: "update",
4663
- ns: "applications",
4664
- id: applicationId,
4665
- attrs: {
4666
- stripeCustomerId: result.customerId,
4667
- stripeSubscriptionId: result.subscriptionId,
4668
- stripeRuntime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4669
- refundPolicyAckAt: Date.now()
4670
- }
4671
- }
4672
- ]);
4673
- return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group) });
4800
+ return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group, tier) });
4674
4801
  }
4675
4802
  async function ingestWebhook(req, env, ctx) {
4676
4803
  const db = ctx.makeDb(env);
@@ -4782,18 +4909,18 @@ var handlePayments = async (req, url, env, ctx) => {
4782
4909
  };
4783
4910
 
4784
4911
  // src/worker-routes-gifts.ts
4785
- var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
4912
+ var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
4786
4913
  var rows2 = (value) => Array.isArray(value) ? value : [];
4787
4914
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
4788
4915
  var userFor = async (req, env, ctx) => {
4789
4916
  const user = await ctx.verifyUser(req, env);
4790
4917
  return user?.email ? user : null;
4791
4918
  };
4792
- var loadClaim = async (db, id) => first3((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
4919
+ var loadClaim = async (db, id) => first4((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
4793
4920
  async function consumeRateLimit(db, chapterId, requester, now) {
4794
4921
  const bucket = await giftRequesterBucket(chapterId, requester);
4795
4922
  const id = `gift-rate:${chapterId}:${bucket}`;
4796
- const current = first3((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
4923
+ const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
4797
4924
  const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
4798
4925
  const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
4799
4926
  if (attemptCount > 10) return false;
@@ -4818,7 +4945,7 @@ var handleGifts = async (req, url, env, ctx) => {
4818
4945
  purchaserEmailNormalized: normalizeChapterEmail(user.email),
4819
4946
  chapterId: ctx.chapter.id
4820
4947
  }, limit: 200 } } });
4821
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4948
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4822
4949
  return json({
4823
4950
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
4824
4951
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
@@ -4840,7 +4967,7 @@ var handleGifts = async (req, url, env, ctx) => {
4840
4967
  const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4841
4968
  const offer = findGiftOffer(control3?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
4842
4969
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
4843
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4970
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4844
4971
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
4845
4972
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
4846
4973
  const emailNormalized = normalizeChapterEmail(user.email);
@@ -4909,16 +5036,16 @@ var handleGifts = async (req, url, env, ctx) => {
4909
5036
  return invalid();
4910
5037
  }
4911
5038
  if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code) || !applicationId) return invalid();
4912
- const app = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
5039
+ const app = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4913
5040
  const emailNormalized = normalizeChapterEmail(user.email);
4914
5041
  if (!app || String(app.groupId ?? "") !== ctx.chapter.id || String(app.emailNormalized ?? "") !== emailNormalized || typeof app.clerkUserId === "string" && app.clerkUserId !== user.userId) return invalid();
4915
5042
  const codeHash = await hashGiftCode(ctx.chapter.id, code);
4916
- const claim = first3((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
5043
+ const claim = first4((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
4917
5044
  if (!claim || claim.chapterId !== ctx.chapter.id) return invalid();
4918
5045
  const sameRedemption = (candidate) => candidate.status === "redeemed" && candidate.applicationId === applicationId && candidate.beneficiaryEmailNormalized === emailNormalized && candidate.beneficiaryUserId === user.userId;
4919
5046
  const success = async () => {
4920
- const current = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4921
- const group2 = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5047
+ const current = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
5048
+ const group2 = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4922
5049
  const state = current && group2 ? await continueJoin(db, ctx, env, url.origin, current, group2) : null;
4923
5050
  return json({
4924
5051
  ok: true,
@@ -4937,7 +5064,7 @@ var handleGifts = async (req, url, env, ctx) => {
4937
5064
  return invalid();
4938
5065
  }
4939
5066
  const target = claim.target;
4940
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5067
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4941
5068
  const tier = group ? findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null) : void 0;
4942
5069
  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();
4943
5070
  const redeemed = await redeemGiftClaim(db, claim, {