@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.cjs
CHANGED
|
@@ -115,12 +115,16 @@ function createWorkerContext(options) {
|
|
|
115
115
|
}
|
|
116
116
|
async function isAdminEmail(db, email) {
|
|
117
117
|
if (!email) return false;
|
|
118
|
-
const { admins } = await db.query({ admins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });
|
|
118
|
+
const { admins } = await db.query({ admins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
|
|
119
119
|
return Array.isArray(admins) && admins.length > 0;
|
|
120
120
|
}
|
|
121
121
|
async function isSuperAdminEmail(db, email) {
|
|
122
122
|
if (!auth.superAdmins || !email) return false;
|
|
123
|
-
|
|
123
|
+
if (auth.superAdminSource === "odla") {
|
|
124
|
+
const { $users } = await db.query({ $users: { $: { where: { email: email.trim().toLowerCase(), role: "super_admin" }, limit: 1 } } });
|
|
125
|
+
return Array.isArray($users) && $users.length > 0;
|
|
126
|
+
}
|
|
127
|
+
const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
|
|
124
128
|
return Array.isArray(superAdmins) && superAdmins.length > 0;
|
|
125
129
|
}
|
|
126
130
|
async function roleFor(db, u) {
|
|
@@ -759,8 +763,8 @@ var APPLICATION_LABELS = {
|
|
|
759
763
|
state: "State"
|
|
760
764
|
};
|
|
761
765
|
function humanize(id) {
|
|
762
|
-
const
|
|
763
|
-
return
|
|
766
|
+
const text6 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
767
|
+
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id;
|
|
764
768
|
}
|
|
765
769
|
function applicationDefs(chapter) {
|
|
766
770
|
try {
|
|
@@ -1290,10 +1294,10 @@ function planDelivery(input) {
|
|
|
1290
1294
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1291
1295
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1292
1296
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1293
|
-
const
|
|
1297
|
+
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1294
1298
|
|
|
1295
1299
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1296
|
-
return { deliver: true, transport, to, subject, text:
|
|
1300
|
+
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
1297
1301
|
}
|
|
1298
1302
|
|
|
1299
1303
|
// src/notify.ts
|
|
@@ -2181,10 +2185,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2181
2185
|
errors.push(`${path} must be a string`);
|
|
2182
2186
|
return void 0;
|
|
2183
2187
|
}
|
|
2184
|
-
const
|
|
2185
|
-
if (required && !
|
|
2186
|
-
if (
|
|
2187
|
-
return
|
|
2188
|
+
const text6 = value.trim();
|
|
2189
|
+
if (required && !text6) errors.push(`${path} is required`);
|
|
2190
|
+
if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2191
|
+
return text6;
|
|
2188
2192
|
}
|
|
2189
2193
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2190
2194
|
const errors = [];
|
|
@@ -2233,8 +2237,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2233
2237
|
else {
|
|
2234
2238
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2235
2239
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2236
|
-
const
|
|
2237
|
-
return
|
|
2240
|
+
const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2241
|
+
return text6 ? [text6] : [];
|
|
2238
2242
|
});
|
|
2239
2243
|
}
|
|
2240
2244
|
}
|
|
@@ -3946,17 +3950,17 @@ async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
|
3946
3950
|
try {
|
|
3947
3951
|
const group = await groupFor(db, app);
|
|
3948
3952
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
3949
|
-
const
|
|
3953
|
+
const text6 = (value) => typeof value === "string" ? value : "";
|
|
3950
3954
|
await sendTemplated(deps(db, env), {
|
|
3951
3955
|
group: emailGroupFrom(group),
|
|
3952
3956
|
template: "adminNotification",
|
|
3953
3957
|
to: group.notificationEmail,
|
|
3954
3958
|
vars: {
|
|
3955
|
-
firstName:
|
|
3956
|
-
lastName:
|
|
3957
|
-
email:
|
|
3958
|
-
phone:
|
|
3959
|
-
state:
|
|
3959
|
+
firstName: text6(app.firstName),
|
|
3960
|
+
lastName: text6(app.lastName),
|
|
3961
|
+
email: text6(app.email),
|
|
3962
|
+
phone: text6(app.phone),
|
|
3963
|
+
state: text6(app.state)
|
|
3960
3964
|
},
|
|
3961
3965
|
dedupeKey: `${eventId}:admin`,
|
|
3962
3966
|
applicationId: String(app.id)
|
|
@@ -5720,11 +5724,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
5720
5724
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
5721
5725
|
const t = templates[key];
|
|
5722
5726
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
5723
|
-
const
|
|
5724
|
-
if (!subject || !
|
|
5727
|
+
const text6 = typeof t?.text === "string" ? t.text : "";
|
|
5728
|
+
if (!subject || !text6.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
5725
5729
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
5726
|
-
if (
|
|
5727
|
-
clean[key] = { subject, text:
|
|
5730
|
+
if (text6.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
5731
|
+
clean[key] = { subject, text: text6, enabled: t?.enabled !== false };
|
|
5728
5732
|
}
|
|
5729
5733
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5730
5734
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -5941,7 +5945,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
5941
5945
|
fields,
|
|
5942
5946
|
sharedNotes,
|
|
5943
5947
|
editableFields,
|
|
5944
|
-
stageTransitions
|
|
5948
|
+
stageTransitions,
|
|
5949
|
+
commercialParity
|
|
5945
5950
|
}) => ({
|
|
5946
5951
|
id,
|
|
5947
5952
|
name,
|
|
@@ -5949,7 +5954,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
5949
5954
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
5950
5955
|
sharedNotes,
|
|
5951
5956
|
editableFields,
|
|
5952
|
-
stageTransitions
|
|
5957
|
+
stageTransitions,
|
|
5958
|
+
commercialParity
|
|
5953
5959
|
}))
|
|
5954
5960
|
});
|
|
5955
5961
|
};
|
|
@@ -7207,9 +7213,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7207
7213
|
if (req.method === "POST") {
|
|
7208
7214
|
let body;
|
|
7209
7215
|
try {
|
|
7210
|
-
const
|
|
7211
|
-
if (new TextEncoder().encode(
|
|
7212
|
-
const parsed = JSON.parse(
|
|
7216
|
+
const text6 = await req.text();
|
|
7217
|
+
if (new TextEncoder().encode(text6).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7218
|
+
const parsed = JSON.parse(text6);
|
|
7213
7219
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7214
7220
|
body = parsed;
|
|
7215
7221
|
} catch {
|
|
@@ -7804,6 +7810,298 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
|
7804
7810
|
};
|
|
7805
7811
|
}
|
|
7806
7812
|
|
|
7813
|
+
// src/worker-routes-network-commercial-parity.ts
|
|
7814
|
+
var import_db12 = require("@odla-ai/db");
|
|
7815
|
+
|
|
7816
|
+
// src/network-commercial-parity.ts
|
|
7817
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
7818
|
+
var MAX_RECORDS = 500;
|
|
7819
|
+
var text4 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
7820
|
+
function parityMessage(partition, kind, value) {
|
|
7821
|
+
return [
|
|
7822
|
+
"chapter-commercial-parity",
|
|
7823
|
+
"v1",
|
|
7824
|
+
partition.appId,
|
|
7825
|
+
partition.chapterId,
|
|
7826
|
+
partition.environment,
|
|
7827
|
+
partition.dataEnvironment,
|
|
7828
|
+
partition.runtime,
|
|
7829
|
+
kind,
|
|
7830
|
+
value
|
|
7831
|
+
].join("");
|
|
7832
|
+
}
|
|
7833
|
+
async function parityKey(secret) {
|
|
7834
|
+
const encoder4 = new TextEncoder();
|
|
7835
|
+
return crypto.subtle.importKey(
|
|
7836
|
+
"raw",
|
|
7837
|
+
encoder4.encode(secret),
|
|
7838
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
7839
|
+
false,
|
|
7840
|
+
["sign"]
|
|
7841
|
+
);
|
|
7842
|
+
}
|
|
7843
|
+
async function digestWithKey(key, partition, kind, value) {
|
|
7844
|
+
const encoder4 = new TextEncoder();
|
|
7845
|
+
const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
|
|
7846
|
+
return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7847
|
+
}
|
|
7848
|
+
async function buildChapterCommercialParitySnapshot(input) {
|
|
7849
|
+
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
7850
|
+
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
7851
|
+
const eligible = input.rows.filter((row) => text4(row.id) && (text4(row.stripeCustomerId) || text4(row.stripeSubscriptionId)));
|
|
7852
|
+
const selected2 = eligible.slice(0, maxRecords);
|
|
7853
|
+
const key = await parityKey(input.secret);
|
|
7854
|
+
const records = await Promise.all(selected2.map(async (row) => {
|
|
7855
|
+
const applicationId = text4(row.id);
|
|
7856
|
+
const customerId = text4(row.stripeCustomerId);
|
|
7857
|
+
const subscriptionId = text4(row.stripeSubscriptionId);
|
|
7858
|
+
return {
|
|
7859
|
+
applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
|
|
7860
|
+
customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
|
|
7861
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
|
|
7862
|
+
status: text4(row.status, 64),
|
|
7863
|
+
canceled: row.canceled === true
|
|
7864
|
+
};
|
|
7865
|
+
}));
|
|
7866
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
7867
|
+
const truncated = input.scanTruncated || eligible.length > maxRecords;
|
|
7868
|
+
return {
|
|
7869
|
+
version: 1,
|
|
7870
|
+
site: { id: input.siteId },
|
|
7871
|
+
partition: input.partition,
|
|
7872
|
+
generatedAt: input.now ?? Date.now(),
|
|
7873
|
+
complete: !truncated,
|
|
7874
|
+
truncated,
|
|
7875
|
+
counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
|
|
7876
|
+
records
|
|
7877
|
+
};
|
|
7878
|
+
}
|
|
7879
|
+
function recordKey(record4) {
|
|
7880
|
+
return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
|
|
7881
|
+
}
|
|
7882
|
+
function validSnapshot(snapshot, source) {
|
|
7883
|
+
const partition = snapshot?.partition;
|
|
7884
|
+
const records = snapshot?.records;
|
|
7885
|
+
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;
|
|
7886
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7887
|
+
for (const record4 of records) {
|
|
7888
|
+
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;
|
|
7889
|
+
const key = recordKey(record4);
|
|
7890
|
+
if (seen.has(key)) return false;
|
|
7891
|
+
seen.add(key);
|
|
7892
|
+
}
|
|
7893
|
+
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;
|
|
7894
|
+
}
|
|
7895
|
+
async function reconcileCommercialParitySnapshot(input) {
|
|
7896
|
+
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
7897
|
+
const sourceKey = networkCommercialSourceKey(input.source);
|
|
7898
|
+
const key = await parityKey(input.secret);
|
|
7899
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text4(row.applicationId) && text4(row.objectId)).map(async (row) => {
|
|
7900
|
+
const customerId = text4(row.customerId);
|
|
7901
|
+
const subscriptionId = text4(row.subscriptionId) ?? text4(row.objectId);
|
|
7902
|
+
return {
|
|
7903
|
+
applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
|
|
7904
|
+
customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
|
|
7905
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
7906
|
+
status: null,
|
|
7907
|
+
canceled: false,
|
|
7908
|
+
stripeStatus: text4(row.status, 64),
|
|
7909
|
+
stripeCanceled: row.status === "canceled"
|
|
7910
|
+
};
|
|
7911
|
+
}));
|
|
7912
|
+
const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
|
|
7913
|
+
const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
|
|
7914
|
+
const exceptions = [];
|
|
7915
|
+
const records = [];
|
|
7916
|
+
for (const [key2, chapter] of chapterByKey) {
|
|
7917
|
+
const stripe = shadowByKey.get(key2);
|
|
7918
|
+
if (!stripe) {
|
|
7919
|
+
exceptions.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
|
|
7920
|
+
continue;
|
|
7921
|
+
}
|
|
7922
|
+
const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
|
|
7923
|
+
records.push(compared);
|
|
7924
|
+
if (chapter.canceled !== stripe.stripeCanceled) {
|
|
7925
|
+
exceptions.push({
|
|
7926
|
+
kind: "cancellation_mismatch",
|
|
7927
|
+
applicationDigest: chapter.applicationDigest,
|
|
7928
|
+
detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
|
|
7929
|
+
});
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
for (const [key2, stripe] of shadowByKey) {
|
|
7933
|
+
if (!chapterByKey.has(key2)) {
|
|
7934
|
+
exceptions.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
if (!input.snapshot.complete || input.snapshot.truncated) {
|
|
7938
|
+
exceptions.push({
|
|
7939
|
+
kind: "chapter_truncated",
|
|
7940
|
+
detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
|
|
7941
|
+
});
|
|
7942
|
+
}
|
|
7943
|
+
if (chapterByKey.size !== shadowByKey.size) {
|
|
7944
|
+
exceptions.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
|
|
7945
|
+
}
|
|
7946
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
7947
|
+
return {
|
|
7948
|
+
ok: input.snapshot.complete && exceptions.length === 0,
|
|
7949
|
+
complete: input.snapshot.complete,
|
|
7950
|
+
counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
|
|
7951
|
+
records,
|
|
7952
|
+
exceptions
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
|
|
7956
|
+
// src/network-commercial-parity-response.ts
|
|
7957
|
+
var MAX_RESPONSE_BYTES = 512e3;
|
|
7958
|
+
async function readBoundedCommercialParityResponse(response) {
|
|
7959
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
7960
|
+
if (declared > MAX_RESPONSE_BYTES) return null;
|
|
7961
|
+
if (!response.body) return "";
|
|
7962
|
+
const reader = response.body.getReader();
|
|
7963
|
+
const chunks = [];
|
|
7964
|
+
let total = 0;
|
|
7965
|
+
for (; ; ) {
|
|
7966
|
+
const next = await reader.read();
|
|
7967
|
+
if (next.done) break;
|
|
7968
|
+
total += next.value.byteLength;
|
|
7969
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
7970
|
+
await reader.cancel();
|
|
7971
|
+
return null;
|
|
7972
|
+
}
|
|
7973
|
+
chunks.push(next.value);
|
|
7974
|
+
}
|
|
7975
|
+
const body = new Uint8Array(total);
|
|
7976
|
+
let offset = 0;
|
|
7977
|
+
for (const chunk of chunks) {
|
|
7978
|
+
body.set(chunk, offset);
|
|
7979
|
+
offset += chunk.byteLength;
|
|
7980
|
+
}
|
|
7981
|
+
return new TextDecoder().decode(body);
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
// src/worker-routes-network-commercial-parity.ts
|
|
7985
|
+
var PARITY_PATH = "/api/network/commercial-parity";
|
|
7986
|
+
var PAGE2 = 500;
|
|
7987
|
+
var MAX_SCAN = 5e3;
|
|
7988
|
+
var MAX_RECORDS2 = 500;
|
|
7989
|
+
async function localCommercialRows(db) {
|
|
7990
|
+
const rows6 = [];
|
|
7991
|
+
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
7992
|
+
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
7993
|
+
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
7994
|
+
rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
|
|
7995
|
+
if (page.length < PAGE2) return { rows: rows6, truncated: false };
|
|
7996
|
+
}
|
|
7997
|
+
return { rows: rows6, truncated: true };
|
|
7998
|
+
}
|
|
7999
|
+
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
8000
|
+
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
8001
|
+
if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
|
|
8002
|
+
const readers = ctx.chapter.network.readers.filter((reader) => reader.commercialParity);
|
|
8003
|
+
if (readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
8004
|
+
const db = ctx.makeDb(env);
|
|
8005
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
8006
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
8007
|
+
const verified = await (0, import_db12.verifyFederatedRequest)(req, { secret, senders: readers.map((reader) => reader.id) });
|
|
8008
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
8009
|
+
const partition = {
|
|
8010
|
+
appId: env.ODLA_APP_ID,
|
|
8011
|
+
chapterId: ctx.chapter.id,
|
|
8012
|
+
environment: env.ODLA_ENV,
|
|
8013
|
+
dataEnvironment: env.ODLA_ENV,
|
|
8014
|
+
runtime: env.ODLA_RUNTIME?.trim() || "default"
|
|
8015
|
+
};
|
|
8016
|
+
try {
|
|
8017
|
+
const local3 = await localCommercialRows(db);
|
|
8018
|
+
return json(await buildChapterCommercialParitySnapshot({
|
|
8019
|
+
rows: local3.rows,
|
|
8020
|
+
secret,
|
|
8021
|
+
partition,
|
|
8022
|
+
siteId: ctx.chapter.id,
|
|
8023
|
+
scanned: local3.rows.length,
|
|
8024
|
+
scanTruncated: local3.truncated,
|
|
8025
|
+
maxRecords: MAX_RECORDS2
|
|
8026
|
+
}));
|
|
8027
|
+
} catch {
|
|
8028
|
+
return json({ error: "commercial parity unavailable" }, 503);
|
|
8029
|
+
}
|
|
8030
|
+
};
|
|
8031
|
+
async function reconcileTarget(db, env, ctx, target, projection) {
|
|
8032
|
+
const base = { targetId: target.id, name: target.name, url: target.url };
|
|
8033
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
8034
|
+
if (!secret) return { ...base, available: false, ok: false, error: { code: "edge_secret_missing" } };
|
|
8035
|
+
const destination = new URL(PARITY_PATH, target.url);
|
|
8036
|
+
try {
|
|
8037
|
+
const transport = resolveNetworkTargetTransport(env, target);
|
|
8038
|
+
const headers = await (0, import_db12.signFederatedRequest)({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
|
|
8039
|
+
const response = await transport.fetch(destination, { headers, signal: AbortSignal.timeout(1e4) });
|
|
8040
|
+
const responseBody = await readBoundedCommercialParityResponse(response);
|
|
8041
|
+
const snapshot = responseBody !== null ? (() => {
|
|
8042
|
+
try {
|
|
8043
|
+
return JSON.parse(responseBody);
|
|
8044
|
+
} catch {
|
|
8045
|
+
return null;
|
|
8046
|
+
}
|
|
8047
|
+
})() : null;
|
|
8048
|
+
if (!response.ok || !snapshot) {
|
|
8049
|
+
return {
|
|
8050
|
+
...base,
|
|
8051
|
+
available: false,
|
|
8052
|
+
ok: false,
|
|
8053
|
+
transport: transport.kind,
|
|
8054
|
+
error: { code: "chapter_response_failed", status: response.status }
|
|
8055
|
+
};
|
|
8056
|
+
}
|
|
8057
|
+
const sources = ctx.chapter.networkCommercialReadModel?.sources ?? [];
|
|
8058
|
+
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);
|
|
8059
|
+
if (matchingSources.length !== 1) {
|
|
8060
|
+
return {
|
|
8061
|
+
...base,
|
|
8062
|
+
available: true,
|
|
8063
|
+
ok: false,
|
|
8064
|
+
transport: transport.kind,
|
|
8065
|
+
error: { code: matchingSources.length === 0 ? "source_not_configured" : "source_ambiguous" }
|
|
8066
|
+
};
|
|
8067
|
+
}
|
|
8068
|
+
return {
|
|
8069
|
+
...base,
|
|
8070
|
+
available: true,
|
|
8071
|
+
transport: transport.kind,
|
|
8072
|
+
generatedAt: snapshot.generatedAt,
|
|
8073
|
+
...await reconcileCommercialParitySnapshot({
|
|
8074
|
+
projection,
|
|
8075
|
+
source: matchingSources[0],
|
|
8076
|
+
snapshot,
|
|
8077
|
+
secret
|
|
8078
|
+
})
|
|
8079
|
+
};
|
|
8080
|
+
} catch (error) {
|
|
8081
|
+
return {
|
|
8082
|
+
...base,
|
|
8083
|
+
available: false,
|
|
8084
|
+
ok: false,
|
|
8085
|
+
error: {
|
|
8086
|
+
code: error instanceof NetworkTargetTransportError ? "binding_unavailable" : error instanceof Error && error.message === "invalid chapter commercial parity snapshot" ? "invalid_chapter_snapshot" : "chapter_request_failed"
|
|
8087
|
+
}
|
|
8088
|
+
};
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
|
|
8092
|
+
const targets = ctx.chapter.network.targets.filter((target) => target.commercialParity);
|
|
8093
|
+
const results = await Promise.all(targets.map((target) => reconcileTarget(db, env, ctx, target, projection)));
|
|
8094
|
+
return {
|
|
8095
|
+
reconciled: true,
|
|
8096
|
+
provider: "chapters",
|
|
8097
|
+
writable: false,
|
|
8098
|
+
configured: targets.length,
|
|
8099
|
+
available: results.filter((result) => result.available).length,
|
|
8100
|
+
ok: results.every((result) => result.ok),
|
|
8101
|
+
targets: results
|
|
8102
|
+
};
|
|
8103
|
+
}
|
|
8104
|
+
|
|
7807
8105
|
// src/worker-routes-network-commercial.ts
|
|
7808
8106
|
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
7809
8107
|
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
@@ -7907,6 +8205,10 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
7907
8205
|
} catch {
|
|
7908
8206
|
return json({ error: "invalid JSON body" }, 400);
|
|
7909
8207
|
}
|
|
8208
|
+
if (parsed.command === "reconcile-chapters") {
|
|
8209
|
+
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
8210
|
+
return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
|
|
8211
|
+
}
|
|
7910
8212
|
if (parsed.command === "reconcile-stripe") {
|
|
7911
8213
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7912
8214
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
@@ -7929,7 +8231,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
7929
8231
|
}
|
|
7930
8232
|
}
|
|
7931
8233
|
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
7932
|
-
return json({ error: "rebuild or reconcile-
|
|
8234
|
+
return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also requires a valid mutationId" }, 400);
|
|
7933
8235
|
}
|
|
7934
8236
|
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
7935
8237
|
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
@@ -8018,7 +8320,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
8018
8320
|
};
|
|
8019
8321
|
|
|
8020
8322
|
// src/worker-routes-builder-library.ts
|
|
8021
|
-
var
|
|
8323
|
+
var import_db13 = require("@odla-ai/db");
|
|
8022
8324
|
|
|
8023
8325
|
// src/builder-library-validation.ts
|
|
8024
8326
|
var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -8041,7 +8343,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
8041
8343
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
8042
8344
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
8043
8345
|
}
|
|
8044
|
-
function
|
|
8346
|
+
function text5(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
8045
8347
|
if (value === void 0 && !required) return void 0;
|
|
8046
8348
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
8047
8349
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -8067,7 +8369,7 @@ function scopeValue(value, path, errors) {
|
|
|
8067
8369
|
return { kind: raw2.kind };
|
|
8068
8370
|
}
|
|
8069
8371
|
if (raw2.kind === "chapter") {
|
|
8070
|
-
const chapterId =
|
|
8372
|
+
const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
8071
8373
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
8072
8374
|
return { kind: "chapter", chapterId };
|
|
8073
8375
|
}
|
|
@@ -8081,22 +8383,22 @@ function resourceValue(value, path, errors) {
|
|
|
8081
8383
|
return null;
|
|
8082
8384
|
}
|
|
8083
8385
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
8084
|
-
const id =
|
|
8386
|
+
const id = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
8085
8387
|
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
8086
|
-
const kind =
|
|
8388
|
+
const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
8087
8389
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
8088
|
-
const title =
|
|
8089
|
-
const summary =
|
|
8090
|
-
const status =
|
|
8390
|
+
const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
8391
|
+
const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
8392
|
+
const status = text5(raw2.status, `${path}.status`, errors) ?? "";
|
|
8091
8393
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
8092
8394
|
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
8093
8395
|
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
8094
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
8396
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
8095
8397
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
8096
8398
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
8097
8399
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
8098
|
-
const bodyMarkdown =
|
|
8099
|
-
const url =
|
|
8400
|
+
const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
8401
|
+
const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
|
|
8100
8402
|
if (url) {
|
|
8101
8403
|
try {
|
|
8102
8404
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -8108,9 +8410,9 @@ function resourceValue(value, path, errors) {
|
|
|
8108
8410
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
8109
8411
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
8110
8412
|
const brandAsset = asset ? {
|
|
8111
|
-
bookId:
|
|
8112
|
-
assetId:
|
|
8113
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
8413
|
+
bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
8414
|
+
assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
8415
|
+
...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
8114
8416
|
} : void 0;
|
|
8115
8417
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
8116
8418
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -8125,17 +8427,17 @@ function resourceValue(value, path, errors) {
|
|
|
8125
8427
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
8126
8428
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
8127
8429
|
const promotedFrom = promotedRaw ? {
|
|
8128
|
-
chapterId:
|
|
8129
|
-
resourceId:
|
|
8430
|
+
chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
8431
|
+
resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
8130
8432
|
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
8131
8433
|
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
8132
|
-
promotedBy:
|
|
8434
|
+
promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
8133
8435
|
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
8134
8436
|
} : void 0;
|
|
8135
8437
|
const provenance = {
|
|
8136
|
-
createdBy:
|
|
8438
|
+
createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
8137
8439
|
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
8138
|
-
updatedBy:
|
|
8440
|
+
updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
8139
8441
|
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
8140
8442
|
...promotedFrom ? { promotedFrom } : {}
|
|
8141
8443
|
};
|
|
@@ -8154,7 +8456,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
8154
8456
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
8155
8457
|
const sourceType = sourceRaw?.type;
|
|
8156
8458
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
8157
|
-
const sourceAppId =
|
|
8459
|
+
const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
8158
8460
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
8159
8461
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
8160
8462
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -8162,8 +8464,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
8162
8464
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
8163
8465
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
8164
8466
|
const target = targetRaw ? {
|
|
8165
|
-
appId:
|
|
8166
|
-
environment:
|
|
8467
|
+
appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
8468
|
+
environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
8167
8469
|
} : void 0;
|
|
8168
8470
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
8169
8471
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -8301,7 +8603,7 @@ async function verifySource(req, db, ctx) {
|
|
|
8301
8603
|
if (ctx.chapter.mode === "chapter") {
|
|
8302
8604
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
8303
8605
|
if (!secret) return null;
|
|
8304
|
-
const verified = await (0,
|
|
8606
|
+
const verified = await (0, import_db13.verifyFederatedRequest)(req, {
|
|
8305
8607
|
secret,
|
|
8306
8608
|
senders: sources.map((source2) => source2.sourceId)
|
|
8307
8609
|
});
|
|
@@ -8314,7 +8616,7 @@ async function verifySource(req, db, ctx) {
|
|
|
8314
8616
|
if (!target) continue;
|
|
8315
8617
|
const secret = await getVaultSecret(db, target.secretName);
|
|
8316
8618
|
if (!secret) continue;
|
|
8317
|
-
const verified = await (0,
|
|
8619
|
+
const verified = await (0, import_db13.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
|
|
8318
8620
|
if (verified.ok) return { source, body: verified.body };
|
|
8319
8621
|
}
|
|
8320
8622
|
return null;
|
|
@@ -8504,11 +8806,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
8504
8806
|
};
|
|
8505
8807
|
|
|
8506
8808
|
// src/signup-control-delivery.ts
|
|
8507
|
-
var
|
|
8809
|
+
var import_db14 = require("@odla-ai/db");
|
|
8508
8810
|
async function deliverSignupControl(options) {
|
|
8509
8811
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
8510
8812
|
const body = JSON.stringify(options.envelope);
|
|
8511
|
-
const signed = await (0,
|
|
8813
|
+
const signed = await (0, import_db14.signFederatedRequest)({
|
|
8512
8814
|
secret: options.secret,
|
|
8513
8815
|
sender: options.sender,
|
|
8514
8816
|
method: "POST",
|
|
@@ -8529,11 +8831,11 @@ async function deliverSignupControl(options) {
|
|
|
8529
8831
|
}
|
|
8530
8832
|
|
|
8531
8833
|
// src/member-content-delivery.ts
|
|
8532
|
-
var
|
|
8834
|
+
var import_db15 = require("@odla-ai/db");
|
|
8533
8835
|
async function deliverMemberContent(options) {
|
|
8534
8836
|
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
8535
8837
|
const body = JSON.stringify(options.envelope);
|
|
8536
|
-
const signed = await (0,
|
|
8838
|
+
const signed = await (0, import_db15.signFederatedRequest)({
|
|
8537
8839
|
secret: options.secret,
|
|
8538
8840
|
sender: options.sender,
|
|
8539
8841
|
method: "POST",
|
|
@@ -8554,11 +8856,11 @@ async function deliverMemberContent(options) {
|
|
|
8554
8856
|
}
|
|
8555
8857
|
|
|
8556
8858
|
// src/builder-library-delivery.ts
|
|
8557
|
-
var
|
|
8859
|
+
var import_db16 = require("@odla-ai/db");
|
|
8558
8860
|
async function deliverBuilderLibrary(options) {
|
|
8559
8861
|
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
8560
8862
|
const body = JSON.stringify(options.envelope);
|
|
8561
|
-
const signed = await (0,
|
|
8863
|
+
const signed = await (0, import_db16.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
8562
8864
|
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
8563
8865
|
const result = await response.json();
|
|
8564
8866
|
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
@@ -8572,7 +8874,7 @@ async function deliverBuilderLibrary(options) {
|
|
|
8572
8874
|
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
8573
8875
|
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
8574
8876
|
var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
|
|
8575
|
-
var
|
|
8877
|
+
var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
|
|
8576
8878
|
var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
|
|
8577
8879
|
var MAX_TOKENS = 256;
|
|
8578
8880
|
var MAX_VALUE_LENGTH = 1e3;
|
|
@@ -8616,7 +8918,7 @@ function stateFor(revision, options) {
|
|
|
8616
8918
|
metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
|
|
8617
8919
|
};
|
|
8618
8920
|
}
|
|
8619
|
-
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !
|
|
8921
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
|
|
8620
8922
|
throw new TypeError("signup-control theme revision metadata is invalid");
|
|
8621
8923
|
}
|
|
8622
8924
|
return {
|
|
@@ -8713,6 +9015,7 @@ var BUILTIN_ROUTES = [
|
|
|
8713
9015
|
handleNetworkSnapshot,
|
|
8714
9016
|
handleNetworkRecords,
|
|
8715
9017
|
handleNetworkSharedNotes,
|
|
9018
|
+
handleNetworkCommercialParity,
|
|
8716
9019
|
handleNetworkCommercialStripeWebhook,
|
|
8717
9020
|
handleFormation,
|
|
8718
9021
|
handleMember,
|