@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.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(worker_exports, {
|
|
|
23
23
|
chapterWorker: () => chapterWorker,
|
|
24
24
|
createWorkerContext: () => createWorkerContext,
|
|
25
25
|
deliverAdmissionGrants: () => deliverAdmissionGrants,
|
|
26
|
+
deliverBuilderLibrary: () => deliverBuilderLibrary,
|
|
26
27
|
deliverMemberContent: () => deliverMemberContent,
|
|
27
28
|
deliverSignupControl: () => deliverSignupControl,
|
|
28
29
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
@@ -548,9 +549,9 @@ function canonicalValue(value) {
|
|
|
548
549
|
}
|
|
549
550
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
550
551
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
551
|
-
const
|
|
552
|
-
const entries = Object.keys(
|
|
553
|
-
const item =
|
|
552
|
+
const record4 = value;
|
|
553
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
554
|
+
const item = record4[key];
|
|
554
555
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
555
556
|
});
|
|
556
557
|
return `{${entries.join(",")}}`;
|
|
@@ -750,8 +751,8 @@ var APPLICATION_LABELS = {
|
|
|
750
751
|
state: "State"
|
|
751
752
|
};
|
|
752
753
|
function humanize(id) {
|
|
753
|
-
const
|
|
754
|
-
return
|
|
754
|
+
const text4 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
755
|
+
return text4 ? `${text4[0].toUpperCase()}${text4.slice(1)}` : id;
|
|
755
756
|
}
|
|
756
757
|
function applicationDefs(chapter) {
|
|
757
758
|
try {
|
|
@@ -942,8 +943,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
942
943
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
943
944
|
const page = Array.isArray(data) ? data : [];
|
|
944
945
|
for (const u of page) {
|
|
945
|
-
const
|
|
946
|
-
if (
|
|
946
|
+
const record4 = toRecord(u);
|
|
947
|
+
if (record4) out.push(record4);
|
|
947
948
|
}
|
|
948
949
|
if (page.length < PAGE) break;
|
|
949
950
|
}
|
|
@@ -1000,34 +1001,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1000
1001
|
if (readable && raw.length <= 64) return raw;
|
|
1001
1002
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1002
1003
|
}
|
|
1003
|
-
function normalizeSharedRecord(
|
|
1004
|
-
if ("input" in
|
|
1004
|
+
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
1005
|
+
if ("input" in record4 && record4.version === 2) {
|
|
1005
1006
|
return {
|
|
1006
1007
|
version: 2,
|
|
1007
|
-
sourceId:
|
|
1008
|
-
sourceRecordId:
|
|
1009
|
-
type:
|
|
1010
|
-
input:
|
|
1008
|
+
sourceId: record4.source.siteId,
|
|
1009
|
+
sourceRecordId: record4.source.recordId,
|
|
1010
|
+
type: record4.type,
|
|
1011
|
+
input: record4.input
|
|
1011
1012
|
};
|
|
1012
1013
|
}
|
|
1013
|
-
if ("input" in
|
|
1014
|
+
if ("input" in record4) {
|
|
1014
1015
|
return {
|
|
1015
1016
|
version: 1,
|
|
1016
1017
|
sourceId: fallbackSourceId,
|
|
1017
|
-
sourceRecordId:
|
|
1018
|
-
type:
|
|
1019
|
-
input:
|
|
1018
|
+
sourceRecordId: record4.hubRecordId,
|
|
1019
|
+
type: record4.type,
|
|
1020
|
+
input: record4.input
|
|
1020
1021
|
};
|
|
1021
1022
|
}
|
|
1022
|
-
if ("type" in
|
|
1023
|
-
const input = { name:
|
|
1023
|
+
if ("type" in record4 && record4.type === "company") {
|
|
1024
|
+
const input = { name: record4.name };
|
|
1024
1025
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1025
|
-
if (
|
|
1026
|
+
if (record4[key]) input[key] = record4[key];
|
|
1026
1027
|
}
|
|
1027
1028
|
return {
|
|
1028
1029
|
version: 1,
|
|
1029
1030
|
sourceId: fallbackSourceId,
|
|
1030
|
-
sourceRecordId:
|
|
1031
|
+
sourceRecordId: record4.hubRecordId,
|
|
1031
1032
|
type: "company",
|
|
1032
1033
|
input
|
|
1033
1034
|
};
|
|
@@ -1035,28 +1036,28 @@ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
|
1035
1036
|
return {
|
|
1036
1037
|
version: 1,
|
|
1037
1038
|
sourceId: fallbackSourceId,
|
|
1038
|
-
sourceRecordId:
|
|
1039
|
+
sourceRecordId: record4.hubRecordId,
|
|
1039
1040
|
type: "person",
|
|
1040
|
-
input: sharedPersonInput(
|
|
1041
|
+
input: sharedPersonInput(record4)
|
|
1041
1042
|
};
|
|
1042
1043
|
}
|
|
1043
|
-
function sharedRecordFromCrm(crm,
|
|
1044
|
-
if (target.fields && !target.fields[
|
|
1045
|
-
throw new Error(`${target.name} does not accept "${
|
|
1044
|
+
function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
1045
|
+
if (target.fields && !target.fields[record4.type]) {
|
|
1046
|
+
throw new Error(`${target.name} does not accept "${record4.type}" records`);
|
|
1046
1047
|
}
|
|
1047
|
-
const def = crm.type(
|
|
1048
|
-
const fields = target.fields?.[
|
|
1048
|
+
const def = crm.type(record4.type);
|
|
1049
|
+
const fields = target.fields?.[record4.type] ?? DEFAULT_SHARE_FIELDS[record4.type];
|
|
1049
1050
|
if (!fields) {
|
|
1050
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
1051
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record4.type}" records`);
|
|
1051
1052
|
}
|
|
1052
1053
|
const nameField = def.nameField ?? "name";
|
|
1053
1054
|
const input = {};
|
|
1054
1055
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
1055
|
-
const value =
|
|
1056
|
+
const value = record4.fields?.[field];
|
|
1056
1057
|
if (value !== void 0) input[field] = value;
|
|
1057
1058
|
}
|
|
1058
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
1059
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
1059
|
+
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
1060
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
1060
1061
|
}
|
|
1061
1062
|
async function upsertPerson(deps, opts) {
|
|
1062
1063
|
const email = opts.email.toLowerCase();
|
|
@@ -1070,11 +1071,11 @@ async function upsertPerson(deps, opts) {
|
|
|
1070
1071
|
const created = await (0, import_crm2.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1071
1072
|
return { recordId: created.id };
|
|
1072
1073
|
}
|
|
1073
|
-
async function findSharedRecord(deps,
|
|
1074
|
+
async function findSharedRecord(deps, record4, tag) {
|
|
1074
1075
|
const structured = await (0, import_crm2.getRecordByOrigin)(
|
|
1075
1076
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1076
|
-
|
|
1077
|
-
|
|
1077
|
+
record4.sourceId,
|
|
1078
|
+
record4.sourceRecordId
|
|
1078
1079
|
);
|
|
1079
1080
|
if (structured) return structured;
|
|
1080
1081
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1083,53 +1084,53 @@ async function findSharedRecord(deps, record3, tag) {
|
|
|
1083
1084
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1084
1085
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1085
1086
|
}
|
|
1086
|
-
const def = deps.crm.type(
|
|
1087
|
+
const def = deps.crm.type(record4.type);
|
|
1087
1088
|
const emailField = def.emailField;
|
|
1088
|
-
if (emailField && typeof
|
|
1089
|
-
const primaryEmail =
|
|
1089
|
+
if (emailField && typeof record4.input[emailField] === "string") {
|
|
1090
|
+
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
1090
1091
|
const found = await deps.db.query({
|
|
1091
|
-
crm_record: { $: { where: { type:
|
|
1092
|
+
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
1092
1093
|
});
|
|
1093
1094
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1094
1095
|
}
|
|
1095
|
-
const domain =
|
|
1096
|
+
const domain = record4.input.domain;
|
|
1096
1097
|
const domainSlot = def.fields.domain?.slot;
|
|
1097
1098
|
if (typeof domain === "string" && domainSlot) {
|
|
1098
1099
|
const found = await deps.db.query({
|
|
1099
|
-
crm_record: { $: { where: { type:
|
|
1100
|
+
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
1100
1101
|
});
|
|
1101
1102
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1102
1103
|
}
|
|
1103
1104
|
const nameField = def.nameField ?? "name";
|
|
1104
|
-
const name =
|
|
1105
|
-
if (
|
|
1105
|
+
const name = record4.input[nameField];
|
|
1106
|
+
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
1106
1107
|
const found = await deps.db.query({
|
|
1107
|
-
crm_record: { $: { where: { type:
|
|
1108
|
+
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
1108
1109
|
});
|
|
1109
1110
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1110
1111
|
}
|
|
1111
1112
|
return void 0;
|
|
1112
1113
|
}
|
|
1113
1114
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1114
|
-
const
|
|
1115
|
-
if (!
|
|
1115
|
+
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
1116
|
+
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
1116
1117
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1117
1118
|
}
|
|
1118
|
-
if (options.sourceId && options.sourceId !==
|
|
1119
|
+
if (options.sourceId && options.sourceId !== record4.sourceId) {
|
|
1119
1120
|
throw new Error("signed sender does not match payload source");
|
|
1120
1121
|
}
|
|
1121
|
-
const tag = networkSourceTag(
|
|
1122
|
+
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
1122
1123
|
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1123
|
-
const existing = await findSharedRecord(deps,
|
|
1124
|
+
const existing = await findSharedRecord(deps, record4, tag);
|
|
1124
1125
|
let recordId;
|
|
1125
1126
|
if (existing && typeof existing.id === "string") {
|
|
1126
|
-
await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input:
|
|
1127
|
+
await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record4.input });
|
|
1127
1128
|
recordId = existing.id;
|
|
1128
1129
|
} else {
|
|
1129
|
-
recordId = `network_${shortHash(`${
|
|
1130
|
+
recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
|
|
1130
1131
|
await (0, import_crm2.createRecord)({ ...crmDeps3, newId: () => recordId }, {
|
|
1131
|
-
type:
|
|
1132
|
-
input:
|
|
1132
|
+
type: record4.type,
|
|
1133
|
+
input: record4.input,
|
|
1133
1134
|
mutationId: `share-create:${tag}`
|
|
1134
1135
|
});
|
|
1135
1136
|
}
|
|
@@ -1139,10 +1140,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1139
1140
|
);
|
|
1140
1141
|
await (0, import_crm2.upsertRecordOrigin)(crmDeps3, {
|
|
1141
1142
|
recordId,
|
|
1142
|
-
sourceId:
|
|
1143
|
-
sourceRecordId:
|
|
1143
|
+
sourceId: record4.sourceId,
|
|
1144
|
+
sourceRecordId: record4.sourceRecordId,
|
|
1144
1145
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1145
|
-
payloadVersion:
|
|
1146
|
+
payloadVersion: record4.version
|
|
1146
1147
|
});
|
|
1147
1148
|
return { recordId };
|
|
1148
1149
|
}
|
|
@@ -1281,10 +1282,10 @@ function planDelivery(input) {
|
|
|
1281
1282
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1282
1283
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1283
1284
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1284
|
-
const
|
|
1285
|
+
const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1285
1286
|
|
|
1286
1287
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1287
|
-
return { deliver: true, transport, to, subject, text:
|
|
1288
|
+
return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
|
|
1288
1289
|
}
|
|
1289
1290
|
|
|
1290
1291
|
// src/notify.ts
|
|
@@ -2067,8 +2068,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
2067
2068
|
payload,
|
|
2068
2069
|
sourceId ? { sourceId } : {}
|
|
2069
2070
|
);
|
|
2070
|
-
const
|
|
2071
|
-
return json({ recordId, type:
|
|
2071
|
+
const record4 = normalizeSharedRecord(payload, sourceId);
|
|
2072
|
+
return json({ recordId, type: record4.type });
|
|
2072
2073
|
} catch (error) {
|
|
2073
2074
|
return json({
|
|
2074
2075
|
error: error instanceof Error ? error.message : "invalid shared record"
|
|
@@ -2122,10 +2123,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2122
2123
|
errors.push(`${path} must be a string`);
|
|
2123
2124
|
return void 0;
|
|
2124
2125
|
}
|
|
2125
|
-
const
|
|
2126
|
-
if (required && !
|
|
2127
|
-
if (
|
|
2128
|
-
return
|
|
2126
|
+
const text4 = value.trim();
|
|
2127
|
+
if (required && !text4) errors.push(`${path} is required`);
|
|
2128
|
+
if (text4.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2129
|
+
return text4;
|
|
2129
2130
|
}
|
|
2130
2131
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2131
2132
|
const errors = [];
|
|
@@ -2174,8 +2175,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2174
2175
|
else {
|
|
2175
2176
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2176
2177
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2177
|
-
const
|
|
2178
|
-
return
|
|
2178
|
+
const text4 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2179
|
+
return text4 ? [text4] : [];
|
|
2179
2180
|
});
|
|
2180
2181
|
}
|
|
2181
2182
|
}
|
|
@@ -2505,9 +2506,9 @@ function canonicalValue2(value) {
|
|
|
2505
2506
|
}
|
|
2506
2507
|
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2507
2508
|
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2508
|
-
const
|
|
2509
|
-
const entries = Object.keys(
|
|
2510
|
-
const item =
|
|
2509
|
+
const record4 = value;
|
|
2510
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
2511
|
+
const item = record4[key];
|
|
2511
2512
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2512
2513
|
});
|
|
2513
2514
|
return `{${entries.join(",")}}`;
|
|
@@ -2937,9 +2938,9 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
2937
2938
|
tiers
|
|
2938
2939
|
});
|
|
2939
2940
|
if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
|
|
2940
|
-
const
|
|
2941
|
-
if (
|
|
2942
|
-
const localCheck = validateMemberContentDocument(
|
|
2941
|
+
const local3 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2942
|
+
if (local3) {
|
|
2943
|
+
const localCheck = validateMemberContentDocument(local3.document, {
|
|
2943
2944
|
sourceType: "chapter",
|
|
2944
2945
|
sourceAppId: ctx.chapter.id,
|
|
2945
2946
|
tiers,
|
|
@@ -3373,9 +3374,21 @@ function webhookMutationId(eventId) {
|
|
|
3373
3374
|
function findApplicationRef(obj) {
|
|
3374
3375
|
const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
|
|
3375
3376
|
const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
|
|
3376
|
-
const
|
|
3377
|
+
const candidates = [
|
|
3378
|
+
metaOf(obj),
|
|
3379
|
+
metaOf(obj.subscription_details),
|
|
3380
|
+
metaOf(obj.parent?.subscription_details)
|
|
3381
|
+
];
|
|
3382
|
+
const applicationId = candidates.map(pick).find(Boolean);
|
|
3383
|
+
const appId = candidates.map((metadata) => typeof metadata.odlaAppId === "string" ? metadata.odlaAppId : void 0).find(Boolean);
|
|
3384
|
+
const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
|
|
3377
3385
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
3378
|
-
return {
|
|
3386
|
+
return {
|
|
3387
|
+
...applicationId ? { applicationId } : {},
|
|
3388
|
+
...customerId ? { customerId } : {},
|
|
3389
|
+
...appId ? { appId } : {},
|
|
3390
|
+
...environment ? { environment } : {}
|
|
3391
|
+
};
|
|
3379
3392
|
}
|
|
3380
3393
|
function findGiftRef(obj) {
|
|
3381
3394
|
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
@@ -3466,7 +3479,13 @@ function createStripeProvider(config) {
|
|
|
3466
3479
|
return {
|
|
3467
3480
|
async createSubscription(input) {
|
|
3468
3481
|
const sk = requireSecret(secretKey);
|
|
3469
|
-
const meta = {
|
|
3482
|
+
const meta = {
|
|
3483
|
+
applicationId: input.applicationId,
|
|
3484
|
+
groupId: input.groupId,
|
|
3485
|
+
email: input.email,
|
|
3486
|
+
odlaAppId: input.appId,
|
|
3487
|
+
odlaEnvironment: input.environment
|
|
3488
|
+
};
|
|
3470
3489
|
let customerId = input.existingCustomerId;
|
|
3471
3490
|
if (!customerId) {
|
|
3472
3491
|
const cust = await stripeCall(
|
|
@@ -3945,8 +3964,8 @@ async function startSubscription(req, env, ctx) {
|
|
|
3945
3964
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
3946
3965
|
if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
|
|
3947
3966
|
if (typeof app.giftOfferId === "string") {
|
|
3948
|
-
const
|
|
3949
|
-
const offer = findGiftOffer(
|
|
3967
|
+
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
3968
|
+
const offer = findGiftOffer(control3?.document.giftOffers, app.giftOfferId, Date.now());
|
|
3950
3969
|
if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
|
|
3951
3970
|
return json({ error: "gift offer is unavailable" }, 409);
|
|
3952
3971
|
}
|
|
@@ -3986,6 +4005,8 @@ async function startSubscription(req, env, ctx) {
|
|
|
3986
4005
|
let result;
|
|
3987
4006
|
try {
|
|
3988
4007
|
result = await provider.createSubscription({
|
|
4008
|
+
appId: env.ODLA_APP_ID,
|
|
4009
|
+
environment: env.ODLA_ENV,
|
|
3989
4010
|
applicationId,
|
|
3990
4011
|
groupId: String(group.id),
|
|
3991
4012
|
email: String(app.email ?? ""),
|
|
@@ -4015,6 +4036,10 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4015
4036
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
4016
4037
|
const { eventId, event } = ingest;
|
|
4017
4038
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4039
|
+
const identifiedEnvironment = event.appId !== void 0 || event.environment !== void 0;
|
|
4040
|
+
if (env.ODLA_APP_ID && identifiedEnvironment && (event.appId !== env.ODLA_APP_ID || event.environment !== env.ODLA_ENV)) {
|
|
4041
|
+
return json({ ok: true, matched: false });
|
|
4042
|
+
}
|
|
4018
4043
|
if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
|
|
4019
4044
|
const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id: event.giftIntentId }, limit: 1 });
|
|
4020
4045
|
if (!intent || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV) {
|
|
@@ -4105,14 +4130,14 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4105
4130
|
if (req.method === "GET" && url.pathname === "/api/gifts") {
|
|
4106
4131
|
const user = await userFor(req, env, ctx);
|
|
4107
4132
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4108
|
-
const
|
|
4133
|
+
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4109
4134
|
const found = await db.query({ giftClaims: { $: { where: {
|
|
4110
4135
|
purchaserEmailNormalized: normalizeChapterEmail(user.email),
|
|
4111
4136
|
chapterId: ctx.chapter.id
|
|
4112
4137
|
}, limit: 200 } } });
|
|
4113
4138
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4114
4139
|
return json({
|
|
4115
|
-
offers: publicGiftOffers(
|
|
4140
|
+
offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
|
|
4116
4141
|
claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
|
|
4117
4142
|
refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
|
|
4118
4143
|
});
|
|
@@ -4128,8 +4153,8 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4128
4153
|
}
|
|
4129
4154
|
const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
|
|
4130
4155
|
if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
|
|
4131
|
-
const
|
|
4132
|
-
const offer = findGiftOffer(
|
|
4156
|
+
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4157
|
+
const offer = findGiftOffer(control3?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
|
|
4133
4158
|
if (!offer) return json({ error: "gift offer is unavailable" }, 409);
|
|
4134
4159
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4135
4160
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
@@ -4961,11 +4986,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
4961
4986
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
4962
4987
|
const t = templates[key];
|
|
4963
4988
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
4964
|
-
const
|
|
4965
|
-
if (!subject || !
|
|
4989
|
+
const text4 = typeof t?.text === "string" ? t.text : "";
|
|
4990
|
+
if (!subject || !text4.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
4966
4991
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
4967
|
-
if (
|
|
4968
|
-
clean[key] = { subject, text:
|
|
4992
|
+
if (text4.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
4993
|
+
clean[key] = { subject, text: text4, enabled: t?.enabled !== false };
|
|
4969
4994
|
}
|
|
4970
4995
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4971
4996
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -5085,7 +5110,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
5085
5110
|
// src/worker-network-push.ts
|
|
5086
5111
|
var import_crm5 = require("@odla-ai/crm");
|
|
5087
5112
|
var import_db6 = require("@odla-ai/db");
|
|
5088
|
-
async function pushNetworkRecord(db, env, ctx, target,
|
|
5113
|
+
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
5089
5114
|
const secret = await getVaultSecret(db, target.secretName);
|
|
5090
5115
|
const crmDeps3 = {
|
|
5091
5116
|
crm: ctx.chapter.crm,
|
|
@@ -5096,7 +5121,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5096
5121
|
if (!secret) {
|
|
5097
5122
|
const error = `vault secret "${target.secretName}" is missing`;
|
|
5098
5123
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
5099
|
-
recordId:
|
|
5124
|
+
recordId: record4.id,
|
|
5100
5125
|
targetId: target.id,
|
|
5101
5126
|
status: "failed",
|
|
5102
5127
|
payloadVersion: 2,
|
|
@@ -5106,7 +5131,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5106
5131
|
}
|
|
5107
5132
|
let payload;
|
|
5108
5133
|
try {
|
|
5109
|
-
payload = sharedRecordFromCrm(ctx.chapter.crm,
|
|
5134
|
+
payload = sharedRecordFromCrm(ctx.chapter.crm, record4, target, ctx.chapter.id);
|
|
5110
5135
|
} catch (err) {
|
|
5111
5136
|
return {
|
|
5112
5137
|
id: target.id,
|
|
@@ -5135,7 +5160,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5135
5160
|
if (!res.ok) {
|
|
5136
5161
|
const error = responseBody.error ?? "follower rejected the record";
|
|
5137
5162
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
5138
|
-
recordId:
|
|
5163
|
+
recordId: record4.id,
|
|
5139
5164
|
targetId: target.id,
|
|
5140
5165
|
status: "failed",
|
|
5141
5166
|
payloadVersion: 2,
|
|
@@ -5150,7 +5175,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5150
5175
|
};
|
|
5151
5176
|
}
|
|
5152
5177
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
5153
|
-
recordId:
|
|
5178
|
+
recordId: record4.id,
|
|
5154
5179
|
targetId: target.id,
|
|
5155
5180
|
status: "delivered",
|
|
5156
5181
|
payloadVersion: 2,
|
|
@@ -5166,7 +5191,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
|
5166
5191
|
} catch (err) {
|
|
5167
5192
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
5168
5193
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
5169
|
-
recordId:
|
|
5194
|
+
recordId: record4.id,
|
|
5170
5195
|
targetId: target.id,
|
|
5171
5196
|
status: "failed",
|
|
5172
5197
|
payloadVersion: 2,
|
|
@@ -5304,10 +5329,10 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5304
5329
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
5305
5330
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
5306
5331
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
5307
|
-
const
|
|
5308
|
-
if (!
|
|
5332
|
+
const record4 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
5333
|
+
if (!record4) return json({ error: "record not found" }, 404);
|
|
5309
5334
|
const results = await Promise.all(
|
|
5310
|
-
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target,
|
|
5335
|
+
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
5311
5336
|
);
|
|
5312
5337
|
return json({ ok: results.every((result) => result.ok), results });
|
|
5313
5338
|
};
|
|
@@ -5332,18 +5357,18 @@ function integer(value, fallback, min, max) {
|
|
|
5332
5357
|
const parsed = Number(value);
|
|
5333
5358
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
5334
5359
|
}
|
|
5335
|
-
function projectRecord(
|
|
5360
|
+
function projectRecord(record4, allowed2) {
|
|
5336
5361
|
const fields = Object.fromEntries(
|
|
5337
|
-
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(
|
|
5362
|
+
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
5338
5363
|
);
|
|
5339
5364
|
return {
|
|
5340
|
-
id:
|
|
5341
|
-
type:
|
|
5342
|
-
name:
|
|
5343
|
-
...
|
|
5365
|
+
id: record4.id,
|
|
5366
|
+
type: record4.type,
|
|
5367
|
+
name: record4.name,
|
|
5368
|
+
...record4.stage ? { stage: record4.stage } : {},
|
|
5344
5369
|
fields,
|
|
5345
|
-
createdAt:
|
|
5346
|
-
updatedAt:
|
|
5370
|
+
createdAt: record4.createdAt,
|
|
5371
|
+
updatedAt: record4.updatedAt
|
|
5347
5372
|
};
|
|
5348
5373
|
}
|
|
5349
5374
|
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
@@ -5381,7 +5406,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5381
5406
|
version: 1,
|
|
5382
5407
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5383
5408
|
type,
|
|
5384
|
-
records: page.records.map((
|
|
5409
|
+
records: page.records.map((record4) => projectRecord(record4, allowed2)),
|
|
5385
5410
|
total: page.total,
|
|
5386
5411
|
limit: page.limit,
|
|
5387
5412
|
offset: page.offset
|
|
@@ -5602,8 +5627,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5602
5627
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
5603
5628
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
5604
5629
|
}
|
|
5605
|
-
const
|
|
5606
|
-
if (!
|
|
5630
|
+
const record4 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
5631
|
+
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
5607
5632
|
if (req.method === "GET") {
|
|
5608
5633
|
return json({
|
|
5609
5634
|
version: 1,
|
|
@@ -5794,12 +5819,473 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
5794
5819
|
}
|
|
5795
5820
|
};
|
|
5796
5821
|
|
|
5797
|
-
// src/
|
|
5822
|
+
// src/worker-routes-builder-library.ts
|
|
5798
5823
|
var import_db10 = require("@odla-ai/db");
|
|
5824
|
+
|
|
5825
|
+
// src/builder-library-validation.ts
|
|
5826
|
+
var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5827
|
+
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
5828
|
+
var MAX_RESOURCES = 200;
|
|
5829
|
+
var MAX_MARKDOWN3 = 65536;
|
|
5830
|
+
var MAX_TEXT2 = 500;
|
|
5831
|
+
var MAX_TAGS = 20;
|
|
5832
|
+
var KINDS = /* @__PURE__ */ new Set(["document", "note", "link", "inspiration", "image", "file"]);
|
|
5833
|
+
var STATUSES = /* @__PURE__ */ new Set(["draft", "published", "archived"]);
|
|
5834
|
+
var ROOT_KEYS4 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "resources"]);
|
|
5835
|
+
var SOURCE_KEYS2 = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
5836
|
+
var TARGET_KEYS2 = /* @__PURE__ */ new Set(["appId", "environment"]);
|
|
5837
|
+
var RESOURCE_KEYS = /* @__PURE__ */ new Set(["id", "kind", "title", "summary", "tags", "scope", "status", "revision", "bodyMarkdown", "url", "brandAsset", "provenance"]);
|
|
5838
|
+
var SCOPE_KEYS = /* @__PURE__ */ new Set(["kind", "chapterId"]);
|
|
5839
|
+
var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
|
|
5840
|
+
var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
|
|
5841
|
+
var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
|
|
5842
|
+
var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5843
|
+
function unknownKeys3(value, allowed2, path, errors) {
|
|
5844
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
5845
|
+
}
|
|
5846
|
+
function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
5847
|
+
if (value === void 0 && !required) return void 0;
|
|
5848
|
+
if (typeof value !== "string" || required && !value.trim()) {
|
|
5849
|
+
errors.push(`${path} must be a non-empty string`);
|
|
5850
|
+
return void 0;
|
|
5851
|
+
}
|
|
5852
|
+
const result = value.trim();
|
|
5853
|
+
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
5854
|
+
return result;
|
|
5855
|
+
}
|
|
5856
|
+
function integer2(value, path, errors, minimum = 1) {
|
|
5857
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
5858
|
+
return Number(value);
|
|
5859
|
+
}
|
|
5860
|
+
function scopeValue(value, path, errors) {
|
|
5861
|
+
const raw = record3(value);
|
|
5862
|
+
if (!raw) {
|
|
5863
|
+
errors.push(`${path} must be an object`);
|
|
5864
|
+
return null;
|
|
5865
|
+
}
|
|
5866
|
+
unknownKeys3(raw, SCOPE_KEYS, path, errors);
|
|
5867
|
+
if (raw.kind === "global" || raw.kind === "built_not_found") {
|
|
5868
|
+
if (raw.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
5869
|
+
return { kind: raw.kind };
|
|
5870
|
+
}
|
|
5871
|
+
if (raw.kind === "chapter") {
|
|
5872
|
+
const chapterId = text3(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
5873
|
+
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
5874
|
+
return { kind: "chapter", chapterId };
|
|
5875
|
+
}
|
|
5876
|
+
errors.push(`${path}.kind must be chapter, built_not_found, or global`);
|
|
5877
|
+
return null;
|
|
5878
|
+
}
|
|
5879
|
+
function resourceValue(value, path, errors) {
|
|
5880
|
+
const raw = record3(value);
|
|
5881
|
+
if (!raw) {
|
|
5882
|
+
errors.push(`${path} must be an object`);
|
|
5883
|
+
return null;
|
|
5884
|
+
}
|
|
5885
|
+
unknownKeys3(raw, RESOURCE_KEYS, path, errors);
|
|
5886
|
+
const id = text3(raw.id, `${path}.id`, errors, 80) ?? "";
|
|
5887
|
+
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
5888
|
+
const kind = text3(raw.kind, `${path}.kind`, errors) ?? "";
|
|
5889
|
+
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
5890
|
+
const title = text3(raw.title, `${path}.title`, errors, 240) ?? "";
|
|
5891
|
+
const summary = text3(raw.summary, `${path}.summary`, errors, 1e3, false);
|
|
5892
|
+
const status = text3(raw.status, `${path}.status`, errors) ?? "";
|
|
5893
|
+
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
5894
|
+
const revision = integer2(raw.revision, `${path}.revision`, errors);
|
|
5895
|
+
const scope = scopeValue(raw.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
5896
|
+
const tags = Array.isArray(raw.tags) ? raw.tags.map((tag, index) => text3(tag, `${path}.tags[${index}]`, errors, 80) ?? "") : [];
|
|
5897
|
+
if (!Array.isArray(raw.tags)) errors.push(`${path}.tags must be an array`);
|
|
5898
|
+
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
5899
|
+
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
5900
|
+
const bodyMarkdown = text3(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
5901
|
+
const url = text3(raw.url, `${path}.url`, errors, 2048, false);
|
|
5902
|
+
if (url) {
|
|
5903
|
+
try {
|
|
5904
|
+
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
5905
|
+
} catch {
|
|
5906
|
+
errors.push(`${path}.url must be a valid https URL`);
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
const asset = raw.brandAsset === void 0 ? null : record3(raw.brandAsset);
|
|
5910
|
+
if (raw.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
5911
|
+
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
5912
|
+
const brandAsset = asset ? {
|
|
5913
|
+
bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
5914
|
+
assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
5915
|
+
...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
5916
|
+
} : void 0;
|
|
5917
|
+
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
5918
|
+
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
5919
|
+
if ((kind === "image" || kind === "file") && !brandAsset) errors.push(`${path}.brandAsset is required for ${kind}`);
|
|
5920
|
+
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
5921
|
+
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
5922
|
+
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
5923
|
+
const provenanceRaw = record3(raw.provenance);
|
|
5924
|
+
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
5925
|
+
if (provenanceRaw) unknownKeys3(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
5926
|
+
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record3(provenanceRaw.promotedFrom);
|
|
5927
|
+
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
5928
|
+
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
5929
|
+
const promotedFrom = promotedRaw ? {
|
|
5930
|
+
chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
5931
|
+
resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
5932
|
+
resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
5933
|
+
documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
5934
|
+
promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
5935
|
+
promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
5936
|
+
} : void 0;
|
|
5937
|
+
const provenance = {
|
|
5938
|
+
createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
5939
|
+
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
5940
|
+
updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
5941
|
+
updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
5942
|
+
...promotedFrom ? { promotedFrom } : {}
|
|
5943
|
+
};
|
|
5944
|
+
return { id, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
5945
|
+
}
|
|
5946
|
+
function validateBuilderLibraryDocument(value, options = {}) {
|
|
5947
|
+
const errors = [];
|
|
5948
|
+
const raw = record3(value);
|
|
5949
|
+
if (!raw) return { ok: false, errors: ["document must be an object"] };
|
|
5950
|
+
unknownKeys3(raw, ROOT_KEYS4, "document", errors);
|
|
5951
|
+
if (raw.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
5952
|
+
const revision = integer2(raw.revision, "document.revision", errors);
|
|
5953
|
+
const generatedAt = integer2(raw.generatedAt, "document.generatedAt", errors, 0);
|
|
5954
|
+
const sourceRaw = record3(raw.source);
|
|
5955
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
5956
|
+
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
5957
|
+
const sourceType = sourceRaw?.type;
|
|
5958
|
+
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
5959
|
+
const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
5960
|
+
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
5961
|
+
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
5962
|
+
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
5963
|
+
const targetRaw = raw.target === void 0 ? null : record3(raw.target);
|
|
5964
|
+
if (raw.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
5965
|
+
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
5966
|
+
const target = targetRaw ? {
|
|
5967
|
+
appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
5968
|
+
environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
5969
|
+
} : void 0;
|
|
5970
|
+
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
5971
|
+
if (!Array.isArray(raw.resources)) errors.push("document.resources must be an array");
|
|
5972
|
+
const inputs = Array.isArray(raw.resources) ? raw.resources : [];
|
|
5973
|
+
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
5974
|
+
const resources = inputs.map((item, index) => resourceValue(item, `document.resources[${index}]`, errors)).filter(Boolean);
|
|
5975
|
+
const ids = resources.map(({ id }) => id);
|
|
5976
|
+
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
5977
|
+
for (const resource of resources) {
|
|
5978
|
+
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
5979
|
+
errors.push(`document.resources.${resource.id}: Chapter sources may author only their own chapter scope`);
|
|
5980
|
+
}
|
|
5981
|
+
if (sourceType === "built_not_found" && resource.scope.kind === "chapter") {
|
|
5982
|
+
errors.push(`document.resources.${resource.id}: Built Not Found sources may author only built_not_found or global scope`);
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
if (errors.length) return { ok: false, errors };
|
|
5986
|
+
return { ok: true, value: { schemaVersion: 1, revision, generatedAt, source: { type: sourceType, appId: sourceAppId }, ...target ? { target } : {}, resources } };
|
|
5987
|
+
}
|
|
5988
|
+
|
|
5989
|
+
// src/builder-library.ts
|
|
5990
|
+
var BUILDER_LIBRARY_SCHEMA_VERSION = 1;
|
|
5991
|
+
var BUILDER_LIBRARY_ADMIN_PATH = "/api/admin/builder-library";
|
|
5992
|
+
var BUILDER_LIBRARY_MANIFEST_PATH = "/api/admin/builder-library/manifest";
|
|
5993
|
+
var BUILDER_LIBRARY_PROMOTE_PATH = "/api/admin/builder-library/promote";
|
|
5994
|
+
var BUILDER_LIBRARY_CONTROL_PATH = "/api/builder-library-control";
|
|
5995
|
+
function canViewBuilderLibraryResource(resource, viewer) {
|
|
5996
|
+
if (viewer.kind === "built_not_found") return true;
|
|
5997
|
+
return resource.scope.kind === "global" || resource.scope.kind === "chapter" && resource.scope.chapterId === viewer.chapterId;
|
|
5998
|
+
}
|
|
5999
|
+
function buildBuilderLibraryManifest(documents, viewer, generatedAt = Date.now(), limit = 100) {
|
|
6000
|
+
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));
|
|
6001
|
+
return {
|
|
6002
|
+
schemaVersion: BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
6003
|
+
generatedAt,
|
|
6004
|
+
viewer,
|
|
6005
|
+
revisions: documents.map(({ source, revision }) => ({ source, revision })),
|
|
6006
|
+
resources: ordered.slice(0, limit).map(({ resource }) => resource),
|
|
6007
|
+
truncated: ordered.length > limit
|
|
6008
|
+
};
|
|
6009
|
+
}
|
|
6010
|
+
function promoteBuilderLibraryResource(input) {
|
|
6011
|
+
if (input.resource.scope.kind !== "chapter") throw new TypeError("only a Chapter-scoped resource can be promoted");
|
|
6012
|
+
const chapterId = input.resource.scope.chapterId;
|
|
6013
|
+
return {
|
|
6014
|
+
...input.resource,
|
|
6015
|
+
id: input.globalId ?? `${chapterId}-${input.resource.id}`,
|
|
6016
|
+
scope: { kind: "global" },
|
|
6017
|
+
revision: 1,
|
|
6018
|
+
provenance: {
|
|
6019
|
+
createdBy: input.actor,
|
|
6020
|
+
createdAt: input.promotedAt,
|
|
6021
|
+
updatedBy: input.actor,
|
|
6022
|
+
updatedAt: input.promotedAt,
|
|
6023
|
+
promotedFrom: {
|
|
6024
|
+
chapterId,
|
|
6025
|
+
resourceId: input.resource.id,
|
|
6026
|
+
resourceRevision: input.resource.revision,
|
|
6027
|
+
documentRevision: input.chapterDocumentRevision,
|
|
6028
|
+
promotedBy: input.actor,
|
|
6029
|
+
promotedAt: input.promotedAt
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
};
|
|
6033
|
+
}
|
|
6034
|
+
|
|
6035
|
+
// src/builder-library-canonical.ts
|
|
6036
|
+
var encoder3 = new TextEncoder();
|
|
6037
|
+
function canonicalValue3(value) {
|
|
6038
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return JSON.stringify(value);
|
|
6039
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue3).join(",")}]`;
|
|
6040
|
+
if (typeof value !== "object") throw new TypeError("Builder Library contains an unsupported value");
|
|
6041
|
+
const record4 = value;
|
|
6042
|
+
return `{${Object.keys(record4).sort().flatMap((key) => record4[key] === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue3(record4[key])}`]).join(",")}}`;
|
|
6043
|
+
}
|
|
6044
|
+
function canonicalBuilderLibrary(value) {
|
|
6045
|
+
return canonicalValue3(value);
|
|
6046
|
+
}
|
|
6047
|
+
async function builderLibraryDigest(document) {
|
|
6048
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalBuilderLibrary(document)));
|
|
6049
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
6050
|
+
}
|
|
6051
|
+
|
|
6052
|
+
// src/builder-library-store.ts
|
|
6053
|
+
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
6054
|
+
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
6055
|
+
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
6056
|
+
const rows4 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
6057
|
+
return Array.isArray(rows4) ? rows4.map(control2).filter(Boolean) : [];
|
|
6058
|
+
}
|
|
6059
|
+
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
6060
|
+
const rows4 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
6061
|
+
return control2(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
6062
|
+
}
|
|
6063
|
+
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
6064
|
+
const id = `builder-library-local:${environment}`;
|
|
6065
|
+
const rows4 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
6066
|
+
return local2(Array.isArray(rows4) ? rows4[0] : void 0);
|
|
6067
|
+
}
|
|
6068
|
+
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
6069
|
+
const { document, digest } = envelope;
|
|
6070
|
+
const target = document.target;
|
|
6071
|
+
const id = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
6072
|
+
const ops = [];
|
|
6073
|
+
if (previous?.active) ops.push({ t: "update", ns: "builderLibraryControls", id: previous.id, attrs: { active: false } });
|
|
6074
|
+
ops.push({ t: "update", ns: "builderLibraryControls", id, attrs: {
|
|
6075
|
+
id,
|
|
6076
|
+
sourceId: document.source.appId,
|
|
6077
|
+
targetId: target.appId,
|
|
6078
|
+
environment: target.environment,
|
|
6079
|
+
revision: document.revision,
|
|
6080
|
+
digest,
|
|
6081
|
+
document,
|
|
6082
|
+
active: true,
|
|
6083
|
+
receivedAt
|
|
6084
|
+
} });
|
|
6085
|
+
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest}` });
|
|
6086
|
+
}
|
|
6087
|
+
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
6088
|
+
const id = `builder-library-local:${environment}`;
|
|
6089
|
+
const current = await loadLocalBuilderLibrary(db, document.source.appId, environment);
|
|
6090
|
+
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("builder_library_revision_conflict");
|
|
6091
|
+
const row = { id, appId: document.source.appId, environment, revision: document.revision, document, updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
6092
|
+
await db.transact([{ t: "update", ns: "builderLibraryLocal", id, attrs: row }], {
|
|
6093
|
+
mutationId: `builder-library-local:${environment}:${document.revision}`,
|
|
6094
|
+
guards: current ? [{ ns: "builderLibraryLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "builderLibraryLocal", id, exists: false }]
|
|
6095
|
+
});
|
|
6096
|
+
return row;
|
|
6097
|
+
}
|
|
6098
|
+
|
|
6099
|
+
// src/worker-routes-builder-library.ts
|
|
6100
|
+
var BODY_CAP2 = 1e6;
|
|
6101
|
+
async function verifySource(req, db, sources) {
|
|
6102
|
+
for (const source of sources) {
|
|
6103
|
+
const secret = await getVaultSecret(db, source.secretName);
|
|
6104
|
+
if (!secret) continue;
|
|
6105
|
+
const verified = await (0, import_db10.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
|
|
6106
|
+
if (verified.ok) return { source, body: verified.body };
|
|
6107
|
+
}
|
|
6108
|
+
return null;
|
|
6109
|
+
}
|
|
6110
|
+
var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
6111
|
+
if (req.method !== "POST" || url.pathname !== BUILDER_LIBRARY_CONTROL_PATH) return null;
|
|
6112
|
+
const settings = ctx.chapter.builderLibrary;
|
|
6113
|
+
if (!settings) return json({ error: "not found" }, 404);
|
|
6114
|
+
const db = ctx.makeDb(env);
|
|
6115
|
+
const verified = await verifySource(req, db, settings.sources);
|
|
6116
|
+
if (!verified) return json({ error: "unauthorized" }, 401);
|
|
6117
|
+
let input;
|
|
6118
|
+
try {
|
|
6119
|
+
input = JSON.parse(verified.body);
|
|
6120
|
+
} catch {
|
|
6121
|
+
return json({ ok: false, reason: "invalid_document", errors: ["request body must be JSON"] }, 400);
|
|
6122
|
+
}
|
|
6123
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
6124
|
+
const sourceType = ctx.chapter.mode === "hub" ? "chapter" : "built_not_found";
|
|
6125
|
+
const checked = validateBuilderLibraryDocument(input.document, {
|
|
6126
|
+
sourceType,
|
|
6127
|
+
sourceAppId: verified.source.sourceId,
|
|
6128
|
+
targetAppId: ctx.chapter.id
|
|
6129
|
+
});
|
|
6130
|
+
if (!checked.ok) return json({ ok: false, reason: "invalid_document", errors: checked.errors }, 400);
|
|
6131
|
+
const document = checked.value;
|
|
6132
|
+
if (ctx.chapter.mode === "chapter" && document.resources.some((resource) => resource.scope.kind !== "global")) {
|
|
6133
|
+
return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global resources to a Chapter"] }, 400);
|
|
6134
|
+
}
|
|
6135
|
+
if (!document.target || document.target.environment !== env.ODLA_ENV) return json({ ok: false, reason: "wrong_environment" }, 409);
|
|
6136
|
+
if (!digest || await builderLibraryDigest(document) !== digest) return json({ ok: false, reason: "invalid_digest" }, 400);
|
|
6137
|
+
const latest = await loadLatestBuilderLibraryControl(db, document.source.appId, ctx.chapter.id, env.ODLA_ENV);
|
|
6138
|
+
if (latest && document.revision < latest.revision) return json({ ok: false, reason: "stale_revision" }, 409);
|
|
6139
|
+
if (latest && document.revision === latest.revision) {
|
|
6140
|
+
if (latest.digest !== digest) return json({ ok: false, reason: "conflicting_revision" }, 409);
|
|
6141
|
+
return json({ ok: true, target: document.target, revision: latest.revision, digest: latest.digest, status: "replayed", acknowledgedAt: latest.receivedAt });
|
|
6142
|
+
}
|
|
6143
|
+
const receivedAt = Date.now();
|
|
6144
|
+
try {
|
|
6145
|
+
await materializeBuilderLibraryControl(db, { document, digest }, latest, receivedAt);
|
|
6146
|
+
} catch {
|
|
6147
|
+
return json({ ok: false, reason: "materialization_failed" }, 503);
|
|
6148
|
+
}
|
|
6149
|
+
return json({ ok: true, target: document.target, revision: document.revision, digest, status: "accepted", acknowledgedAt: receivedAt });
|
|
6150
|
+
};
|
|
6151
|
+
function actorFor(user) {
|
|
6152
|
+
return user.email ?? user.userId;
|
|
6153
|
+
}
|
|
6154
|
+
function withLocalAuthority2(value, mode, appId, revision, now, actor, current) {
|
|
6155
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
6156
|
+
const raw = value;
|
|
6157
|
+
const currentById = new Map((current?.resources ?? []).map((item) => [item.id, item]));
|
|
6158
|
+
const resources = Array.isArray(raw.resources) ? raw.resources.map((input) => {
|
|
6159
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
6160
|
+
const item = input;
|
|
6161
|
+
const existing = typeof item.id === "string" ? currentById.get(item.id) : void 0;
|
|
6162
|
+
return {
|
|
6163
|
+
...item,
|
|
6164
|
+
revision: (existing?.revision ?? 0) + 1,
|
|
6165
|
+
provenance: {
|
|
6166
|
+
createdBy: existing?.provenance.createdBy ?? actor,
|
|
6167
|
+
createdAt: existing?.provenance.createdAt ?? now,
|
|
6168
|
+
updatedBy: actor,
|
|
6169
|
+
updatedAt: now,
|
|
6170
|
+
...existing?.provenance.promotedFrom ? { promotedFrom: existing.provenance.promotedFrom } : {}
|
|
6171
|
+
}
|
|
6172
|
+
};
|
|
6173
|
+
}) : raw.resources;
|
|
6174
|
+
return {
|
|
6175
|
+
...raw,
|
|
6176
|
+
schemaVersion: 1,
|
|
6177
|
+
revision,
|
|
6178
|
+
generatedAt: now,
|
|
6179
|
+
source: { type: mode === "hub" ? "built_not_found" : "chapter", appId },
|
|
6180
|
+
resources
|
|
6181
|
+
};
|
|
6182
|
+
}
|
|
6183
|
+
var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
6184
|
+
const paths = /* @__PURE__ */ new Set([BUILDER_LIBRARY_ADMIN_PATH, BUILDER_LIBRARY_MANIFEST_PATH, BUILDER_LIBRARY_PROMOTE_PATH]);
|
|
6185
|
+
if (!paths.has(url.pathname)) return null;
|
|
6186
|
+
if (!ctx.chapter.builderLibrary) return json({ error: "not found" }, 404);
|
|
6187
|
+
const user = await ctx.verifyUser(req, env);
|
|
6188
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
6189
|
+
const db = ctx.makeDb(env);
|
|
6190
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
6191
|
+
const [local3, controls] = await Promise.all([
|
|
6192
|
+
loadLocalBuilderLibrary(db, ctx.chapter.id, env.ODLA_ENV),
|
|
6193
|
+
loadActiveBuilderLibraryControls(db, ctx.chapter.id, env.ODLA_ENV)
|
|
6194
|
+
]);
|
|
6195
|
+
if (url.pathname === BUILDER_LIBRARY_MANIFEST_PATH) {
|
|
6196
|
+
if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
6197
|
+
const chapterId = url.searchParams.get("chapterId")?.trim();
|
|
6198
|
+
if (ctx.chapter.mode === "hub" && (!chapterId || !ctx.chapter.builderLibrary.sources.some((source) => source.sourceId === chapterId))) {
|
|
6199
|
+
return json({ error: "chapterId must name a configured Chapter source" }, 400);
|
|
6200
|
+
}
|
|
6201
|
+
const viewer = ctx.chapter.mode === "hub" ? { kind: "chapter", chapterId } : { kind: "chapter", chapterId: ctx.chapter.id };
|
|
6202
|
+
const documents = [
|
|
6203
|
+
...local3 ? [local3.document] : [],
|
|
6204
|
+
...controls.filter((row) => ctx.chapter.mode === "chapter" || row.sourceId === chapterId).map((row) => row.document)
|
|
6205
|
+
];
|
|
6206
|
+
return json(buildBuilderLibraryManifest(documents, viewer));
|
|
6207
|
+
}
|
|
6208
|
+
if (url.pathname === BUILDER_LIBRARY_PROMOTE_PATH) {
|
|
6209
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
6210
|
+
if (ctx.chapter.mode !== "hub") return json({ error: "only Built Not Found can promote Chapter material" }, 403);
|
|
6211
|
+
let input2;
|
|
6212
|
+
try {
|
|
6213
|
+
input2 = await req.json();
|
|
6214
|
+
} catch {
|
|
6215
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
6216
|
+
}
|
|
6217
|
+
const chapterId = typeof input2.chapterId === "string" ? input2.chapterId : "";
|
|
6218
|
+
const resourceId = typeof input2.resourceId === "string" ? input2.resourceId : "";
|
|
6219
|
+
const expectedRevision2 = Number(input2.expectedRevision);
|
|
6220
|
+
const source = controls.find((row) => row.sourceId === chapterId);
|
|
6221
|
+
const resource = source?.document.resources.find((item) => item.id === resourceId);
|
|
6222
|
+
if (!source || !resource) return json({ error: "Chapter resource not found" }, 404);
|
|
6223
|
+
if (!Number.isInteger(expectedRevision2) || expectedRevision2 !== (local3?.revision ?? 0)) return json({ error: "Builder Library changed; reload before promoting" }, 409);
|
|
6224
|
+
const now2 = Date.now();
|
|
6225
|
+
let promoted;
|
|
6226
|
+
try {
|
|
6227
|
+
promoted = promoteBuilderLibraryResource({ resource, chapterDocumentRevision: source.revision, actor: actorFor(user), promotedAt: now2, ...typeof input2.globalId === "string" ? { globalId: input2.globalId } : {} });
|
|
6228
|
+
} catch {
|
|
6229
|
+
return json({ error: "resource cannot be promoted" }, 400);
|
|
6230
|
+
}
|
|
6231
|
+
if (local3?.document.resources.some((item) => item.id === promoted.id)) return json({ error: `global resource id "${promoted.id}" already exists` }, 409);
|
|
6232
|
+
const document = {
|
|
6233
|
+
schemaVersion: 1,
|
|
6234
|
+
revision: expectedRevision2 + 1,
|
|
6235
|
+
generatedAt: now2,
|
|
6236
|
+
source: { type: "built_not_found", appId: ctx.chapter.id },
|
|
6237
|
+
resources: [...local3?.document.resources ?? [], promoted]
|
|
6238
|
+
};
|
|
6239
|
+
const checked2 = validateBuilderLibraryDocument(document, { sourceType: "built_not_found", sourceAppId: ctx.chapter.id });
|
|
6240
|
+
if (!checked2.ok) return json({ error: "promoted resource is invalid", errors: checked2.errors }, 400);
|
|
6241
|
+
try {
|
|
6242
|
+
const saved = await saveLocalBuilderLibrary(db, checked2.value, env.ODLA_ENV, expectedRevision2, now2, actorFor(user));
|
|
6243
|
+
return json({ ok: true, revision: saved.revision, resource: promoted });
|
|
6244
|
+
} catch {
|
|
6245
|
+
return json({ error: "Builder Library changed; reload before promoting" }, 409);
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
if (req.method === "GET") {
|
|
6249
|
+
return json({
|
|
6250
|
+
mode: ctx.chapter.mode,
|
|
6251
|
+
local: local3 ? { revision: local3.revision, document: local3.document } : null,
|
|
6252
|
+
sources: controls.map(({ sourceId, revision, digest, document, receivedAt }) => ({ sourceId, revision, digest, document, receivedAt })),
|
|
6253
|
+
localAuthoring: ctx.chapter.builderLibrary.localAuthoring,
|
|
6254
|
+
visibility: { builtNotFoundSeesChapterSources: ctx.chapter.mode === "hub", chaptersSeeSiblingSources: false }
|
|
6255
|
+
});
|
|
6256
|
+
}
|
|
6257
|
+
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
6258
|
+
if (!ctx.chapter.builderLibrary.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
6259
|
+
const raw = await req.text();
|
|
6260
|
+
if (raw.length > BODY_CAP2) return json({ error: "request body too large" }, 413);
|
|
6261
|
+
let input;
|
|
6262
|
+
try {
|
|
6263
|
+
input = JSON.parse(raw);
|
|
6264
|
+
} catch {
|
|
6265
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
6266
|
+
}
|
|
6267
|
+
const expectedRevision = Number(input.expectedRevision);
|
|
6268
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision < 0) return json({ error: "expectedRevision must be a non-negative integer" }, 400);
|
|
6269
|
+
const now = Date.now();
|
|
6270
|
+
const actor = actorFor(user);
|
|
6271
|
+
const authoritative = withLocalAuthority2(input.document, ctx.chapter.mode, ctx.chapter.id, expectedRevision + 1, now, actor, local3?.document);
|
|
6272
|
+
const checked = validateBuilderLibraryDocument(authoritative, { sourceType: ctx.chapter.mode === "hub" ? "built_not_found" : "chapter", sourceAppId: ctx.chapter.id });
|
|
6273
|
+
if (!checked.ok) return json({ error: "invalid Builder Library", errors: checked.errors }, 400);
|
|
6274
|
+
try {
|
|
6275
|
+
const saved = await saveLocalBuilderLibrary(db, checked.value, env.ODLA_ENV, expectedRevision, now, actor);
|
|
6276
|
+
return json({ ok: true, revision: saved.revision, document: saved.document });
|
|
6277
|
+
} catch (error) {
|
|
6278
|
+
if (error instanceof Error && error.message === "builder_library_revision_conflict") return json({ error: "Builder Library changed; reload before saving" }, 409);
|
|
6279
|
+
return json({ error: "Builder Library could not be saved" }, 503);
|
|
6280
|
+
}
|
|
6281
|
+
};
|
|
6282
|
+
|
|
6283
|
+
// src/signup-control-delivery.ts
|
|
6284
|
+
var import_db11 = require("@odla-ai/db");
|
|
5799
6285
|
async function deliverSignupControl(options) {
|
|
5800
6286
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
5801
6287
|
const body = JSON.stringify(options.envelope);
|
|
5802
|
-
const signed = await (0,
|
|
6288
|
+
const signed = await (0, import_db11.signFederatedRequest)({
|
|
5803
6289
|
secret: options.secret,
|
|
5804
6290
|
sender: options.sender,
|
|
5805
6291
|
method: "POST",
|
|
@@ -5820,11 +6306,11 @@ async function deliverSignupControl(options) {
|
|
|
5820
6306
|
}
|
|
5821
6307
|
|
|
5822
6308
|
// src/member-content-delivery.ts
|
|
5823
|
-
var
|
|
6309
|
+
var import_db12 = require("@odla-ai/db");
|
|
5824
6310
|
async function deliverMemberContent(options) {
|
|
5825
6311
|
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
5826
6312
|
const body = JSON.stringify(options.envelope);
|
|
5827
|
-
const signed = await (0,
|
|
6313
|
+
const signed = await (0, import_db12.signFederatedRequest)({
|
|
5828
6314
|
secret: options.secret,
|
|
5829
6315
|
sender: options.sender,
|
|
5830
6316
|
method: "POST",
|
|
@@ -5844,12 +6330,27 @@ async function deliverMemberContent(options) {
|
|
|
5844
6330
|
return result;
|
|
5845
6331
|
}
|
|
5846
6332
|
|
|
6333
|
+
// src/builder-library-delivery.ts
|
|
6334
|
+
var import_db13 = require("@odla-ai/db");
|
|
6335
|
+
async function deliverBuilderLibrary(options) {
|
|
6336
|
+
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
6337
|
+
const body = JSON.stringify(options.envelope);
|
|
6338
|
+
const signed = await (0, import_db13.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
6339
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
6340
|
+
const result = await response.json();
|
|
6341
|
+
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
6342
|
+
const failure = result;
|
|
6343
|
+
throw new Error(`Builder Library rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
|
|
6344
|
+
}
|
|
6345
|
+
return result;
|
|
6346
|
+
}
|
|
6347
|
+
|
|
5847
6348
|
// src/admission-grants-delivery.ts
|
|
5848
|
-
var
|
|
6349
|
+
var import_db14 = require("@odla-ai/db");
|
|
5849
6350
|
async function deliverAdmissionGrants(options) {
|
|
5850
6351
|
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
5851
6352
|
const body = JSON.stringify(options.envelope);
|
|
5852
|
-
const signed = await (0,
|
|
6353
|
+
const signed = await (0, import_db14.signFederatedRequest)({
|
|
5853
6354
|
secret: options.secret,
|
|
5854
6355
|
sender: options.sender,
|
|
5855
6356
|
method: "POST",
|
|
@@ -6009,6 +6510,7 @@ var BUILTIN_ROUTES = [
|
|
|
6009
6510
|
handleNetworkShared,
|
|
6010
6511
|
handleSignupControl,
|
|
6011
6512
|
handleMemberContentControl,
|
|
6513
|
+
handleBuilderLibraryControl,
|
|
6012
6514
|
handleAdmissionGrants,
|
|
6013
6515
|
handleNetworkSnapshot,
|
|
6014
6516
|
handleNetworkRecords,
|
|
@@ -6032,6 +6534,7 @@ var BUILTIN_ROUTES = [
|
|
|
6032
6534
|
handleAdminDashboard,
|
|
6033
6535
|
handleAdminBilling,
|
|
6034
6536
|
handleAdminMemberContent,
|
|
6537
|
+
handleAdminBuilderLibrary,
|
|
6035
6538
|
// Lifecycle actions
|
|
6036
6539
|
handleAdminMeetingReschedule,
|
|
6037
6540
|
handleAdminMeetingCancel,
|