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