@odla-ai/chapter 0.47.8 → 0.47.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +24 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +144 -32
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +144 -32
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -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
|
|
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
|
-
...
|
|
1502
|
-
currency:
|
|
1503
|
-
interval:
|
|
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 ?? {},
|
|
@@ -4468,12 +4468,130 @@ async function resolveStripeWebhookSecret(db, runtime) {
|
|
|
4468
4468
|
|
|
4469
4469
|
// src/payments-route-support.ts
|
|
4470
4470
|
var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
|
|
4471
|
-
function paymentLineItems(group) {
|
|
4471
|
+
function paymentLineItems(group, tier) {
|
|
4472
4472
|
const standard = Number(group.standardPriceCents ?? 0);
|
|
4473
4473
|
const discount = Number(group.foundingDiscountCents ?? 0);
|
|
4474
|
+
const legacyTier = !tier || tier.id === "standard" && tier.priceCents === standard && tier.stripePriceId === group.stripePriceId;
|
|
4475
|
+
if (!legacyTier) {
|
|
4476
|
+
return { standardCents: tier.priceCents, discountCents: 0, dueTodayCents: tier.priceCents };
|
|
4477
|
+
}
|
|
4474
4478
|
return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
|
|
4475
4479
|
}
|
|
4476
4480
|
|
|
4481
|
+
// src/subscription-checkout.ts
|
|
4482
|
+
var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
|
|
4483
|
+
function mismatch() {
|
|
4484
|
+
const error = new Error("subscription checkout intent does not match this checkout");
|
|
4485
|
+
error.code = "checkout_intent_mismatch";
|
|
4486
|
+
throw error;
|
|
4487
|
+
}
|
|
4488
|
+
function intentAttrs(input) {
|
|
4489
|
+
return {
|
|
4490
|
+
id: input.applicationId,
|
|
4491
|
+
applicationId: input.applicationId,
|
|
4492
|
+
appId: input.appId,
|
|
4493
|
+
chapterId: input.chapterId,
|
|
4494
|
+
environment: input.environment,
|
|
4495
|
+
...input.runtime ? { runtime: input.runtime } : {},
|
|
4496
|
+
groupId: input.groupId,
|
|
4497
|
+
tierId: input.tierId,
|
|
4498
|
+
priceId: input.priceId,
|
|
4499
|
+
priceCents: input.priceCents,
|
|
4500
|
+
currency: input.currency,
|
|
4501
|
+
interval: input.interval,
|
|
4502
|
+
purchaserEmailNormalized: input.emailNormalized,
|
|
4503
|
+
...input.existingCustomerId ? { requestedCustomerId: input.existingCustomerId } : {},
|
|
4504
|
+
status: "created",
|
|
4505
|
+
createdAt: input.now,
|
|
4506
|
+
updatedAt: input.now
|
|
4507
|
+
};
|
|
4508
|
+
}
|
|
4509
|
+
function matchesIntent(stored, expected) {
|
|
4510
|
+
return [
|
|
4511
|
+
"id",
|
|
4512
|
+
"applicationId",
|
|
4513
|
+
"appId",
|
|
4514
|
+
"chapterId",
|
|
4515
|
+
"environment",
|
|
4516
|
+
"runtime",
|
|
4517
|
+
"groupId",
|
|
4518
|
+
"tierId",
|
|
4519
|
+
"priceId",
|
|
4520
|
+
"priceCents",
|
|
4521
|
+
"currency",
|
|
4522
|
+
"interval",
|
|
4523
|
+
"purchaserEmailNormalized",
|
|
4524
|
+
"requestedCustomerId"
|
|
4525
|
+
].every((key) => stored[key] === expected[key]);
|
|
4526
|
+
}
|
|
4527
|
+
async function loadIntent(db, id) {
|
|
4528
|
+
return first3((await db.query({
|
|
4529
|
+
subscriptionCheckoutIntents: { $: { where: { id }, limit: 1 } }
|
|
4530
|
+
})).subscriptionCheckoutIntents);
|
|
4531
|
+
}
|
|
4532
|
+
async function startSubscriptionCheckout(db, provider, input) {
|
|
4533
|
+
const expected = intentAttrs(input);
|
|
4534
|
+
let stored = await loadIntent(db, input.applicationId);
|
|
4535
|
+
if (!stored) {
|
|
4536
|
+
const write = await db.transact([{
|
|
4537
|
+
t: "update",
|
|
4538
|
+
ns: "subscriptionCheckoutIntents",
|
|
4539
|
+
id: input.applicationId,
|
|
4540
|
+
attrs: expected
|
|
4541
|
+
}], {
|
|
4542
|
+
mutationId: `subscription-checkout-intent:${input.applicationId}`,
|
|
4543
|
+
guards: [{ ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: false }]
|
|
4544
|
+
});
|
|
4545
|
+
stored = write.duplicate ? await loadIntent(db, input.applicationId) : expected;
|
|
4546
|
+
}
|
|
4547
|
+
if (!stored || !matchesIntent(stored, expected)) mismatch();
|
|
4548
|
+
if (stored.status !== "created" && stored.status !== "checkout_ready") mismatch();
|
|
4549
|
+
const result = await provider.createSubscription({
|
|
4550
|
+
appId: input.appId,
|
|
4551
|
+
chapterId: input.chapterId,
|
|
4552
|
+
environment: input.environment,
|
|
4553
|
+
...input.runtime ? { runtime: input.runtime } : {},
|
|
4554
|
+
applicationId: input.applicationId,
|
|
4555
|
+
groupId: input.groupId,
|
|
4556
|
+
email: input.email,
|
|
4557
|
+
name: input.name,
|
|
4558
|
+
priceId: input.priceId,
|
|
4559
|
+
existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
|
|
4560
|
+
});
|
|
4561
|
+
if (stored.providerCustomerId && stored.providerCustomerId !== result.customerId || stored.providerSubscriptionId && stored.providerSubscriptionId !== result.subscriptionId) mismatch();
|
|
4562
|
+
await db.transact([
|
|
4563
|
+
{
|
|
4564
|
+
t: "update",
|
|
4565
|
+
ns: "subscriptionCheckoutIntents",
|
|
4566
|
+
id: input.applicationId,
|
|
4567
|
+
attrs: {
|
|
4568
|
+
status: "checkout_ready",
|
|
4569
|
+
providerCustomerId: result.customerId,
|
|
4570
|
+
providerSubscriptionId: result.subscriptionId,
|
|
4571
|
+
updatedAt: input.now
|
|
4572
|
+
}
|
|
4573
|
+
},
|
|
4574
|
+
{
|
|
4575
|
+
t: "update",
|
|
4576
|
+
ns: "applications",
|
|
4577
|
+
id: input.applicationId,
|
|
4578
|
+
attrs: {
|
|
4579
|
+
stripeCustomerId: result.customerId,
|
|
4580
|
+
stripeSubscriptionId: result.subscriptionId,
|
|
4581
|
+
...input.runtime ? { stripeRuntime: input.runtime } : {},
|
|
4582
|
+
refundPolicyAckAt: input.now
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
], {
|
|
4586
|
+
mutationId: `subscription-checkout-ready:${input.applicationId}:${result.subscriptionId}`,
|
|
4587
|
+
guards: [
|
|
4588
|
+
{ ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: true, equals: { status: stored.status } },
|
|
4589
|
+
{ ns: "applications", id: input.applicationId, exists: true, equals: { status: "submitted" } }
|
|
4590
|
+
]
|
|
4591
|
+
});
|
|
4592
|
+
return result;
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4477
4595
|
// src/member-system-contract.ts
|
|
4478
4596
|
function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
4479
4597
|
const keys = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -4642,35 +4760,29 @@ async function startSubscription(req, env, ctx) {
|
|
|
4642
4760
|
const provider = createStripeProvider({ secretKey });
|
|
4643
4761
|
let result;
|
|
4644
4762
|
try {
|
|
4645
|
-
result = await provider
|
|
4763
|
+
result = await startSubscriptionCheckout(db, provider, {
|
|
4646
4764
|
appId: env.ODLA_APP_ID,
|
|
4647
4765
|
chapterId: ctx.chapter.id,
|
|
4648
4766
|
environment: env.ODLA_ENV,
|
|
4649
4767
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4650
4768
|
applicationId,
|
|
4651
4769
|
groupId: String(group.id),
|
|
4770
|
+
tierId: tier.id,
|
|
4771
|
+
priceCents: tier.priceCents,
|
|
4772
|
+
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
4773
|
+
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
4652
4774
|
email: String(app.email ?? ""),
|
|
4775
|
+
emailNormalized: normalizeChapterEmail(String(app.email ?? "")),
|
|
4653
4776
|
name: `${app.firstName ?? ""} ${app.lastName ?? ""}`.trim(),
|
|
4654
4777
|
priceId: String(priceId),
|
|
4655
|
-
existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0
|
|
4778
|
+
existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0,
|
|
4779
|
+
now: Date.now()
|
|
4656
4780
|
});
|
|
4657
4781
|
} catch (err) {
|
|
4658
|
-
|
|
4782
|
+
const code = paymentErrorCode(err);
|
|
4783
|
+
return json({ error: "payment setup failed", code }, code === "checkout_intent_mismatch" ? 409 : 502);
|
|
4659
4784
|
}
|
|
4660
|
-
|
|
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) });
|
|
4785
|
+
return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group, tier) });
|
|
4674
4786
|
}
|
|
4675
4787
|
async function ingestWebhook(req, env, ctx) {
|
|
4676
4788
|
const db = ctx.makeDb(env);
|
|
@@ -4782,18 +4894,18 @@ var handlePayments = async (req, url, env, ctx) => {
|
|
|
4782
4894
|
};
|
|
4783
4895
|
|
|
4784
4896
|
// src/worker-routes-gifts.ts
|
|
4785
|
-
var
|
|
4897
|
+
var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
|
|
4786
4898
|
var rows2 = (value) => Array.isArray(value) ? value : [];
|
|
4787
4899
|
var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
|
|
4788
4900
|
var userFor = async (req, env, ctx) => {
|
|
4789
4901
|
const user = await ctx.verifyUser(req, env);
|
|
4790
4902
|
return user?.email ? user : null;
|
|
4791
4903
|
};
|
|
4792
|
-
var loadClaim = async (db, id) =>
|
|
4904
|
+
var loadClaim = async (db, id) => first4((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
|
|
4793
4905
|
async function consumeRateLimit(db, chapterId, requester, now) {
|
|
4794
4906
|
const bucket = await giftRequesterBucket(chapterId, requester);
|
|
4795
4907
|
const id = `gift-rate:${chapterId}:${bucket}`;
|
|
4796
|
-
const current =
|
|
4908
|
+
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
|
|
4797
4909
|
const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
|
|
4798
4910
|
const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
|
|
4799
4911
|
if (attemptCount > 10) return false;
|
|
@@ -4818,7 +4930,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4818
4930
|
purchaserEmailNormalized: normalizeChapterEmail(user.email),
|
|
4819
4931
|
chapterId: ctx.chapter.id
|
|
4820
4932
|
}, limit: 200 } } });
|
|
4821
|
-
const group =
|
|
4933
|
+
const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4822
4934
|
return json({
|
|
4823
4935
|
offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
|
|
4824
4936
|
claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
|
|
@@ -4840,7 +4952,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4840
4952
|
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4841
4953
|
const offer = findGiftOffer(control3?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
|
|
4842
4954
|
if (!offer) return json({ error: "gift offer is unavailable" }, 409);
|
|
4843
|
-
const group =
|
|
4955
|
+
const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4844
4956
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
4845
4957
|
if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
|
|
4846
4958
|
const emailNormalized = normalizeChapterEmail(user.email);
|
|
@@ -4909,16 +5021,16 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4909
5021
|
return invalid();
|
|
4910
5022
|
}
|
|
4911
5023
|
if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code) || !applicationId) return invalid();
|
|
4912
|
-
const app =
|
|
5024
|
+
const app = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
|
|
4913
5025
|
const emailNormalized = normalizeChapterEmail(user.email);
|
|
4914
5026
|
if (!app || String(app.groupId ?? "") !== ctx.chapter.id || String(app.emailNormalized ?? "") !== emailNormalized || typeof app.clerkUserId === "string" && app.clerkUserId !== user.userId) return invalid();
|
|
4915
5027
|
const codeHash = await hashGiftCode(ctx.chapter.id, code);
|
|
4916
|
-
const claim =
|
|
5028
|
+
const claim = first4((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
|
|
4917
5029
|
if (!claim || claim.chapterId !== ctx.chapter.id) return invalid();
|
|
4918
5030
|
const sameRedemption = (candidate) => candidate.status === "redeemed" && candidate.applicationId === applicationId && candidate.beneficiaryEmailNormalized === emailNormalized && candidate.beneficiaryUserId === user.userId;
|
|
4919
5031
|
const success = async () => {
|
|
4920
|
-
const current =
|
|
4921
|
-
const group2 =
|
|
5032
|
+
const current = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
|
|
5033
|
+
const group2 = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4922
5034
|
const state = current && group2 ? await continueJoin(db, ctx, env, url.origin, current, group2) : null;
|
|
4923
5035
|
return json({
|
|
4924
5036
|
ok: true,
|
|
@@ -4937,7 +5049,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4937
5049
|
return invalid();
|
|
4938
5050
|
}
|
|
4939
5051
|
const target = claim.target;
|
|
4940
|
-
const group =
|
|
5052
|
+
const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4941
5053
|
const tier = group ? findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null) : void 0;
|
|
4942
5054
|
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
5055
|
const redeemed = await redeemGiftClaim(db, claim, {
|