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