@odla-ai/chapter 0.45.0 → 0.47.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 +22 -2
- package/dist/{copy-context-CIxmneHI.d.ts → copy-context-20WFI5TI.d.ts} +17 -4
- package/dist/index.cjs +212 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -10
- package/dist/index.d.ts +104 -10
- package/dist/index.js +212 -58
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +363 -60
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +17 -4
- package/dist/worker/index.d.ts +17 -4
- package/dist/worker/index.js +363 -60
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -80,12 +80,16 @@ function createWorkerContext(options) {
|
|
|
80
80
|
}
|
|
81
81
|
async function isAdminEmail(db, email) {
|
|
82
82
|
if (!email) return false;
|
|
83
|
-
const { admins } = await db.query({ admins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });
|
|
83
|
+
const { admins } = await db.query({ admins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
|
|
84
84
|
return Array.isArray(admins) && admins.length > 0;
|
|
85
85
|
}
|
|
86
86
|
async function isSuperAdminEmail(db, email) {
|
|
87
87
|
if (!auth.superAdmins || !email) return false;
|
|
88
|
-
|
|
88
|
+
if (auth.superAdminSource === "odla") {
|
|
89
|
+
const { $users } = await db.query({ $users: { $: { where: { email: email.trim().toLowerCase(), role: "super_admin" }, limit: 1 } } });
|
|
90
|
+
return Array.isArray($users) && $users.length > 0;
|
|
91
|
+
}
|
|
92
|
+
const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
|
|
89
93
|
return Array.isArray(superAdmins) && superAdmins.length > 0;
|
|
90
94
|
}
|
|
91
95
|
async function roleFor(db, u) {
|
|
@@ -724,8 +728,8 @@ var APPLICATION_LABELS = {
|
|
|
724
728
|
state: "State"
|
|
725
729
|
};
|
|
726
730
|
function humanize(id) {
|
|
727
|
-
const
|
|
728
|
-
return
|
|
731
|
+
const text6 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
732
|
+
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id;
|
|
729
733
|
}
|
|
730
734
|
function applicationDefs(chapter) {
|
|
731
735
|
try {
|
|
@@ -1260,10 +1264,10 @@ function planDelivery(input) {
|
|
|
1260
1264
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1261
1265
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1262
1266
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1263
|
-
const
|
|
1267
|
+
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1264
1268
|
|
|
1265
1269
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1266
|
-
return { deliver: true, transport, to, subject, text:
|
|
1270
|
+
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
1267
1271
|
}
|
|
1268
1272
|
|
|
1269
1273
|
// src/notify.ts
|
|
@@ -2151,10 +2155,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2151
2155
|
errors.push(`${path} must be a string`);
|
|
2152
2156
|
return void 0;
|
|
2153
2157
|
}
|
|
2154
|
-
const
|
|
2155
|
-
if (required && !
|
|
2156
|
-
if (
|
|
2157
|
-
return
|
|
2158
|
+
const text6 = value.trim();
|
|
2159
|
+
if (required && !text6) errors.push(`${path} is required`);
|
|
2160
|
+
if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2161
|
+
return text6;
|
|
2158
2162
|
}
|
|
2159
2163
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2160
2164
|
const errors = [];
|
|
@@ -2203,8 +2207,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2203
2207
|
else {
|
|
2204
2208
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2205
2209
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2206
|
-
const
|
|
2207
|
-
return
|
|
2210
|
+
const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2211
|
+
return text6 ? [text6] : [];
|
|
2208
2212
|
});
|
|
2209
2213
|
}
|
|
2210
2214
|
}
|
|
@@ -3916,17 +3920,17 @@ async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
|
3916
3920
|
try {
|
|
3917
3921
|
const group = await groupFor(db, app);
|
|
3918
3922
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
3919
|
-
const
|
|
3923
|
+
const text6 = (value) => typeof value === "string" ? value : "";
|
|
3920
3924
|
await sendTemplated(deps(db, env), {
|
|
3921
3925
|
group: emailGroupFrom(group),
|
|
3922
3926
|
template: "adminNotification",
|
|
3923
3927
|
to: group.notificationEmail,
|
|
3924
3928
|
vars: {
|
|
3925
|
-
firstName:
|
|
3926
|
-
lastName:
|
|
3927
|
-
email:
|
|
3928
|
-
phone:
|
|
3929
|
-
state:
|
|
3929
|
+
firstName: text6(app.firstName),
|
|
3930
|
+
lastName: text6(app.lastName),
|
|
3931
|
+
email: text6(app.email),
|
|
3932
|
+
phone: text6(app.phone),
|
|
3933
|
+
state: text6(app.state)
|
|
3930
3934
|
},
|
|
3931
3935
|
dedupeKey: `${eventId}:admin`,
|
|
3932
3936
|
applicationId: String(app.id)
|
|
@@ -5690,11 +5694,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
5690
5694
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
5691
5695
|
const t = templates[key];
|
|
5692
5696
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
5693
|
-
const
|
|
5694
|
-
if (!subject || !
|
|
5697
|
+
const text6 = typeof t?.text === "string" ? t.text : "";
|
|
5698
|
+
if (!subject || !text6.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
5695
5699
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
5696
|
-
if (
|
|
5697
|
-
clean[key] = { subject, text:
|
|
5700
|
+
if (text6.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
5701
|
+
clean[key] = { subject, text: text6, enabled: t?.enabled !== false };
|
|
5698
5702
|
}
|
|
5699
5703
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5700
5704
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -5911,7 +5915,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
5911
5915
|
fields,
|
|
5912
5916
|
sharedNotes,
|
|
5913
5917
|
editableFields,
|
|
5914
|
-
stageTransitions
|
|
5918
|
+
stageTransitions,
|
|
5919
|
+
commercialParity
|
|
5915
5920
|
}) => ({
|
|
5916
5921
|
id,
|
|
5917
5922
|
name,
|
|
@@ -5919,7 +5924,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
5919
5924
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
5920
5925
|
sharedNotes,
|
|
5921
5926
|
editableFields,
|
|
5922
|
-
stageTransitions
|
|
5927
|
+
stageTransitions,
|
|
5928
|
+
commercialParity
|
|
5923
5929
|
}))
|
|
5924
5930
|
});
|
|
5925
5931
|
};
|
|
@@ -7181,9 +7187,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7181
7187
|
if (req.method === "POST") {
|
|
7182
7188
|
let body;
|
|
7183
7189
|
try {
|
|
7184
|
-
const
|
|
7185
|
-
if (new TextEncoder().encode(
|
|
7186
|
-
const parsed = JSON.parse(
|
|
7190
|
+
const text6 = await req.text();
|
|
7191
|
+
if (new TextEncoder().encode(text6).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7192
|
+
const parsed = JSON.parse(text6);
|
|
7187
7193
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7188
7194
|
body = parsed;
|
|
7189
7195
|
} catch {
|
|
@@ -7778,6 +7784,298 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
|
7778
7784
|
};
|
|
7779
7785
|
}
|
|
7780
7786
|
|
|
7787
|
+
// src/worker-routes-network-commercial-parity.ts
|
|
7788
|
+
import { signFederatedRequest as signFederatedRequest6, verifyFederatedRequest as verifyFederatedRequest8 } from "@odla-ai/db";
|
|
7789
|
+
|
|
7790
|
+
// src/network-commercial-parity.ts
|
|
7791
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
7792
|
+
var MAX_RECORDS = 500;
|
|
7793
|
+
var text4 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
7794
|
+
function parityMessage(partition, kind, value) {
|
|
7795
|
+
return [
|
|
7796
|
+
"chapter-commercial-parity",
|
|
7797
|
+
"v1",
|
|
7798
|
+
partition.appId,
|
|
7799
|
+
partition.chapterId,
|
|
7800
|
+
partition.environment,
|
|
7801
|
+
partition.dataEnvironment,
|
|
7802
|
+
partition.runtime,
|
|
7803
|
+
kind,
|
|
7804
|
+
value
|
|
7805
|
+
].join("");
|
|
7806
|
+
}
|
|
7807
|
+
async function parityKey(secret) {
|
|
7808
|
+
const encoder4 = new TextEncoder();
|
|
7809
|
+
return crypto.subtle.importKey(
|
|
7810
|
+
"raw",
|
|
7811
|
+
encoder4.encode(secret),
|
|
7812
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
7813
|
+
false,
|
|
7814
|
+
["sign"]
|
|
7815
|
+
);
|
|
7816
|
+
}
|
|
7817
|
+
async function digestWithKey(key, partition, kind, value) {
|
|
7818
|
+
const encoder4 = new TextEncoder();
|
|
7819
|
+
const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
|
|
7820
|
+
return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7821
|
+
}
|
|
7822
|
+
async function buildChapterCommercialParitySnapshot(input) {
|
|
7823
|
+
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
7824
|
+
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
7825
|
+
const eligible = input.rows.filter((row) => text4(row.id) && (text4(row.stripeCustomerId) || text4(row.stripeSubscriptionId)));
|
|
7826
|
+
const selected2 = eligible.slice(0, maxRecords);
|
|
7827
|
+
const key = await parityKey(input.secret);
|
|
7828
|
+
const records = await Promise.all(selected2.map(async (row) => {
|
|
7829
|
+
const applicationId = text4(row.id);
|
|
7830
|
+
const customerId = text4(row.stripeCustomerId);
|
|
7831
|
+
const subscriptionId = text4(row.stripeSubscriptionId);
|
|
7832
|
+
return {
|
|
7833
|
+
applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
|
|
7834
|
+
customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
|
|
7835
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
|
|
7836
|
+
status: text4(row.status, 64),
|
|
7837
|
+
canceled: row.canceled === true
|
|
7838
|
+
};
|
|
7839
|
+
}));
|
|
7840
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
7841
|
+
const truncated = input.scanTruncated || eligible.length > maxRecords;
|
|
7842
|
+
return {
|
|
7843
|
+
version: 1,
|
|
7844
|
+
site: { id: input.siteId },
|
|
7845
|
+
partition: input.partition,
|
|
7846
|
+
generatedAt: input.now ?? Date.now(),
|
|
7847
|
+
complete: !truncated,
|
|
7848
|
+
truncated,
|
|
7849
|
+
counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
|
|
7850
|
+
records
|
|
7851
|
+
};
|
|
7852
|
+
}
|
|
7853
|
+
function recordKey(record4) {
|
|
7854
|
+
return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
|
|
7855
|
+
}
|
|
7856
|
+
function validSnapshot(snapshot, source) {
|
|
7857
|
+
const partition = snapshot?.partition;
|
|
7858
|
+
const records = snapshot?.records;
|
|
7859
|
+
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;
|
|
7860
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7861
|
+
for (const record4 of records) {
|
|
7862
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text4(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
7863
|
+
const key = recordKey(record4);
|
|
7864
|
+
if (seen.has(key)) return false;
|
|
7865
|
+
seen.add(key);
|
|
7866
|
+
}
|
|
7867
|
+
return Number.isSafeInteger(snapshot.counts.scanned) && Number.isSafeInteger(snapshot.counts.eligible) && Number.isSafeInteger(snapshot.counts.returned) && snapshot.counts.returned === records.length && snapshot.counts.scanned >= snapshot.counts.eligible && snapshot.counts.eligible >= snapshot.counts.returned;
|
|
7868
|
+
}
|
|
7869
|
+
async function reconcileCommercialParitySnapshot(input) {
|
|
7870
|
+
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
7871
|
+
const sourceKey = networkCommercialSourceKey(input.source);
|
|
7872
|
+
const key = await parityKey(input.secret);
|
|
7873
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text4(row.applicationId) && text4(row.objectId)).map(async (row) => {
|
|
7874
|
+
const customerId = text4(row.customerId);
|
|
7875
|
+
const subscriptionId = text4(row.subscriptionId) ?? text4(row.objectId);
|
|
7876
|
+
return {
|
|
7877
|
+
applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
|
|
7878
|
+
customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
|
|
7879
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
7880
|
+
status: null,
|
|
7881
|
+
canceled: false,
|
|
7882
|
+
stripeStatus: text4(row.status, 64),
|
|
7883
|
+
stripeCanceled: row.status === "canceled"
|
|
7884
|
+
};
|
|
7885
|
+
}));
|
|
7886
|
+
const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
|
|
7887
|
+
const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
|
|
7888
|
+
const exceptions = [];
|
|
7889
|
+
const records = [];
|
|
7890
|
+
for (const [key2, chapter] of chapterByKey) {
|
|
7891
|
+
const stripe = shadowByKey.get(key2);
|
|
7892
|
+
if (!stripe) {
|
|
7893
|
+
exceptions.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
|
|
7894
|
+
continue;
|
|
7895
|
+
}
|
|
7896
|
+
const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
|
|
7897
|
+
records.push(compared);
|
|
7898
|
+
if (chapter.canceled !== stripe.stripeCanceled) {
|
|
7899
|
+
exceptions.push({
|
|
7900
|
+
kind: "cancellation_mismatch",
|
|
7901
|
+
applicationDigest: chapter.applicationDigest,
|
|
7902
|
+
detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
|
|
7903
|
+
});
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
for (const [key2, stripe] of shadowByKey) {
|
|
7907
|
+
if (!chapterByKey.has(key2)) {
|
|
7908
|
+
exceptions.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
if (!input.snapshot.complete || input.snapshot.truncated) {
|
|
7912
|
+
exceptions.push({
|
|
7913
|
+
kind: "chapter_truncated",
|
|
7914
|
+
detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
|
|
7915
|
+
});
|
|
7916
|
+
}
|
|
7917
|
+
if (chapterByKey.size !== shadowByKey.size) {
|
|
7918
|
+
exceptions.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
|
|
7919
|
+
}
|
|
7920
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
7921
|
+
return {
|
|
7922
|
+
ok: input.snapshot.complete && exceptions.length === 0,
|
|
7923
|
+
complete: input.snapshot.complete,
|
|
7924
|
+
counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
|
|
7925
|
+
records,
|
|
7926
|
+
exceptions
|
|
7927
|
+
};
|
|
7928
|
+
}
|
|
7929
|
+
|
|
7930
|
+
// src/network-commercial-parity-response.ts
|
|
7931
|
+
var MAX_RESPONSE_BYTES = 512e3;
|
|
7932
|
+
async function readBoundedCommercialParityResponse(response) {
|
|
7933
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
7934
|
+
if (declared > MAX_RESPONSE_BYTES) return null;
|
|
7935
|
+
if (!response.body) return "";
|
|
7936
|
+
const reader = response.body.getReader();
|
|
7937
|
+
const chunks = [];
|
|
7938
|
+
let total = 0;
|
|
7939
|
+
for (; ; ) {
|
|
7940
|
+
const next = await reader.read();
|
|
7941
|
+
if (next.done) break;
|
|
7942
|
+
total += next.value.byteLength;
|
|
7943
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
7944
|
+
await reader.cancel();
|
|
7945
|
+
return null;
|
|
7946
|
+
}
|
|
7947
|
+
chunks.push(next.value);
|
|
7948
|
+
}
|
|
7949
|
+
const body = new Uint8Array(total);
|
|
7950
|
+
let offset = 0;
|
|
7951
|
+
for (const chunk of chunks) {
|
|
7952
|
+
body.set(chunk, offset);
|
|
7953
|
+
offset += chunk.byteLength;
|
|
7954
|
+
}
|
|
7955
|
+
return new TextDecoder().decode(body);
|
|
7956
|
+
}
|
|
7957
|
+
|
|
7958
|
+
// src/worker-routes-network-commercial-parity.ts
|
|
7959
|
+
var PARITY_PATH = "/api/network/commercial-parity";
|
|
7960
|
+
var PAGE2 = 500;
|
|
7961
|
+
var MAX_SCAN = 5e3;
|
|
7962
|
+
var MAX_RECORDS2 = 500;
|
|
7963
|
+
async function localCommercialRows(db) {
|
|
7964
|
+
const rows6 = [];
|
|
7965
|
+
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
7966
|
+
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
7967
|
+
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
7968
|
+
rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
|
|
7969
|
+
if (page.length < PAGE2) return { rows: rows6, truncated: false };
|
|
7970
|
+
}
|
|
7971
|
+
return { rows: rows6, truncated: true };
|
|
7972
|
+
}
|
|
7973
|
+
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
7974
|
+
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
7975
|
+
if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
|
|
7976
|
+
const readers = ctx.chapter.network.readers.filter((reader) => reader.commercialParity);
|
|
7977
|
+
if (readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
7978
|
+
const db = ctx.makeDb(env);
|
|
7979
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
7980
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
7981
|
+
const verified = await verifyFederatedRequest8(req, { secret, senders: readers.map((reader) => reader.id) });
|
|
7982
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
7983
|
+
const partition = {
|
|
7984
|
+
appId: env.ODLA_APP_ID,
|
|
7985
|
+
chapterId: ctx.chapter.id,
|
|
7986
|
+
environment: env.ODLA_ENV,
|
|
7987
|
+
dataEnvironment: env.ODLA_ENV,
|
|
7988
|
+
runtime: env.ODLA_RUNTIME?.trim() || "default"
|
|
7989
|
+
};
|
|
7990
|
+
try {
|
|
7991
|
+
const local3 = await localCommercialRows(db);
|
|
7992
|
+
return json(await buildChapterCommercialParitySnapshot({
|
|
7993
|
+
rows: local3.rows,
|
|
7994
|
+
secret,
|
|
7995
|
+
partition,
|
|
7996
|
+
siteId: ctx.chapter.id,
|
|
7997
|
+
scanned: local3.rows.length,
|
|
7998
|
+
scanTruncated: local3.truncated,
|
|
7999
|
+
maxRecords: MAX_RECORDS2
|
|
8000
|
+
}));
|
|
8001
|
+
} catch {
|
|
8002
|
+
return json({ error: "commercial parity unavailable" }, 503);
|
|
8003
|
+
}
|
|
8004
|
+
};
|
|
8005
|
+
async function reconcileTarget(db, env, ctx, target, projection) {
|
|
8006
|
+
const base = { targetId: target.id, name: target.name, url: target.url };
|
|
8007
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
8008
|
+
if (!secret) return { ...base, available: false, ok: false, error: { code: "edge_secret_missing" } };
|
|
8009
|
+
const destination = new URL(PARITY_PATH, target.url);
|
|
8010
|
+
try {
|
|
8011
|
+
const transport = resolveNetworkTargetTransport(env, target);
|
|
8012
|
+
const headers = await signFederatedRequest6({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
|
|
8013
|
+
const response = await transport.fetch(destination, { headers, signal: AbortSignal.timeout(1e4) });
|
|
8014
|
+
const responseBody = await readBoundedCommercialParityResponse(response);
|
|
8015
|
+
const snapshot = responseBody !== null ? (() => {
|
|
8016
|
+
try {
|
|
8017
|
+
return JSON.parse(responseBody);
|
|
8018
|
+
} catch {
|
|
8019
|
+
return null;
|
|
8020
|
+
}
|
|
8021
|
+
})() : null;
|
|
8022
|
+
if (!response.ok || !snapshot) {
|
|
8023
|
+
return {
|
|
8024
|
+
...base,
|
|
8025
|
+
available: false,
|
|
8026
|
+
ok: false,
|
|
8027
|
+
transport: transport.kind,
|
|
8028
|
+
error: { code: "chapter_response_failed", status: response.status }
|
|
8029
|
+
};
|
|
8030
|
+
}
|
|
8031
|
+
const sources = ctx.chapter.networkCommercialReadModel?.sources ?? [];
|
|
8032
|
+
const matchingSources = sources.filter((candidate) => candidate.appId === snapshot.partition?.appId && candidate.chapterId === snapshot.partition?.chapterId && candidate.environment === snapshot.partition?.environment && candidate.dataEnvironment === snapshot.partition?.dataEnvironment && candidate.runtime === snapshot.partition?.runtime);
|
|
8033
|
+
if (matchingSources.length !== 1) {
|
|
8034
|
+
return {
|
|
8035
|
+
...base,
|
|
8036
|
+
available: true,
|
|
8037
|
+
ok: false,
|
|
8038
|
+
transport: transport.kind,
|
|
8039
|
+
error: { code: matchingSources.length === 0 ? "source_not_configured" : "source_ambiguous" }
|
|
8040
|
+
};
|
|
8041
|
+
}
|
|
8042
|
+
return {
|
|
8043
|
+
...base,
|
|
8044
|
+
available: true,
|
|
8045
|
+
transport: transport.kind,
|
|
8046
|
+
generatedAt: snapshot.generatedAt,
|
|
8047
|
+
...await reconcileCommercialParitySnapshot({
|
|
8048
|
+
projection,
|
|
8049
|
+
source: matchingSources[0],
|
|
8050
|
+
snapshot,
|
|
8051
|
+
secret
|
|
8052
|
+
})
|
|
8053
|
+
};
|
|
8054
|
+
} catch (error) {
|
|
8055
|
+
return {
|
|
8056
|
+
...base,
|
|
8057
|
+
available: false,
|
|
8058
|
+
ok: false,
|
|
8059
|
+
error: {
|
|
8060
|
+
code: error instanceof NetworkTargetTransportError ? "binding_unavailable" : error instanceof Error && error.message === "invalid chapter commercial parity snapshot" ? "invalid_chapter_snapshot" : "chapter_request_failed"
|
|
8061
|
+
}
|
|
8062
|
+
};
|
|
8063
|
+
}
|
|
8064
|
+
}
|
|
8065
|
+
async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
|
|
8066
|
+
const targets = ctx.chapter.network.targets.filter((target) => target.commercialParity);
|
|
8067
|
+
const results = await Promise.all(targets.map((target) => reconcileTarget(db, env, ctx, target, projection)));
|
|
8068
|
+
return {
|
|
8069
|
+
reconciled: true,
|
|
8070
|
+
provider: "chapters",
|
|
8071
|
+
writable: false,
|
|
8072
|
+
configured: targets.length,
|
|
8073
|
+
available: results.filter((result) => result.available).length,
|
|
8074
|
+
ok: results.every((result) => result.ok),
|
|
8075
|
+
targets: results
|
|
8076
|
+
};
|
|
8077
|
+
}
|
|
8078
|
+
|
|
7781
8079
|
// src/worker-routes-network-commercial.ts
|
|
7782
8080
|
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
7783
8081
|
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
@@ -7881,6 +8179,10 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
7881
8179
|
} catch {
|
|
7882
8180
|
return json({ error: "invalid JSON body" }, 400);
|
|
7883
8181
|
}
|
|
8182
|
+
if (parsed.command === "reconcile-chapters") {
|
|
8183
|
+
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
8184
|
+
return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
|
|
8185
|
+
}
|
|
7884
8186
|
if (parsed.command === "reconcile-stripe") {
|
|
7885
8187
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7886
8188
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
@@ -7903,7 +8205,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
7903
8205
|
}
|
|
7904
8206
|
}
|
|
7905
8207
|
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
7906
|
-
return json({ error: "rebuild or reconcile-
|
|
8208
|
+
return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also requires a valid mutationId" }, 400);
|
|
7907
8209
|
}
|
|
7908
8210
|
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
7909
8211
|
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
@@ -7992,7 +8294,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
7992
8294
|
};
|
|
7993
8295
|
|
|
7994
8296
|
// src/worker-routes-builder-library.ts
|
|
7995
|
-
import { verifyFederatedRequest as
|
|
8297
|
+
import { verifyFederatedRequest as verifyFederatedRequest9 } from "@odla-ai/db";
|
|
7996
8298
|
|
|
7997
8299
|
// src/builder-library-validation.ts
|
|
7998
8300
|
var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -8015,7 +8317,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
8015
8317
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
8016
8318
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
8017
8319
|
}
|
|
8018
|
-
function
|
|
8320
|
+
function text5(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
8019
8321
|
if (value === void 0 && !required) return void 0;
|
|
8020
8322
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
8021
8323
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -8041,7 +8343,7 @@ function scopeValue(value, path, errors) {
|
|
|
8041
8343
|
return { kind: raw2.kind };
|
|
8042
8344
|
}
|
|
8043
8345
|
if (raw2.kind === "chapter") {
|
|
8044
|
-
const chapterId =
|
|
8346
|
+
const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
8045
8347
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
8046
8348
|
return { kind: "chapter", chapterId };
|
|
8047
8349
|
}
|
|
@@ -8055,22 +8357,22 @@ function resourceValue(value, path, errors) {
|
|
|
8055
8357
|
return null;
|
|
8056
8358
|
}
|
|
8057
8359
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
8058
|
-
const id =
|
|
8360
|
+
const id = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
8059
8361
|
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
8060
|
-
const kind =
|
|
8362
|
+
const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
8061
8363
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
8062
|
-
const title =
|
|
8063
|
-
const summary =
|
|
8064
|
-
const status =
|
|
8364
|
+
const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
8365
|
+
const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
8366
|
+
const status = text5(raw2.status, `${path}.status`, errors) ?? "";
|
|
8065
8367
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
8066
8368
|
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
8067
8369
|
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
8068
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
8370
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
8069
8371
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
8070
8372
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
8071
8373
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
8072
|
-
const bodyMarkdown =
|
|
8073
|
-
const url =
|
|
8374
|
+
const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
8375
|
+
const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
|
|
8074
8376
|
if (url) {
|
|
8075
8377
|
try {
|
|
8076
8378
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -8082,9 +8384,9 @@ function resourceValue(value, path, errors) {
|
|
|
8082
8384
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
8083
8385
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
8084
8386
|
const brandAsset = asset ? {
|
|
8085
|
-
bookId:
|
|
8086
|
-
assetId:
|
|
8087
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
8387
|
+
bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
8388
|
+
assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
8389
|
+
...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
8088
8390
|
} : void 0;
|
|
8089
8391
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
8090
8392
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -8099,17 +8401,17 @@ function resourceValue(value, path, errors) {
|
|
|
8099
8401
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
8100
8402
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
8101
8403
|
const promotedFrom = promotedRaw ? {
|
|
8102
|
-
chapterId:
|
|
8103
|
-
resourceId:
|
|
8404
|
+
chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
8405
|
+
resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
8104
8406
|
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
8105
8407
|
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
8106
|
-
promotedBy:
|
|
8408
|
+
promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
8107
8409
|
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
8108
8410
|
} : void 0;
|
|
8109
8411
|
const provenance = {
|
|
8110
|
-
createdBy:
|
|
8412
|
+
createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
8111
8413
|
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
8112
|
-
updatedBy:
|
|
8414
|
+
updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
8113
8415
|
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
8114
8416
|
...promotedFrom ? { promotedFrom } : {}
|
|
8115
8417
|
};
|
|
@@ -8128,7 +8430,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
8128
8430
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
8129
8431
|
const sourceType = sourceRaw?.type;
|
|
8130
8432
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
8131
|
-
const sourceAppId =
|
|
8433
|
+
const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
8132
8434
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
8133
8435
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
8134
8436
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -8136,8 +8438,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
8136
8438
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
8137
8439
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
8138
8440
|
const target = targetRaw ? {
|
|
8139
|
-
appId:
|
|
8140
|
-
environment:
|
|
8441
|
+
appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
8442
|
+
environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
8141
8443
|
} : void 0;
|
|
8142
8444
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
8143
8445
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -8275,7 +8577,7 @@ async function verifySource(req, db, ctx) {
|
|
|
8275
8577
|
if (ctx.chapter.mode === "chapter") {
|
|
8276
8578
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
8277
8579
|
if (!secret) return null;
|
|
8278
|
-
const verified = await
|
|
8580
|
+
const verified = await verifyFederatedRequest9(req, {
|
|
8279
8581
|
secret,
|
|
8280
8582
|
senders: sources.map((source2) => source2.sourceId)
|
|
8281
8583
|
});
|
|
@@ -8288,7 +8590,7 @@ async function verifySource(req, db, ctx) {
|
|
|
8288
8590
|
if (!target) continue;
|
|
8289
8591
|
const secret = await getVaultSecret(db, target.secretName);
|
|
8290
8592
|
if (!secret) continue;
|
|
8291
|
-
const verified = await
|
|
8593
|
+
const verified = await verifyFederatedRequest9(req.clone(), { secret, senders: [source.sourceId] });
|
|
8292
8594
|
if (verified.ok) return { source, body: verified.body };
|
|
8293
8595
|
}
|
|
8294
8596
|
return null;
|
|
@@ -8478,11 +8780,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
8478
8780
|
};
|
|
8479
8781
|
|
|
8480
8782
|
// src/signup-control-delivery.ts
|
|
8481
|
-
import { signFederatedRequest as
|
|
8783
|
+
import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
|
|
8482
8784
|
async function deliverSignupControl(options) {
|
|
8483
8785
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
8484
8786
|
const body = JSON.stringify(options.envelope);
|
|
8485
|
-
const signed = await
|
|
8787
|
+
const signed = await signFederatedRequest7({
|
|
8486
8788
|
secret: options.secret,
|
|
8487
8789
|
sender: options.sender,
|
|
8488
8790
|
method: "POST",
|
|
@@ -8503,11 +8805,11 @@ async function deliverSignupControl(options) {
|
|
|
8503
8805
|
}
|
|
8504
8806
|
|
|
8505
8807
|
// src/member-content-delivery.ts
|
|
8506
|
-
import { signFederatedRequest as
|
|
8808
|
+
import { signFederatedRequest as signFederatedRequest8 } from "@odla-ai/db";
|
|
8507
8809
|
async function deliverMemberContent(options) {
|
|
8508
8810
|
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
8509
8811
|
const body = JSON.stringify(options.envelope);
|
|
8510
|
-
const signed = await
|
|
8812
|
+
const signed = await signFederatedRequest8({
|
|
8511
8813
|
secret: options.secret,
|
|
8512
8814
|
sender: options.sender,
|
|
8513
8815
|
method: "POST",
|
|
@@ -8528,11 +8830,11 @@ async function deliverMemberContent(options) {
|
|
|
8528
8830
|
}
|
|
8529
8831
|
|
|
8530
8832
|
// src/builder-library-delivery.ts
|
|
8531
|
-
import { signFederatedRequest as
|
|
8833
|
+
import { signFederatedRequest as signFederatedRequest9 } from "@odla-ai/db";
|
|
8532
8834
|
async function deliverBuilderLibrary(options) {
|
|
8533
8835
|
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
8534
8836
|
const body = JSON.stringify(options.envelope);
|
|
8535
|
-
const signed = await
|
|
8837
|
+
const signed = await signFederatedRequest9({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
8536
8838
|
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
8537
8839
|
const result = await response.json();
|
|
8538
8840
|
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
@@ -8546,7 +8848,7 @@ async function deliverBuilderLibrary(options) {
|
|
|
8546
8848
|
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
8547
8849
|
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
8548
8850
|
var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
|
|
8549
|
-
var
|
|
8851
|
+
var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
|
|
8550
8852
|
var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
|
|
8551
8853
|
var MAX_TOKENS = 256;
|
|
8552
8854
|
var MAX_VALUE_LENGTH = 1e3;
|
|
@@ -8590,7 +8892,7 @@ function stateFor(revision, options) {
|
|
|
8590
8892
|
metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
|
|
8591
8893
|
};
|
|
8592
8894
|
}
|
|
8593
|
-
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !
|
|
8895
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
|
|
8594
8896
|
throw new TypeError("signup-control theme revision metadata is invalid");
|
|
8595
8897
|
}
|
|
8596
8898
|
return {
|
|
@@ -8687,6 +8989,7 @@ var BUILTIN_ROUTES = [
|
|
|
8687
8989
|
handleNetworkSnapshot,
|
|
8688
8990
|
handleNetworkRecords,
|
|
8689
8991
|
handleNetworkSharedNotes,
|
|
8992
|
+
handleNetworkCommercialParity,
|
|
8690
8993
|
handleNetworkCommercialStripeWebhook,
|
|
8691
8994
|
handleFormation,
|
|
8692
8995
|
handleMember,
|