@odla-ai/chapter 0.59.0 → 0.60.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.
- package/README.md +31 -2
- package/dist/{chunk-Y2H4VURI.js → chunk-D5UR5H6R.js} +4 -3
- package/dist/chunk-D5UR5H6R.js.map +1 -0
- package/dist/{chunk-WFTWT3V7.js → chunk-J7ES2OWN.js} +22 -6
- package/dist/{chunk-WFTWT3V7.js.map → chunk-J7ES2OWN.js.map} +1 -1
- package/dist/{chunk-GADLTNJF.js → chunk-QNP3UEIL.js} +2 -1
- package/dist/chunk-QNP3UEIL.js.map +1 -0
- package/dist/{copy-context-BuJBQafl.d.ts → copy-context-BP3G1iBd.d.ts} +1 -1
- package/dist/index.cjs +302 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.js +302 -98
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +6 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +3 -3
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +407 -186
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +123 -15
- package/dist/worker/index.d.ts +123 -15
- package/dist/worker/index.js +407 -186
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GADLTNJF.js.map +0 -1
- package/dist/chunk-Y2H4VURI.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ __export(worker_exports, {
|
|
|
42
42
|
deliverMemberContent: () => deliverMemberContent,
|
|
43
43
|
deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
|
|
44
44
|
deliverSignupControl: () => deliverSignupControl,
|
|
45
|
+
disputedChargeCoversSeat: () => disputedChargeCoversSeat,
|
|
45
46
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
46
47
|
grantIncludedNamedSeat: () => grantIncludedNamedSeat,
|
|
47
48
|
liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
|
|
@@ -53,7 +54,9 @@ __export(worker_exports, {
|
|
|
53
54
|
readDisputedCharge: () => readDisputedCharge,
|
|
54
55
|
readNamedSeatOffer: () => readNamedSeatOffer,
|
|
55
56
|
rechargeDisputedMembership: () => rechargeDisputedMembership,
|
|
57
|
+
rechargeDisputedSeat: () => rechargeDisputedSeat,
|
|
56
58
|
reconcileMembershipDispute: () => reconcileMembershipDispute,
|
|
59
|
+
reconcileNamedSeatDispute: () => reconcileNamedSeatDispute,
|
|
57
60
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
58
61
|
regiftNamedSeat: () => regiftNamedSeat,
|
|
59
62
|
releaseNamedSeatRecipientApplications: () => releaseNamedSeatRecipientApplications,
|
|
@@ -1697,9 +1700,9 @@ async function backfillCrm(deps) {
|
|
|
1697
1700
|
}
|
|
1698
1701
|
|
|
1699
1702
|
// src/email-retirement.ts
|
|
1700
|
-
function isRetiredEmailCopy(subject,
|
|
1703
|
+
function isRetiredEmailCopy(subject, text10) {
|
|
1701
1704
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1702
|
-
const bodyCopy =
|
|
1705
|
+
const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1703
1706
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1704
1707
|
}
|
|
1705
1708
|
|
|
@@ -1777,10 +1780,51 @@ function planDelivery(input) {
|
|
|
1777
1780
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1778
1781
|
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1779
1782
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1780
|
-
const
|
|
1783
|
+
const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1781
1784
|
|
|
1782
1785
|
` + renderedText : renderedText;
|
|
1783
|
-
return { deliver: true, transport, to, subject, text:
|
|
1786
|
+
return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// src/email-html.ts
|
|
1790
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1791
|
+
function escapeHtml(s) {
|
|
1792
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1793
|
+
}
|
|
1794
|
+
function lineHtml(line) {
|
|
1795
|
+
let out = "", last = 0;
|
|
1796
|
+
for (const match of line.matchAll(URL_RE)) {
|
|
1797
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
1798
|
+
const url = escapeHtml(match[0]);
|
|
1799
|
+
out += `<a href="${url}">${url}</a>`;
|
|
1800
|
+
last = match.index + match[0].length;
|
|
1801
|
+
}
|
|
1802
|
+
return out + escapeHtml(line.slice(last));
|
|
1803
|
+
}
|
|
1804
|
+
function textToHtml(text10) {
|
|
1805
|
+
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1806
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1807
|
+
return `<!doctype html>
|
|
1808
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1809
|
+
<body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
|
|
1810
|
+
${body}
|
|
1811
|
+
</body></html>`;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// src/email-deliverability.ts
|
|
1815
|
+
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
1816
|
+
function isRecipientFacing(event2) {
|
|
1817
|
+
return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
|
|
1818
|
+
}
|
|
1819
|
+
function listUnsubscribeHeader(replyTo) {
|
|
1820
|
+
const address = replyTo.trim();
|
|
1821
|
+
if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
|
|
1822
|
+
return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
|
|
1823
|
+
}
|
|
1824
|
+
function deliverabilityHeaders(input) {
|
|
1825
|
+
if (!isRecipientFacing(input.event)) return void 0;
|
|
1826
|
+
const value = listUnsubscribeHeader(input.replyTo);
|
|
1827
|
+
return value ? { "List-Unsubscribe": value } : void 0;
|
|
1784
1828
|
}
|
|
1785
1829
|
|
|
1786
1830
|
// src/notify.ts
|
|
@@ -1928,8 +1972,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1928
1972
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1929
1973
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1930
1974
|
function applicantVariables(app) {
|
|
1931
|
-
const
|
|
1932
|
-
return { firstName:
|
|
1975
|
+
const text10 = (v) => typeof v === "string" ? v : "";
|
|
1976
|
+
return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
|
|
1933
1977
|
}
|
|
1934
1978
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1935
1979
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -2054,11 +2098,16 @@ async function sendTemplated(deps, input) {
|
|
|
2054
2098
|
if (decision.transport === "log-only") return { sent: true };
|
|
2055
2099
|
let messageId;
|
|
2056
2100
|
try {
|
|
2101
|
+
const text10 = String(row2.body);
|
|
2102
|
+
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text10);
|
|
2103
|
+
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
2057
2104
|
const response = await deps.sender.send({
|
|
2058
2105
|
from: fromAddress(deps.from, input.group.fromName),
|
|
2059
2106
|
to: [decision.to],
|
|
2060
2107
|
subject: String(row2.subject),
|
|
2061
|
-
text:
|
|
2108
|
+
text: text10,
|
|
2109
|
+
...html ? { html } : {},
|
|
2110
|
+
...headers ? { headers } : {},
|
|
2062
2111
|
replyTo: input.group.replyTo
|
|
2063
2112
|
});
|
|
2064
2113
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
@@ -3663,12 +3712,87 @@ function paymentLineItems(group, tier) {
|
|
|
3663
3712
|
|
|
3664
3713
|
// src/membership-dispute-authority.ts
|
|
3665
3714
|
var import_stripe6 = require("@odla-ai/stripe");
|
|
3715
|
+
|
|
3716
|
+
// src/membership-dispute-transition.ts
|
|
3666
3717
|
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
3667
3718
|
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
3668
3719
|
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
3669
|
-
|
|
3720
|
+
function disputeTransition(row2, fact, eventId, now, terminal) {
|
|
3721
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
3722
|
+
const sameDispute = row2.disputeId === fact.objectId;
|
|
3723
|
+
if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
|
|
3724
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3725
|
+
const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
|
|
3726
|
+
const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
3727
|
+
const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3728
|
+
return {
|
|
3729
|
+
status: next,
|
|
3730
|
+
disputeId: fact.objectId,
|
|
3731
|
+
disputeStatus: status2,
|
|
3732
|
+
disputeEventId: eventId,
|
|
3733
|
+
disputeClosedAt: closed ? now : 0,
|
|
3734
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3735
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3736
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
3737
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
|
|
3738
|
+
revision: Number(row2.revision) + 1,
|
|
3739
|
+
updatedAt: now
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
// src/membership-seat-dispute.ts
|
|
3670
3744
|
var text2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3671
|
-
var
|
|
3745
|
+
var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
|
|
3746
|
+
var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text2(seat.disputeStatus) ? { disputeStatus: text2(seat.disputeStatus) } : {}, applied });
|
|
3747
|
+
function disputedChargeCoversSeat(charge, seat) {
|
|
3748
|
+
return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
|
|
3749
|
+
}
|
|
3750
|
+
async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
|
|
3751
|
+
if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
|
|
3752
|
+
const membership = async (applied, seat) => {
|
|
3753
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3754
|
+
requireMembershipScope(member, input);
|
|
3755
|
+
return {
|
|
3756
|
+
membershipId: id7,
|
|
3757
|
+
status: String(member.status),
|
|
3758
|
+
termEndsAt: Number(member.termEndsAt),
|
|
3759
|
+
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
3760
|
+
autoRenew: member.autoRenew === true,
|
|
3761
|
+
applied,
|
|
3762
|
+
...text2(member.disputeStatus) ? { disputeStatus: text2(member.disputeStatus) } : {},
|
|
3763
|
+
...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
|
|
3764
|
+
};
|
|
3765
|
+
};
|
|
3766
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3767
|
+
const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
3768
|
+
if (!seat) return membership(false);
|
|
3769
|
+
requireMembershipScope(seat, input);
|
|
3770
|
+
if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3771
|
+
if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
|
|
3772
|
+
const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
|
|
3773
|
+
if (!attrs) return membership(false, seat);
|
|
3774
|
+
try {
|
|
3775
|
+
await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
|
|
3776
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
|
|
3777
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3778
|
+
{ ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
|
|
3779
|
+
evidence.guard
|
|
3780
|
+
] });
|
|
3781
|
+
} catch (error) {
|
|
3782
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
const saved = await membershipLoad(db, "networkSeat", String(seat.id));
|
|
3786
|
+
requireMembershipScope(saved, input);
|
|
3787
|
+
return membership(true, saved);
|
|
3788
|
+
}
|
|
3789
|
+
return membershipFail("membership_dispute_retry_required");
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
// src/membership-dispute-authority.ts
|
|
3793
|
+
var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
3794
|
+
var text3 = (value) => typeof value === "string" && value ? value : void 0;
|
|
3795
|
+
var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
|
|
3672
3796
|
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
3673
3797
|
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
3674
3798
|
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
@@ -3682,7 +3806,7 @@ async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
|
3682
3806
|
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
3683
3807
|
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
3684
3808
|
}
|
|
3685
|
-
const seatId =
|
|
3809
|
+
const seatId = text3(charge.body.metadata?.seatId);
|
|
3686
3810
|
return {
|
|
3687
3811
|
chargeId,
|
|
3688
3812
|
customerId,
|
|
@@ -3708,37 +3832,23 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
|
3708
3832
|
termEndsAt: Number(value.termEndsAt),
|
|
3709
3833
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3710
3834
|
autoRenew: value.autoRenew === true,
|
|
3711
|
-
...
|
|
3835
|
+
...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
|
|
3712
3836
|
applied
|
|
3713
3837
|
});
|
|
3714
3838
|
const known = await membershipLoad(db, "networkMembership", id7);
|
|
3715
3839
|
requireMembershipScope(known, input);
|
|
3716
3840
|
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
3841
|
+
const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
|
|
3842
|
+
if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
|
|
3717
3843
|
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
3844
|
+
if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3845
|
+
if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
|
|
3718
3846
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3719
3847
|
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
3720
3848
|
requireMembershipScope(member, input);
|
|
3721
|
-
if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
3722
3849
|
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
3723
|
-
const
|
|
3724
|
-
|
|
3725
|
-
if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
|
|
3726
|
-
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
3727
|
-
const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
|
|
3728
|
-
const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
3729
|
-
const attrs = {
|
|
3730
|
-
status: next,
|
|
3731
|
-
disputeId: fact.objectId,
|
|
3732
|
-
disputeStatus: status2,
|
|
3733
|
-
disputeEventId: input.eventId,
|
|
3734
|
-
disputeClosedAt: closed ? now : 0,
|
|
3735
|
-
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
3736
|
-
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
3737
|
-
...sameDispute ? {} : { disputedAt: now },
|
|
3738
|
-
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
|
|
3739
|
-
revision: Number(member.revision) + 1,
|
|
3740
|
-
updatedAt: now
|
|
3741
|
-
};
|
|
3850
|
+
const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
|
|
3851
|
+
if (!attrs) return receipt(member, false);
|
|
3742
3852
|
try {
|
|
3743
3853
|
await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
|
|
3744
3854
|
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
@@ -4860,6 +4970,9 @@ function namedSeatRecipientState(seat) {
|
|
|
4860
4970
|
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4861
4971
|
case "canceled":
|
|
4862
4972
|
return "canceled";
|
|
4973
|
+
// A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
|
|
4974
|
+
case "payment_disputed":
|
|
4975
|
+
return "disputed";
|
|
4863
4976
|
default:
|
|
4864
4977
|
return "payment_pending";
|
|
4865
4978
|
}
|
|
@@ -5188,6 +5301,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
5188
5301
|
|
|
5189
5302
|
// src/membership-dispute-recharge.ts
|
|
5190
5303
|
var import_stripe9 = require("@odla-ai/stripe");
|
|
5304
|
+
var NS = { membership: "networkMembership", seat: "networkSeat" };
|
|
5191
5305
|
var fail6 = (code, extra = {}) => {
|
|
5192
5306
|
throw Object.assign(new Error(code), { code, ...extra });
|
|
5193
5307
|
};
|
|
@@ -5201,65 +5315,64 @@ async function defaultPaymentMethod(config, subscriptionId, customerId) {
|
|
|
5201
5315
|
if (!customer.ok) fail6("membership_dispute_provider_unavailable");
|
|
5202
5316
|
return refOf2(customer.body.invoice_settings?.default_payment_method);
|
|
5203
5317
|
}
|
|
5204
|
-
async function
|
|
5318
|
+
async function rechargeDisputed(db, kind, id7, secretKey, now) {
|
|
5205
5319
|
membershipClock(now);
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
const
|
|
5320
|
+
const ns = NS[kind];
|
|
5321
|
+
let row2 = await membershipLoad(db, ns, id7);
|
|
5322
|
+
if (!row2) fail6("membership_not_found");
|
|
5323
|
+
const scope2 = { chapterId: String(row2.chapterId), environment: String(row2.environment) };
|
|
5324
|
+
const membershipId = kind === "membership" ? id7 : String(row2.primaryMembershipId);
|
|
5325
|
+
const amountCents = Number(row2.disputeAmountCents ?? row2.amountCents), currency = String(row2.currency);
|
|
5210
5326
|
const done = (value, replayed) => ({
|
|
5211
5327
|
membershipId,
|
|
5328
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5212
5329
|
status: String(value.status),
|
|
5213
5330
|
paymentIntentId: String(value.rechargePaymentIntentId),
|
|
5214
5331
|
amountCents,
|
|
5215
5332
|
currency,
|
|
5216
5333
|
...replayed ? { replayed } : {}
|
|
5217
5334
|
});
|
|
5218
|
-
if (
|
|
5219
|
-
if (
|
|
5335
|
+
if (row2.status !== MEMBERSHIP_DISPUTED_STATUS) {
|
|
5336
|
+
if (row2.disputeStatus === "recharged" && row2.rechargePaymentIntentId) return done(row2, true);
|
|
5220
5337
|
fail6("membership_not_disputed");
|
|
5221
5338
|
}
|
|
5222
|
-
if (
|
|
5339
|
+
if (row2.disputeStatus !== "lost") fail6("membership_dispute_open");
|
|
5223
5340
|
if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
|
|
5224
|
-
let attempt =
|
|
5341
|
+
let attempt = row2.rechargeAttempt;
|
|
5225
5342
|
if (!attempt || attempt.finishedAt !== void 0) {
|
|
5226
|
-
attempt = { key:
|
|
5343
|
+
attempt = { key: `${kind}-recharge:${id7}:${String(row2.disputeId)}:${now}`, startedAt: now };
|
|
5227
5344
|
await db.transact(
|
|
5228
|
-
[membershipUpdate(
|
|
5229
|
-
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{
|
|
5230
|
-
ns: "networkMembership",
|
|
5231
|
-
id: membershipId,
|
|
5232
|
-
exists: true,
|
|
5233
|
-
equals: { revision: member.revision, status: MEMBERSHIP_DISPUTED_STATUS }
|
|
5234
|
-
}] }
|
|
5345
|
+
[membershipUpdate(ns, id7, { rechargeAttempt: attempt, revision: Number(row2.revision) + 1, updatedAt: now })],
|
|
5346
|
+
{ mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{ ns, id: id7, exists: true, equals: { revision: row2.revision, status: MEMBERSHIP_DISPUTED_STATUS } }] }
|
|
5235
5347
|
);
|
|
5236
|
-
|
|
5237
|
-
if (!
|
|
5348
|
+
row2 = await membershipLoad(db, ns, id7);
|
|
5349
|
+
if (!row2 || row2.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
|
|
5238
5350
|
}
|
|
5239
5351
|
const config = { secretKey };
|
|
5240
|
-
const paymentMethod = await defaultPaymentMethod(config, String(
|
|
5352
|
+
const paymentMethod = await defaultPaymentMethod(config, String(row2.providerSubscriptionId), String(row2.providerCustomerId));
|
|
5241
5353
|
if (!paymentMethod) fail6("recharge_payment_method_unavailable");
|
|
5242
5354
|
const intent = await (0, import_stripe9.stripeRequest)(config, "POST", "/v1/payment_intents", {
|
|
5243
5355
|
amount: amountCents,
|
|
5244
5356
|
currency,
|
|
5245
|
-
customer: String(
|
|
5357
|
+
customer: String(row2.providerCustomerId),
|
|
5246
5358
|
payment_method: paymentMethod,
|
|
5247
5359
|
off_session: "true",
|
|
5248
5360
|
confirm: "true",
|
|
5249
|
-
description:
|
|
5361
|
+
description: `${kind === "seat" ? "Named seat" : "Membership"} payment reissued after dispute ${String(row2.disputeId)}`,
|
|
5250
5362
|
metadata: {
|
|
5251
|
-
...stripePartitionMetadata({ appId: String(
|
|
5252
|
-
applicationId: String(
|
|
5363
|
+
...stripePartitionMetadata({ appId: String(row2.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(row2.runtime) }),
|
|
5364
|
+
applicationId: String(row2.applicationId),
|
|
5253
5365
|
membershipId,
|
|
5254
|
-
|
|
5366
|
+
...kind === "seat" ? { seatId: id7 } : {},
|
|
5367
|
+
disputeId: String(row2.disputeId),
|
|
5255
5368
|
membershipRecharge: "true"
|
|
5256
5369
|
}
|
|
5257
5370
|
}, { idempotencyKey: attempt.key });
|
|
5258
5371
|
const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
|
|
5259
5372
|
const finish = async (outcome) => {
|
|
5260
|
-
const current = await membershipLoad(db,
|
|
5373
|
+
const current = await membershipLoad(db, ns, id7);
|
|
5261
5374
|
if (!current) return;
|
|
5262
|
-
await db.transact([membershipUpdate(
|
|
5375
|
+
await db.transact([membershipUpdate(ns, id7, {
|
|
5263
5376
|
rechargeAttempt: { ...attempt, finishedAt: now, outcome },
|
|
5264
5377
|
revision: Number(current.revision) + 1,
|
|
5265
5378
|
updatedAt: now
|
|
@@ -5277,15 +5390,15 @@ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
|
5277
5390
|
fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
|
|
5278
5391
|
}
|
|
5279
5392
|
if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
|
|
5280
|
-
if (refOf2(body.customer) !==
|
|
5393
|
+
if (refOf2(body.customer) !== row2.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
|
|
5281
5394
|
for (let retry = 0; retry < 8; retry++) {
|
|
5282
|
-
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db,
|
|
5395
|
+
const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, ns, id7);
|
|
5283
5396
|
if (!current) fail6("membership_not_found");
|
|
5284
5397
|
if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
|
|
5285
|
-
const status2 = String(current.disputePriorStatus ?? (current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5398
|
+
const status2 = String(current.disputePriorStatus ?? (kind === "seat" ? "active" : current.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
5286
5399
|
try {
|
|
5287
5400
|
await db.transact(
|
|
5288
|
-
[membershipUpdate(
|
|
5401
|
+
[membershipUpdate(ns, id7, {
|
|
5289
5402
|
status: status2,
|
|
5290
5403
|
disputeStatus: "recharged",
|
|
5291
5404
|
rechargePaymentIntentId: String(body.id),
|
|
@@ -5295,7 +5408,7 @@ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
|
5295
5408
|
updatedAt: now
|
|
5296
5409
|
})],
|
|
5297
5410
|
{ mutationId: `membership-recharged:${attempt.key}`, guards: [
|
|
5298
|
-
{ ns
|
|
5411
|
+
{ ns, id: id7, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
|
|
5299
5412
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
5300
5413
|
] }
|
|
5301
5414
|
);
|
|
@@ -5303,10 +5416,12 @@ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
|
|
|
5303
5416
|
if (cause.code !== "transact_guard_failed") throw cause;
|
|
5304
5417
|
continue;
|
|
5305
5418
|
}
|
|
5306
|
-
return done(await membershipLoad(db,
|
|
5419
|
+
return done(await membershipLoad(db, ns, id7));
|
|
5307
5420
|
}
|
|
5308
5421
|
return fail6("membership_dispute_retry_required");
|
|
5309
5422
|
}
|
|
5423
|
+
var rechargeDisputedMembership = (db, membershipId, secretKey, now) => rechargeDisputed(db, "membership", membershipId, secretKey, now);
|
|
5424
|
+
var rechargeDisputedSeat = (db, seatId, secretKey, now) => rechargeDisputed(db, "seat", seatId, secretKey, now);
|
|
5310
5425
|
|
|
5311
5426
|
// src/worker-routes-admin-membership-disputes.ts
|
|
5312
5427
|
var LIST2 = "/api/admin/membership-disputes";
|
|
@@ -5323,6 +5438,9 @@ var PUBLIC2 = [
|
|
|
5323
5438
|
"memberNumber",
|
|
5324
5439
|
"amountCents",
|
|
5325
5440
|
"currency",
|
|
5441
|
+
"recipientName",
|
|
5442
|
+
"primaryMembershipId",
|
|
5443
|
+
"purchasedAt",
|
|
5326
5444
|
"disputeId",
|
|
5327
5445
|
"disputeStatus",
|
|
5328
5446
|
"disputeReason",
|
|
@@ -5370,14 +5488,17 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
|
5370
5488
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5371
5489
|
}
|
|
5372
5490
|
if (!one) {
|
|
5373
|
-
const
|
|
5491
|
+
const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
|
|
5492
|
+
const found = await db.query({ networkMembership: page, networkSeat: page });
|
|
5493
|
+
const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
|
|
5494
|
+
const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
|
|
5374
5495
|
const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5375
|
-
return json({ disputes
|
|
5496
|
+
return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
|
|
5376
5497
|
}
|
|
5377
5498
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5378
5499
|
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5379
5500
|
try {
|
|
5380
|
-
const receipt = await rechargeDisputedMembership(db, one[1], secret, Date.now());
|
|
5501
|
+
const receipt = await (one[1].startsWith("seat_") ? rechargeDisputedSeat : rechargeDisputedMembership)(db, one[1], secret, Date.now());
|
|
5381
5502
|
return json({ ok: true, ...receipt });
|
|
5382
5503
|
} catch (error) {
|
|
5383
5504
|
const failure = error;
|
|
@@ -5397,13 +5518,13 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
|
5397
5518
|
|
|
5398
5519
|
// src/membership-policy-versions.ts
|
|
5399
5520
|
var LIMIT = 200;
|
|
5400
|
-
var
|
|
5521
|
+
var text4 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
5401
5522
|
var instant = (value) => {
|
|
5402
5523
|
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
5403
5524
|
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
5404
5525
|
};
|
|
5405
5526
|
function provenance(row2) {
|
|
5406
|
-
const source =
|
|
5527
|
+
const source = text4(row2.source), actor = text4(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
5407
5528
|
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
5408
5529
|
}
|
|
5409
5530
|
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
@@ -6404,8 +6525,8 @@ var APPLICATION_LABELS = {
|
|
|
6404
6525
|
state: "State"
|
|
6405
6526
|
};
|
|
6406
6527
|
function humanize(id7) {
|
|
6407
|
-
const
|
|
6408
|
-
return
|
|
6528
|
+
const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6529
|
+
return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
|
|
6409
6530
|
}
|
|
6410
6531
|
function applicationDefs(chapter) {
|
|
6411
6532
|
try {
|
|
@@ -6472,7 +6593,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
6472
6593
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
6473
6594
|
var CURRENCY = /^[a-z]{3}$/;
|
|
6474
6595
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6475
|
-
var
|
|
6596
|
+
var text5 = (value, path, errors, max) => {
|
|
6476
6597
|
if (typeof value !== "string" || !value.trim()) {
|
|
6477
6598
|
errors.push(`${path} is required`);
|
|
6478
6599
|
return "";
|
|
@@ -6500,23 +6621,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6500
6621
|
}
|
|
6501
6622
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6502
6623
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6503
|
-
const id7 =
|
|
6624
|
+
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
6504
6625
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6505
6626
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
6506
6627
|
ids2.add(id7);
|
|
6507
6628
|
const targetRaw = record(item.target);
|
|
6508
6629
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
6509
6630
|
if (targetRaw?.type === "tier") {
|
|
6510
|
-
const tierId =
|
|
6631
|
+
const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
6511
6632
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
6512
6633
|
target = { type: "tier", tierId };
|
|
6513
6634
|
} else if (targetRaw?.type === "entitlement") {
|
|
6514
|
-
const entitlementKey =
|
|
6635
|
+
const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
6515
6636
|
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
6516
6637
|
target = { type: "entitlement", entitlementKey };
|
|
6517
6638
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
6518
6639
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
6519
|
-
const stripePriceId =
|
|
6640
|
+
const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
6520
6641
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
6521
6642
|
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
6522
6643
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -6538,9 +6659,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6538
6659
|
redemptionLifetimeHours: lifetime,
|
|
6539
6660
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
6540
6661
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
6541
|
-
title:
|
|
6542
|
-
description:
|
|
6543
|
-
ctaLabel:
|
|
6662
|
+
title: text5(item.title, `${path}.title`, errors, 160),
|
|
6663
|
+
description: text5(item.description, `${path}.description`, errors, 2e3),
|
|
6664
|
+
ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
6544
6665
|
});
|
|
6545
6666
|
}
|
|
6546
6667
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -6956,17 +7077,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
6956
7077
|
if (!reader) return "";
|
|
6957
7078
|
const decoder = new TextDecoder();
|
|
6958
7079
|
let bytes = 0;
|
|
6959
|
-
let
|
|
7080
|
+
let text10 = "";
|
|
6960
7081
|
for (; ; ) {
|
|
6961
7082
|
const { done, value } = await reader.read();
|
|
6962
7083
|
if (req.signal.aborted) return invalid2();
|
|
6963
7084
|
if (done) {
|
|
6964
7085
|
complete = true;
|
|
6965
|
-
return
|
|
7086
|
+
return text10 + decoder.decode();
|
|
6966
7087
|
}
|
|
6967
7088
|
bytes += value.byteLength;
|
|
6968
7089
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6969
|
-
|
|
7090
|
+
text10 += decoder.decode(value, { stream: true });
|
|
6970
7091
|
}
|
|
6971
7092
|
} catch {
|
|
6972
7093
|
return invalid2();
|
|
@@ -7070,9 +7191,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
7070
7191
|
let selection;
|
|
7071
7192
|
if (request?.body) {
|
|
7072
7193
|
try {
|
|
7073
|
-
const
|
|
7074
|
-
if (
|
|
7075
|
-
const body = JSON.parse(
|
|
7194
|
+
const text10 = await readBoundedRequestText(request, 2048);
|
|
7195
|
+
if (text10 instanceof Response) return text10;
|
|
7196
|
+
const body = JSON.parse(text10);
|
|
7076
7197
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
7077
7198
|
selection = body.namedSeat;
|
|
7078
7199
|
} catch {
|
|
@@ -7954,10 +8075,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
7954
8075
|
errors.push(`${path} must be a string`);
|
|
7955
8076
|
return void 0;
|
|
7956
8077
|
}
|
|
7957
|
-
const
|
|
7958
|
-
if (required && !
|
|
7959
|
-
if (
|
|
7960
|
-
return
|
|
8078
|
+
const text10 = value.trim();
|
|
8079
|
+
if (required && !text10) errors.push(`${path} is required`);
|
|
8080
|
+
if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
8081
|
+
return text10;
|
|
7961
8082
|
}
|
|
7962
8083
|
function validateMembershipPageDocument(input, options = {}) {
|
|
7963
8084
|
const errors = [];
|
|
@@ -8006,8 +8127,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
8006
8127
|
else {
|
|
8007
8128
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
8008
8129
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
8009
|
-
const
|
|
8010
|
-
return
|
|
8130
|
+
const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8131
|
+
return text10 ? [text10] : [];
|
|
8011
8132
|
});
|
|
8012
8133
|
}
|
|
8013
8134
|
}
|
|
@@ -8101,7 +8222,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
8101
8222
|
function unknownKeys(value, allowed2, path, errors) {
|
|
8102
8223
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8103
8224
|
}
|
|
8104
|
-
function
|
|
8225
|
+
function text6(value, path, errors, max = 65536) {
|
|
8105
8226
|
if (typeof value !== "string" || !value.trim()) {
|
|
8106
8227
|
errors.push(`${path} is required`);
|
|
8107
8228
|
return "";
|
|
@@ -8124,11 +8245,11 @@ function tiersOf(value, errors) {
|
|
|
8124
8245
|
return [];
|
|
8125
8246
|
}
|
|
8126
8247
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
8127
|
-
const id7 =
|
|
8248
|
+
const id7 = text6(item.id, `${path}.id`, errors, 80);
|
|
8128
8249
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8129
8250
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
8130
8251
|
seen.add(id7);
|
|
8131
|
-
const name =
|
|
8252
|
+
const name = text6(item.name, `${path}.name`, errors, 160);
|
|
8132
8253
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
8133
8254
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
8134
8255
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -8146,7 +8267,7 @@ function tiersOf(value, errors) {
|
|
|
8146
8267
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
8147
8268
|
errors.push(`${path}.interval must be year or month`);
|
|
8148
8269
|
}
|
|
8149
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8270
|
+
const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
8150
8271
|
return [{
|
|
8151
8272
|
id: id7,
|
|
8152
8273
|
name,
|
|
@@ -8204,7 +8325,7 @@ function policyOf(value, errors, tiers) {
|
|
|
8204
8325
|
const result = {};
|
|
8205
8326
|
for (const key3 of allowed2) {
|
|
8206
8327
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
8207
|
-
result[key3] =
|
|
8328
|
+
result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
|
|
8208
8329
|
}
|
|
8209
8330
|
const tierPolicies = {};
|
|
8210
8331
|
if (input.tierPolicies !== void 0) {
|
|
@@ -8219,8 +8340,8 @@ function policyOf(value, errors, tiers) {
|
|
|
8219
8340
|
}
|
|
8220
8341
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
8221
8342
|
tierPolicies[id7] = {
|
|
8222
|
-
refundPolicyText:
|
|
8223
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8343
|
+
refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8344
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
8224
8345
|
};
|
|
8225
8346
|
}
|
|
8226
8347
|
}
|
|
@@ -8260,9 +8381,9 @@ function validateSignupControlDocument(input) {
|
|
|
8260
8381
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
8261
8382
|
const source = recordOf(root.source);
|
|
8262
8383
|
const target = recordOf(root.target);
|
|
8263
|
-
const appId =
|
|
8264
|
-
const chapterId =
|
|
8265
|
-
const environment =
|
|
8384
|
+
const appId = text6(source?.appId, "document.source.appId", errors, 63);
|
|
8385
|
+
const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8386
|
+
const environment = text6(target?.environment, "document.target.environment", errors, 63);
|
|
8266
8387
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
8267
8388
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
8268
8389
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -10533,7 +10654,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
10533
10654
|
|
|
10534
10655
|
// src/worker-payment-receipt.ts
|
|
10535
10656
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10536
|
-
var escape = (
|
|
10657
|
+
var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10537
10658
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10538
10659
|
const id7 = url.searchParams.get("application");
|
|
10539
10660
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
@@ -10544,7 +10665,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10544
10665
|
if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
|
|
10545
10666
|
const receipt = intent.paymentReceipt;
|
|
10546
10667
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10547
|
-
const
|
|
10668
|
+
const text10 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10548
10669
|
if (req.method === "POST") {
|
|
10549
10670
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10550
10671
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10562,7 +10683,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10562
10683
|
template: "paymentReceipt",
|
|
10563
10684
|
to: String(intent.purchaserEmailNormalized),
|
|
10564
10685
|
applicationId: id7,
|
|
10565
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
10686
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text10 },
|
|
10566
10687
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10567
10688
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10568
10689
|
});
|
|
@@ -10572,7 +10693,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10572
10693
|
const nonce = crypto.randomUUID();
|
|
10573
10694
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10574
10695
|
<style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
|
|
10575
|
-
<h1>Payment receipt</h1><pre>${escape(
|
|
10696
|
+
<h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
|
|
10576
10697
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10577
10698
|
headers: {
|
|
10578
10699
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -11042,16 +11163,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
11042
11163
|
};
|
|
11043
11164
|
|
|
11044
11165
|
// src/named-seat-accepted-notice.ts
|
|
11045
|
-
var
|
|
11166
|
+
var text7 = (value) => typeof value === "string" ? value : "";
|
|
11046
11167
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
11047
11168
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
11048
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
11169
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
|
|
11049
11170
|
return { sent: false, reason: "purchaser-missing" };
|
|
11050
11171
|
}
|
|
11051
11172
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
11052
11173
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
11053
11174
|
const config = emailGroupFrom(group);
|
|
11054
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
11175
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
|
|
11055
11176
|
return sendTemplated({
|
|
11056
11177
|
db,
|
|
11057
11178
|
envName: env.ODLA_ENV,
|
|
@@ -11063,12 +11184,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
11063
11184
|
}, {
|
|
11064
11185
|
group: config,
|
|
11065
11186
|
template: "seatAccepted",
|
|
11066
|
-
to:
|
|
11187
|
+
to: text7(purchaser.email),
|
|
11067
11188
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
11068
11189
|
applicationId: input.primaryApplicationId,
|
|
11069
11190
|
tierId: input.skuId,
|
|
11070
11191
|
vars: {
|
|
11071
|
-
firstName:
|
|
11192
|
+
firstName: text7(purchaser.firstName),
|
|
11072
11193
|
purchaserName,
|
|
11073
11194
|
recipientName: input.recipientName,
|
|
11074
11195
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -12455,6 +12576,84 @@ var import_crm16 = require("@odla-ai/crm");
|
|
|
12455
12576
|
|
|
12456
12577
|
// src/email-readiness.ts
|
|
12457
12578
|
var import_crm15 = require("@odla-ai/crm");
|
|
12579
|
+
|
|
12580
|
+
// src/email-spam-signals.ts
|
|
12581
|
+
var TRIGGER_PHRASES = [
|
|
12582
|
+
"act now",
|
|
12583
|
+
"click here",
|
|
12584
|
+
"limited time",
|
|
12585
|
+
"guaranteed",
|
|
12586
|
+
"risk-free",
|
|
12587
|
+
"risk free",
|
|
12588
|
+
"no obligation",
|
|
12589
|
+
"100% free",
|
|
12590
|
+
"winner",
|
|
12591
|
+
"congratulations",
|
|
12592
|
+
"urgent",
|
|
12593
|
+
"double your",
|
|
12594
|
+
"earn money",
|
|
12595
|
+
"make money",
|
|
12596
|
+
"cash bonus",
|
|
12597
|
+
"once in a lifetime",
|
|
12598
|
+
"exclusive deal",
|
|
12599
|
+
"order now",
|
|
12600
|
+
"buy now",
|
|
12601
|
+
"this is not spam",
|
|
12602
|
+
"dear friend",
|
|
12603
|
+
"dear customer"
|
|
12604
|
+
];
|
|
12605
|
+
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
12606
|
+
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
12607
|
+
var SHORTENERS = /^(?:bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|cutt\.ly|tiny\.cc|shorturl\.at|rb\.gy)$/i;
|
|
12608
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+/g;
|
|
12609
|
+
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
12610
|
+
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
12611
|
+
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
12612
|
+
function emailSpamSignals(copy) {
|
|
12613
|
+
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12614
|
+
const text10 = typeof copy.text === "string" ? copy.text : "";
|
|
12615
|
+
const out = [];
|
|
12616
|
+
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
12617
|
+
const shouting = letters.length >= 6 && letters.replace(/[^A-Z]/g, "").length / letters.length > 0.5 || words(subject).some((word) => /^[A-Z]{4,}$/.test(word.replace(/[^A-Za-z]/g, "")));
|
|
12618
|
+
if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
|
|
12619
|
+
if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
|
|
12620
|
+
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
12621
|
+
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
12622
|
+
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
12623
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
|
|
12624
|
+
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12625
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text10))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
12626
|
+
if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
12627
|
+
const urls = [...text10.matchAll(URL_RE2)].map((match) => match[0]);
|
|
12628
|
+
const hosts = urls.map((url) => {
|
|
12629
|
+
try {
|
|
12630
|
+
return new URL(url).hostname;
|
|
12631
|
+
} catch {
|
|
12632
|
+
return "";
|
|
12633
|
+
}
|
|
12634
|
+
});
|
|
12635
|
+
if (urls.some((url) => /^http:\/\//i.test(url))) out.push({ code: "link-insecure", message: "A link uses http:// instead of https://. Filters flag insecure links." });
|
|
12636
|
+
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
12637
|
+
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
12638
|
+
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
12639
|
+
if (urls.length && words(text10).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
12640
|
+
return out;
|
|
12641
|
+
}
|
|
12642
|
+
function emailSenderWarnings(input) {
|
|
12643
|
+
const out = [];
|
|
12644
|
+
const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
|
|
12645
|
+
const reply = (input.replyTo ?? "").trim().toLowerCase();
|
|
12646
|
+
const domain = (address) => address.split("@")[1] ?? "";
|
|
12647
|
+
if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
|
|
12648
|
+
out.push({ code: "sender-no-reply", message: "Mail is sent from a no-reply address. Gmail's sender guidelines advise against it; send from a monitored address so replies and unsubscribe requests reach someone." });
|
|
12649
|
+
}
|
|
12650
|
+
if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
|
|
12651
|
+
out.push({ code: "reply-to-domain", message: `Replies go to ${domain(reply)} while mail is sent from ${domain(from)}. A Reply-To on another domain is a mild spam signal; prefer an address on the sending domain.` });
|
|
12652
|
+
}
|
|
12653
|
+
return out;
|
|
12654
|
+
}
|
|
12655
|
+
|
|
12656
|
+
// src/email-readiness.ts
|
|
12458
12657
|
function validateEmailReadiness(input) {
|
|
12459
12658
|
const issues = [];
|
|
12460
12659
|
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
@@ -12479,9 +12678,15 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
|
12479
12678
|
}
|
|
12480
12679
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
12481
12680
|
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
12482
|
-
return {
|
|
12681
|
+
return {
|
|
12682
|
+
...event2,
|
|
12683
|
+
key: key3,
|
|
12684
|
+
status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
|
|
12685
|
+
issues: problems,
|
|
12686
|
+
warnings: template ? emailSpamSignals(template) : []
|
|
12687
|
+
};
|
|
12483
12688
|
});
|
|
12484
|
-
return { checkedAt: input.now ?? Date.now(), events, issues };
|
|
12689
|
+
return { checkedAt: input.now ?? Date.now(), events, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
12485
12690
|
}
|
|
12486
12691
|
|
|
12487
12692
|
// src/email-admin-config.ts
|
|
@@ -12507,7 +12712,14 @@ async function emailSettings(db, ctx, env) {
|
|
|
12507
12712
|
}
|
|
12508
12713
|
}
|
|
12509
12714
|
const groupValues = Object.fromEntries(["refundPolicyText", "commitmentText", "normsText"].map((key3) => [key3, str2(group?.[key3])]));
|
|
12510
|
-
const validation = validateEmailReadiness({
|
|
12715
|
+
const validation = validateEmailReadiness({
|
|
12716
|
+
events,
|
|
12717
|
+
templates: stored,
|
|
12718
|
+
groupValues,
|
|
12719
|
+
tiers: hub ? void 0 : tiers,
|
|
12720
|
+
tiersUnavailable,
|
|
12721
|
+
sender: { fromEmail: env.EMAIL_FROM ?? null, replyTo: str2(group?.replyTo) }
|
|
12722
|
+
});
|
|
12511
12723
|
return { group, events, data: {
|
|
12512
12724
|
groupId: ctx.chapter.id,
|
|
12513
12725
|
name: ctx.chapter.name,
|
|
@@ -12535,12 +12747,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12535
12747
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12536
12748
|
const t = value;
|
|
12537
12749
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12538
|
-
const subject = str2(t.subject).trim(),
|
|
12539
|
-
const issues = (0, import_crm16.validateEmailCopy)({ subject, text:
|
|
12750
|
+
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12751
|
+
const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text10 }, { allowedVariables: events[key3].variables });
|
|
12540
12752
|
if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
|
|
12541
12753
|
return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
|
|
12542
12754
|
}
|
|
12543
|
-
clean[key3] = { subject, text:
|
|
12755
|
+
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12544
12756
|
}
|
|
12545
12757
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12546
12758
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -12561,7 +12773,8 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12561
12773
|
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
12562
12774
|
]);
|
|
12563
12775
|
}
|
|
12564
|
-
|
|
12776
|
+
const warnings = Object.fromEntries(Object.entries(clean).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
|
|
12777
|
+
return json({ ok: true, warnings });
|
|
12565
12778
|
}
|
|
12566
12779
|
|
|
12567
12780
|
// src/worker-routes-admin-comms.ts
|
|
@@ -14744,9 +14957,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14744
14957
|
if (req.method === "POST") {
|
|
14745
14958
|
let body;
|
|
14746
14959
|
try {
|
|
14747
|
-
const
|
|
14748
|
-
if (new TextEncoder().encode(
|
|
14749
|
-
const parsed = JSON.parse(
|
|
14960
|
+
const text10 = await req.text();
|
|
14961
|
+
if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
14962
|
+
const parsed = JSON.parse(text10);
|
|
14750
14963
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14751
14964
|
body = parsed;
|
|
14752
14965
|
} catch {
|
|
@@ -14915,7 +15128,7 @@ var import_db13 = require("@odla-ai/db");
|
|
|
14915
15128
|
// src/network-commercial-parity.ts
|
|
14916
15129
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
14917
15130
|
var MAX_RECORDS = 500;
|
|
14918
|
-
var
|
|
15131
|
+
var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
14919
15132
|
function parityMessage(partition, kind, value) {
|
|
14920
15133
|
return [
|
|
14921
15134
|
"chapter-commercial-parity",
|
|
@@ -14947,18 +15160,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
14947
15160
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
14948
15161
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
14949
15162
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
14950
|
-
const eligible = input.rows.filter((row2) =>
|
|
15163
|
+
const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
|
|
14951
15164
|
const selected2 = eligible.slice(0, maxRecords);
|
|
14952
15165
|
const key3 = await parityKey(input.secret);
|
|
14953
15166
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
14954
|
-
const applicationId =
|
|
14955
|
-
const customerId =
|
|
14956
|
-
const subscriptionId =
|
|
15167
|
+
const applicationId = text8(row2.id);
|
|
15168
|
+
const customerId = text8(row2.stripeCustomerId);
|
|
15169
|
+
const subscriptionId = text8(row2.stripeSubscriptionId);
|
|
14957
15170
|
return {
|
|
14958
15171
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
14959
15172
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
14960
15173
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
14961
|
-
status:
|
|
15174
|
+
status: text8(row2.status, 64),
|
|
14962
15175
|
canceled: row2.canceled === true
|
|
14963
15176
|
};
|
|
14964
15177
|
}));
|
|
@@ -14984,7 +15197,7 @@ function validSnapshot(snapshot, source) {
|
|
|
14984
15197
|
if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
|
|
14985
15198
|
const seen = /* @__PURE__ */ new Set();
|
|
14986
15199
|
for (const record4 of records) {
|
|
14987
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
15200
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text8(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
14988
15201
|
const key3 = recordKey(record4);
|
|
14989
15202
|
if (seen.has(key3)) return false;
|
|
14990
15203
|
seen.add(key3);
|
|
@@ -14995,16 +15208,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
14995
15208
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
14996
15209
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
14997
15210
|
const key3 = await parityKey(input.secret);
|
|
14998
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
14999
|
-
const customerId =
|
|
15000
|
-
const subscriptionId =
|
|
15211
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
|
|
15212
|
+
const customerId = text8(row2.customerId);
|
|
15213
|
+
const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
|
|
15001
15214
|
return {
|
|
15002
15215
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
15003
15216
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
15004
15217
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
15005
15218
|
status: null,
|
|
15006
15219
|
canceled: false,
|
|
15007
|
-
stripeStatus:
|
|
15220
|
+
stripeStatus: text8(row2.status, 64),
|
|
15008
15221
|
stripeCanceled: row2.status === "canceled"
|
|
15009
15222
|
};
|
|
15010
15223
|
}));
|
|
@@ -15214,18 +15427,24 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
|
15214
15427
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
15215
15428
|
if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
|
|
15216
15429
|
const charge = await readDisputedCharge(secret, fact.chargeId);
|
|
15430
|
+
const scopeOf = (row2) => ({
|
|
15431
|
+
appId: String(row2.appId),
|
|
15432
|
+
chapterId: String(row2.chapterId),
|
|
15433
|
+
environment: String(row2.environment),
|
|
15434
|
+
runtime: String(row2.runtime),
|
|
15435
|
+
applicationId: String(row2.applicationId),
|
|
15436
|
+
skuId: String(row2.skuId)
|
|
15437
|
+
});
|
|
15438
|
+
if (charge.seatId) {
|
|
15439
|
+
const seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
15440
|
+
if (!seat || seat.environment !== env.ODLA_ENV || !disputedChargeCoversSeat(charge, seat)) return { disposition: "not_applicable" };
|
|
15441
|
+
const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
|
|
15442
|
+
return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
|
|
15443
|
+
}
|
|
15217
15444
|
const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
15218
15445
|
const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15219
15446
|
if (!member) return { disposition: "not_applicable" };
|
|
15220
|
-
const
|
|
15221
|
-
appId: String(member.appId),
|
|
15222
|
-
chapterId: String(member.chapterId),
|
|
15223
|
-
environment: String(member.environment),
|
|
15224
|
-
runtime: String(member.runtime),
|
|
15225
|
-
applicationId: String(member.applicationId),
|
|
15226
|
-
skuId: String(member.skuId)
|
|
15227
|
-
};
|
|
15228
|
-
const receipt = await reconcileMembershipDispute(db, { ...target, eventId: fact.eventId }, now, secret, charge);
|
|
15447
|
+
const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
|
|
15229
15448
|
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
15230
15449
|
}
|
|
15231
15450
|
|
|
@@ -15293,11 +15512,11 @@ function safeProjection(projection) {
|
|
|
15293
15512
|
amountReceivedCents: seat.amountReceivedCents,
|
|
15294
15513
|
refundedCents: seat.refundedCents ?? 0
|
|
15295
15514
|
})),
|
|
15296
|
-
disputes: (projection.disputes ?? []).map((
|
|
15297
|
-
...row2(
|
|
15298
|
-
chargeId:
|
|
15299
|
-
paymentIntentId:
|
|
15300
|
-
reason:
|
|
15515
|
+
disputes: (projection.disputes ?? []).map((dispute2) => ({
|
|
15516
|
+
...row2(dispute2),
|
|
15517
|
+
chargeId: dispute2.chargeId ?? null,
|
|
15518
|
+
paymentIntentId: dispute2.paymentIntentId ?? null,
|
|
15519
|
+
reason: dispute2.disputeReason ?? null
|
|
15301
15520
|
})),
|
|
15302
15521
|
exceptions: projection.exceptions
|
|
15303
15522
|
};
|
|
@@ -15331,10 +15550,10 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
15331
15550
|
if (result.status === "collision") {
|
|
15332
15551
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
15333
15552
|
}
|
|
15334
|
-
let
|
|
15553
|
+
let dispute2;
|
|
15335
15554
|
if (fact.objectKind === "dispute" && result.status !== "collision") {
|
|
15336
15555
|
try {
|
|
15337
|
-
|
|
15556
|
+
dispute2 = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
|
|
15338
15557
|
} catch (error) {
|
|
15339
15558
|
console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
|
|
15340
15559
|
return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
|
|
@@ -15344,7 +15563,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
15344
15563
|
ok: true,
|
|
15345
15564
|
status: result.status,
|
|
15346
15565
|
eventId: fact.eventId,
|
|
15347
|
-
...
|
|
15566
|
+
...dispute2 ? { dispute: dispute2.disposition } : {},
|
|
15348
15567
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
15349
15568
|
});
|
|
15350
15569
|
} catch (error) {
|
|
@@ -15542,7 +15761,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
15542
15761
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
15543
15762
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15544
15763
|
}
|
|
15545
|
-
function
|
|
15764
|
+
function text9(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15546
15765
|
if (value === void 0 && !required) return void 0;
|
|
15547
15766
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15548
15767
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15568,7 +15787,7 @@ function scopeValue(value, path, errors) {
|
|
|
15568
15787
|
return { kind: raw2.kind };
|
|
15569
15788
|
}
|
|
15570
15789
|
if (raw2.kind === "chapter") {
|
|
15571
|
-
const chapterId =
|
|
15790
|
+
const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15572
15791
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15573
15792
|
return { kind: "chapter", chapterId };
|
|
15574
15793
|
}
|
|
@@ -15582,22 +15801,22 @@ function resourceValue(value, path, errors) {
|
|
|
15582
15801
|
return null;
|
|
15583
15802
|
}
|
|
15584
15803
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15585
|
-
const id7 =
|
|
15804
|
+
const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15586
15805
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15587
|
-
const kind =
|
|
15806
|
+
const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15588
15807
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15589
|
-
const title =
|
|
15590
|
-
const summary =
|
|
15591
|
-
const status2 =
|
|
15808
|
+
const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
15809
|
+
const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
15810
|
+
const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
|
|
15592
15811
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15593
15812
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15594
15813
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15595
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
15814
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15596
15815
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15597
15816
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15598
15817
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15599
|
-
const bodyMarkdown =
|
|
15600
|
-
const url =
|
|
15818
|
+
const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
15819
|
+
const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15601
15820
|
if (url) {
|
|
15602
15821
|
try {
|
|
15603
15822
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15609,9 +15828,9 @@ function resourceValue(value, path, errors) {
|
|
|
15609
15828
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15610
15829
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15611
15830
|
const brandAsset = asset ? {
|
|
15612
|
-
bookId:
|
|
15613
|
-
assetId:
|
|
15614
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
15831
|
+
bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
15832
|
+
assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
15833
|
+
...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15615
15834
|
} : void 0;
|
|
15616
15835
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15617
15836
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15626,17 +15845,17 @@ function resourceValue(value, path, errors) {
|
|
|
15626
15845
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15627
15846
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15628
15847
|
const promotedFrom = promotedRaw ? {
|
|
15629
|
-
chapterId:
|
|
15630
|
-
resourceId:
|
|
15848
|
+
chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
15849
|
+
resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15631
15850
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15632
15851
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15633
|
-
promotedBy:
|
|
15852
|
+
promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15634
15853
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15635
15854
|
} : void 0;
|
|
15636
15855
|
const provenance2 = {
|
|
15637
|
-
createdBy:
|
|
15856
|
+
createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15638
15857
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15639
|
-
updatedBy:
|
|
15858
|
+
updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15640
15859
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15641
15860
|
...promotedFrom ? { promotedFrom } : {}
|
|
15642
15861
|
};
|
|
@@ -15655,7 +15874,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15655
15874
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15656
15875
|
const sourceType = sourceRaw?.type;
|
|
15657
15876
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15658
|
-
const sourceAppId =
|
|
15877
|
+
const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15659
15878
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15660
15879
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15661
15880
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15663,8 +15882,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15663
15882
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15664
15883
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15665
15884
|
const target = targetRaw ? {
|
|
15666
|
-
appId:
|
|
15667
|
-
environment:
|
|
15885
|
+
appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
15886
|
+
environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15668
15887
|
} : void 0;
|
|
15669
15888
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15670
15889
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -17189,6 +17408,19 @@ var id5 = () => attr("string", false, true, true);
|
|
|
17189
17408
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
17190
17409
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
17191
17410
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
17411
|
+
var dispute = () => ({
|
|
17412
|
+
disputeId: key(true),
|
|
17413
|
+
disputeStatus: attr("string", true),
|
|
17414
|
+
disputeReason: attr("string", true),
|
|
17415
|
+
disputeAmountCents: attr("number", true),
|
|
17416
|
+
disputedAt: attr("number", true),
|
|
17417
|
+
disputeClosedAt: attr("number", true),
|
|
17418
|
+
disputePriorStatus: attr("string", true),
|
|
17419
|
+
disputeEventId: key(true),
|
|
17420
|
+
rechargePaymentIntentId: key(true),
|
|
17421
|
+
rechargedAt: attr("number", true),
|
|
17422
|
+
rechargeAttempt: attr("json", true)
|
|
17423
|
+
});
|
|
17192
17424
|
var membershipAuthorityEntities = {
|
|
17193
17425
|
networkMembership: { attrs: {
|
|
17194
17426
|
id: id5(),
|
|
@@ -17229,19 +17461,7 @@ var membershipAuthorityEntities = {
|
|
|
17229
17461
|
...times(),
|
|
17230
17462
|
refundRequestedAt: attr("number", true),
|
|
17231
17463
|
restartId: key(true),
|
|
17232
|
-
|
|
17233
|
-
// the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
|
|
17234
|
-
disputeId: key(true),
|
|
17235
|
-
disputeStatus: attr("string", true),
|
|
17236
|
-
disputeReason: attr("string", true),
|
|
17237
|
-
disputeAmountCents: attr("number", true),
|
|
17238
|
-
disputedAt: attr("number", true),
|
|
17239
|
-
disputeClosedAt: attr("number", true),
|
|
17240
|
-
disputePriorStatus: attr("string", true),
|
|
17241
|
-
disputeEventId: key(true),
|
|
17242
|
-
rechargePaymentIntentId: key(true),
|
|
17243
|
-
rechargedAt: attr("number", true),
|
|
17244
|
-
rechargeAttempt: attr("json", true)
|
|
17464
|
+
...dispute()
|
|
17245
17465
|
} },
|
|
17246
17466
|
membershipRestart: { attrs: {
|
|
17247
17467
|
id: id5(),
|
|
@@ -17317,7 +17537,8 @@ var membershipAuthorityEntities = {
|
|
|
17317
17537
|
refundedCents: attr("number", true),
|
|
17318
17538
|
providerRefundIds: attr("json", true),
|
|
17319
17539
|
regiftedAt: attr("number", true),
|
|
17320
|
-
formerRecipients: attr("json", true)
|
|
17540
|
+
formerRecipients: attr("json", true),
|
|
17541
|
+
...dispute()
|
|
17321
17542
|
} },
|
|
17322
17543
|
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
17323
17544
|
membershipEffect: { attrs: {
|
|
@@ -17654,7 +17875,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17654
17875
|
};
|
|
17655
17876
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17656
17877
|
let size = 0;
|
|
17657
|
-
let
|
|
17878
|
+
let text10 = "";
|
|
17658
17879
|
const decoder = new TextDecoder();
|
|
17659
17880
|
try {
|
|
17660
17881
|
while (true) {
|
|
@@ -17664,10 +17885,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17664
17885
|
if (done) break;
|
|
17665
17886
|
size += value.byteLength;
|
|
17666
17887
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17667
|
-
|
|
17888
|
+
text10 += decoder.decode(value, { stream: true });
|
|
17668
17889
|
}
|
|
17669
17890
|
try {
|
|
17670
|
-
return JSON.parse(
|
|
17891
|
+
return JSON.parse(text10 + decoder.decode());
|
|
17671
17892
|
} catch {
|
|
17672
17893
|
throw new TypeError(`invalid ${label}`);
|
|
17673
17894
|
}
|