@odla-ai/chapter 0.44.0 → 0.45.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 +41 -0
- package/dist/{copy-context-B4SCy6vx.d.ts → copy-context-CIxmneHI.d.ts} +24 -0
- package/dist/index.cjs +687 -279
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -3
- package/dist/index.d.ts +134 -3
- package/dist/index.js +687 -279
- 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 +903 -305
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +24 -0
- package/dist/worker/index.d.ts +24 -0
- package/dist/worker/index.js +903 -305
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -39,8 +39,8 @@ var import_jose = require("jose");
|
|
|
39
39
|
|
|
40
40
|
// src/auth.ts
|
|
41
41
|
function roleFromClaim(payload, auth) {
|
|
42
|
-
const
|
|
43
|
-
return typeof
|
|
42
|
+
const raw2 = payload[auth.claim];
|
|
43
|
+
return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
|
|
44
44
|
}
|
|
45
45
|
function isAdminRole(role, auth) {
|
|
46
46
|
return role === auth.adminRole;
|
|
@@ -223,7 +223,7 @@ function resolveApplication(a) {
|
|
|
223
223
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
-
const duplicate = [...required, ...optional].find((field,
|
|
226
|
+
const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
227
227
|
if (duplicate) {
|
|
228
228
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
229
229
|
}
|
|
@@ -371,8 +371,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
371
371
|
function byOrder(a, b) {
|
|
372
372
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
373
373
|
}
|
|
374
|
-
function resolveTiers(group,
|
|
375
|
-
const stored =
|
|
374
|
+
function resolveTiers(group, rows6 = []) {
|
|
375
|
+
const stored = rows6.filter((tier) => tier.active);
|
|
376
376
|
if (stored.length) return [...stored].sort(byOrder);
|
|
377
377
|
return [{
|
|
378
378
|
id: "standard",
|
|
@@ -418,15 +418,15 @@ function rowToTier(row, groupId) {
|
|
|
418
418
|
}
|
|
419
419
|
async function loadTiers(db, group) {
|
|
420
420
|
const groupId = String(group.id ?? "");
|
|
421
|
-
let
|
|
421
|
+
let rows6;
|
|
422
422
|
try {
|
|
423
|
-
|
|
423
|
+
rows6 = (await db.query({
|
|
424
424
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
425
425
|
})).tiers;
|
|
426
426
|
} catch {
|
|
427
|
-
|
|
427
|
+
rows6 = void 0;
|
|
428
428
|
}
|
|
429
|
-
const list = Array.isArray(
|
|
429
|
+
const list = Array.isArray(rows6) ? rows6.map((row) => rowToTier(row, groupId)) : [];
|
|
430
430
|
return resolveTiers(group, list);
|
|
431
431
|
}
|
|
432
432
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -563,8 +563,8 @@ function canonicalSignupControl(value) {
|
|
|
563
563
|
return canonicalValue(value);
|
|
564
564
|
}
|
|
565
565
|
async function signupControlDigest(document) {
|
|
566
|
-
const
|
|
567
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
566
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
567
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
// src/admission-grants.ts
|
|
@@ -600,24 +600,24 @@ function validateAdmissionGrantDocument(input) {
|
|
|
600
600
|
const grants = [];
|
|
601
601
|
const seenIds = /* @__PURE__ */ new Set();
|
|
602
602
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
603
|
-
for (const [
|
|
604
|
-
const grant =
|
|
603
|
+
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
604
|
+
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
605
605
|
const id = typeof grant.id === "string" ? grant.id : "";
|
|
606
606
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
607
607
|
const normalized = normalizeAdmissionEmail(email);
|
|
608
608
|
const decision = grant.decision;
|
|
609
609
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
610
610
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
611
|
-
if (!ID.test(id)) errors.push(`document.grants[${
|
|
611
|
+
if (!ID.test(id)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
612
612
|
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
613
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${
|
|
613
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
614
614
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
615
615
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
616
|
-
errors.push(`document.grants[${
|
|
616
|
+
errors.push(`document.grants[${index2}].decision is invalid`);
|
|
617
617
|
}
|
|
618
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${
|
|
618
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
|
|
619
619
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
620
|
-
errors.push(`document.grants[${
|
|
620
|
+
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
621
621
|
}
|
|
622
622
|
seenIds.add(id);
|
|
623
623
|
seenEmails.add(normalized);
|
|
@@ -685,7 +685,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
|
|
|
685
685
|
return row ? row : null;
|
|
686
686
|
}
|
|
687
687
|
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
688
|
-
const { document, digest } = envelope;
|
|
688
|
+
const { document, digest: digest2 } = envelope;
|
|
689
689
|
const current = rows((await db.query({ admissionGrants: { $: {
|
|
690
690
|
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
691
691
|
limit: MAX_GRANTS
|
|
@@ -737,11 +737,11 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
737
737
|
targetId: document.target.chapterId,
|
|
738
738
|
environment: document.target.environment,
|
|
739
739
|
revision: document.revision,
|
|
740
|
-
digest,
|
|
740
|
+
digest: digest2,
|
|
741
741
|
active: true,
|
|
742
742
|
receivedAt
|
|
743
743
|
} });
|
|
744
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${
|
|
744
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest2}` });
|
|
745
745
|
}
|
|
746
746
|
|
|
747
747
|
// src/flow-fields.ts
|
|
@@ -759,8 +759,8 @@ var APPLICATION_LABELS = {
|
|
|
759
759
|
state: "State"
|
|
760
760
|
};
|
|
761
761
|
function humanize(id) {
|
|
762
|
-
const
|
|
763
|
-
return
|
|
762
|
+
const text5 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
763
|
+
return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id;
|
|
764
764
|
}
|
|
765
765
|
function applicationDefs(chapter) {
|
|
766
766
|
try {
|
|
@@ -775,7 +775,7 @@ function applicationType(id, def) {
|
|
|
775
775
|
}
|
|
776
776
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
777
777
|
const defs = applicationDefs(chapter);
|
|
778
|
-
return [...application.required, ...application.optional].map((id,
|
|
778
|
+
return [...application.required, ...application.optional].map((id, order2) => {
|
|
779
779
|
const def = defs[id];
|
|
780
780
|
const conditions = application.conditions[id];
|
|
781
781
|
const type = applicationType(id, def);
|
|
@@ -784,7 +784,7 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
784
784
|
label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
|
|
785
785
|
type,
|
|
786
786
|
required: application.required.includes(id),
|
|
787
|
-
order,
|
|
787
|
+
order: order2,
|
|
788
788
|
maxLength: application.maxLen[id] ?? application.defaultMaxLen,
|
|
789
789
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
790
790
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
@@ -795,14 +795,14 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
795
795
|
function resolveFormationFlowFields(crm, formation) {
|
|
796
796
|
if (!formation.enabled) return [];
|
|
797
797
|
const def = crm.type(formation.type);
|
|
798
|
-
return [...formation.required, ...formation.optional].map((id,
|
|
798
|
+
return [...formation.required, ...formation.optional].map((id, order2) => {
|
|
799
799
|
const field = def.fields[id];
|
|
800
800
|
return {
|
|
801
801
|
id,
|
|
802
802
|
label: field.label ?? humanize(id),
|
|
803
803
|
type: field.type,
|
|
804
804
|
required: formation.required.includes(id),
|
|
805
|
-
order,
|
|
805
|
+
order: order2,
|
|
806
806
|
maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
|
|
807
807
|
...field.options ? { options: [...field.options] } : {},
|
|
808
808
|
...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -846,9 +846,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
846
846
|
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
847
847
|
const ids = /* @__PURE__ */ new Set();
|
|
848
848
|
const offers = [];
|
|
849
|
-
for (const [
|
|
850
|
-
const path = `document.giftOffers.offers[${
|
|
851
|
-
const item = record(
|
|
849
|
+
for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
850
|
+
const path = `document.giftOffers.offers[${index2}]`;
|
|
851
|
+
const item = record(raw2);
|
|
852
852
|
if (!item) {
|
|
853
853
|
errors.push(`${path} must be an object`);
|
|
854
854
|
continue;
|
|
@@ -1005,8 +1005,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1005
1005
|
const typeKey = type.toLowerCase();
|
|
1006
1006
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
1007
1007
|
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
1008
|
-
const
|
|
1009
|
-
if (readable &&
|
|
1008
|
+
const raw2 = `${prefix}:${hubRecordId}`;
|
|
1009
|
+
if (readable && raw2.length <= 64) return raw2;
|
|
1010
1010
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1011
1011
|
}
|
|
1012
1012
|
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
@@ -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 text5 = 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: text5, redirected: redirect };
|
|
1297
1297
|
}
|
|
1298
1298
|
|
|
1299
1299
|
// src/notify.ts
|
|
@@ -1480,27 +1480,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1480
1480
|
if (db.aggregate) {
|
|
1481
1481
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1482
1482
|
}
|
|
1483
|
-
const
|
|
1484
|
-
return Array.isArray(
|
|
1483
|
+
const rows6 = (await db.query({ signupControls: {} })).signupControls;
|
|
1484
|
+
return Array.isArray(rows6) && rows6.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1485
1485
|
}
|
|
1486
1486
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1487
1487
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1488
|
-
const
|
|
1488
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
1489
1489
|
where: { targetId, environment },
|
|
1490
1490
|
order: { revision: "desc" },
|
|
1491
1491
|
limit: 1
|
|
1492
1492
|
} } })).signupControls;
|
|
1493
|
-
const latest = Array.isArray(
|
|
1493
|
+
const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1494
1494
|
return asStored(latest);
|
|
1495
1495
|
}
|
|
1496
1496
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1497
1497
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1498
|
-
const
|
|
1498
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
1499
1499
|
where: { targetId, environment, active: true },
|
|
1500
1500
|
order: { revision: "desc" },
|
|
1501
1501
|
limit: 1
|
|
1502
1502
|
} } })).signupControls;
|
|
1503
|
-
const active = Array.isArray(
|
|
1503
|
+
const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1504
1504
|
return asStored(active);
|
|
1505
1505
|
}
|
|
1506
1506
|
function controlledApplication(base, document) {
|
|
@@ -1529,7 +1529,7 @@ var byPublicId = (ns, value) => ({
|
|
|
1529
1529
|
value
|
|
1530
1530
|
});
|
|
1531
1531
|
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
1532
|
-
const { document, digest } = envelope;
|
|
1532
|
+
const { document, digest: digest2 } = envelope;
|
|
1533
1533
|
const currentRows = (await db.query({ tiers: { $: {
|
|
1534
1534
|
where: { groupId: document.target.chapterId },
|
|
1535
1535
|
limit: 100
|
|
@@ -1580,12 +1580,12 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
1580
1580
|
targetId: document.target.chapterId,
|
|
1581
1581
|
environment: document.target.environment,
|
|
1582
1582
|
revision: document.revision,
|
|
1583
|
-
digest,
|
|
1583
|
+
digest: digest2,
|
|
1584
1584
|
document,
|
|
1585
1585
|
active: true,
|
|
1586
1586
|
receivedAt
|
|
1587
1587
|
} });
|
|
1588
|
-
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${
|
|
1588
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest2}` });
|
|
1589
1589
|
}
|
|
1590
1590
|
|
|
1591
1591
|
// src/session.ts
|
|
@@ -2025,11 +2025,11 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
2025
2025
|
return json({ error: `application cannot resume from status "${String(app.status ?? "")}"` }, 409);
|
|
2026
2026
|
}
|
|
2027
2027
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2028
|
+
const raw2 = await req.text();
|
|
2029
|
+
if (raw2.length > chapter.application.bodyCap) return json({ error: "request body too large" }, 413);
|
|
2030
2030
|
let parsed;
|
|
2031
2031
|
try {
|
|
2032
|
-
parsed = JSON.parse(
|
|
2032
|
+
parsed = JSON.parse(raw2);
|
|
2033
2033
|
} catch {
|
|
2034
2034
|
return json({ error: "invalid JSON body" }, 400);
|
|
2035
2035
|
}
|
|
@@ -2086,23 +2086,23 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
2086
2086
|
const db = ctx.makeDb(env);
|
|
2087
2087
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2088
2088
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2089
|
-
let
|
|
2089
|
+
let raw2;
|
|
2090
2090
|
let sourceId;
|
|
2091
2091
|
if (req.headers.has(import_db2.FEDERATION_HEADERS.version)) {
|
|
2092
2092
|
const verified = await (0, import_db2.verifyFederatedRequest)(req, { secret });
|
|
2093
2093
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2094
|
-
|
|
2094
|
+
raw2 = verified.body;
|
|
2095
2095
|
sourceId = verified.sender;
|
|
2096
2096
|
} else {
|
|
2097
2097
|
const provided = (req.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
2098
2098
|
if (provided.length !== secret.length || provided !== secret) {
|
|
2099
2099
|
return json({ error: "unauthorized" }, 401);
|
|
2100
2100
|
}
|
|
2101
|
-
|
|
2101
|
+
raw2 = await req.text();
|
|
2102
2102
|
}
|
|
2103
2103
|
let payload;
|
|
2104
2104
|
try {
|
|
2105
|
-
payload = JSON.parse(
|
|
2105
|
+
payload = JSON.parse(raw2);
|
|
2106
2106
|
} catch {
|
|
2107
2107
|
return json({ error: "invalid JSON body" }, 400);
|
|
2108
2108
|
}
|
|
@@ -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 text5 = value.trim();
|
|
2185
|
+
if (required && !text5) errors.push(`${path} is required`);
|
|
2186
|
+
if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2187
|
+
return text5;
|
|
2188
2188
|
}
|
|
2189
2189
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2190
2190
|
const errors = [];
|
|
@@ -2202,9 +2202,9 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2202
2202
|
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
2203
2203
|
const seen = /* @__PURE__ */ new Set();
|
|
2204
2204
|
const tiers = [];
|
|
2205
|
-
for (const [
|
|
2206
|
-
const path = `document.tiers[${
|
|
2207
|
-
const item = objectRecord(
|
|
2205
|
+
for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
2206
|
+
const path = `document.tiers[${index2}]`;
|
|
2207
|
+
const item = objectRecord(raw2);
|
|
2208
2208
|
if (!item) {
|
|
2209
2209
|
errors.push(`${path} must be an object`);
|
|
2210
2210
|
continue;
|
|
@@ -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 text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2237
|
+
return text5 ? [text5] : [];
|
|
2238
2238
|
});
|
|
2239
2239
|
}
|
|
2240
2240
|
}
|
|
@@ -2343,9 +2343,9 @@ function tiersOf(value, errors) {
|
|
|
2343
2343
|
}
|
|
2344
2344
|
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
2345
2345
|
const seen = /* @__PURE__ */ new Set();
|
|
2346
|
-
return value.slice(0, 20).flatMap((
|
|
2347
|
-
const path = `document.tiers[${
|
|
2348
|
-
const item = recordOf(
|
|
2346
|
+
return value.slice(0, 20).flatMap((raw2, index2) => {
|
|
2347
|
+
const path = `document.tiers[${index2}]`;
|
|
2348
|
+
const item = recordOf(raw2);
|
|
2349
2349
|
if (!item) {
|
|
2350
2350
|
errors.push(`${path} must be an object`);
|
|
2351
2351
|
return [];
|
|
@@ -2472,7 +2472,7 @@ function validateSignupControlDocument(input) {
|
|
|
2472
2472
|
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
2473
2473
|
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
2474
2474
|
if (!page.ok) errors.push(...page.errors);
|
|
2475
|
-
else if (page.value.tiers.some((tier,
|
|
2475
|
+
else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
|
|
2476
2476
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
2477
2477
|
}
|
|
2478
2478
|
const application = applicationOf(root.application, errors);
|
|
@@ -2520,7 +2520,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2520
2520
|
} catch {
|
|
2521
2521
|
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
2522
2522
|
}
|
|
2523
|
-
const
|
|
2523
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
2524
2524
|
const checked = validateSignupControlDocument(input.document);
|
|
2525
2525
|
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
2526
2526
|
const document = checked.value;
|
|
@@ -2530,11 +2530,11 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2530
2530
|
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
2531
2531
|
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
2532
2532
|
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
2533
|
-
if (!
|
|
2533
|
+
if (!digest2 || await signupControlDigest(document) !== digest2) return reject("invalid_digest", 400);
|
|
2534
2534
|
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2535
2535
|
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
2536
2536
|
if (latest && document.revision === latest.revision) {
|
|
2537
|
-
if (latest.digest !==
|
|
2537
|
+
if (latest.digest !== digest2) return reject("conflicting_revision", 409);
|
|
2538
2538
|
return json({
|
|
2539
2539
|
ok: true,
|
|
2540
2540
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -2546,7 +2546,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2546
2546
|
}
|
|
2547
2547
|
const receivedAt = Date.now();
|
|
2548
2548
|
try {
|
|
2549
|
-
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
2549
|
+
await materializeSignupControl(db, { digest: digest2, document }, latest, receivedAt);
|
|
2550
2550
|
} catch {
|
|
2551
2551
|
return reject("materialization_failed", 503);
|
|
2552
2552
|
}
|
|
@@ -2554,7 +2554,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2554
2554
|
ok: true,
|
|
2555
2555
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
2556
2556
|
revision: document.revision,
|
|
2557
|
-
digest,
|
|
2557
|
+
digest: digest2,
|
|
2558
2558
|
status: "accepted",
|
|
2559
2559
|
acknowledgedAt: receivedAt
|
|
2560
2560
|
});
|
|
@@ -2582,8 +2582,8 @@ function canonicalMemberContent(value) {
|
|
|
2582
2582
|
return canonicalValue2(value);
|
|
2583
2583
|
}
|
|
2584
2584
|
async function memberContentDigest(document) {
|
|
2585
|
-
const
|
|
2586
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
2585
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2586
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2587
2587
|
}
|
|
2588
2588
|
|
|
2589
2589
|
// src/member-content-validation-helpers.ts
|
|
@@ -2620,21 +2620,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
|
2620
2620
|
return valueText;
|
|
2621
2621
|
}
|
|
2622
2622
|
function offerValue(value, path, errors, tiers, audience) {
|
|
2623
|
-
const
|
|
2624
|
-
if (!
|
|
2623
|
+
const raw2 = record2(value);
|
|
2624
|
+
if (!raw2) {
|
|
2625
2625
|
errors.push(`${path} must be an object`);
|
|
2626
2626
|
return void 0;
|
|
2627
2627
|
}
|
|
2628
|
-
unknownKeys2(
|
|
2629
|
-
const tierId = textValue(
|
|
2628
|
+
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
2629
|
+
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2630
2630
|
if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2631
2631
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2632
2632
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2633
2633
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
2634
2634
|
}
|
|
2635
|
-
const label = textValue(
|
|
2636
|
-
const bodyMarkdown = textValue(
|
|
2637
|
-
const ctaLabel = textValue(
|
|
2635
|
+
const label = textValue(raw2.label, `${path}.label`, errors);
|
|
2636
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
|
|
2637
|
+
const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2638
2638
|
return {
|
|
2639
2639
|
tierId,
|
|
2640
2640
|
...label ? { label } : {},
|
|
@@ -2686,37 +2686,37 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2686
2686
|
const ids = /* @__PURE__ */ new Set();
|
|
2687
2687
|
const slugs = /* @__PURE__ */ new Set();
|
|
2688
2688
|
const pages = [];
|
|
2689
|
-
for (const [
|
|
2690
|
-
const path = `document.pages[${
|
|
2691
|
-
const
|
|
2692
|
-
if (!
|
|
2689
|
+
for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2690
|
+
const path = `document.pages[${index2}]`;
|
|
2691
|
+
const raw2 = record2(value);
|
|
2692
|
+
if (!raw2) {
|
|
2693
2693
|
errors.push(`${path} must be an object`);
|
|
2694
2694
|
continue;
|
|
2695
2695
|
}
|
|
2696
|
-
unknownKeys2(
|
|
2697
|
-
const id = textValue(
|
|
2698
|
-
const slug = textValue(
|
|
2699
|
-
const title = textValue(
|
|
2696
|
+
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
2697
|
+
const id = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2698
|
+
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2699
|
+
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2700
2700
|
if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2701
2701
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2702
2702
|
if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
|
|
2703
2703
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2704
2704
|
ids.add(id);
|
|
2705
2705
|
slugs.add(slug);
|
|
2706
|
-
const navRaw = record2(
|
|
2707
|
-
let navigation = { section: "General", order:
|
|
2706
|
+
const navRaw = record2(raw2.navigation);
|
|
2707
|
+
let navigation = { section: "General", order: index2 };
|
|
2708
2708
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2709
2709
|
else {
|
|
2710
2710
|
unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2711
2711
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2712
|
-
const
|
|
2713
|
-
if (!Number.isInteger(
|
|
2712
|
+
const order2 = navRaw.order;
|
|
2713
|
+
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
2714
2714
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2715
2715
|
}
|
|
2716
2716
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2717
|
-
navigation = { section, order: Number.isInteger(
|
|
2717
|
+
navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
|
|
2718
2718
|
}
|
|
2719
|
-
const audienceRaw = record2(
|
|
2719
|
+
const audienceRaw = record2(raw2.audience);
|
|
2720
2720
|
let audience = { kind: "all-members" };
|
|
2721
2721
|
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2722
2722
|
else {
|
|
@@ -2738,12 +2738,12 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2738
2738
|
audience = { kind: "tiers", tierIds: normalized };
|
|
2739
2739
|
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2740
2740
|
}
|
|
2741
|
-
const summary = textValue(
|
|
2742
|
-
const teaserMarkdown = textValue(
|
|
2743
|
-
const bodyMarkdown = textValue(
|
|
2741
|
+
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
2742
|
+
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
|
|
2743
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
|
|
2744
2744
|
let upsell;
|
|
2745
|
-
if (
|
|
2746
|
-
const upsellRaw = record2(
|
|
2745
|
+
if (raw2.upsell !== void 0) {
|
|
2746
|
+
const upsellRaw = record2(raw2.upsell);
|
|
2747
2747
|
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2748
2748
|
else {
|
|
2749
2749
|
unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
@@ -2760,10 +2760,10 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2760
2760
|
pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2761
2761
|
}
|
|
2762
2762
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2763
|
-
for (const [
|
|
2763
|
+
for (const [index2, page] of pages.entries()) {
|
|
2764
2764
|
const parentId = page.navigation.parentId;
|
|
2765
|
-
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${
|
|
2766
|
-
if (parentId === page.id) errors.push(`document.pages[${
|
|
2765
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
|
|
2766
|
+
if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
|
|
2767
2767
|
}
|
|
2768
2768
|
for (const page of pages) {
|
|
2769
2769
|
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
@@ -2784,21 +2784,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2784
2784
|
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2785
2785
|
const seen = /* @__PURE__ */ new Set();
|
|
2786
2786
|
upsellOverrides = [];
|
|
2787
|
-
for (const [
|
|
2788
|
-
const path = `document.upsellOverrides[${
|
|
2789
|
-
const
|
|
2790
|
-
if (!
|
|
2787
|
+
for (const [index2, value] of values.entries()) {
|
|
2788
|
+
const path = `document.upsellOverrides[${index2}]`;
|
|
2789
|
+
const raw2 = record2(value);
|
|
2790
|
+
if (!raw2) {
|
|
2791
2791
|
errors.push(`${path} must be an object`);
|
|
2792
2792
|
continue;
|
|
2793
2793
|
}
|
|
2794
|
-
unknownKeys2(
|
|
2795
|
-
const pageId = textValue(
|
|
2794
|
+
unknownKeys2(raw2, OVERRIDE_KEYS, path, errors);
|
|
2795
|
+
const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2796
2796
|
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2797
2797
|
seen.add(pageId);
|
|
2798
2798
|
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2799
2799
|
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2800
2800
|
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2801
|
-
const offer = offerValue(
|
|
2801
|
+
const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2802
2802
|
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2803
2803
|
}
|
|
2804
2804
|
}
|
|
@@ -2876,36 +2876,36 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2876
2876
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2877
2877
|
async function exists(db, ns, where) {
|
|
2878
2878
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2879
|
-
const
|
|
2880
|
-
return
|
|
2879
|
+
const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2880
|
+
return rows6.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2881
2881
|
}
|
|
2882
2882
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2883
2883
|
const where = { targetId, environment, active: true };
|
|
2884
2884
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2885
|
-
const
|
|
2885
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
2886
2886
|
where,
|
|
2887
2887
|
order: { revision: "desc" },
|
|
2888
2888
|
limit: 1
|
|
2889
2889
|
} } })).memberContentControls;
|
|
2890
|
-
return control(Array.isArray(
|
|
2890
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2891
2891
|
}
|
|
2892
2892
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2893
2893
|
const where = { targetId, environment };
|
|
2894
2894
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2895
|
-
const
|
|
2895
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
2896
2896
|
where,
|
|
2897
2897
|
order: { revision: "desc" },
|
|
2898
2898
|
limit: 1
|
|
2899
2899
|
} } })).memberContentControls;
|
|
2900
|
-
return control(Array.isArray(
|
|
2900
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2901
2901
|
}
|
|
2902
2902
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2903
2903
|
const id = `member-content-local:${environment}`;
|
|
2904
|
-
const
|
|
2905
|
-
return local(Array.isArray(
|
|
2904
|
+
const rows6 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2905
|
+
return local(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
2906
2906
|
}
|
|
2907
2907
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2908
|
-
const { document, digest } = envelope;
|
|
2908
|
+
const { document, digest: digest2 } = envelope;
|
|
2909
2909
|
const target = document.target;
|
|
2910
2910
|
const id = `member-content-control:${target.environment}:${document.revision}`;
|
|
2911
2911
|
const ops = [];
|
|
@@ -2918,12 +2918,12 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
2918
2918
|
targetId: target.chapterId,
|
|
2919
2919
|
environment: target.environment,
|
|
2920
2920
|
revision: document.revision,
|
|
2921
|
-
digest,
|
|
2921
|
+
digest: digest2,
|
|
2922
2922
|
document,
|
|
2923
2923
|
active: true,
|
|
2924
2924
|
receivedAt
|
|
2925
2925
|
} });
|
|
2926
|
-
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${
|
|
2926
|
+
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest2}` });
|
|
2927
2927
|
}
|
|
2928
2928
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
2929
2929
|
const id = `member-content-local:${environment}`;
|
|
@@ -2957,12 +2957,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2957
2957
|
}
|
|
2958
2958
|
async function memberTierId(db, chapterId, email) {
|
|
2959
2959
|
if (!email) return null;
|
|
2960
|
-
let
|
|
2960
|
+
let rows6 = (await db.query({ applications: { $: {
|
|
2961
2961
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2962
2962
|
limit: 20
|
|
2963
2963
|
} } })).applications;
|
|
2964
|
-
if (!
|
|
2965
|
-
const latest = Array.isArray(
|
|
2964
|
+
if (!rows6?.length) rows6 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2965
|
+
const latest = Array.isArray(rows6) ? [...rows6].filter((row) => String(row.groupId ?? chapterId) === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
|
|
2966
2966
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2967
2967
|
}
|
|
2968
2968
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -2987,7 +2987,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
2987
2987
|
} catch {
|
|
2988
2988
|
return reject2("invalid_document", 400, ["request body must be JSON"]);
|
|
2989
2989
|
}
|
|
2990
|
-
const
|
|
2990
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
2991
2991
|
const initial = validateMemberContentDocument(input.document, {
|
|
2992
2992
|
sourceType: "parent"
|
|
2993
2993
|
});
|
|
@@ -3015,11 +3015,11 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3015
3015
|
return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
|
|
3016
3016
|
}
|
|
3017
3017
|
}
|
|
3018
|
-
if (!
|
|
3018
|
+
if (!digest2 || await memberContentDigest(document) !== digest2) return reject2("invalid_digest", 400);
|
|
3019
3019
|
const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
3020
3020
|
if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
|
|
3021
3021
|
if (latest && document.revision === latest.revision) {
|
|
3022
|
-
if (latest.digest !==
|
|
3022
|
+
if (latest.digest !== digest2) return reject2("conflicting_revision", 409);
|
|
3023
3023
|
return json({
|
|
3024
3024
|
ok: true,
|
|
3025
3025
|
target: document.target,
|
|
@@ -3031,7 +3031,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3031
3031
|
}
|
|
3032
3032
|
const receivedAt = Date.now();
|
|
3033
3033
|
try {
|
|
3034
|
-
await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
|
|
3034
|
+
await materializeMemberContentControl(db, { digest: digest2, document }, latest, receivedAt);
|
|
3035
3035
|
} catch {
|
|
3036
3036
|
return reject2("materialization_failed", 503);
|
|
3037
3037
|
}
|
|
@@ -3039,7 +3039,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3039
3039
|
ok: true,
|
|
3040
3040
|
target: document.target,
|
|
3041
3041
|
revision: document.revision,
|
|
3042
|
-
digest,
|
|
3042
|
+
digest: digest2,
|
|
3043
3043
|
status: "accepted",
|
|
3044
3044
|
acknowledgedAt: receivedAt
|
|
3045
3045
|
});
|
|
@@ -3091,11 +3091,11 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
3091
3091
|
}
|
|
3092
3092
|
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
3093
3093
|
if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
3094
|
-
const
|
|
3095
|
-
if (
|
|
3094
|
+
const raw2 = await req.text();
|
|
3095
|
+
if (raw2.length > BODY_CAP) return json({ error: "request body too large" }, 413);
|
|
3096
3096
|
let input;
|
|
3097
3097
|
try {
|
|
3098
|
-
input = JSON.parse(
|
|
3098
|
+
input = JSON.parse(raw2);
|
|
3099
3099
|
} catch {
|
|
3100
3100
|
return json({ error: "invalid JSON body" }, 400);
|
|
3101
3101
|
}
|
|
@@ -3163,18 +3163,18 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3163
3163
|
} catch {
|
|
3164
3164
|
return reject3("invalid_document", 400, ["request body must be JSON"]);
|
|
3165
3165
|
}
|
|
3166
|
-
const
|
|
3166
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
3167
3167
|
const checked = validateAdmissionGrantDocument(input.document);
|
|
3168
3168
|
if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
|
|
3169
3169
|
const document = checked.value;
|
|
3170
3170
|
if (document.source.appId !== claimedSender) return reject3("wrong_source", 403);
|
|
3171
3171
|
if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
|
|
3172
3172
|
if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
|
|
3173
|
-
if (!
|
|
3173
|
+
if (!digest2 || await signupControlDigest(document) !== digest2) return reject3("invalid_digest", 400);
|
|
3174
3174
|
const latest = await loadLatestAdmissionRevision(db, claimedSender, ctx.chapter.id, env.ODLA_ENV);
|
|
3175
3175
|
if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
|
|
3176
3176
|
if (latest && document.revision === latest.revision) {
|
|
3177
|
-
if (latest.digest !==
|
|
3177
|
+
if (latest.digest !== digest2) return reject3("conflicting_revision", 409);
|
|
3178
3178
|
return json({
|
|
3179
3179
|
ok: true,
|
|
3180
3180
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -3186,7 +3186,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3186
3186
|
}
|
|
3187
3187
|
const receivedAt = Date.now();
|
|
3188
3188
|
try {
|
|
3189
|
-
await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
|
|
3189
|
+
await materializeAdmissionGrants(db, { digest: digest2, document }, latest, receivedAt);
|
|
3190
3190
|
} catch {
|
|
3191
3191
|
return reject3("materialization_failed", 503);
|
|
3192
3192
|
}
|
|
@@ -3194,7 +3194,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3194
3194
|
ok: true,
|
|
3195
3195
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
3196
3196
|
revision: document.revision,
|
|
3197
|
-
digest,
|
|
3197
|
+
digest: digest2,
|
|
3198
3198
|
status: "accepted",
|
|
3199
3199
|
acknowledgedAt: receivedAt
|
|
3200
3200
|
});
|
|
@@ -3397,9 +3397,9 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
3397
3397
|
if (record4.type !== "person") return null;
|
|
3398
3398
|
const field = ctx.chapter.crm.config.types.person?.emailField;
|
|
3399
3399
|
if (!field) return null;
|
|
3400
|
-
const
|
|
3401
|
-
if (typeof
|
|
3402
|
-
const normalized = normalizeAdmissionEmail(
|
|
3400
|
+
const raw2 = record4.fields[field] ?? record4.primaryEmail;
|
|
3401
|
+
if (typeof raw2 !== "string") return null;
|
|
3402
|
+
const normalized = normalizeAdmissionEmail(raw2);
|
|
3403
3403
|
return isValidEmail(normalized) ? normalized : null;
|
|
3404
3404
|
}
|
|
3405
3405
|
async function grantInput(db, ctx, assignment) {
|
|
@@ -3419,7 +3419,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3419
3419
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
3420
3420
|
const id = assignmentId(opts.target.id, opts.personId);
|
|
3421
3421
|
const existing = current.find((row) => row.id === id);
|
|
3422
|
-
const
|
|
3422
|
+
const digest2 = await signupControlDigest({
|
|
3423
3423
|
targetId: opts.target.id,
|
|
3424
3424
|
personId: opts.personId,
|
|
3425
3425
|
decision: opts.decision,
|
|
@@ -3434,7 +3434,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3434
3434
|
} } });
|
|
3435
3435
|
const receipt = admissionRows(priorMutation2.leaderAdmissionMutations)[0];
|
|
3436
3436
|
if (receipt) {
|
|
3437
|
-
if (receipt.requestDigest !==
|
|
3437
|
+
if (receipt.requestDigest !== digest2 || receipt.personId !== opts.personId) {
|
|
3438
3438
|
throw new Error("mutation_conflict");
|
|
3439
3439
|
}
|
|
3440
3440
|
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
@@ -3442,7 +3442,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3442
3442
|
return { delivery: saved, duplicate: true };
|
|
3443
3443
|
}
|
|
3444
3444
|
if (existing?.lastMutationId === opts.mutationId) {
|
|
3445
|
-
if (existing.lastRequestDigest !==
|
|
3445
|
+
if (existing.lastRequestDigest !== digest2) throw new Error("mutation_conflict");
|
|
3446
3446
|
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3447
3447
|
if (!saved) throw new Error("delivery_missing");
|
|
3448
3448
|
return { delivery: saved, duplicate: true };
|
|
@@ -3457,7 +3457,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3457
3457
|
...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {},
|
|
3458
3458
|
active: opts.active,
|
|
3459
3459
|
lastMutationId: opts.mutationId,
|
|
3460
|
-
lastRequestDigest:
|
|
3460
|
+
lastRequestDigest: digest2,
|
|
3461
3461
|
createdAt: existing?.createdAt ?? now,
|
|
3462
3462
|
createdBy: existing?.createdBy ?? opts.actor,
|
|
3463
3463
|
updatedAt: now,
|
|
@@ -3493,7 +3493,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3493
3493
|
id: mutationReceiptId,
|
|
3494
3494
|
targetId: opts.target.id,
|
|
3495
3495
|
personId: opts.personId,
|
|
3496
|
-
requestDigest:
|
|
3496
|
+
requestDigest: digest2,
|
|
3497
3497
|
revision,
|
|
3498
3498
|
createdAt: now
|
|
3499
3499
|
} }
|
|
@@ -3688,8 +3688,8 @@ function makeCalendar(env) {
|
|
|
3688
3688
|
}
|
|
3689
3689
|
async function firstRow(db, ns, q) {
|
|
3690
3690
|
const res = await db.query({ [ns]: { $: q } });
|
|
3691
|
-
const
|
|
3692
|
-
return Array.isArray(
|
|
3691
|
+
const rows6 = res[ns];
|
|
3692
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3693
3693
|
}
|
|
3694
3694
|
async function computeSlots(cal, cfg) {
|
|
3695
3695
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -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 text5 = (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: text5(app.firstName),
|
|
3956
|
+
lastName: text5(app.lastName),
|
|
3957
|
+
email: text5(app.email),
|
|
3958
|
+
phone: text5(app.phone),
|
|
3959
|
+
state: text5(app.state)
|
|
3960
3960
|
},
|
|
3961
3961
|
dedupeKey: `${eventId}:admin`,
|
|
3962
3962
|
applicationId: String(app.id)
|
|
@@ -4089,8 +4089,8 @@ function createStripeProvider(config) {
|
|
|
4089
4089
|
const sk = requireSecret(secretKey);
|
|
4090
4090
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4091
4091
|
if (!charges.ok) fail("charges list", charges);
|
|
4092
|
-
const
|
|
4093
|
-
const paid =
|
|
4092
|
+
const rows6 = charges.body.data ?? [];
|
|
4093
|
+
const paid = rows6.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4094
4094
|
const charge = paid[paid.length - 1];
|
|
4095
4095
|
if (!charge) {
|
|
4096
4096
|
const err = new Error("no paid charge to refund");
|
|
@@ -4198,8 +4198,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4198
4198
|
throw new Error("gift issuance conflict");
|
|
4199
4199
|
}
|
|
4200
4200
|
}
|
|
4201
|
-
function buyerGiftClaims(
|
|
4202
|
-
return
|
|
4201
|
+
function buyerGiftClaims(rows6, now) {
|
|
4202
|
+
return rows6.map((row) => {
|
|
4203
4203
|
const stored = String(row.status ?? "pending");
|
|
4204
4204
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4205
4205
|
return {
|
|
@@ -4491,8 +4491,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4491
4491
|
|
|
4492
4492
|
// src/stripe-webhook-partition.ts
|
|
4493
4493
|
var firstRow2 = async (db, ns, q) => {
|
|
4494
|
-
const
|
|
4495
|
-
return Array.isArray(
|
|
4494
|
+
const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4495
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
4496
4496
|
};
|
|
4497
4497
|
var stripeEventDigest = async (rawBody) => {
|
|
4498
4498
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4539,9 +4539,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
4539
4539
|
const raced = await receiptFor(db, input.eventId);
|
|
4540
4540
|
return raced?.digest === input.digest ? "replayed" : "collision";
|
|
4541
4541
|
}
|
|
4542
|
-
async function quarantineStripeEvent(db, eventId,
|
|
4542
|
+
async function quarantineStripeEvent(db, eventId, digest2, event) {
|
|
4543
4543
|
const disposition = "quarantine_unmatched";
|
|
4544
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
4544
|
+
const stored = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition, matched: false });
|
|
4545
4545
|
if (stored === "collision") {
|
|
4546
4546
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4547
4547
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4549,10 +4549,10 @@ async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
|
4549
4549
|
reportStripePartition("warn", eventId, event, disposition);
|
|
4550
4550
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
4551
4551
|
}
|
|
4552
|
-
async function replayedStripeReceipt(db, eventId,
|
|
4552
|
+
async function replayedStripeReceipt(db, eventId, digest2, event) {
|
|
4553
4553
|
const existing = await receiptFor(db, eventId);
|
|
4554
4554
|
if (!existing) return null;
|
|
4555
|
-
if (existing.digest !==
|
|
4555
|
+
if (existing.digest !== digest2) {
|
|
4556
4556
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4557
4557
|
return json({ ok: true, matched: false, collision: true });
|
|
4558
4558
|
}
|
|
@@ -4562,8 +4562,8 @@ async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
|
4562
4562
|
// src/worker-routes-payments.ts
|
|
4563
4563
|
async function firstRow3(db, ns, q) {
|
|
4564
4564
|
const res = await db.query({ [ns]: { $: q } });
|
|
4565
|
-
const
|
|
4566
|
-
return Array.isArray(
|
|
4565
|
+
const rows6 = res[ns];
|
|
4566
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
4567
4567
|
}
|
|
4568
4568
|
async function findApplication(db, event) {
|
|
4569
4569
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -4687,7 +4687,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4687
4687
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
4688
4688
|
const { eventId, event } = ingest;
|
|
4689
4689
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4690
|
-
const
|
|
4690
|
+
const digest2 = await stripeEventDigest(rawBody);
|
|
4691
4691
|
const identity = stripeEventIdentity(event);
|
|
4692
4692
|
const target = stripeEventTarget(env, ctx);
|
|
4693
4693
|
const preliminary = classifyMemberStripeEvent(identity, target, false);
|
|
@@ -4701,23 +4701,23 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4701
4701
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4702
4702
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4703
4703
|
}
|
|
4704
|
-
const replay2 = await replayedStripeReceipt(db, eventId,
|
|
4704
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest2, event);
|
|
4705
4705
|
if (replay2) return replay2;
|
|
4706
4706
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
4707
4707
|
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
4708
4708
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
4709
|
-
return quarantineStripeEvent(db, eventId,
|
|
4709
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4710
4710
|
}
|
|
4711
4711
|
if (event.kind === "gift_paid") {
|
|
4712
4712
|
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
4713
|
-
return quarantineStripeEvent(db, eventId,
|
|
4713
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4714
4714
|
}
|
|
4715
4715
|
try {
|
|
4716
4716
|
const issued = await issueGiftClaim(db, intent, {
|
|
4717
4717
|
eventId,
|
|
4718
4718
|
objectId: event.providerObjectId
|
|
4719
4719
|
}, Date.now());
|
|
4720
|
-
const stored3 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: true });
|
|
4720
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition: disposition2, matched: true });
|
|
4721
4721
|
if (stored3 === "collision") {
|
|
4722
4722
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4723
4723
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4733,7 +4733,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4733
4733
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
4734
4734
|
const claim = await firstRow3(db, "giftClaims", { where: { intentId: event.giftIntentId }, limit: 1 });
|
|
4735
4735
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
4736
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4736
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4737
4737
|
if (stored2 === "collision") {
|
|
4738
4738
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4739
4739
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4745,12 +4745,12 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4745
4745
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4746
4746
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4747
4747
|
}
|
|
4748
|
-
const replay = await replayedStripeReceipt(db, eventId,
|
|
4748
|
+
const replay = await replayedStripeReceipt(db, eventId, digest2, event);
|
|
4749
4749
|
if (replay) return replay;
|
|
4750
4750
|
const localApplication = Boolean(app && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
4751
4751
|
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
4752
4752
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
4753
|
-
return quarantineStripeEvent(db, eventId,
|
|
4753
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4754
4754
|
}
|
|
4755
4755
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
4756
4756
|
let next = app;
|
|
@@ -4773,7 +4773,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4773
4773
|
});
|
|
4774
4774
|
}
|
|
4775
4775
|
}
|
|
4776
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: true });
|
|
4776
|
+
const stored = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition, matched: true });
|
|
4777
4777
|
if (stored === "collision") {
|
|
4778
4778
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4779
4779
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -5029,8 +5029,8 @@ async function upcomingEvents(env) {
|
|
|
5029
5029
|
const res = await cal.availability.upcoming();
|
|
5030
5030
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5031
5031
|
}
|
|
5032
|
-
function toReconcile(
|
|
5033
|
-
return
|
|
5032
|
+
function toReconcile(rows6) {
|
|
5033
|
+
return rows6.map((m) => ({
|
|
5034
5034
|
id: String(m.id),
|
|
5035
5035
|
applicationId: String(m.applicationId),
|
|
5036
5036
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5052,12 +5052,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5052
5052
|
const query = await db.query({
|
|
5053
5053
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5054
5054
|
});
|
|
5055
|
-
let
|
|
5056
|
-
if (Number.isFinite(from))
|
|
5057
|
-
if (Number.isFinite(to))
|
|
5055
|
+
let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5056
|
+
if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5057
|
+
if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5058
5058
|
let decisions = [];
|
|
5059
5059
|
try {
|
|
5060
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5060
|
+
decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
|
|
5061
5061
|
} catch {
|
|
5062
5062
|
}
|
|
5063
5063
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5084,7 +5084,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5084
5084
|
};
|
|
5085
5085
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5086
5086
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5087
|
-
const meetings =
|
|
5087
|
+
const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5088
5088
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5089
5089
|
};
|
|
5090
5090
|
|
|
@@ -5105,16 +5105,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
5105
5105
|
});
|
|
5106
5106
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
5107
5107
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
5108
|
-
const
|
|
5109
|
-
if (
|
|
5110
|
-
const result = await backfillCrm(crmDeps2(
|
|
5108
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5109
|
+
if (gate12 instanceof Response) return gate12;
|
|
5110
|
+
const result = await backfillCrm(crmDeps2(gate12.db, ctx));
|
|
5111
5111
|
return json({ ok: true, ...result });
|
|
5112
5112
|
};
|
|
5113
5113
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
5114
5114
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
5115
|
-
const
|
|
5116
|
-
if (
|
|
5117
|
-
const { db } =
|
|
5115
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5116
|
+
if (gate12 instanceof Response) return gate12;
|
|
5117
|
+
const { db } = gate12;
|
|
5118
5118
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
5119
5119
|
const [data, roleList] = await Promise.all([
|
|
5120
5120
|
db.query({
|
|
@@ -5160,16 +5160,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5160
5160
|
});
|
|
5161
5161
|
}
|
|
5162
5162
|
}
|
|
5163
|
-
const
|
|
5163
|
+
const rows6 = [...people.values()].sort(
|
|
5164
5164
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5165
5165
|
);
|
|
5166
|
-
return json({ people:
|
|
5166
|
+
return json({ people: rows6 });
|
|
5167
5167
|
};
|
|
5168
5168
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5169
5169
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
5170
|
-
const
|
|
5171
|
-
if (
|
|
5172
|
-
const { db } =
|
|
5170
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5171
|
+
if (gate12 instanceof Response) return gate12;
|
|
5172
|
+
const { db } = gate12;
|
|
5173
5173
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
5174
5174
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
5175
5175
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -5180,9 +5180,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
5180
5180
|
};
|
|
5181
5181
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
5182
5182
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
5183
|
-
const
|
|
5184
|
-
if (
|
|
5185
|
-
const { db, actor } =
|
|
5183
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5184
|
+
if (gate12 instanceof Response) return gate12;
|
|
5185
|
+
const { db, actor } = gate12;
|
|
5186
5186
|
let body;
|
|
5187
5187
|
try {
|
|
5188
5188
|
body = await req.json();
|
|
@@ -5218,8 +5218,8 @@ function canonical(value) {
|
|
|
5218
5218
|
}
|
|
5219
5219
|
return JSON.stringify(value) ?? "undefined";
|
|
5220
5220
|
}
|
|
5221
|
-
function positiveInt(
|
|
5222
|
-
const parsed = Number(
|
|
5221
|
+
function positiveInt(raw2, fallback, max) {
|
|
5222
|
+
const parsed = Number(raw2);
|
|
5223
5223
|
return Number.isSafeInteger(parsed) && parsed >= 0 ? Math.min(parsed, max) : fallback;
|
|
5224
5224
|
}
|
|
5225
5225
|
var handleAdminClerkPrivateProfileSync = async (req, url, env, ctx) => {
|
|
@@ -5300,14 +5300,14 @@ function periodSeries(points, now, count, periodMs) {
|
|
|
5300
5300
|
for (const point of points) {
|
|
5301
5301
|
if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
|
|
5302
5302
|
if (point.t >= start && point.t <= now) {
|
|
5303
|
-
const
|
|
5304
|
-
current[
|
|
5303
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
|
|
5304
|
+
current[index2] = (current[index2] ?? 0) + point.v;
|
|
5305
5305
|
} else if (point.t >= previousStart && point.t < start) {
|
|
5306
|
-
const
|
|
5307
|
-
previous[
|
|
5306
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
|
|
5307
|
+
previous[index2] = (previous[index2] ?? 0) + point.v;
|
|
5308
5308
|
}
|
|
5309
5309
|
}
|
|
5310
|
-
const labels = current.map((_,
|
|
5310
|
+
const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
|
|
5311
5311
|
return { labels, current, previous };
|
|
5312
5312
|
}
|
|
5313
5313
|
function dashboardMetricData(points, now) {
|
|
@@ -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 text5 = typeof t?.text === "string" ? t.text : "";
|
|
5724
|
+
if (!subject || !text5.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 (text5.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
5727
|
+
clean[key] = { subject, text: text5, enabled: t?.enabled !== false };
|
|
5728
5728
|
}
|
|
5729
5729
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5730
5730
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -6769,18 +6769,18 @@ function emailLooksReal(value) {
|
|
|
6769
6769
|
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
6770
6770
|
const errors = [];
|
|
6771
6771
|
if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, errors: ["import must be an object"] };
|
|
6772
|
-
const
|
|
6773
|
-
if (
|
|
6774
|
-
if (
|
|
6775
|
-
const sourceSystem = typeof
|
|
6776
|
-
const sourceRecordId = typeof
|
|
6777
|
-
const provenance = typeof
|
|
6778
|
-
const displayName = typeof
|
|
6772
|
+
const raw2 = input;
|
|
6773
|
+
if (raw2.sourceKind !== "external_reimport") errors.push("sourceKind must be external_reimport");
|
|
6774
|
+
if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
6775
|
+
const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
|
|
6776
|
+
const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
|
|
6777
|
+
const provenance = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
|
|
6778
|
+
const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
|
|
6779
6779
|
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
6780
6780
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6781
6781
|
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6782
6782
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6783
|
-
const rawConsent =
|
|
6783
|
+
const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
|
|
6784
6784
|
const consentStates = ["unknown", "granted", "denied"];
|
|
6785
6785
|
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
6786
6786
|
const discretionary = typeof rawConsent.discretionary === "string" ? rawConsent.discretionary : "";
|
|
@@ -6790,10 +6790,10 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
6790
6790
|
errors.push("consent must include marketing and discretionary states, recordedAt, and a bounded basis");
|
|
6791
6791
|
}
|
|
6792
6792
|
const identities = [];
|
|
6793
|
-
if (!Array.isArray(
|
|
6793
|
+
if (!Array.isArray(raw2.identities) || raw2.identities.length < 1 || raw2.identities.length > 8) {
|
|
6794
6794
|
errors.push("one to eight verified identities are required");
|
|
6795
6795
|
} else {
|
|
6796
|
-
for (const item of
|
|
6796
|
+
for (const item of raw2.identities) {
|
|
6797
6797
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6798
6798
|
errors.push("identity must be an object");
|
|
6799
6799
|
continue;
|
|
@@ -6825,9 +6825,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
6825
6825
|
seen.add(key);
|
|
6826
6826
|
}
|
|
6827
6827
|
const chapterLinks = [];
|
|
6828
|
-
if (
|
|
6829
|
-
if (!Array.isArray(
|
|
6830
|
-
else for (const item of
|
|
6828
|
+
if (raw2.chapterLinks !== void 0) {
|
|
6829
|
+
if (!Array.isArray(raw2.chapterLinks) || raw2.chapterLinks.length > allowedChapterIds.length) errors.push("chapterLinks are invalid or too numerous");
|
|
6830
|
+
else for (const item of raw2.chapterLinks) {
|
|
6831
6831
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6832
6832
|
errors.push("chapter link must be an object");
|
|
6833
6833
|
continue;
|
|
@@ -6863,9 +6863,9 @@ async function sha256(value) {
|
|
|
6863
6863
|
}
|
|
6864
6864
|
async function networkIdentityClaim(input) {
|
|
6865
6865
|
const normalized = normalizeNetworkIdentity(input.kind, input.value);
|
|
6866
|
-
const
|
|
6866
|
+
const digest2 = await sha256(`${input.kind}\0${normalized}`);
|
|
6867
6867
|
const hint = input.kind === "email" ? `${normalized.slice(0, 1)}***@${normalized.split("@")[1]}` : input.kind === "phone" ? `***${normalized.slice(-4)}` : void 0;
|
|
6868
|
-
return { id: `identity_${
|
|
6868
|
+
return { id: `identity_${digest2}`, kind: input.kind, value: normalized, ...hint ? { hint } : {}, verifiedAt: input.verifiedAt };
|
|
6869
6869
|
}
|
|
6870
6870
|
async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
6871
6871
|
return `alias_${await sha256(`${sourceSystem}\0${sourceRecordId}`)}`;
|
|
@@ -7207,9 +7207,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7207
7207
|
if (req.method === "POST") {
|
|
7208
7208
|
let body;
|
|
7209
7209
|
try {
|
|
7210
|
-
const
|
|
7211
|
-
if (new TextEncoder().encode(
|
|
7212
|
-
const parsed = JSON.parse(
|
|
7210
|
+
const text5 = await req.text();
|
|
7211
|
+
if (new TextEncoder().encode(text5).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7212
|
+
const parsed = JSON.parse(text5);
|
|
7213
7213
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7214
7214
|
body = parsed;
|
|
7215
7215
|
} catch {
|
|
@@ -7341,6 +7341,602 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7341
7341
|
});
|
|
7342
7342
|
};
|
|
7343
7343
|
|
|
7344
|
+
// src/worker-routes-network-commercial.ts
|
|
7345
|
+
var import_stripe3 = require("@odla-ai/stripe");
|
|
7346
|
+
|
|
7347
|
+
// src/config-network-commercial.ts
|
|
7348
|
+
function networkCommercialSourceKey(source) {
|
|
7349
|
+
return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
|
|
7350
|
+
}
|
|
7351
|
+
|
|
7352
|
+
// src/network-commercial.ts
|
|
7353
|
+
var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7354
|
+
var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
|
|
7355
|
+
var integer2 = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
7356
|
+
var bool = (value) => typeof value === "boolean" ? value : void 0;
|
|
7357
|
+
var providerId = (value, prefix) => {
|
|
7358
|
+
const valueText = text3(value);
|
|
7359
|
+
return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
|
|
7360
|
+
};
|
|
7361
|
+
function metadataCandidates(object) {
|
|
7362
|
+
const parent = raw(object.parent);
|
|
7363
|
+
const subscriptionDetails = raw(object.subscription_details);
|
|
7364
|
+
const parentSubscription = raw(parent.subscription_details);
|
|
7365
|
+
return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
|
|
7366
|
+
}
|
|
7367
|
+
function metadataValue(candidates, key) {
|
|
7368
|
+
return candidates.map((candidate) => text3(candidate[key])).find(Boolean);
|
|
7369
|
+
}
|
|
7370
|
+
function refId(value, prefix) {
|
|
7371
|
+
if (typeof value === "string") return providerId(value, prefix);
|
|
7372
|
+
return providerId(raw(value).id, prefix);
|
|
7373
|
+
}
|
|
7374
|
+
function subscriptionRef(object) {
|
|
7375
|
+
return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
|
|
7376
|
+
}
|
|
7377
|
+
function firstPrice(object) {
|
|
7378
|
+
const data = raw(object.items).data;
|
|
7379
|
+
const item = Array.isArray(data) ? raw(data[0]) : {};
|
|
7380
|
+
const priceId = refId(item.price, "price_");
|
|
7381
|
+
const quantity = integer2(item.quantity);
|
|
7382
|
+
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
7383
|
+
}
|
|
7384
|
+
function normalizeNetworkCommercialStripeEvent(input, digest2) {
|
|
7385
|
+
const event = raw(input);
|
|
7386
|
+
const eventId = providerId(event.id, "evt_") ?? "";
|
|
7387
|
+
const eventType = text3(event.type) ?? "";
|
|
7388
|
+
const object = raw(raw(event.data).object);
|
|
7389
|
+
const objectId = providerId(object.id) ?? "";
|
|
7390
|
+
const candidates = metadataCandidates(object);
|
|
7391
|
+
const identity = {};
|
|
7392
|
+
const identityKeys = {
|
|
7393
|
+
appId: "odlaAppId",
|
|
7394
|
+
chapterId: "chapterId",
|
|
7395
|
+
environment: "odlaEnvironment",
|
|
7396
|
+
dataEnvironment: "odlaDataEnvironment",
|
|
7397
|
+
runtime: "odlaRuntime"
|
|
7398
|
+
};
|
|
7399
|
+
for (const [target, source] of Object.entries(identityKeys)) {
|
|
7400
|
+
const value = metadataValue(candidates, source);
|
|
7401
|
+
if (value) identity[target] = value;
|
|
7402
|
+
}
|
|
7403
|
+
let objectKind = "ignored";
|
|
7404
|
+
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
7405
|
+
else if (eventType.startsWith("customer.")) objectKind = "customer";
|
|
7406
|
+
else if (eventType.startsWith("invoice.")) objectKind = "invoice";
|
|
7407
|
+
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
7408
|
+
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
7409
|
+
const customerId = refId(object.customer, "cus_");
|
|
7410
|
+
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
|
|
7411
|
+
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
|
|
7412
|
+
const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
|
|
7413
|
+
const paymentIntentId = refId(object.payment_intent, "pi_");
|
|
7414
|
+
const price = firstPrice(object);
|
|
7415
|
+
const amountCents = integer2(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
|
|
7416
|
+
const currency = text3(object.currency)?.toLowerCase();
|
|
7417
|
+
const status = text3(object.status);
|
|
7418
|
+
return {
|
|
7419
|
+
eventId,
|
|
7420
|
+
digest: digest2,
|
|
7421
|
+
eventType,
|
|
7422
|
+
eventCreated: integer2(event.created) ?? 0,
|
|
7423
|
+
livemode: event.livemode === true,
|
|
7424
|
+
objectKind,
|
|
7425
|
+
objectId,
|
|
7426
|
+
identity,
|
|
7427
|
+
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
7428
|
+
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
7429
|
+
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
7430
|
+
...customerId ? { customerId } : {},
|
|
7431
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
7432
|
+
...invoiceId ? { invoiceId } : {},
|
|
7433
|
+
...chargeId ? { chargeId } : {},
|
|
7434
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
7435
|
+
...price,
|
|
7436
|
+
...status ? { status } : {},
|
|
7437
|
+
...currency ? { currency } : {},
|
|
7438
|
+
...amountCents !== void 0 ? { amountCents } : {},
|
|
7439
|
+
...integer2(object.amount_paid) !== void 0 ? { amountPaidCents: integer2(object.amount_paid) } : {},
|
|
7440
|
+
...integer2(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer2(object.amount_remaining) } : {},
|
|
7441
|
+
...integer2(object.current_period_start) !== void 0 ? { currentPeriodStart: integer2(object.current_period_start) } : {},
|
|
7442
|
+
...integer2(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer2(object.current_period_end) } : {},
|
|
7443
|
+
...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
|
|
7444
|
+
};
|
|
7445
|
+
}
|
|
7446
|
+
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
7447
|
+
function directSource(fact, sources) {
|
|
7448
|
+
const mode = fact.livemode ? "live" : "test";
|
|
7449
|
+
const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
|
|
7450
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
|
|
7451
|
+
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
7452
|
+
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
7453
|
+
return null;
|
|
7454
|
+
}
|
|
7455
|
+
var objectRefs = (fact) => [
|
|
7456
|
+
fact.customerId && `customer:${fact.customerId}`,
|
|
7457
|
+
fact.subscriptionId && `subscription:${fact.subscriptionId}`,
|
|
7458
|
+
fact.invoiceId && `invoice:${fact.invoiceId}`,
|
|
7459
|
+
fact.chargeId && `charge:${fact.chargeId}`,
|
|
7460
|
+
fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
|
|
7461
|
+
].filter((value) => Boolean(value));
|
|
7462
|
+
function ownRef(fact) {
|
|
7463
|
+
if (!fact.objectId) return null;
|
|
7464
|
+
if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
|
|
7465
|
+
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
7466
|
+
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
7467
|
+
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
7468
|
+
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
7469
|
+
}
|
|
7470
|
+
var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
|
|
7471
|
+
function projectNetworkCommercialEvents(input, sources) {
|
|
7472
|
+
const facts2 = [...input].sort(order);
|
|
7473
|
+
const exceptions = [];
|
|
7474
|
+
const unique = [];
|
|
7475
|
+
const digests = /* @__PURE__ */ new Map();
|
|
7476
|
+
for (const fact of facts2) {
|
|
7477
|
+
const prior = digests.get(fact.eventId);
|
|
7478
|
+
if (prior === void 0) {
|
|
7479
|
+
digests.set(fact.eventId, fact.digest);
|
|
7480
|
+
unique.push(fact);
|
|
7481
|
+
} else if (prior !== fact.digest) exceptions.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7482
|
+
}
|
|
7483
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
7484
|
+
const refs = /* @__PURE__ */ new Map();
|
|
7485
|
+
for (const fact of unique) {
|
|
7486
|
+
const direct = directSource(fact, sources);
|
|
7487
|
+
if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
|
|
7488
|
+
}
|
|
7489
|
+
let changed = true;
|
|
7490
|
+
while (changed) {
|
|
7491
|
+
changed = false;
|
|
7492
|
+
for (const fact of unique) {
|
|
7493
|
+
const assigned = assignments.get(fact.eventId);
|
|
7494
|
+
if (assigned) {
|
|
7495
|
+
const key = networkCommercialSourceKey(assigned);
|
|
7496
|
+
for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
|
|
7497
|
+
const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
|
|
7498
|
+
const before = keys2.size;
|
|
7499
|
+
keys2.add(key);
|
|
7500
|
+
refs.set(ref, keys2);
|
|
7501
|
+
if (keys2.size !== before) changed = true;
|
|
7502
|
+
}
|
|
7503
|
+
continue;
|
|
7504
|
+
}
|
|
7505
|
+
if (directSource(fact, sources) === "wrong") continue;
|
|
7506
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
7507
|
+
if (keys.size !== 1) continue;
|
|
7508
|
+
const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
|
|
7509
|
+
if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
|
|
7510
|
+
assignments.set(fact.eventId, candidate);
|
|
7511
|
+
changed = true;
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
}
|
|
7515
|
+
const latest = /* @__PURE__ */ new Map();
|
|
7516
|
+
for (const fact of unique) {
|
|
7517
|
+
if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
|
|
7518
|
+
exceptions.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
7521
|
+
const source = assignments.get(fact.eventId);
|
|
7522
|
+
if (!source) {
|
|
7523
|
+
const direct = directSource(fact, sources);
|
|
7524
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
7525
|
+
exceptions.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7526
|
+
continue;
|
|
7527
|
+
}
|
|
7528
|
+
const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
|
|
7529
|
+
const key = `${fact.objectKind}:${fact.objectId}`;
|
|
7530
|
+
const prior = latest.get(key);
|
|
7531
|
+
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7532
|
+
}
|
|
7533
|
+
const rows6 = [...latest.values()];
|
|
7534
|
+
const customers = rows6.filter((row) => row.objectKind === "customer");
|
|
7535
|
+
const subscriptions = rows6.filter((row) => row.objectKind === "subscription");
|
|
7536
|
+
const invoices = rows6.filter((row) => row.objectKind === "invoice");
|
|
7537
|
+
const refunds = rows6.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
7538
|
+
for (const charge of rows6.filter((row) => row.eventType === "charge.refunded")) {
|
|
7539
|
+
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7540
|
+
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
const totalsMap = /* @__PURE__ */ new Map();
|
|
7544
|
+
const add = (row, field, amount) => {
|
|
7545
|
+
if (!row.currency) return;
|
|
7546
|
+
const key = `${row.sourceKey}:${row.currency}`;
|
|
7547
|
+
const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
7548
|
+
total[field] += amount;
|
|
7549
|
+
total.netCents = total.paidCents - total.refundedCents;
|
|
7550
|
+
totalsMap.set(key, total);
|
|
7551
|
+
};
|
|
7552
|
+
for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
|
|
7553
|
+
for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
|
|
7554
|
+
return { customers, subscriptions, invoices, refunds, exceptions, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
// src/network-commercial-store.ts
|
|
7558
|
+
var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
|
|
7559
|
+
var projectionId = (kind, sourceKey, providerId2) => `network-commercial:${kind}:${sourceKey}:${providerId2}`;
|
|
7560
|
+
var exceptionId = (eventId, kind) => `network-commercial-exception:${eventId}:${kind}`;
|
|
7561
|
+
var materializedNamespaces = [
|
|
7562
|
+
"networkCommercialCustomers",
|
|
7563
|
+
"networkCommercialSubscriptions",
|
|
7564
|
+
"networkCommercialInvoices",
|
|
7565
|
+
"networkCommercialRefunds"
|
|
7566
|
+
];
|
|
7567
|
+
async function rows5(db, ns) {
|
|
7568
|
+
const all = [];
|
|
7569
|
+
const pageSize = 500;
|
|
7570
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
7571
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset } } });
|
|
7572
|
+
const page = Array.isArray(result[ns]) ? result[ns] : [];
|
|
7573
|
+
all.push(...page);
|
|
7574
|
+
if (page.length < pageSize) return all;
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7577
|
+
async function loadNetworkCommercialProjection(db, sources) {
|
|
7578
|
+
const stored = await rows5(db, "networkCommercialEvents");
|
|
7579
|
+
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7580
|
+
const persisted = await rows5(db, "networkCommercialExceptions");
|
|
7581
|
+
for (const row of persisted) {
|
|
7582
|
+
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7583
|
+
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
7584
|
+
projection.exceptions.push({
|
|
7585
|
+
eventId: String(row.eventId),
|
|
7586
|
+
kind: "collision",
|
|
7587
|
+
...typeof row.sourceKey === "string" ? { sourceKey: row.sourceKey } : {},
|
|
7588
|
+
detail: row.detail
|
|
7589
|
+
});
|
|
7590
|
+
}
|
|
7591
|
+
return projection;
|
|
7592
|
+
}
|
|
7593
|
+
function projectionOps(projection, now) {
|
|
7594
|
+
const groups = [
|
|
7595
|
+
["networkCommercialCustomers", "customer", projection.customers],
|
|
7596
|
+
["networkCommercialSubscriptions", "subscription", projection.subscriptions],
|
|
7597
|
+
["networkCommercialInvoices", "invoice", projection.invoices],
|
|
7598
|
+
["networkCommercialRefunds", "refund", projection.refunds]
|
|
7599
|
+
];
|
|
7600
|
+
return groups.flatMap(([ns, kind, projected]) => projected.map((snapshot) => ({
|
|
7601
|
+
t: "update",
|
|
7602
|
+
ns,
|
|
7603
|
+
id: projectionId(kind, snapshot.sourceKey, snapshot.objectId),
|
|
7604
|
+
attrs: {
|
|
7605
|
+
id: projectionId(kind, snapshot.sourceKey, snapshot.objectId),
|
|
7606
|
+
providerId: snapshot.objectId,
|
|
7607
|
+
sourceKey: snapshot.sourceKey,
|
|
7608
|
+
status: "active",
|
|
7609
|
+
snapshot,
|
|
7610
|
+
eventCreated: snapshot.eventCreated,
|
|
7611
|
+
updatedAt: now
|
|
7612
|
+
}
|
|
7613
|
+
})));
|
|
7614
|
+
}
|
|
7615
|
+
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7616
|
+
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7617
|
+
const [existing, ...materialized] = await Promise.all([
|
|
7618
|
+
rows5(db, "networkCommercialExceptions"),
|
|
7619
|
+
...materializedNamespaces.map((ns) => rows5(db, ns))
|
|
7620
|
+
]);
|
|
7621
|
+
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7622
|
+
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
7623
|
+
const ops = [
|
|
7624
|
+
...projectionOps(projection, now),
|
|
7625
|
+
...materializedNamespaces.flatMap((ns, index2) => materialized[index2].filter((row) => row.status === "active" && !projectedIds.has(String(row.id))).map((row) => ({ t: "update", ns, id: String(row.id), attrs: { status: "inactive", updatedAt: now } }))),
|
|
7626
|
+
...existing.filter((row) => row.status === "active" && !activeIds.has(String(row.id))).map((row) => ({
|
|
7627
|
+
t: "update",
|
|
7628
|
+
ns: "networkCommercialExceptions",
|
|
7629
|
+
id: String(row.id),
|
|
7630
|
+
attrs: { status: "resolved", updatedAt: now }
|
|
7631
|
+
})),
|
|
7632
|
+
...projection.exceptions.map((item) => ({
|
|
7633
|
+
t: "update",
|
|
7634
|
+
ns: "networkCommercialExceptions",
|
|
7635
|
+
id: exceptionId(item.eventId, item.kind),
|
|
7636
|
+
attrs: {
|
|
7637
|
+
id: exceptionId(item.eventId, item.kind),
|
|
7638
|
+
eventId: item.eventId,
|
|
7639
|
+
kind: item.kind,
|
|
7640
|
+
...item.sourceKey ? { sourceKey: item.sourceKey } : {},
|
|
7641
|
+
status: "active",
|
|
7642
|
+
detail: item.detail,
|
|
7643
|
+
updatedAt: now
|
|
7644
|
+
}
|
|
7645
|
+
}))
|
|
7646
|
+
];
|
|
7647
|
+
if (ops.length > 0) await db.transact(ops, { mutationId });
|
|
7648
|
+
return projection;
|
|
7649
|
+
}
|
|
7650
|
+
async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now()) {
|
|
7651
|
+
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
7652
|
+
const existing = found.networkCommercialEvents?.[0];
|
|
7653
|
+
if (existing) {
|
|
7654
|
+
const projection2 = await loadNetworkCommercialProjection(db, sources);
|
|
7655
|
+
if (existing.digest === fact.digest) return { status: "replayed", projection: projection2 };
|
|
7656
|
+
const id2 = exceptionId(fact.eventId, "collision");
|
|
7657
|
+
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id2, attrs: {
|
|
7658
|
+
id: id2,
|
|
7659
|
+
eventId: fact.eventId,
|
|
7660
|
+
kind: "collision",
|
|
7661
|
+
status: "active",
|
|
7662
|
+
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
7663
|
+
updatedAt: now
|
|
7664
|
+
} }], { mutationId: `network-commercial-collision:${fact.eventId}:${fact.digest}` });
|
|
7665
|
+
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
7666
|
+
}
|
|
7667
|
+
const id = eventRowId(fact.eventId);
|
|
7668
|
+
let lostRace = false;
|
|
7669
|
+
let duplicate = false;
|
|
7670
|
+
try {
|
|
7671
|
+
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id, attrs: {
|
|
7672
|
+
id,
|
|
7673
|
+
eventId: fact.eventId,
|
|
7674
|
+
digest: fact.digest,
|
|
7675
|
+
eventType: fact.eventType,
|
|
7676
|
+
eventCreated: fact.eventCreated,
|
|
7677
|
+
livemode: fact.livemode,
|
|
7678
|
+
fact,
|
|
7679
|
+
receivedAt: now
|
|
7680
|
+
} }], {
|
|
7681
|
+
mutationId: `network-commercial-ingest:${fact.eventId}:${fact.digest}`,
|
|
7682
|
+
guards: [{ ns: "networkCommercialEvents", id, exists: false }]
|
|
7683
|
+
});
|
|
7684
|
+
duplicate = write.duplicate;
|
|
7685
|
+
} catch (error) {
|
|
7686
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
7687
|
+
lostRace = true;
|
|
7688
|
+
}
|
|
7689
|
+
if (duplicate || lostRace) {
|
|
7690
|
+
const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
7691
|
+
const row = raced.networkCommercialEvents?.[0];
|
|
7692
|
+
const projection2 = await loadNetworkCommercialProjection(db, sources);
|
|
7693
|
+
if (row?.digest === fact.digest) return { status: "replayed", projection: projection2 };
|
|
7694
|
+
const id2 = exceptionId(fact.eventId, "collision");
|
|
7695
|
+
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id2, attrs: {
|
|
7696
|
+
id: id2,
|
|
7697
|
+
eventId: fact.eventId,
|
|
7698
|
+
kind: "collision",
|
|
7699
|
+
status: "active",
|
|
7700
|
+
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
7701
|
+
updatedAt: now
|
|
7702
|
+
} }], { mutationId: `network-commercial-collision:${fact.eventId}:${fact.digest}` });
|
|
7703
|
+
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
7704
|
+
}
|
|
7705
|
+
const projection = await rebuildNetworkCommercialProjection(
|
|
7706
|
+
db,
|
|
7707
|
+
sources,
|
|
7708
|
+
`network-commercial-project:${fact.eventId}:${fact.digest}`,
|
|
7709
|
+
now
|
|
7710
|
+
);
|
|
7711
|
+
return { status: "stored", projection };
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
// src/network-commercial-reconcile.ts
|
|
7715
|
+
var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
7716
|
+
async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
7717
|
+
const objects = [];
|
|
7718
|
+
let startingAfter;
|
|
7719
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
|
|
7720
|
+
const page = await client.request("GET", path, {
|
|
7721
|
+
...params,
|
|
7722
|
+
limit: 100,
|
|
7723
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
7724
|
+
});
|
|
7725
|
+
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7726
|
+
throw new Error("Stripe returned an invalid commercial list response");
|
|
7727
|
+
}
|
|
7728
|
+
const rows6 = page.data.map(asRaw).filter((row) => row !== null);
|
|
7729
|
+
if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
7730
|
+
objects.push(...rows6);
|
|
7731
|
+
if (!page.has_more) return { objects, complete: true };
|
|
7732
|
+
const cursor = rows6.at(-1)?.id;
|
|
7733
|
+
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7734
|
+
startingAfter = cursor;
|
|
7735
|
+
}
|
|
7736
|
+
return { objects, complete: false };
|
|
7737
|
+
}
|
|
7738
|
+
function facts(snapshots) {
|
|
7739
|
+
const groups = [
|
|
7740
|
+
["customer.updated", snapshots.customers.objects],
|
|
7741
|
+
["customer.subscription.updated", snapshots.subscriptions.objects],
|
|
7742
|
+
["invoice.updated", snapshots.invoices.objects],
|
|
7743
|
+
["refund.updated", snapshots.refunds.objects]
|
|
7744
|
+
];
|
|
7745
|
+
return groups.flatMap(([eventType, objects]) => objects.map((object) => {
|
|
7746
|
+
const id = typeof object.id === "string" ? object.id : "invalid";
|
|
7747
|
+
return normalizeNetworkCommercialStripeEvent({
|
|
7748
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id}`,
|
|
7749
|
+
type: eventType,
|
|
7750
|
+
created: typeof object.created === "number" ? object.created : 0,
|
|
7751
|
+
livemode: object.livemode === true,
|
|
7752
|
+
data: { object }
|
|
7753
|
+
}, `stripe-snapshot:${eventType}:${id}`);
|
|
7754
|
+
}));
|
|
7755
|
+
}
|
|
7756
|
+
var selected = (row) => ({
|
|
7757
|
+
status: row.status ?? null,
|
|
7758
|
+
currency: row.currency ?? null,
|
|
7759
|
+
amountCents: row.amountCents ?? null,
|
|
7760
|
+
amountPaidCents: row.amountPaidCents ?? null,
|
|
7761
|
+
amountRemainingCents: row.amountRemainingCents ?? null,
|
|
7762
|
+
customerId: row.customerId ?? null,
|
|
7763
|
+
subscriptionId: row.subscriptionId ?? null,
|
|
7764
|
+
invoiceId: row.invoiceId ?? null,
|
|
7765
|
+
priceId: row.priceId ?? null,
|
|
7766
|
+
quantity: row.quantity ?? null,
|
|
7767
|
+
applicationId: row.applicationId ?? null,
|
|
7768
|
+
personId: row.personId ?? null,
|
|
7769
|
+
membershipId: row.membershipId ?? null,
|
|
7770
|
+
currentPeriodStart: row.currentPeriodStart ?? null,
|
|
7771
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
7772
|
+
cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
|
|
7773
|
+
});
|
|
7774
|
+
var index = (projection) => new Map(
|
|
7775
|
+
[...projection.customers, ...projection.subscriptions, ...projection.invoices, ...projection.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
|
|
7776
|
+
);
|
|
7777
|
+
function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
7778
|
+
const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
|
|
7779
|
+
const shadowRows = index(shadow);
|
|
7780
|
+
const stripeRows = index(stripe);
|
|
7781
|
+
const exceptions = [];
|
|
7782
|
+
const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
|
|
7783
|
+
if (!complete) exceptions.push({ kind: "stripe_snapshot_truncated", detail: {} });
|
|
7784
|
+
for (const [key, row] of shadowRows) {
|
|
7785
|
+
const other = stripeRows.get(key);
|
|
7786
|
+
if (!other) {
|
|
7787
|
+
exceptions.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
7788
|
+
} else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
|
|
7789
|
+
exceptions.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
for (const [key, row] of stripeRows) {
|
|
7793
|
+
if (!shadowRows.has(key)) exceptions.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
7794
|
+
}
|
|
7795
|
+
if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
|
|
7796
|
+
exceptions.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
|
|
7797
|
+
}
|
|
7798
|
+
return {
|
|
7799
|
+
ok: exceptions.length === 0,
|
|
7800
|
+
complete,
|
|
7801
|
+
ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
|
|
7802
|
+
exceptions,
|
|
7803
|
+
stripeTotals: stripe.totals
|
|
7804
|
+
};
|
|
7805
|
+
}
|
|
7806
|
+
|
|
7807
|
+
// src/worker-routes-network-commercial.ts
|
|
7808
|
+
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
7809
|
+
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
7810
|
+
var MAX_BODY3 = 512e3;
|
|
7811
|
+
var MUTATION_ID5 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7812
|
+
async function digest(body) {
|
|
7813
|
+
const value = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
7814
|
+
return [...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7815
|
+
}
|
|
7816
|
+
async function boundedBody(req) {
|
|
7817
|
+
if (Number(req.headers.get("content-length") ?? 0) > MAX_BODY3) return null;
|
|
7818
|
+
const body = await req.text();
|
|
7819
|
+
return new TextEncoder().encode(body).byteLength <= MAX_BODY3 ? body : null;
|
|
7820
|
+
}
|
|
7821
|
+
async function gate11(req, env, ctx) {
|
|
7822
|
+
const db = ctx.makeDb(env);
|
|
7823
|
+
const user = await ctx.verifyUser(req, env);
|
|
7824
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7825
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7826
|
+
return { db, user };
|
|
7827
|
+
}
|
|
7828
|
+
function safeProjection(projection) {
|
|
7829
|
+
const row = (item) => ({
|
|
7830
|
+
sourceKey: item.sourceKey,
|
|
7831
|
+
providerId: item.objectId,
|
|
7832
|
+
eventType: item.eventType,
|
|
7833
|
+
eventCreated: item.eventCreated,
|
|
7834
|
+
status: item.status ?? null,
|
|
7835
|
+
currency: item.currency ?? null,
|
|
7836
|
+
amountCents: item.amountCents ?? null,
|
|
7837
|
+
amountPaidCents: item.amountPaidCents ?? null,
|
|
7838
|
+
amountRemainingCents: item.amountRemainingCents ?? null,
|
|
7839
|
+
customerId: item.customerId ?? null,
|
|
7840
|
+
subscriptionId: item.subscriptionId ?? null,
|
|
7841
|
+
invoiceId: item.invoiceId ?? null,
|
|
7842
|
+
priceId: item.priceId ?? null,
|
|
7843
|
+
quantity: item.quantity ?? null,
|
|
7844
|
+
applicationId: item.applicationId ?? null,
|
|
7845
|
+
personId: item.personId ?? null,
|
|
7846
|
+
membershipId: item.membershipId ?? null,
|
|
7847
|
+
currentPeriodStart: item.currentPeriodStart ?? null,
|
|
7848
|
+
currentPeriodEnd: item.currentPeriodEnd ?? null,
|
|
7849
|
+
cancelAtPeriodEnd: item.cancelAtPeriodEnd ?? null
|
|
7850
|
+
});
|
|
7851
|
+
return {
|
|
7852
|
+
version: 1,
|
|
7853
|
+
mode: "shadow",
|
|
7854
|
+
writable: false,
|
|
7855
|
+
authoritativeForCharges: false,
|
|
7856
|
+
totals: projection.totals,
|
|
7857
|
+
customers: projection.customers.map(row),
|
|
7858
|
+
subscriptions: projection.subscriptions.map(row),
|
|
7859
|
+
invoices: projection.invoices.map(row),
|
|
7860
|
+
refunds: projection.refunds.map(row),
|
|
7861
|
+
exceptions: projection.exceptions
|
|
7862
|
+
};
|
|
7863
|
+
}
|
|
7864
|
+
var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
7865
|
+
if (url.pathname !== WEBHOOK_PATH) return null;
|
|
7866
|
+
if (!ctx.chapter.networkCommercialReadModel) return json({ error: "not found" }, 404);
|
|
7867
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7868
|
+
const body = await boundedBody(req);
|
|
7869
|
+
if (body === null) return json({ error: "request is too large" }, 413);
|
|
7870
|
+
const signature = req.headers.get("stripe-signature") ?? "";
|
|
7871
|
+
const db = ctx.makeDb(env);
|
|
7872
|
+
let secret;
|
|
7873
|
+
try {
|
|
7874
|
+
secret = await db.secrets.get("network_commercial_stripe_webhook_secret");
|
|
7875
|
+
} catch {
|
|
7876
|
+
return json({ error: "commercial webhook is not configured" }, 503);
|
|
7877
|
+
}
|
|
7878
|
+
try {
|
|
7879
|
+
const event = await (0, import_stripe3.constructStripeEvent)(body, signature, secret);
|
|
7880
|
+
const fact = normalizeNetworkCommercialStripeEvent(event, await digest(body));
|
|
7881
|
+
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
7882
|
+
const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact);
|
|
7883
|
+
if (result.status === "collision") {
|
|
7884
|
+
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
7885
|
+
}
|
|
7886
|
+
return json({ ok: true, status: result.status, eventId: fact.eventId });
|
|
7887
|
+
} catch (error) {
|
|
7888
|
+
if (error instanceof import_stripe3.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
7889
|
+
throw error;
|
|
7890
|
+
}
|
|
7891
|
+
};
|
|
7892
|
+
var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
7893
|
+
if (url.pathname !== ADMIN_PATH2) return null;
|
|
7894
|
+
if (!ctx.chapter.networkCommercialReadModel) return json({ error: "not found" }, 404);
|
|
7895
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7896
|
+
const got = await gate11(req, env, ctx);
|
|
7897
|
+
if ("response" in got) return got.response;
|
|
7898
|
+
const db = got.db;
|
|
7899
|
+
if (req.method === "POST") {
|
|
7900
|
+
const body = await boundedBody(req);
|
|
7901
|
+
if (body === null) return json({ error: "request is too large" }, 413);
|
|
7902
|
+
let parsed;
|
|
7903
|
+
try {
|
|
7904
|
+
const value = JSON.parse(body);
|
|
7905
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("shape");
|
|
7906
|
+
parsed = value;
|
|
7907
|
+
} catch {
|
|
7908
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7909
|
+
}
|
|
7910
|
+
if (parsed.command === "reconcile-stripe") {
|
|
7911
|
+
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7912
|
+
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
7913
|
+
try {
|
|
7914
|
+
const stripe = (0, import_stripe3.initStripe)({ secretKey });
|
|
7915
|
+
const [customers, subscriptions, invoices, refunds] = await Promise.all([
|
|
7916
|
+
listStripeObjects(stripe, "/v1/customers"),
|
|
7917
|
+
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
7918
|
+
listStripeObjects(stripe, "/v1/invoices"),
|
|
7919
|
+
listStripeObjects(stripe, "/v1/refunds")
|
|
7920
|
+
]);
|
|
7921
|
+
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
7922
|
+
return json({
|
|
7923
|
+
reconciled: true,
|
|
7924
|
+
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds }, ctx.chapter.networkCommercialReadModel.sources)
|
|
7925
|
+
});
|
|
7926
|
+
} catch (error) {
|
|
7927
|
+
if (error instanceof import_stripe3.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
7928
|
+
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
7932
|
+
return json({ error: "rebuild or reconcile-stripe command is required; rebuild also requires a valid mutationId" }, 400);
|
|
7933
|
+
}
|
|
7934
|
+
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
7935
|
+
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
7936
|
+
}
|
|
7937
|
+
return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
|
|
7938
|
+
};
|
|
7939
|
+
|
|
7344
7940
|
// src/worker-routes-formation.ts
|
|
7345
7941
|
var import_crm11 = require("@odla-ai/crm");
|
|
7346
7942
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
@@ -7357,11 +7953,11 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
7357
7953
|
});
|
|
7358
7954
|
}
|
|
7359
7955
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|
|
7360
|
-
const
|
|
7361
|
-
if (
|
|
7956
|
+
const raw2 = await req.text();
|
|
7957
|
+
if (raw2.length > formation.bodyCap) return json({ error: "request body too large" }, 413);
|
|
7362
7958
|
let body;
|
|
7363
7959
|
try {
|
|
7364
|
-
body = JSON.parse(
|
|
7960
|
+
body = JSON.parse(raw2);
|
|
7365
7961
|
} catch {
|
|
7366
7962
|
return json({ error: "invalid JSON body" }, 400);
|
|
7367
7963
|
}
|
|
@@ -7445,7 +8041,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
7445
8041
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
7446
8042
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
7447
8043
|
}
|
|
7448
|
-
function
|
|
8044
|
+
function text4(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
7449
8045
|
if (value === void 0 && !required) return void 0;
|
|
7450
8046
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
7451
8047
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -7455,23 +8051,23 @@ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
|
7455
8051
|
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
7456
8052
|
return result;
|
|
7457
8053
|
}
|
|
7458
|
-
function
|
|
8054
|
+
function integer3(value, path, errors, minimum = 1) {
|
|
7459
8055
|
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
7460
8056
|
return Number(value);
|
|
7461
8057
|
}
|
|
7462
8058
|
function scopeValue(value, path, errors) {
|
|
7463
|
-
const
|
|
7464
|
-
if (!
|
|
8059
|
+
const raw2 = record3(value);
|
|
8060
|
+
if (!raw2) {
|
|
7465
8061
|
errors.push(`${path} must be an object`);
|
|
7466
8062
|
return null;
|
|
7467
8063
|
}
|
|
7468
|
-
unknownKeys3(
|
|
7469
|
-
if (
|
|
7470
|
-
if (
|
|
7471
|
-
return { kind:
|
|
8064
|
+
unknownKeys3(raw2, SCOPE_KEYS, path, errors);
|
|
8065
|
+
if (raw2.kind === "global" || raw2.kind === "built_not_found") {
|
|
8066
|
+
if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
8067
|
+
return { kind: raw2.kind };
|
|
7472
8068
|
}
|
|
7473
|
-
if (
|
|
7474
|
-
const chapterId =
|
|
8069
|
+
if (raw2.kind === "chapter") {
|
|
8070
|
+
const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
7475
8071
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
7476
8072
|
return { kind: "chapter", chapterId };
|
|
7477
8073
|
}
|
|
@@ -7479,28 +8075,28 @@ function scopeValue(value, path, errors) {
|
|
|
7479
8075
|
return null;
|
|
7480
8076
|
}
|
|
7481
8077
|
function resourceValue(value, path, errors) {
|
|
7482
|
-
const
|
|
7483
|
-
if (!
|
|
8078
|
+
const raw2 = record3(value);
|
|
8079
|
+
if (!raw2) {
|
|
7484
8080
|
errors.push(`${path} must be an object`);
|
|
7485
8081
|
return null;
|
|
7486
8082
|
}
|
|
7487
|
-
unknownKeys3(
|
|
7488
|
-
const id =
|
|
8083
|
+
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
8084
|
+
const id = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
7489
8085
|
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
7490
|
-
const kind =
|
|
8086
|
+
const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
7491
8087
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
7492
|
-
const title =
|
|
7493
|
-
const summary =
|
|
7494
|
-
const status =
|
|
8088
|
+
const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
8089
|
+
const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
8090
|
+
const status = text4(raw2.status, `${path}.status`, errors) ?? "";
|
|
7495
8091
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
7496
|
-
const revision =
|
|
7497
|
-
const scope = scopeValue(
|
|
7498
|
-
const tags = Array.isArray(
|
|
7499
|
-
if (!Array.isArray(
|
|
8092
|
+
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
8093
|
+
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
8094
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
8095
|
+
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
7500
8096
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
7501
8097
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
7502
|
-
const bodyMarkdown =
|
|
7503
|
-
const url =
|
|
8098
|
+
const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
8099
|
+
const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
|
|
7504
8100
|
if (url) {
|
|
7505
8101
|
try {
|
|
7506
8102
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -7508,13 +8104,13 @@ function resourceValue(value, path, errors) {
|
|
|
7508
8104
|
errors.push(`${path}.url must be a valid https URL`);
|
|
7509
8105
|
}
|
|
7510
8106
|
}
|
|
7511
|
-
const asset =
|
|
7512
|
-
if (
|
|
8107
|
+
const asset = raw2.brandAsset === void 0 ? null : record3(raw2.brandAsset);
|
|
8108
|
+
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
7513
8109
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
7514
8110
|
const brandAsset = asset ? {
|
|
7515
|
-
bookId:
|
|
7516
|
-
assetId:
|
|
7517
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
8111
|
+
bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
8112
|
+
assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
8113
|
+
...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
7518
8114
|
} : void 0;
|
|
7519
8115
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
7520
8116
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -7522,58 +8118,58 @@ function resourceValue(value, path, errors) {
|
|
|
7522
8118
|
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
7523
8119
|
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
7524
8120
|
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
7525
|
-
const provenanceRaw = record3(
|
|
8121
|
+
const provenanceRaw = record3(raw2.provenance);
|
|
7526
8122
|
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
7527
8123
|
if (provenanceRaw) unknownKeys3(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
7528
8124
|
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record3(provenanceRaw.promotedFrom);
|
|
7529
8125
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
7530
8126
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
7531
8127
|
const promotedFrom = promotedRaw ? {
|
|
7532
|
-
chapterId:
|
|
7533
|
-
resourceId:
|
|
7534
|
-
resourceRevision:
|
|
7535
|
-
documentRevision:
|
|
7536
|
-
promotedBy:
|
|
7537
|
-
promotedAt:
|
|
8128
|
+
chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
8129
|
+
resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
8130
|
+
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
8131
|
+
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
8132
|
+
promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
8133
|
+
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
7538
8134
|
} : void 0;
|
|
7539
8135
|
const provenance = {
|
|
7540
|
-
createdBy:
|
|
7541
|
-
createdAt:
|
|
7542
|
-
updatedBy:
|
|
7543
|
-
updatedAt:
|
|
8136
|
+
createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
8137
|
+
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
8138
|
+
updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
8139
|
+
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
7544
8140
|
...promotedFrom ? { promotedFrom } : {}
|
|
7545
8141
|
};
|
|
7546
8142
|
return { id, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
7547
8143
|
}
|
|
7548
8144
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
7549
8145
|
const errors = [];
|
|
7550
|
-
const
|
|
7551
|
-
if (!
|
|
7552
|
-
unknownKeys3(
|
|
7553
|
-
if (
|
|
7554
|
-
const revision =
|
|
7555
|
-
const generatedAt =
|
|
7556
|
-
const sourceRaw = record3(
|
|
8146
|
+
const raw2 = record3(value);
|
|
8147
|
+
if (!raw2) return { ok: false, errors: ["document must be an object"] };
|
|
8148
|
+
unknownKeys3(raw2, ROOT_KEYS4, "document", errors);
|
|
8149
|
+
if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
8150
|
+
const revision = integer3(raw2.revision, "document.revision", errors);
|
|
8151
|
+
const generatedAt = integer3(raw2.generatedAt, "document.generatedAt", errors, 0);
|
|
8152
|
+
const sourceRaw = record3(raw2.source);
|
|
7557
8153
|
if (!sourceRaw) errors.push("document.source must be an object");
|
|
7558
8154
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
7559
8155
|
const sourceType = sourceRaw?.type;
|
|
7560
8156
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
7561
|
-
const sourceAppId =
|
|
8157
|
+
const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
7562
8158
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7563
8159
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
7564
8160
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
7565
|
-
const targetRaw =
|
|
7566
|
-
if (
|
|
8161
|
+
const targetRaw = raw2.target === void 0 ? null : record3(raw2.target);
|
|
8162
|
+
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
7567
8163
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
7568
8164
|
const target = targetRaw ? {
|
|
7569
|
-
appId:
|
|
7570
|
-
environment:
|
|
8165
|
+
appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
8166
|
+
environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
7571
8167
|
} : void 0;
|
|
7572
8168
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
7573
|
-
if (!Array.isArray(
|
|
7574
|
-
const inputs = Array.isArray(
|
|
8169
|
+
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
8170
|
+
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
7575
8171
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
7576
|
-
const resources = inputs.map((item,
|
|
8172
|
+
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
7577
8173
|
const ids = resources.map(({ id }) => id);
|
|
7578
8174
|
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
7579
8175
|
for (const resource of resources) {
|
|
@@ -7647,28 +8243,28 @@ function canonicalBuilderLibrary(value) {
|
|
|
7647
8243
|
return canonicalValue3(value);
|
|
7648
8244
|
}
|
|
7649
8245
|
async function builderLibraryDigest(document) {
|
|
7650
|
-
const
|
|
7651
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
8246
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalBuilderLibrary(document)));
|
|
8247
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
7652
8248
|
}
|
|
7653
8249
|
|
|
7654
8250
|
// src/builder-library-store.ts
|
|
7655
8251
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7656
8252
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7657
8253
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
7658
|
-
const
|
|
7659
|
-
return Array.isArray(
|
|
8254
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
8255
|
+
return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
|
|
7660
8256
|
}
|
|
7661
8257
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
7662
|
-
const
|
|
7663
|
-
return control2(Array.isArray(
|
|
8258
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
8259
|
+
return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7664
8260
|
}
|
|
7665
8261
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
7666
8262
|
const id = `builder-library-local:${environment}`;
|
|
7667
|
-
const
|
|
7668
|
-
return local2(Array.isArray(
|
|
8263
|
+
const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
8264
|
+
return local2(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
7669
8265
|
}
|
|
7670
8266
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
7671
|
-
const { document, digest } = envelope;
|
|
8267
|
+
const { document, digest: digest2 } = envelope;
|
|
7672
8268
|
const target = document.target;
|
|
7673
8269
|
const id = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
7674
8270
|
const ops = [];
|
|
@@ -7679,12 +8275,12 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
7679
8275
|
targetId: target.appId,
|
|
7680
8276
|
environment: target.environment,
|
|
7681
8277
|
revision: document.revision,
|
|
7682
|
-
digest,
|
|
8278
|
+
digest: digest2,
|
|
7683
8279
|
document,
|
|
7684
8280
|
active: true,
|
|
7685
8281
|
receivedAt
|
|
7686
8282
|
} });
|
|
7687
|
-
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${
|
|
8283
|
+
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest2}` });
|
|
7688
8284
|
}
|
|
7689
8285
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7690
8286
|
const id = `builder-library-local:${environment}`;
|
|
@@ -7737,7 +8333,7 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
|
7737
8333
|
} catch {
|
|
7738
8334
|
return json({ ok: false, reason: "invalid_document", errors: ["request body must be JSON"] }, 400);
|
|
7739
8335
|
}
|
|
7740
|
-
const
|
|
8336
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
7741
8337
|
const sourceType = ctx.chapter.mode === "hub" ? "chapter" : "built_not_found";
|
|
7742
8338
|
const checked = validateBuilderLibraryDocument(input.document, {
|
|
7743
8339
|
sourceType,
|
|
@@ -7751,29 +8347,29 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
|
7751
8347
|
return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global or this Chapter's resources"] }, 400);
|
|
7752
8348
|
}
|
|
7753
8349
|
if (!document.target || document.target.environment !== env.ODLA_ENV) return json({ ok: false, reason: "wrong_environment" }, 409);
|
|
7754
|
-
if (!
|
|
8350
|
+
if (!digest2 || await builderLibraryDigest(document) !== digest2) return json({ ok: false, reason: "invalid_digest" }, 400);
|
|
7755
8351
|
const latest = await loadLatestBuilderLibraryControl(db, document.source.appId, ctx.chapter.id, env.ODLA_ENV);
|
|
7756
8352
|
if (latest && document.revision < latest.revision) return json({ ok: false, reason: "stale_revision" }, 409);
|
|
7757
8353
|
if (latest && document.revision === latest.revision) {
|
|
7758
|
-
if (latest.digest !==
|
|
8354
|
+
if (latest.digest !== digest2) return json({ ok: false, reason: "conflicting_revision" }, 409);
|
|
7759
8355
|
return json({ ok: true, target: document.target, revision: latest.revision, digest: latest.digest, status: "replayed", acknowledgedAt: latest.receivedAt });
|
|
7760
8356
|
}
|
|
7761
8357
|
const receivedAt = Date.now();
|
|
7762
8358
|
try {
|
|
7763
|
-
await materializeBuilderLibraryControl(db, { document, digest }, latest, receivedAt);
|
|
8359
|
+
await materializeBuilderLibraryControl(db, { document, digest: digest2 }, latest, receivedAt);
|
|
7764
8360
|
} catch {
|
|
7765
8361
|
return json({ ok: false, reason: "materialization_failed" }, 503);
|
|
7766
8362
|
}
|
|
7767
|
-
return json({ ok: true, target: document.target, revision: document.revision, digest, status: "accepted", acknowledgedAt: receivedAt });
|
|
8363
|
+
return json({ ok: true, target: document.target, revision: document.revision, digest: digest2, status: "accepted", acknowledgedAt: receivedAt });
|
|
7768
8364
|
};
|
|
7769
8365
|
function actorFor(user) {
|
|
7770
8366
|
return user.email ?? user.userId;
|
|
7771
8367
|
}
|
|
7772
8368
|
function withLocalAuthority2(value, mode, appId, revision, now, actor, current) {
|
|
7773
8369
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
7774
|
-
const
|
|
8370
|
+
const raw2 = value;
|
|
7775
8371
|
const currentById = new Map((current?.resources ?? []).map((item) => [item.id, item]));
|
|
7776
|
-
const resources = Array.isArray(
|
|
8372
|
+
const resources = Array.isArray(raw2.resources) ? raw2.resources.map((input) => {
|
|
7777
8373
|
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
7778
8374
|
const item = input;
|
|
7779
8375
|
const existing = typeof item.id === "string" ? currentById.get(item.id) : void 0;
|
|
@@ -7788,9 +8384,9 @@ function withLocalAuthority2(value, mode, appId, revision, now, actor, current)
|
|
|
7788
8384
|
...existing?.provenance.promotedFrom ? { promotedFrom: existing.provenance.promotedFrom } : {}
|
|
7789
8385
|
}
|
|
7790
8386
|
};
|
|
7791
|
-
}) :
|
|
8387
|
+
}) : raw2.resources;
|
|
7792
8388
|
return {
|
|
7793
|
-
...
|
|
8389
|
+
...raw2,
|
|
7794
8390
|
schemaVersion: 1,
|
|
7795
8391
|
revision,
|
|
7796
8392
|
generatedAt: now,
|
|
@@ -7871,7 +8467,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
7871
8467
|
return json({
|
|
7872
8468
|
mode: ctx.chapter.mode,
|
|
7873
8469
|
local: local3 ? { revision: local3.revision, document: local3.document } : null,
|
|
7874
|
-
sources: controls.map(({ sourceId, revision, digest, document, receivedAt }) => ({ sourceId, revision, digest, document, receivedAt })),
|
|
8470
|
+
sources: controls.map(({ sourceId, revision, digest: digest2, document, receivedAt }) => ({ sourceId, revision, digest: digest2, document, receivedAt })),
|
|
7875
8471
|
localAuthoring: ctx.chapter.builderLibrary.localAuthoring,
|
|
7876
8472
|
chapterTargets: ctx.chapter.mode === "hub" ? ctx.chapter.builderLibrary.sources.map((source) => ({ chapterId: source.sourceId })) : [{ chapterId: ctx.chapter.id }],
|
|
7877
8473
|
visibility: { builtNotFoundSeesChapterSources: ctx.chapter.mode === "hub", chaptersSeeSiblingSources: false }
|
|
@@ -7879,11 +8475,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
7879
8475
|
}
|
|
7880
8476
|
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
7881
8477
|
if (!ctx.chapter.builderLibrary.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
7882
|
-
const
|
|
7883
|
-
if (
|
|
8478
|
+
const raw2 = await req.text();
|
|
8479
|
+
if (raw2.length > BODY_CAP2) return json({ error: "request body too large" }, 413);
|
|
7884
8480
|
let input;
|
|
7885
8481
|
try {
|
|
7886
|
-
input = JSON.parse(
|
|
8482
|
+
input = JSON.parse(raw2);
|
|
7887
8483
|
} catch {
|
|
7888
8484
|
return json({ error: "invalid JSON body" }, 400);
|
|
7889
8485
|
}
|
|
@@ -7988,14 +8584,14 @@ function validatedTokens(input) {
|
|
|
7988
8584
|
}
|
|
7989
8585
|
const tokens = {};
|
|
7990
8586
|
let serializedLength = 0;
|
|
7991
|
-
for (const [name,
|
|
8587
|
+
for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
7992
8588
|
if (!TOKEN_NAME.test(name)) {
|
|
7993
8589
|
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
7994
8590
|
}
|
|
7995
|
-
if (typeof
|
|
8591
|
+
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
7996
8592
|
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
7997
8593
|
}
|
|
7998
|
-
const value = String(
|
|
8594
|
+
const value = String(raw2).trim();
|
|
7999
8595
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
8000
8596
|
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
8001
8597
|
}
|
|
@@ -8117,6 +8713,7 @@ var BUILTIN_ROUTES = [
|
|
|
8117
8713
|
handleNetworkSnapshot,
|
|
8118
8714
|
handleNetworkRecords,
|
|
8119
8715
|
handleNetworkSharedNotes,
|
|
8716
|
+
handleNetworkCommercialStripeWebhook,
|
|
8120
8717
|
handleFormation,
|
|
8121
8718
|
handleMember,
|
|
8122
8719
|
handleMemberContent,
|
|
@@ -8157,6 +8754,7 @@ var BUILTIN_ROUTES = [
|
|
|
8157
8754
|
handleAdminNetworkNotes,
|
|
8158
8755
|
handleAdminNetworkSharedNotes,
|
|
8159
8756
|
handleAdminNetworkPersonRegistry,
|
|
8757
|
+
handleAdminNetworkCommercialReadModel,
|
|
8160
8758
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
8161
8759
|
// get first refusal through options.routes, then this terminates unknown API
|
|
8162
8760
|
// paths with an explicit machine-readable 404.
|