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