@odla-ai/chapter 0.38.1 → 0.39.1
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 +49 -0
- package/dist/{chunk-SI3JHSEX.js → chunk-4K2Q5JBC.js} +660 -553
- package/dist/chunk-4K2Q5JBC.js.map +1 -0
- package/dist/{copy-context-Pbs3keLx.d.ts → copy-context-Buz_27Ed.d.ts} +26 -0
- package/dist/index.cjs +471 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -3
- package/dist/index.d.ts +202 -3
- package/dist/index.js +471 -132
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +106 -3
- package/dist/ui/admin/index.js +5 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +614 -111
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +127 -1
- package/dist/worker/index.d.ts +127 -1
- package/dist/worker/index.js +610 -107
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +15 -0
- package/dist/chunk-SI3JHSEX.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -516,9 +516,9 @@ function canonicalValue(value) {
|
|
|
516
516
|
}
|
|
517
517
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
518
518
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
519
|
-
const
|
|
520
|
-
const entries = Object.keys(
|
|
521
|
-
const item =
|
|
519
|
+
const record4 = value;
|
|
520
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
521
|
+
const item = record4[key];
|
|
522
522
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
523
523
|
});
|
|
524
524
|
return `{${entries.join(",")}}`;
|
|
@@ -718,8 +718,8 @@ var APPLICATION_LABELS = {
|
|
|
718
718
|
state: "State"
|
|
719
719
|
};
|
|
720
720
|
function humanize(id) {
|
|
721
|
-
const
|
|
722
|
-
return
|
|
721
|
+
const text4 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
722
|
+
return text4 ? `${text4[0].toUpperCase()}${text4.slice(1)}` : id;
|
|
723
723
|
}
|
|
724
724
|
function applicationDefs(chapter) {
|
|
725
725
|
try {
|
|
@@ -910,8 +910,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
910
910
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
911
911
|
const page = Array.isArray(data) ? data : [];
|
|
912
912
|
for (const u of page) {
|
|
913
|
-
const
|
|
914
|
-
if (
|
|
913
|
+
const record4 = toRecord(u);
|
|
914
|
+
if (record4) out.push(record4);
|
|
915
915
|
}
|
|
916
916
|
if (page.length < PAGE) break;
|
|
917
917
|
}
|
|
@@ -973,34 +973,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
973
973
|
if (readable && raw.length <= 64) return raw;
|
|
974
974
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
975
975
|
}
|
|
976
|
-
function normalizeSharedRecord(
|
|
977
|
-
if ("input" in
|
|
976
|
+
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
977
|
+
if ("input" in record4 && record4.version === 2) {
|
|
978
978
|
return {
|
|
979
979
|
version: 2,
|
|
980
|
-
sourceId:
|
|
981
|
-
sourceRecordId:
|
|
982
|
-
type:
|
|
983
|
-
input:
|
|
980
|
+
sourceId: record4.source.siteId,
|
|
981
|
+
sourceRecordId: record4.source.recordId,
|
|
982
|
+
type: record4.type,
|
|
983
|
+
input: record4.input
|
|
984
984
|
};
|
|
985
985
|
}
|
|
986
|
-
if ("input" in
|
|
986
|
+
if ("input" in record4) {
|
|
987
987
|
return {
|
|
988
988
|
version: 1,
|
|
989
989
|
sourceId: fallbackSourceId,
|
|
990
|
-
sourceRecordId:
|
|
991
|
-
type:
|
|
992
|
-
input:
|
|
990
|
+
sourceRecordId: record4.hubRecordId,
|
|
991
|
+
type: record4.type,
|
|
992
|
+
input: record4.input
|
|
993
993
|
};
|
|
994
994
|
}
|
|
995
|
-
if ("type" in
|
|
996
|
-
const input = { name:
|
|
995
|
+
if ("type" in record4 && record4.type === "company") {
|
|
996
|
+
const input = { name: record4.name };
|
|
997
997
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
998
|
-
if (
|
|
998
|
+
if (record4[key]) input[key] = record4[key];
|
|
999
999
|
}
|
|
1000
1000
|
return {
|
|
1001
1001
|
version: 1,
|
|
1002
1002
|
sourceId: fallbackSourceId,
|
|
1003
|
-
sourceRecordId:
|
|
1003
|
+
sourceRecordId: record4.hubRecordId,
|
|
1004
1004
|
type: "company",
|
|
1005
1005
|
input
|
|
1006
1006
|
};
|
|
@@ -1008,28 +1008,28 @@ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
|
1008
1008
|
return {
|
|
1009
1009
|
version: 1,
|
|
1010
1010
|
sourceId: fallbackSourceId,
|
|
1011
|
-
sourceRecordId:
|
|
1011
|
+
sourceRecordId: record4.hubRecordId,
|
|
1012
1012
|
type: "person",
|
|
1013
|
-
input: sharedPersonInput(
|
|
1013
|
+
input: sharedPersonInput(record4)
|
|
1014
1014
|
};
|
|
1015
1015
|
}
|
|
1016
|
-
function sharedRecordFromCrm(crm,
|
|
1017
|
-
if (target.fields && !target.fields[
|
|
1018
|
-
throw new Error(`${target.name} does not accept "${
|
|
1016
|
+
function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
1017
|
+
if (target.fields && !target.fields[record4.type]) {
|
|
1018
|
+
throw new Error(`${target.name} does not accept "${record4.type}" records`);
|
|
1019
1019
|
}
|
|
1020
|
-
const def = crm.type(
|
|
1021
|
-
const fields = target.fields?.[
|
|
1020
|
+
const def = crm.type(record4.type);
|
|
1021
|
+
const fields = target.fields?.[record4.type] ?? DEFAULT_SHARE_FIELDS[record4.type];
|
|
1022
1022
|
if (!fields) {
|
|
1023
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
1023
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record4.type}" records`);
|
|
1024
1024
|
}
|
|
1025
1025
|
const nameField = def.nameField ?? "name";
|
|
1026
1026
|
const input = {};
|
|
1027
1027
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
1028
|
-
const value =
|
|
1028
|
+
const value = record4.fields?.[field];
|
|
1029
1029
|
if (value !== void 0) input[field] = value;
|
|
1030
1030
|
}
|
|
1031
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
1032
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
1031
|
+
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
1032
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
1033
1033
|
}
|
|
1034
1034
|
async function upsertPerson(deps, opts) {
|
|
1035
1035
|
const email = opts.email.toLowerCase();
|
|
@@ -1043,11 +1043,11 @@ async function upsertPerson(deps, opts) {
|
|
|
1043
1043
|
const created = await createRecord(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1044
1044
|
return { recordId: created.id };
|
|
1045
1045
|
}
|
|
1046
|
-
async function findSharedRecord(deps,
|
|
1046
|
+
async function findSharedRecord(deps, record4, tag) {
|
|
1047
1047
|
const structured = await getRecordByOrigin(
|
|
1048
1048
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1049
|
-
|
|
1050
|
-
|
|
1049
|
+
record4.sourceId,
|
|
1050
|
+
record4.sourceRecordId
|
|
1051
1051
|
);
|
|
1052
1052
|
if (structured) return structured;
|
|
1053
1053
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1056,53 +1056,53 @@ async function findSharedRecord(deps, record3, tag) {
|
|
|
1056
1056
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1057
1057
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1058
1058
|
}
|
|
1059
|
-
const def = deps.crm.type(
|
|
1059
|
+
const def = deps.crm.type(record4.type);
|
|
1060
1060
|
const emailField = def.emailField;
|
|
1061
|
-
if (emailField && typeof
|
|
1062
|
-
const primaryEmail =
|
|
1061
|
+
if (emailField && typeof record4.input[emailField] === "string") {
|
|
1062
|
+
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
1063
1063
|
const found = await deps.db.query({
|
|
1064
|
-
crm_record: { $: { where: { type:
|
|
1064
|
+
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
1065
1065
|
});
|
|
1066
1066
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1067
1067
|
}
|
|
1068
|
-
const domain =
|
|
1068
|
+
const domain = record4.input.domain;
|
|
1069
1069
|
const domainSlot = def.fields.domain?.slot;
|
|
1070
1070
|
if (typeof domain === "string" && domainSlot) {
|
|
1071
1071
|
const found = await deps.db.query({
|
|
1072
|
-
crm_record: { $: { where: { type:
|
|
1072
|
+
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
1073
1073
|
});
|
|
1074
1074
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1075
1075
|
}
|
|
1076
1076
|
const nameField = def.nameField ?? "name";
|
|
1077
|
-
const name =
|
|
1078
|
-
if (
|
|
1077
|
+
const name = record4.input[nameField];
|
|
1078
|
+
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
1079
1079
|
const found = await deps.db.query({
|
|
1080
|
-
crm_record: { $: { where: { type:
|
|
1080
|
+
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
1081
1081
|
});
|
|
1082
1082
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1083
1083
|
}
|
|
1084
1084
|
return void 0;
|
|
1085
1085
|
}
|
|
1086
1086
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1087
|
-
const
|
|
1088
|
-
if (!
|
|
1087
|
+
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
1088
|
+
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
1089
1089
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1090
1090
|
}
|
|
1091
|
-
if (options.sourceId && options.sourceId !==
|
|
1091
|
+
if (options.sourceId && options.sourceId !== record4.sourceId) {
|
|
1092
1092
|
throw new Error("signed sender does not match payload source");
|
|
1093
1093
|
}
|
|
1094
|
-
const tag = networkSourceTag(
|
|
1094
|
+
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
1095
1095
|
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1096
|
-
const existing = await findSharedRecord(deps,
|
|
1096
|
+
const existing = await findSharedRecord(deps, record4, tag);
|
|
1097
1097
|
let recordId;
|
|
1098
1098
|
if (existing && typeof existing.id === "string") {
|
|
1099
|
-
await updateRecord(crmDeps3, { id: existing.id, input:
|
|
1099
|
+
await updateRecord(crmDeps3, { id: existing.id, input: record4.input });
|
|
1100
1100
|
recordId = existing.id;
|
|
1101
1101
|
} else {
|
|
1102
|
-
recordId = `network_${shortHash(`${
|
|
1102
|
+
recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
|
|
1103
1103
|
await createRecord({ ...crmDeps3, newId: () => recordId }, {
|
|
1104
|
-
type:
|
|
1105
|
-
input:
|
|
1104
|
+
type: record4.type,
|
|
1105
|
+
input: record4.input,
|
|
1106
1106
|
mutationId: `share-create:${tag}`
|
|
1107
1107
|
});
|
|
1108
1108
|
}
|
|
@@ -1112,10 +1112,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1112
1112
|
);
|
|
1113
1113
|
await upsertRecordOrigin(crmDeps3, {
|
|
1114
1114
|
recordId,
|
|
1115
|
-
sourceId:
|
|
1116
|
-
sourceRecordId:
|
|
1115
|
+
sourceId: record4.sourceId,
|
|
1116
|
+
sourceRecordId: record4.sourceRecordId,
|
|
1117
1117
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1118
|
-
payloadVersion:
|
|
1118
|
+
payloadVersion: record4.version
|
|
1119
1119
|
});
|
|
1120
1120
|
return { recordId };
|
|
1121
1121
|
}
|
|
@@ -1254,10 +1254,10 @@ function planDelivery(input) {
|
|
|
1254
1254
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1255
1255
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1256
1256
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1257
|
-
const
|
|
1257
|
+
const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1258
1258
|
|
|
1259
1259
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1260
|
-
return { deliver: true, transport, to, subject, text:
|
|
1260
|
+
return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
|
|
1261
1261
|
}
|
|
1262
1262
|
|
|
1263
1263
|
// src/notify.ts
|
|
@@ -2040,8 +2040,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
2040
2040
|
payload,
|
|
2041
2041
|
sourceId ? { sourceId } : {}
|
|
2042
2042
|
);
|
|
2043
|
-
const
|
|
2044
|
-
return json({ recordId, type:
|
|
2043
|
+
const record4 = normalizeSharedRecord(payload, sourceId);
|
|
2044
|
+
return json({ recordId, type: record4.type });
|
|
2045
2045
|
} catch (error) {
|
|
2046
2046
|
return json({
|
|
2047
2047
|
error: error instanceof Error ? error.message : "invalid shared record"
|
|
@@ -2095,10 +2095,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2095
2095
|
errors.push(`${path} must be a string`);
|
|
2096
2096
|
return void 0;
|
|
2097
2097
|
}
|
|
2098
|
-
const
|
|
2099
|
-
if (required && !
|
|
2100
|
-
if (
|
|
2101
|
-
return
|
|
2098
|
+
const text4 = value.trim();
|
|
2099
|
+
if (required && !text4) errors.push(`${path} is required`);
|
|
2100
|
+
if (text4.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2101
|
+
return text4;
|
|
2102
2102
|
}
|
|
2103
2103
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2104
2104
|
const errors = [];
|
|
@@ -2147,8 +2147,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2147
2147
|
else {
|
|
2148
2148
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2149
2149
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2150
|
-
const
|
|
2151
|
-
return
|
|
2150
|
+
const text4 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2151
|
+
return text4 ? [text4] : [];
|
|
2152
2152
|
});
|
|
2153
2153
|
}
|
|
2154
2154
|
}
|
|
@@ -2478,9 +2478,9 @@ function canonicalValue2(value) {
|
|
|
2478
2478
|
}
|
|
2479
2479
|
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2480
2480
|
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2481
|
-
const
|
|
2482
|
-
const entries = Object.keys(
|
|
2483
|
-
const item =
|
|
2481
|
+
const record4 = value;
|
|
2482
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
2483
|
+
const item = record4[key];
|
|
2484
2484
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2485
2485
|
});
|
|
2486
2486
|
return `{${entries.join(",")}}`;
|
|
@@ -2910,9 +2910,9 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
2910
2910
|
tiers
|
|
2911
2911
|
});
|
|
2912
2912
|
if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
|
|
2913
|
-
const
|
|
2914
|
-
if (
|
|
2915
|
-
const localCheck = validateMemberContentDocument(
|
|
2913
|
+
const local3 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2914
|
+
if (local3) {
|
|
2915
|
+
const localCheck = validateMemberContentDocument(local3.document, {
|
|
2916
2916
|
sourceType: "chapter",
|
|
2917
2917
|
sourceAppId: ctx.chapter.id,
|
|
2918
2918
|
tiers,
|
|
@@ -3346,9 +3346,21 @@ function webhookMutationId(eventId) {
|
|
|
3346
3346
|
function findApplicationRef(obj) {
|
|
3347
3347
|
const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
|
|
3348
3348
|
const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
|
|
3349
|
-
const
|
|
3349
|
+
const candidates = [
|
|
3350
|
+
metaOf(obj),
|
|
3351
|
+
metaOf(obj.subscription_details),
|
|
3352
|
+
metaOf(obj.parent?.subscription_details)
|
|
3353
|
+
];
|
|
3354
|
+
const applicationId = candidates.map(pick).find(Boolean);
|
|
3355
|
+
const appId = candidates.map((metadata) => typeof metadata.odlaAppId === "string" ? metadata.odlaAppId : void 0).find(Boolean);
|
|
3356
|
+
const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
|
|
3350
3357
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
3351
|
-
return {
|
|
3358
|
+
return {
|
|
3359
|
+
...applicationId ? { applicationId } : {},
|
|
3360
|
+
...customerId ? { customerId } : {},
|
|
3361
|
+
...appId ? { appId } : {},
|
|
3362
|
+
...environment ? { environment } : {}
|
|
3363
|
+
};
|
|
3352
3364
|
}
|
|
3353
3365
|
function findGiftRef(obj) {
|
|
3354
3366
|
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
@@ -3439,7 +3451,13 @@ function createStripeProvider(config) {
|
|
|
3439
3451
|
return {
|
|
3440
3452
|
async createSubscription(input) {
|
|
3441
3453
|
const sk = requireSecret(secretKey);
|
|
3442
|
-
const meta = {
|
|
3454
|
+
const meta = {
|
|
3455
|
+
applicationId: input.applicationId,
|
|
3456
|
+
groupId: input.groupId,
|
|
3457
|
+
email: input.email,
|
|
3458
|
+
odlaAppId: input.appId,
|
|
3459
|
+
odlaEnvironment: input.environment
|
|
3460
|
+
};
|
|
3443
3461
|
let customerId = input.existingCustomerId;
|
|
3444
3462
|
if (!customerId) {
|
|
3445
3463
|
const cust = await stripeCall(
|
|
@@ -3918,8 +3936,8 @@ async function startSubscription(req, env, ctx) {
|
|
|
3918
3936
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
3919
3937
|
if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
|
|
3920
3938
|
if (typeof app.giftOfferId === "string") {
|
|
3921
|
-
const
|
|
3922
|
-
const offer = findGiftOffer(
|
|
3939
|
+
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
3940
|
+
const offer = findGiftOffer(control3?.document.giftOffers, app.giftOfferId, Date.now());
|
|
3923
3941
|
if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
|
|
3924
3942
|
return json({ error: "gift offer is unavailable" }, 409);
|
|
3925
3943
|
}
|
|
@@ -3959,6 +3977,8 @@ async function startSubscription(req, env, ctx) {
|
|
|
3959
3977
|
let result;
|
|
3960
3978
|
try {
|
|
3961
3979
|
result = await provider.createSubscription({
|
|
3980
|
+
appId: env.ODLA_APP_ID,
|
|
3981
|
+
environment: env.ODLA_ENV,
|
|
3962
3982
|
applicationId,
|
|
3963
3983
|
groupId: String(group.id),
|
|
3964
3984
|
email: String(app.email ?? ""),
|
|
@@ -3988,6 +4008,10 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
3988
4008
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
3989
4009
|
const { eventId, event } = ingest;
|
|
3990
4010
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4011
|
+
const identifiedEnvironment = event.appId !== void 0 || event.environment !== void 0;
|
|
4012
|
+
if (env.ODLA_APP_ID && identifiedEnvironment && (event.appId !== env.ODLA_APP_ID || event.environment !== env.ODLA_ENV)) {
|
|
4013
|
+
return json({ ok: true, matched: false });
|
|
4014
|
+
}
|
|
3991
4015
|
if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
|
|
3992
4016
|
const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id: event.giftIntentId }, limit: 1 });
|
|
3993
4017
|
if (!intent || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV) {
|
|
@@ -4078,14 +4102,14 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4078
4102
|
if (req.method === "GET" && url.pathname === "/api/gifts") {
|
|
4079
4103
|
const user = await userFor(req, env, ctx);
|
|
4080
4104
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4081
|
-
const
|
|
4105
|
+
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4082
4106
|
const found = await db.query({ giftClaims: { $: { where: {
|
|
4083
4107
|
purchaserEmailNormalized: normalizeChapterEmail(user.email),
|
|
4084
4108
|
chapterId: ctx.chapter.id
|
|
4085
4109
|
}, limit: 200 } } });
|
|
4086
4110
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4087
4111
|
return json({
|
|
4088
|
-
offers: publicGiftOffers(
|
|
4112
|
+
offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
|
|
4089
4113
|
claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
|
|
4090
4114
|
refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
|
|
4091
4115
|
});
|
|
@@ -4101,8 +4125,8 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4101
4125
|
}
|
|
4102
4126
|
const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
|
|
4103
4127
|
if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
|
|
4104
|
-
const
|
|
4105
|
-
const offer = findGiftOffer(
|
|
4128
|
+
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4129
|
+
const offer = findGiftOffer(control3?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
|
|
4106
4130
|
if (!offer) return json({ error: "gift offer is unavailable" }, 409);
|
|
4107
4131
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4108
4132
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
@@ -4934,11 +4958,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
4934
4958
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
4935
4959
|
const t = templates[key];
|
|
4936
4960
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
4937
|
-
const
|
|
4938
|
-
if (!subject || !
|
|
4961
|
+
const text4 = typeof t?.text === "string" ? t.text : "";
|
|
4962
|
+
if (!subject || !text4.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
4939
4963
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
4940
|
-
if (
|
|
4941
|
-
clean[key] = { subject, text:
|
|
4964
|
+
if (text4.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
4965
|
+
clean[key] = { subject, text: text4, enabled: t?.enabled !== false };
|
|
4942
4966
|
}
|
|
4943
4967
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4944
4968
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -5058,7 +5082,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
5058
5082
|
// src/worker-network-push.ts
|
|
5059
5083
|
import { recordDeliveryAttempt } from "@odla-ai/crm";
|
|
5060
5084
|
import { signFederatedRequest } from "@odla-ai/db";
|
|
5061
|
-
async function pushNetworkRecord(db, env, ctx, target,
|
|
5085
|
+
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
5062
5086
|
const secret = await getVaultSecret(db, target.secretName);
|
|
5063
5087
|
const crmDeps3 = {
|
|
5064
5088
|
crm: ctx.chapter.crm,
|
|
@@ -5069,7 +5093,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5069
5093
|
if (!secret) {
|
|
5070
5094
|
const error = `vault secret "${target.secretName}" is missing`;
|
|
5071
5095
|
await recordDeliveryAttempt(crmDeps3, {
|
|
5072
|
-
recordId:
|
|
5096
|
+
recordId: record4.id,
|
|
5073
5097
|
targetId: target.id,
|
|
5074
5098
|
status: "failed",
|
|
5075
5099
|
payloadVersion: 2,
|
|
@@ -5079,7 +5103,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5079
5103
|
}
|
|
5080
5104
|
let payload;
|
|
5081
5105
|
try {
|
|
5082
|
-
payload = sharedRecordFromCrm(ctx.chapter.crm,
|
|
5106
|
+
payload = sharedRecordFromCrm(ctx.chapter.crm, record4, target, ctx.chapter.id);
|
|
5083
5107
|
} catch (err) {
|
|
5084
5108
|
return {
|
|
5085
5109
|
id: target.id,
|
|
@@ -5108,7 +5132,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5108
5132
|
if (!res.ok) {
|
|
5109
5133
|
const error = responseBody.error ?? "follower rejected the record";
|
|
5110
5134
|
await recordDeliveryAttempt(crmDeps3, {
|
|
5111
|
-
recordId:
|
|
5135
|
+
recordId: record4.id,
|
|
5112
5136
|
targetId: target.id,
|
|
5113
5137
|
status: "failed",
|
|
5114
5138
|
payloadVersion: 2,
|
|
@@ -5123,7 +5147,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5123
5147
|
};
|
|
5124
5148
|
}
|
|
5125
5149
|
await recordDeliveryAttempt(crmDeps3, {
|
|
5126
|
-
recordId:
|
|
5150
|
+
recordId: record4.id,
|
|
5127
5151
|
targetId: target.id,
|
|
5128
5152
|
status: "delivered",
|
|
5129
5153
|
payloadVersion: 2,
|
|
@@ -5139,7 +5163,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5139
5163
|
} catch (err) {
|
|
5140
5164
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
5141
5165
|
await recordDeliveryAttempt(crmDeps3, {
|
|
5142
|
-
recordId:
|
|
5166
|
+
recordId: record4.id,
|
|
5143
5167
|
targetId: target.id,
|
|
5144
5168
|
status: "failed",
|
|
5145
5169
|
payloadVersion: 2,
|
|
@@ -5277,10 +5301,10 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5277
5301
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
5278
5302
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
5279
5303
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
5280
|
-
const
|
|
5281
|
-
if (!
|
|
5304
|
+
const record4 = await getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
5305
|
+
if (!record4) return json({ error: "record not found" }, 404);
|
|
5282
5306
|
const results = await Promise.all(
|
|
5283
|
-
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target,
|
|
5307
|
+
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
5284
5308
|
);
|
|
5285
5309
|
return json({ ok: results.every((result) => result.ok), results });
|
|
5286
5310
|
};
|
|
@@ -5305,18 +5329,18 @@ function integer(value, fallback, min, max) {
|
|
|
5305
5329
|
const parsed = Number(value);
|
|
5306
5330
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
5307
5331
|
}
|
|
5308
|
-
function projectRecord(
|
|
5332
|
+
function projectRecord(record4, allowed2) {
|
|
5309
5333
|
const fields = Object.fromEntries(
|
|
5310
|
-
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(
|
|
5334
|
+
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
5311
5335
|
);
|
|
5312
5336
|
return {
|
|
5313
|
-
id:
|
|
5314
|
-
type:
|
|
5315
|
-
name:
|
|
5316
|
-
...
|
|
5337
|
+
id: record4.id,
|
|
5338
|
+
type: record4.type,
|
|
5339
|
+
name: record4.name,
|
|
5340
|
+
...record4.stage ? { stage: record4.stage } : {},
|
|
5317
5341
|
fields,
|
|
5318
|
-
createdAt:
|
|
5319
|
-
updatedAt:
|
|
5342
|
+
createdAt: record4.createdAt,
|
|
5343
|
+
updatedAt: record4.updatedAt
|
|
5320
5344
|
};
|
|
5321
5345
|
}
|
|
5322
5346
|
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
@@ -5354,7 +5378,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5354
5378
|
version: 1,
|
|
5355
5379
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5356
5380
|
type,
|
|
5357
|
-
records: page.records.map((
|
|
5381
|
+
records: page.records.map((record4) => projectRecord(record4, allowed2)),
|
|
5358
5382
|
total: page.total,
|
|
5359
5383
|
limit: page.limit,
|
|
5360
5384
|
offset: page.offset
|
|
@@ -5579,8 +5603,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5579
5603
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
5580
5604
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
5581
5605
|
}
|
|
5582
|
-
const
|
|
5583
|
-
if (!
|
|
5606
|
+
const record4 = await getRecord2({ crm: ctx.chapter.crm, db }, recordId);
|
|
5607
|
+
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
5584
5608
|
if (req.method === "GET") {
|
|
5585
5609
|
return json({
|
|
5586
5610
|
version: 1,
|
|
@@ -5771,6 +5795,467 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
5771
5795
|
}
|
|
5772
5796
|
};
|
|
5773
5797
|
|
|
5798
|
+
// src/worker-routes-builder-library.ts
|
|
5799
|
+
import { verifyFederatedRequest as verifyFederatedRequest8 } from "@odla-ai/db";
|
|
5800
|
+
|
|
5801
|
+
// src/builder-library-validation.ts
|
|
5802
|
+
var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5803
|
+
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
5804
|
+
var MAX_RESOURCES = 200;
|
|
5805
|
+
var MAX_MARKDOWN3 = 65536;
|
|
5806
|
+
var MAX_TEXT2 = 500;
|
|
5807
|
+
var MAX_TAGS = 20;
|
|
5808
|
+
var KINDS = /* @__PURE__ */ new Set(["document", "note", "link", "inspiration", "image", "file"]);
|
|
5809
|
+
var STATUSES = /* @__PURE__ */ new Set(["draft", "published", "archived"]);
|
|
5810
|
+
var ROOT_KEYS4 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "resources"]);
|
|
5811
|
+
var SOURCE_KEYS2 = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
5812
|
+
var TARGET_KEYS2 = /* @__PURE__ */ new Set(["appId", "environment"]);
|
|
5813
|
+
var RESOURCE_KEYS = /* @__PURE__ */ new Set(["id", "kind", "title", "summary", "tags", "scope", "status", "revision", "bodyMarkdown", "url", "brandAsset", "provenance"]);
|
|
5814
|
+
var SCOPE_KEYS = /* @__PURE__ */ new Set(["kind", "chapterId"]);
|
|
5815
|
+
var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
|
|
5816
|
+
var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
|
|
5817
|
+
var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
|
|
5818
|
+
var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5819
|
+
function unknownKeys3(value, allowed2, path, errors) {
|
|
5820
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
5821
|
+
}
|
|
5822
|
+
function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
5823
|
+
if (value === void 0 && !required) return void 0;
|
|
5824
|
+
if (typeof value !== "string" || required && !value.trim()) {
|
|
5825
|
+
errors.push(`${path} must be a non-empty string`);
|
|
5826
|
+
return void 0;
|
|
5827
|
+
}
|
|
5828
|
+
const result = value.trim();
|
|
5829
|
+
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
5830
|
+
return result;
|
|
5831
|
+
}
|
|
5832
|
+
function integer2(value, path, errors, minimum = 1) {
|
|
5833
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
5834
|
+
return Number(value);
|
|
5835
|
+
}
|
|
5836
|
+
function scopeValue(value, path, errors) {
|
|
5837
|
+
const raw = record3(value);
|
|
5838
|
+
if (!raw) {
|
|
5839
|
+
errors.push(`${path} must be an object`);
|
|
5840
|
+
return null;
|
|
5841
|
+
}
|
|
5842
|
+
unknownKeys3(raw, SCOPE_KEYS, path, errors);
|
|
5843
|
+
if (raw.kind === "global" || raw.kind === "built_not_found") {
|
|
5844
|
+
if (raw.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
5845
|
+
return { kind: raw.kind };
|
|
5846
|
+
}
|
|
5847
|
+
if (raw.kind === "chapter") {
|
|
5848
|
+
const chapterId = text3(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
5849
|
+
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
5850
|
+
return { kind: "chapter", chapterId };
|
|
5851
|
+
}
|
|
5852
|
+
errors.push(`${path}.kind must be chapter, built_not_found, or global`);
|
|
5853
|
+
return null;
|
|
5854
|
+
}
|
|
5855
|
+
function resourceValue(value, path, errors) {
|
|
5856
|
+
const raw = record3(value);
|
|
5857
|
+
if (!raw) {
|
|
5858
|
+
errors.push(`${path} must be an object`);
|
|
5859
|
+
return null;
|
|
5860
|
+
}
|
|
5861
|
+
unknownKeys3(raw, RESOURCE_KEYS, path, errors);
|
|
5862
|
+
const id = text3(raw.id, `${path}.id`, errors, 80) ?? "";
|
|
5863
|
+
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
5864
|
+
const kind = text3(raw.kind, `${path}.kind`, errors) ?? "";
|
|
5865
|
+
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
5866
|
+
const title = text3(raw.title, `${path}.title`, errors, 240) ?? "";
|
|
5867
|
+
const summary = text3(raw.summary, `${path}.summary`, errors, 1e3, false);
|
|
5868
|
+
const status = text3(raw.status, `${path}.status`, errors) ?? "";
|
|
5869
|
+
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
5870
|
+
const revision = integer2(raw.revision, `${path}.revision`, errors);
|
|
5871
|
+
const scope = scopeValue(raw.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
5872
|
+
const tags = Array.isArray(raw.tags) ? raw.tags.map((tag, index) => text3(tag, `${path}.tags[${index}]`, errors, 80) ?? "") : [];
|
|
5873
|
+
if (!Array.isArray(raw.tags)) errors.push(`${path}.tags must be an array`);
|
|
5874
|
+
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
5875
|
+
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
5876
|
+
const bodyMarkdown = text3(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
5877
|
+
const url = text3(raw.url, `${path}.url`, errors, 2048, false);
|
|
5878
|
+
if (url) {
|
|
5879
|
+
try {
|
|
5880
|
+
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
5881
|
+
} catch {
|
|
5882
|
+
errors.push(`${path}.url must be a valid https URL`);
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
const asset = raw.brandAsset === void 0 ? null : record3(raw.brandAsset);
|
|
5886
|
+
if (raw.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
5887
|
+
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
5888
|
+
const brandAsset = asset ? {
|
|
5889
|
+
bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
5890
|
+
assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
5891
|
+
...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
5892
|
+
} : void 0;
|
|
5893
|
+
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
5894
|
+
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
5895
|
+
if ((kind === "image" || kind === "file") && !brandAsset) errors.push(`${path}.brandAsset is required for ${kind}`);
|
|
5896
|
+
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
5897
|
+
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
5898
|
+
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
5899
|
+
const provenanceRaw = record3(raw.provenance);
|
|
5900
|
+
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
5901
|
+
if (provenanceRaw) unknownKeys3(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
5902
|
+
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record3(provenanceRaw.promotedFrom);
|
|
5903
|
+
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
5904
|
+
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
5905
|
+
const promotedFrom = promotedRaw ? {
|
|
5906
|
+
chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
5907
|
+
resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
5908
|
+
resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
5909
|
+
documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
5910
|
+
promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
5911
|
+
promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
5912
|
+
} : void 0;
|
|
5913
|
+
const provenance = {
|
|
5914
|
+
createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
5915
|
+
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
5916
|
+
updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
5917
|
+
updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
5918
|
+
...promotedFrom ? { promotedFrom } : {}
|
|
5919
|
+
};
|
|
5920
|
+
return { id, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
5921
|
+
}
|
|
5922
|
+
function validateBuilderLibraryDocument(value, options = {}) {
|
|
5923
|
+
const errors = [];
|
|
5924
|
+
const raw = record3(value);
|
|
5925
|
+
if (!raw) return { ok: false, errors: ["document must be an object"] };
|
|
5926
|
+
unknownKeys3(raw, ROOT_KEYS4, "document", errors);
|
|
5927
|
+
if (raw.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
5928
|
+
const revision = integer2(raw.revision, "document.revision", errors);
|
|
5929
|
+
const generatedAt = integer2(raw.generatedAt, "document.generatedAt", errors, 0);
|
|
5930
|
+
const sourceRaw = record3(raw.source);
|
|
5931
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
5932
|
+
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
5933
|
+
const sourceType = sourceRaw?.type;
|
|
5934
|
+
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
5935
|
+
const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
5936
|
+
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
5937
|
+
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
5938
|
+
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
5939
|
+
const targetRaw = raw.target === void 0 ? null : record3(raw.target);
|
|
5940
|
+
if (raw.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
5941
|
+
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
5942
|
+
const target = targetRaw ? {
|
|
5943
|
+
appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
5944
|
+
environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
5945
|
+
} : void 0;
|
|
5946
|
+
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
5947
|
+
if (!Array.isArray(raw.resources)) errors.push("document.resources must be an array");
|
|
5948
|
+
const inputs = Array.isArray(raw.resources) ? raw.resources : [];
|
|
5949
|
+
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
5950
|
+
const resources = inputs.map((item, index) => resourceValue(item, `document.resources[${index}]`, errors)).filter(Boolean);
|
|
5951
|
+
const ids = resources.map(({ id }) => id);
|
|
5952
|
+
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
5953
|
+
for (const resource of resources) {
|
|
5954
|
+
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
5955
|
+
errors.push(`document.resources.${resource.id}: Chapter sources may author only their own chapter scope`);
|
|
5956
|
+
}
|
|
5957
|
+
if (sourceType === "built_not_found" && resource.scope.kind === "chapter") {
|
|
5958
|
+
errors.push(`document.resources.${resource.id}: Built Not Found sources may author only built_not_found or global scope`);
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
if (errors.length) return { ok: false, errors };
|
|
5962
|
+
return { ok: true, value: { schemaVersion: 1, revision, generatedAt, source: { type: sourceType, appId: sourceAppId }, ...target ? { target } : {}, resources } };
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5965
|
+
// src/builder-library.ts
|
|
5966
|
+
var BUILDER_LIBRARY_SCHEMA_VERSION = 1;
|
|
5967
|
+
var BUILDER_LIBRARY_ADMIN_PATH = "/api/admin/builder-library";
|
|
5968
|
+
var BUILDER_LIBRARY_MANIFEST_PATH = "/api/admin/builder-library/manifest";
|
|
5969
|
+
var BUILDER_LIBRARY_PROMOTE_PATH = "/api/admin/builder-library/promote";
|
|
5970
|
+
var BUILDER_LIBRARY_CONTROL_PATH = "/api/builder-library-control";
|
|
5971
|
+
function canViewBuilderLibraryResource(resource, viewer) {
|
|
5972
|
+
if (viewer.kind === "built_not_found") return true;
|
|
5973
|
+
return resource.scope.kind === "global" || resource.scope.kind === "chapter" && resource.scope.chapterId === viewer.chapterId;
|
|
5974
|
+
}
|
|
5975
|
+
function buildBuilderLibraryManifest(documents, viewer, generatedAt = Date.now(), limit = 100) {
|
|
5976
|
+
const ordered = documents.flatMap((document) => document.resources.map((resource) => ({ document, resource }))).filter(({ resource }) => resource.status === "published" && canViewBuilderLibraryResource(resource, viewer)).sort((a, b) => a.resource.title.localeCompare(b.resource.title) || a.document.source.appId.localeCompare(b.document.source.appId) || a.resource.id.localeCompare(b.resource.id));
|
|
5977
|
+
return {
|
|
5978
|
+
schemaVersion: BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
5979
|
+
generatedAt,
|
|
5980
|
+
viewer,
|
|
5981
|
+
revisions: documents.map(({ source, revision }) => ({ source, revision })),
|
|
5982
|
+
resources: ordered.slice(0, limit).map(({ resource }) => resource),
|
|
5983
|
+
truncated: ordered.length > limit
|
|
5984
|
+
};
|
|
5985
|
+
}
|
|
5986
|
+
function promoteBuilderLibraryResource(input) {
|
|
5987
|
+
if (input.resource.scope.kind !== "chapter") throw new TypeError("only a Chapter-scoped resource can be promoted");
|
|
5988
|
+
const chapterId = input.resource.scope.chapterId;
|
|
5989
|
+
return {
|
|
5990
|
+
...input.resource,
|
|
5991
|
+
id: input.globalId ?? `${chapterId}-${input.resource.id}`,
|
|
5992
|
+
scope: { kind: "global" },
|
|
5993
|
+
revision: 1,
|
|
5994
|
+
provenance: {
|
|
5995
|
+
createdBy: input.actor,
|
|
5996
|
+
createdAt: input.promotedAt,
|
|
5997
|
+
updatedBy: input.actor,
|
|
5998
|
+
updatedAt: input.promotedAt,
|
|
5999
|
+
promotedFrom: {
|
|
6000
|
+
chapterId,
|
|
6001
|
+
resourceId: input.resource.id,
|
|
6002
|
+
resourceRevision: input.resource.revision,
|
|
6003
|
+
documentRevision: input.chapterDocumentRevision,
|
|
6004
|
+
promotedBy: input.actor,
|
|
6005
|
+
promotedAt: input.promotedAt
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
};
|
|
6009
|
+
}
|
|
6010
|
+
|
|
6011
|
+
// src/builder-library-canonical.ts
|
|
6012
|
+
var encoder3 = new TextEncoder();
|
|
6013
|
+
function canonicalValue3(value) {
|
|
6014
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return JSON.stringify(value);
|
|
6015
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue3).join(",")}]`;
|
|
6016
|
+
if (typeof value !== "object") throw new TypeError("Builder Library contains an unsupported value");
|
|
6017
|
+
const record4 = value;
|
|
6018
|
+
return `{${Object.keys(record4).sort().flatMap((key) => record4[key] === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue3(record4[key])}`]).join(",")}}`;
|
|
6019
|
+
}
|
|
6020
|
+
function canonicalBuilderLibrary(value) {
|
|
6021
|
+
return canonicalValue3(value);
|
|
6022
|
+
}
|
|
6023
|
+
async function builderLibraryDigest(document) {
|
|
6024
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalBuilderLibrary(document)));
|
|
6025
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
// src/builder-library-store.ts
|
|
6029
|
+
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
6030
|
+
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
6031
|
+
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
6032
|
+
const rows4 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
6033
|
+
return Array.isArray(rows4) ? rows4.map(control2).filter(Boolean) : [];
|
|
6034
|
+
}
|
|
6035
|
+
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
6036
|
+
const rows4 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
6037
|
+
return control2(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
6038
|
+
}
|
|
6039
|
+
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
6040
|
+
const id = `builder-library-local:${environment}`;
|
|
6041
|
+
const rows4 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
6042
|
+
return local2(Array.isArray(rows4) ? rows4[0] : void 0);
|
|
6043
|
+
}
|
|
6044
|
+
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
6045
|
+
const { document, digest } = envelope;
|
|
6046
|
+
const target = document.target;
|
|
6047
|
+
const id = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
6048
|
+
const ops = [];
|
|
6049
|
+
if (previous?.active) ops.push({ t: "update", ns: "builderLibraryControls", id: previous.id, attrs: { active: false } });
|
|
6050
|
+
ops.push({ t: "update", ns: "builderLibraryControls", id, attrs: {
|
|
6051
|
+
id,
|
|
6052
|
+
sourceId: document.source.appId,
|
|
6053
|
+
targetId: target.appId,
|
|
6054
|
+
environment: target.environment,
|
|
6055
|
+
revision: document.revision,
|
|
6056
|
+
digest,
|
|
6057
|
+
document,
|
|
6058
|
+
active: true,
|
|
6059
|
+
receivedAt
|
|
6060
|
+
} });
|
|
6061
|
+
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest}` });
|
|
6062
|
+
}
|
|
6063
|
+
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
6064
|
+
const id = `builder-library-local:${environment}`;
|
|
6065
|
+
const current = await loadLocalBuilderLibrary(db, document.source.appId, environment);
|
|
6066
|
+
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("builder_library_revision_conflict");
|
|
6067
|
+
const row = { id, appId: document.source.appId, environment, revision: document.revision, document, updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
6068
|
+
await db.transact([{ t: "update", ns: "builderLibraryLocal", id, attrs: row }], {
|
|
6069
|
+
mutationId: `builder-library-local:${environment}:${document.revision}`,
|
|
6070
|
+
guards: current ? [{ ns: "builderLibraryLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "builderLibraryLocal", id, exists: false }]
|
|
6071
|
+
});
|
|
6072
|
+
return row;
|
|
6073
|
+
}
|
|
6074
|
+
|
|
6075
|
+
// src/worker-routes-builder-library.ts
|
|
6076
|
+
var BODY_CAP2 = 1e6;
|
|
6077
|
+
async function verifySource(req, db, sources) {
|
|
6078
|
+
for (const source of sources) {
|
|
6079
|
+
const secret = await getVaultSecret(db, source.secretName);
|
|
6080
|
+
if (!secret) continue;
|
|
6081
|
+
const verified = await verifyFederatedRequest8(req.clone(), { secret, senders: [source.sourceId] });
|
|
6082
|
+
if (verified.ok) return { source, body: verified.body };
|
|
6083
|
+
}
|
|
6084
|
+
return null;
|
|
6085
|
+
}
|
|
6086
|
+
var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
6087
|
+
if (req.method !== "POST" || url.pathname !== BUILDER_LIBRARY_CONTROL_PATH) return null;
|
|
6088
|
+
const settings = ctx.chapter.builderLibrary;
|
|
6089
|
+
if (!settings) return json({ error: "not found" }, 404);
|
|
6090
|
+
const db = ctx.makeDb(env);
|
|
6091
|
+
const verified = await verifySource(req, db, settings.sources);
|
|
6092
|
+
if (!verified) return json({ error: "unauthorized" }, 401);
|
|
6093
|
+
let input;
|
|
6094
|
+
try {
|
|
6095
|
+
input = JSON.parse(verified.body);
|
|
6096
|
+
} catch {
|
|
6097
|
+
return json({ ok: false, reason: "invalid_document", errors: ["request body must be JSON"] }, 400);
|
|
6098
|
+
}
|
|
6099
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
6100
|
+
const sourceType = ctx.chapter.mode === "hub" ? "chapter" : "built_not_found";
|
|
6101
|
+
const checked = validateBuilderLibraryDocument(input.document, {
|
|
6102
|
+
sourceType,
|
|
6103
|
+
sourceAppId: verified.source.sourceId,
|
|
6104
|
+
targetAppId: ctx.chapter.id
|
|
6105
|
+
});
|
|
6106
|
+
if (!checked.ok) return json({ ok: false, reason: "invalid_document", errors: checked.errors }, 400);
|
|
6107
|
+
const document = checked.value;
|
|
6108
|
+
if (ctx.chapter.mode === "chapter" && document.resources.some((resource) => resource.scope.kind !== "global")) {
|
|
6109
|
+
return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global resources to a Chapter"] }, 400);
|
|
6110
|
+
}
|
|
6111
|
+
if (!document.target || document.target.environment !== env.ODLA_ENV) return json({ ok: false, reason: "wrong_environment" }, 409);
|
|
6112
|
+
if (!digest || await builderLibraryDigest(document) !== digest) return json({ ok: false, reason: "invalid_digest" }, 400);
|
|
6113
|
+
const latest = await loadLatestBuilderLibraryControl(db, document.source.appId, ctx.chapter.id, env.ODLA_ENV);
|
|
6114
|
+
if (latest && document.revision < latest.revision) return json({ ok: false, reason: "stale_revision" }, 409);
|
|
6115
|
+
if (latest && document.revision === latest.revision) {
|
|
6116
|
+
if (latest.digest !== digest) return json({ ok: false, reason: "conflicting_revision" }, 409);
|
|
6117
|
+
return json({ ok: true, target: document.target, revision: latest.revision, digest: latest.digest, status: "replayed", acknowledgedAt: latest.receivedAt });
|
|
6118
|
+
}
|
|
6119
|
+
const receivedAt = Date.now();
|
|
6120
|
+
try {
|
|
6121
|
+
await materializeBuilderLibraryControl(db, { document, digest }, latest, receivedAt);
|
|
6122
|
+
} catch {
|
|
6123
|
+
return json({ ok: false, reason: "materialization_failed" }, 503);
|
|
6124
|
+
}
|
|
6125
|
+
return json({ ok: true, target: document.target, revision: document.revision, digest, status: "accepted", acknowledgedAt: receivedAt });
|
|
6126
|
+
};
|
|
6127
|
+
function actorFor(user) {
|
|
6128
|
+
return user.email ?? user.userId;
|
|
6129
|
+
}
|
|
6130
|
+
function withLocalAuthority2(value, mode, appId, revision, now, actor, current) {
|
|
6131
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
6132
|
+
const raw = value;
|
|
6133
|
+
const currentById = new Map((current?.resources ?? []).map((item) => [item.id, item]));
|
|
6134
|
+
const resources = Array.isArray(raw.resources) ? raw.resources.map((input) => {
|
|
6135
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
6136
|
+
const item = input;
|
|
6137
|
+
const existing = typeof item.id === "string" ? currentById.get(item.id) : void 0;
|
|
6138
|
+
return {
|
|
6139
|
+
...item,
|
|
6140
|
+
revision: (existing?.revision ?? 0) + 1,
|
|
6141
|
+
provenance: {
|
|
6142
|
+
createdBy: existing?.provenance.createdBy ?? actor,
|
|
6143
|
+
createdAt: existing?.provenance.createdAt ?? now,
|
|
6144
|
+
updatedBy: actor,
|
|
6145
|
+
updatedAt: now,
|
|
6146
|
+
...existing?.provenance.promotedFrom ? { promotedFrom: existing.provenance.promotedFrom } : {}
|
|
6147
|
+
}
|
|
6148
|
+
};
|
|
6149
|
+
}) : raw.resources;
|
|
6150
|
+
return {
|
|
6151
|
+
...raw,
|
|
6152
|
+
schemaVersion: 1,
|
|
6153
|
+
revision,
|
|
6154
|
+
generatedAt: now,
|
|
6155
|
+
source: { type: mode === "hub" ? "built_not_found" : "chapter", appId },
|
|
6156
|
+
resources
|
|
6157
|
+
};
|
|
6158
|
+
}
|
|
6159
|
+
var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
6160
|
+
const paths = /* @__PURE__ */ new Set([BUILDER_LIBRARY_ADMIN_PATH, BUILDER_LIBRARY_MANIFEST_PATH, BUILDER_LIBRARY_PROMOTE_PATH]);
|
|
6161
|
+
if (!paths.has(url.pathname)) return null;
|
|
6162
|
+
if (!ctx.chapter.builderLibrary) return json({ error: "not found" }, 404);
|
|
6163
|
+
const user = await ctx.verifyUser(req, env);
|
|
6164
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
6165
|
+
const db = ctx.makeDb(env);
|
|
6166
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
6167
|
+
const [local3, controls] = await Promise.all([
|
|
6168
|
+
loadLocalBuilderLibrary(db, ctx.chapter.id, env.ODLA_ENV),
|
|
6169
|
+
loadActiveBuilderLibraryControls(db, ctx.chapter.id, env.ODLA_ENV)
|
|
6170
|
+
]);
|
|
6171
|
+
if (url.pathname === BUILDER_LIBRARY_MANIFEST_PATH) {
|
|
6172
|
+
if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
6173
|
+
const chapterId = url.searchParams.get("chapterId")?.trim();
|
|
6174
|
+
if (ctx.chapter.mode === "hub" && (!chapterId || !ctx.chapter.builderLibrary.sources.some((source) => source.sourceId === chapterId))) {
|
|
6175
|
+
return json({ error: "chapterId must name a configured Chapter source" }, 400);
|
|
6176
|
+
}
|
|
6177
|
+
const viewer = ctx.chapter.mode === "hub" ? { kind: "chapter", chapterId } : { kind: "chapter", chapterId: ctx.chapter.id };
|
|
6178
|
+
const documents = [
|
|
6179
|
+
...local3 ? [local3.document] : [],
|
|
6180
|
+
...controls.filter((row) => ctx.chapter.mode === "chapter" || row.sourceId === chapterId).map((row) => row.document)
|
|
6181
|
+
];
|
|
6182
|
+
return json(buildBuilderLibraryManifest(documents, viewer));
|
|
6183
|
+
}
|
|
6184
|
+
if (url.pathname === BUILDER_LIBRARY_PROMOTE_PATH) {
|
|
6185
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
6186
|
+
if (ctx.chapter.mode !== "hub") return json({ error: "only Built Not Found can promote Chapter material" }, 403);
|
|
6187
|
+
let input2;
|
|
6188
|
+
try {
|
|
6189
|
+
input2 = await req.json();
|
|
6190
|
+
} catch {
|
|
6191
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
6192
|
+
}
|
|
6193
|
+
const chapterId = typeof input2.chapterId === "string" ? input2.chapterId : "";
|
|
6194
|
+
const resourceId = typeof input2.resourceId === "string" ? input2.resourceId : "";
|
|
6195
|
+
const expectedRevision2 = Number(input2.expectedRevision);
|
|
6196
|
+
const source = controls.find((row) => row.sourceId === chapterId);
|
|
6197
|
+
const resource = source?.document.resources.find((item) => item.id === resourceId);
|
|
6198
|
+
if (!source || !resource) return json({ error: "Chapter resource not found" }, 404);
|
|
6199
|
+
if (!Number.isInteger(expectedRevision2) || expectedRevision2 !== (local3?.revision ?? 0)) return json({ error: "Builder Library changed; reload before promoting" }, 409);
|
|
6200
|
+
const now2 = Date.now();
|
|
6201
|
+
let promoted;
|
|
6202
|
+
try {
|
|
6203
|
+
promoted = promoteBuilderLibraryResource({ resource, chapterDocumentRevision: source.revision, actor: actorFor(user), promotedAt: now2, ...typeof input2.globalId === "string" ? { globalId: input2.globalId } : {} });
|
|
6204
|
+
} catch {
|
|
6205
|
+
return json({ error: "resource cannot be promoted" }, 400);
|
|
6206
|
+
}
|
|
6207
|
+
if (local3?.document.resources.some((item) => item.id === promoted.id)) return json({ error: `global resource id "${promoted.id}" already exists` }, 409);
|
|
6208
|
+
const document = {
|
|
6209
|
+
schemaVersion: 1,
|
|
6210
|
+
revision: expectedRevision2 + 1,
|
|
6211
|
+
generatedAt: now2,
|
|
6212
|
+
source: { type: "built_not_found", appId: ctx.chapter.id },
|
|
6213
|
+
resources: [...local3?.document.resources ?? [], promoted]
|
|
6214
|
+
};
|
|
6215
|
+
const checked2 = validateBuilderLibraryDocument(document, { sourceType: "built_not_found", sourceAppId: ctx.chapter.id });
|
|
6216
|
+
if (!checked2.ok) return json({ error: "promoted resource is invalid", errors: checked2.errors }, 400);
|
|
6217
|
+
try {
|
|
6218
|
+
const saved = await saveLocalBuilderLibrary(db, checked2.value, env.ODLA_ENV, expectedRevision2, now2, actorFor(user));
|
|
6219
|
+
return json({ ok: true, revision: saved.revision, resource: promoted });
|
|
6220
|
+
} catch {
|
|
6221
|
+
return json({ error: "Builder Library changed; reload before promoting" }, 409);
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
if (req.method === "GET") {
|
|
6225
|
+
return json({
|
|
6226
|
+
mode: ctx.chapter.mode,
|
|
6227
|
+
local: local3 ? { revision: local3.revision, document: local3.document } : null,
|
|
6228
|
+
sources: controls.map(({ sourceId, revision, digest, document, receivedAt }) => ({ sourceId, revision, digest, document, receivedAt })),
|
|
6229
|
+
localAuthoring: ctx.chapter.builderLibrary.localAuthoring,
|
|
6230
|
+
visibility: { builtNotFoundSeesChapterSources: ctx.chapter.mode === "hub", chaptersSeeSiblingSources: false }
|
|
6231
|
+
});
|
|
6232
|
+
}
|
|
6233
|
+
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
6234
|
+
if (!ctx.chapter.builderLibrary.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
6235
|
+
const raw = await req.text();
|
|
6236
|
+
if (raw.length > BODY_CAP2) return json({ error: "request body too large" }, 413);
|
|
6237
|
+
let input;
|
|
6238
|
+
try {
|
|
6239
|
+
input = JSON.parse(raw);
|
|
6240
|
+
} catch {
|
|
6241
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
6242
|
+
}
|
|
6243
|
+
const expectedRevision = Number(input.expectedRevision);
|
|
6244
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision < 0) return json({ error: "expectedRevision must be a non-negative integer" }, 400);
|
|
6245
|
+
const now = Date.now();
|
|
6246
|
+
const actor = actorFor(user);
|
|
6247
|
+
const authoritative = withLocalAuthority2(input.document, ctx.chapter.mode, ctx.chapter.id, expectedRevision + 1, now, actor, local3?.document);
|
|
6248
|
+
const checked = validateBuilderLibraryDocument(authoritative, { sourceType: ctx.chapter.mode === "hub" ? "built_not_found" : "chapter", sourceAppId: ctx.chapter.id });
|
|
6249
|
+
if (!checked.ok) return json({ error: "invalid Builder Library", errors: checked.errors }, 400);
|
|
6250
|
+
try {
|
|
6251
|
+
const saved = await saveLocalBuilderLibrary(db, checked.value, env.ODLA_ENV, expectedRevision, now, actor);
|
|
6252
|
+
return json({ ok: true, revision: saved.revision, document: saved.document });
|
|
6253
|
+
} catch (error) {
|
|
6254
|
+
if (error instanceof Error && error.message === "builder_library_revision_conflict") return json({ error: "Builder Library changed; reload before saving" }, 409);
|
|
6255
|
+
return json({ error: "Builder Library could not be saved" }, 503);
|
|
6256
|
+
}
|
|
6257
|
+
};
|
|
6258
|
+
|
|
5774
6259
|
// src/signup-control-delivery.ts
|
|
5775
6260
|
import { signFederatedRequest as signFederatedRequest5 } from "@odla-ai/db";
|
|
5776
6261
|
async function deliverSignupControl(options) {
|
|
@@ -5821,12 +6306,27 @@ async function deliverMemberContent(options) {
|
|
|
5821
6306
|
return result;
|
|
5822
6307
|
}
|
|
5823
6308
|
|
|
5824
|
-
// src/
|
|
6309
|
+
// src/builder-library-delivery.ts
|
|
5825
6310
|
import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
|
|
6311
|
+
async function deliverBuilderLibrary(options) {
|
|
6312
|
+
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
6313
|
+
const body = JSON.stringify(options.envelope);
|
|
6314
|
+
const signed = await signFederatedRequest7({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
6315
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
6316
|
+
const result = await response.json();
|
|
6317
|
+
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
6318
|
+
const failure = result;
|
|
6319
|
+
throw new Error(`Builder Library rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
|
|
6320
|
+
}
|
|
6321
|
+
return result;
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
// src/admission-grants-delivery.ts
|
|
6325
|
+
import { signFederatedRequest as signFederatedRequest8 } from "@odla-ai/db";
|
|
5826
6326
|
async function deliverAdmissionGrants(options) {
|
|
5827
6327
|
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
5828
6328
|
const body = JSON.stringify(options.envelope);
|
|
5829
|
-
const signed = await
|
|
6329
|
+
const signed = await signFederatedRequest8({
|
|
5830
6330
|
secret: options.secret,
|
|
5831
6331
|
sender: options.sender,
|
|
5832
6332
|
method: "POST",
|
|
@@ -5986,6 +6486,7 @@ var BUILTIN_ROUTES = [
|
|
|
5986
6486
|
handleNetworkShared,
|
|
5987
6487
|
handleSignupControl,
|
|
5988
6488
|
handleMemberContentControl,
|
|
6489
|
+
handleBuilderLibraryControl,
|
|
5989
6490
|
handleAdmissionGrants,
|
|
5990
6491
|
handleNetworkSnapshot,
|
|
5991
6492
|
handleNetworkRecords,
|
|
@@ -6009,6 +6510,7 @@ var BUILTIN_ROUTES = [
|
|
|
6009
6510
|
handleAdminDashboard,
|
|
6010
6511
|
handleAdminBilling,
|
|
6011
6512
|
handleAdminMemberContent,
|
|
6513
|
+
handleAdminBuilderLibrary,
|
|
6012
6514
|
// Lifecycle actions
|
|
6013
6515
|
handleAdminMeetingReschedule,
|
|
6014
6516
|
handleAdminMeetingCancel,
|
|
@@ -6050,6 +6552,7 @@ export {
|
|
|
6050
6552
|
chapterWorker,
|
|
6051
6553
|
createWorkerContext,
|
|
6052
6554
|
deliverAdmissionGrants,
|
|
6555
|
+
deliverBuilderLibrary,
|
|
6053
6556
|
deliverMemberContent,
|
|
6054
6557
|
deliverSignupControl,
|
|
6055
6558
|
renderActiveSignupControlHtml,
|