@odla-ai/chapter 0.39.0 → 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.
@@ -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 record3 = value;
552
- const entries = Object.keys(record3).sort().flatMap((key) => {
553
- const item = record3[key];
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 text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
754
- return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
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 record3 = toRecord(u);
946
- if (record3) out.push(record3);
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(record3, fallbackSourceId = "legacy-source") {
1004
- if ("input" in record3 && record3.version === 2) {
1004
+ function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
1005
+ if ("input" in record4 && record4.version === 2) {
1005
1006
  return {
1006
1007
  version: 2,
1007
- sourceId: record3.source.siteId,
1008
- sourceRecordId: record3.source.recordId,
1009
- type: record3.type,
1010
- input: record3.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 record3) {
1014
+ if ("input" in record4) {
1014
1015
  return {
1015
1016
  version: 1,
1016
1017
  sourceId: fallbackSourceId,
1017
- sourceRecordId: record3.hubRecordId,
1018
- type: record3.type,
1019
- input: record3.input
1018
+ sourceRecordId: record4.hubRecordId,
1019
+ type: record4.type,
1020
+ input: record4.input
1020
1021
  };
1021
1022
  }
1022
- if ("type" in record3 && record3.type === "company") {
1023
- const input = { name: record3.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 (record3[key]) input[key] = record3[key];
1026
+ if (record4[key]) input[key] = record4[key];
1026
1027
  }
1027
1028
  return {
1028
1029
  version: 1,
1029
1030
  sourceId: fallbackSourceId,
1030
- sourceRecordId: record3.hubRecordId,
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: record3.hubRecordId,
1039
+ sourceRecordId: record4.hubRecordId,
1039
1040
  type: "person",
1040
- input: sharedPersonInput(record3)
1041
+ input: sharedPersonInput(record4)
1041
1042
  };
1042
1043
  }
1043
- function sharedRecordFromCrm(crm, record3, target, sourceId) {
1044
- if (target.fields && !target.fields[record3.type]) {
1045
- throw new Error(`${target.name} does not accept "${record3.type}" records`);
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(record3.type);
1048
- const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
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 "${record3.type}" records`);
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 = record3.fields?.[field];
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] = record3.name;
1059
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
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, record3, tag) {
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
- record3.sourceId,
1077
- record3.sourceRecordId
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(record3.type);
1087
+ const def = deps.crm.type(record4.type);
1087
1088
  const emailField = def.emailField;
1088
- if (emailField && typeof record3.input[emailField] === "string") {
1089
- const primaryEmail = record3.input[emailField].toLowerCase();
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: record3.type, primaryEmail }, limit: 1 } }
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 = record3.input.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: record3.type, [domainSlot]: domain }, limit: 1 } }
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 = record3.input[nameField];
1105
- if (record3.type === "company" && typeof name === "string" && name.trim()) {
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: record3.type, name: name.trim() }, limit: 1 } }
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 record3 = normalizeSharedRecord(shared, options.sourceId);
1115
- if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
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 !== record3.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(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
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, record3, tag);
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: record3.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(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
1130
+ recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
1130
1131
  await (0, import_crm2.createRecord)({ ...crmDeps3, newId: () => recordId }, {
1131
- type: record3.type,
1132
- input: record3.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: record3.sourceId,
1143
- sourceRecordId: record3.sourceRecordId,
1143
+ sourceId: record4.sourceId,
1144
+ sourceRecordId: record4.sourceRecordId,
1144
1145
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1145
- payloadVersion: record3.version
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 text3 = redirect ? `(dev redirect; original recipient: ${input.to})
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: text3, redirected: redirect };
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 record3 = normalizeSharedRecord(payload, sourceId);
2071
- return json({ recordId, type: record3.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 text3 = value.trim();
2126
- if (required && !text3) errors.push(`${path} is required`);
2127
- if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2128
- return text3;
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 text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2178
- return text3 ? [text3] : [];
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 record3 = value;
2509
- const entries = Object.keys(record3).sort().flatMap((key) => {
2510
- const item = record3[key];
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 local2 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
2941
- if (local2) {
2942
- const localCheck = validateMemberContentDocument(local2.document, {
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,
@@ -3963,8 +3964,8 @@ async function startSubscription(req, env, ctx) {
3963
3964
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
3964
3965
  if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
3965
3966
  if (typeof app.giftOfferId === "string") {
3966
- const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3967
- const offer = findGiftOffer(control2?.document.giftOffers, app.giftOfferId, Date.now());
3967
+ const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3968
+ const offer = findGiftOffer(control3?.document.giftOffers, app.giftOfferId, Date.now());
3968
3969
  if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
3969
3970
  return json({ error: "gift offer is unavailable" }, 409);
3970
3971
  }
@@ -4129,14 +4130,14 @@ var handleGifts = async (req, url, env, ctx) => {
4129
4130
  if (req.method === "GET" && url.pathname === "/api/gifts") {
4130
4131
  const user = await userFor(req, env, ctx);
4131
4132
  if (!user) return json({ error: "unauthorized" }, 401);
4132
- const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4133
+ const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4133
4134
  const found = await db.query({ giftClaims: { $: { where: {
4134
4135
  purchaserEmailNormalized: normalizeChapterEmail(user.email),
4135
4136
  chapterId: ctx.chapter.id
4136
4137
  }, limit: 200 } } });
4137
4138
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4138
4139
  return json({
4139
- offers: publicGiftOffers(control2?.document.giftOffers, Date.now()),
4140
+ offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
4140
4141
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
4141
4142
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
4142
4143
  });
@@ -4152,8 +4153,8 @@ var handleGifts = async (req, url, env, ctx) => {
4152
4153
  }
4153
4154
  const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
4154
4155
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
4155
- const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4156
- const offer = findGiftOffer(control2?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
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());
4157
4158
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
4158
4159
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4159
4160
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
@@ -4985,11 +4986,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
4985
4986
  for (const key of EMAIL_TEMPLATE_NAMES) {
4986
4987
  const t = templates[key];
4987
4988
  const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
4988
- const text3 = typeof t?.text === "string" ? t.text : "";
4989
- if (!subject || !text3.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
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);
4990
4991
  if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
4991
- if (text3.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
4992
- clean[key] = { subject, text: text3, enabled: t?.enabled !== false };
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 };
4993
4994
  }
4994
4995
  const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4995
4996
  const notificationEmail = str2(body.notificationEmail).trim();
@@ -5109,7 +5110,7 @@ function fetchNetworkTarget(env, target, destination, init) {
5109
5110
  // src/worker-network-push.ts
5110
5111
  var import_crm5 = require("@odla-ai/crm");
5111
5112
  var import_db6 = require("@odla-ai/db");
5112
- async function pushNetworkRecord(db, env, ctx, target, record3) {
5113
+ async function pushNetworkRecord(db, env, ctx, target, record4) {
5113
5114
  const secret = await getVaultSecret(db, target.secretName);
5114
5115
  const crmDeps3 = {
5115
5116
  crm: ctx.chapter.crm,
@@ -5120,7 +5121,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
5120
5121
  if (!secret) {
5121
5122
  const error = `vault secret "${target.secretName}" is missing`;
5122
5123
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5123
- recordId: record3.id,
5124
+ recordId: record4.id,
5124
5125
  targetId: target.id,
5125
5126
  status: "failed",
5126
5127
  payloadVersion: 2,
@@ -5130,7 +5131,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
5130
5131
  }
5131
5132
  let payload;
5132
5133
  try {
5133
- payload = sharedRecordFromCrm(ctx.chapter.crm, record3, target, ctx.chapter.id);
5134
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record4, target, ctx.chapter.id);
5134
5135
  } catch (err) {
5135
5136
  return {
5136
5137
  id: target.id,
@@ -5159,7 +5160,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
5159
5160
  if (!res.ok) {
5160
5161
  const error = responseBody.error ?? "follower rejected the record";
5161
5162
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5162
- recordId: record3.id,
5163
+ recordId: record4.id,
5163
5164
  targetId: target.id,
5164
5165
  status: "failed",
5165
5166
  payloadVersion: 2,
@@ -5174,7 +5175,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
5174
5175
  };
5175
5176
  }
5176
5177
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5177
- recordId: record3.id,
5178
+ recordId: record4.id,
5178
5179
  targetId: target.id,
5179
5180
  status: "delivered",
5180
5181
  payloadVersion: 2,
@@ -5190,7 +5191,7 @@ async function pushNetworkRecord(db, env, ctx, target, record3) {
5190
5191
  } catch (err) {
5191
5192
  const error = err instanceof Error ? err.message : "delivery failed";
5192
5193
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5193
- recordId: record3.id,
5194
+ recordId: record4.id,
5194
5195
  targetId: target.id,
5195
5196
  status: "failed",
5196
5197
  payloadVersion: 2,
@@ -5328,10 +5329,10 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
5328
5329
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
5329
5330
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
5330
5331
  if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
5331
- const record3 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5332
- if (!record3) return json({ error: "record not found" }, 404);
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);
5333
5334
  const results = await Promise.all(
5334
- targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record3))
5335
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
5335
5336
  );
5336
5337
  return json({ ok: results.every((result) => result.ok), results });
5337
5338
  };
@@ -5356,18 +5357,18 @@ function integer(value, fallback, min, max) {
5356
5357
  const parsed = Number(value);
5357
5358
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
5358
5359
  }
5359
- function projectRecord(record3, allowed2) {
5360
+ function projectRecord(record4, allowed2) {
5360
5361
  const fields = Object.fromEntries(
5361
- allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record3.fields, name) ? [[name, record3.fields[name]]] : [])
5362
+ allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
5362
5363
  );
5363
5364
  return {
5364
- id: record3.id,
5365
- type: record3.type,
5366
- name: record3.name,
5367
- ...record3.stage ? { stage: record3.stage } : {},
5365
+ id: record4.id,
5366
+ type: record4.type,
5367
+ name: record4.name,
5368
+ ...record4.stage ? { stage: record4.stage } : {},
5368
5369
  fields,
5369
- createdAt: record3.createdAt,
5370
- updatedAt: record3.updatedAt
5370
+ createdAt: record4.createdAt,
5371
+ updatedAt: record4.updatedAt
5371
5372
  };
5372
5373
  }
5373
5374
  var handleNetworkRecords = async (req, url, env, ctx) => {
@@ -5405,7 +5406,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
5405
5406
  version: 1,
5406
5407
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
5407
5408
  type,
5408
- records: page.records.map((record3) => projectRecord(record3, allowed2)),
5409
+ records: page.records.map((record4) => projectRecord(record4, allowed2)),
5409
5410
  total: page.total,
5410
5411
  limit: page.limit,
5411
5412
  offset: page.offset
@@ -5626,8 +5627,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5626
5627
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
5627
5628
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
5628
5629
  }
5629
- const record3 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
5630
- if (!record3 || record3.type !== recordType) return json({ error: "record not found" }, 404);
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);
5631
5632
  if (req.method === "GET") {
5632
5633
  return json({
5633
5634
  version: 1,
@@ -5818,12 +5819,473 @@ var handleFormation = async (req, url, env, ctx) => {
5818
5819
  }
5819
5820
  };
5820
5821
 
5821
- // src/signup-control-delivery.ts
5822
+ // src/worker-routes-builder-library.ts
5822
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");
5823
6285
  async function deliverSignupControl(options) {
5824
6286
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
5825
6287
  const body = JSON.stringify(options.envelope);
5826
- const signed = await (0, import_db10.signFederatedRequest)({
6288
+ const signed = await (0, import_db11.signFederatedRequest)({
5827
6289
  secret: options.secret,
5828
6290
  sender: options.sender,
5829
6291
  method: "POST",
@@ -5844,11 +6306,11 @@ async function deliverSignupControl(options) {
5844
6306
  }
5845
6307
 
5846
6308
  // src/member-content-delivery.ts
5847
- var import_db11 = require("@odla-ai/db");
6309
+ var import_db12 = require("@odla-ai/db");
5848
6310
  async function deliverMemberContent(options) {
5849
6311
  const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
5850
6312
  const body = JSON.stringify(options.envelope);
5851
- const signed = await (0, import_db11.signFederatedRequest)({
6313
+ const signed = await (0, import_db12.signFederatedRequest)({
5852
6314
  secret: options.secret,
5853
6315
  sender: options.sender,
5854
6316
  method: "POST",
@@ -5868,12 +6330,27 @@ async function deliverMemberContent(options) {
5868
6330
  return result;
5869
6331
  }
5870
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
+
5871
6348
  // src/admission-grants-delivery.ts
5872
- var import_db12 = require("@odla-ai/db");
6349
+ var import_db14 = require("@odla-ai/db");
5873
6350
  async function deliverAdmissionGrants(options) {
5874
6351
  const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
5875
6352
  const body = JSON.stringify(options.envelope);
5876
- const signed = await (0, import_db12.signFederatedRequest)({
6353
+ const signed = await (0, import_db14.signFederatedRequest)({
5877
6354
  secret: options.secret,
5878
6355
  sender: options.sender,
5879
6356
  method: "POST",
@@ -6033,6 +6510,7 @@ var BUILTIN_ROUTES = [
6033
6510
  handleNetworkShared,
6034
6511
  handleSignupControl,
6035
6512
  handleMemberContentControl,
6513
+ handleBuilderLibraryControl,
6036
6514
  handleAdmissionGrants,
6037
6515
  handleNetworkSnapshot,
6038
6516
  handleNetworkRecords,
@@ -6056,6 +6534,7 @@ var BUILTIN_ROUTES = [
6056
6534
  handleAdminDashboard,
6057
6535
  handleAdminBilling,
6058
6536
  handleAdminMemberContent,
6537
+ handleAdminBuilderLibrary,
6059
6538
  // Lifecycle actions
6060
6539
  handleAdminMeetingReschedule,
6061
6540
  handleAdminMeetingCancel,