@odla-ai/chapter 0.32.3 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/dist/{chunk-NYMLKPQF.js → chunk-UJIVBGL5.js} +65 -5
- package/dist/chunk-UJIVBGL5.js.map +1 -0
- package/dist/{copy-context-BXBEdp4p.d.ts → copy-context-c1tqPXOR.d.ts} +20 -0
- package/dist/index.cjs +277 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -2
- package/dist/index.d.ts +130 -2
- package/dist/index.js +277 -15
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +74 -42
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +633 -23
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +145 -1
- package/dist/worker/index.d.ts +145 -1
- package/dist/worker/index.js +628 -18
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +3 -0
- package/runbooks/greenfield.md +11 -0
- package/dist/chunk-NYMLKPQF.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var worker_exports = {};
|
|
22
22
|
__export(worker_exports, {
|
|
23
23
|
chapterWorker: () => chapterWorker,
|
|
24
|
-
createWorkerContext: () => createWorkerContext
|
|
24
|
+
createWorkerContext: () => createWorkerContext,
|
|
25
|
+
deliverSignupControl: () => deliverSignupControl
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(worker_exports);
|
|
27
28
|
|
|
@@ -195,6 +196,56 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
// src/member.ts
|
|
199
|
+
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
200
|
+
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
201
|
+
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
202
|
+
function resolveApplication(a) {
|
|
203
|
+
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
204
|
+
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
205
|
+
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
206
|
+
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
207
|
+
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
208
|
+
}
|
|
209
|
+
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
210
|
+
if (unsafe) {
|
|
211
|
+
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
|
|
215
|
+
if (duplicate) {
|
|
216
|
+
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
217
|
+
}
|
|
218
|
+
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
219
|
+
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
220
|
+
}
|
|
221
|
+
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
222
|
+
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
223
|
+
}
|
|
224
|
+
const conditions = a?.conditions ?? {};
|
|
225
|
+
for (const [field, declared] of Object.entries(conditions)) {
|
|
226
|
+
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
227
|
+
const expression = declared?.[key];
|
|
228
|
+
if (expression === void 0) continue;
|
|
229
|
+
if (typeof expression !== "string" || !expression.trim()) {
|
|
230
|
+
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
231
|
+
}
|
|
232
|
+
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
required,
|
|
237
|
+
optional,
|
|
238
|
+
conditions,
|
|
239
|
+
maxLen: a?.maxLen ?? {},
|
|
240
|
+
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
241
|
+
bodyCap: a?.bodyCap ?? 32768,
|
|
242
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
243
|
+
profileFields: a?.profileFields ?? [],
|
|
244
|
+
crmFields: a?.crmFields ?? [],
|
|
245
|
+
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
246
|
+
validateEmail: a?.validateEmail ?? true
|
|
247
|
+
};
|
|
248
|
+
}
|
|
198
249
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
199
250
|
function isValidEmail(value) {
|
|
200
251
|
return typeof value === "string" && EMAIL_RE.test(value);
|
|
@@ -471,6 +522,101 @@ function canApprove(status, p) {
|
|
|
471
522
|
return p.approvableFrom.includes(status);
|
|
472
523
|
}
|
|
473
524
|
|
|
525
|
+
// src/signup-control-store.ts
|
|
526
|
+
function asStored(row) {
|
|
527
|
+
if (!row || typeof row.document !== "object" || row.document === null) return null;
|
|
528
|
+
return row;
|
|
529
|
+
}
|
|
530
|
+
async function hasSignupControl(db, targetId, environment, active) {
|
|
531
|
+
const where = { targetId, environment, ...active ? { active: true } : {} };
|
|
532
|
+
if (db.aggregate) {
|
|
533
|
+
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
534
|
+
}
|
|
535
|
+
const rows2 = (await db.query({ signupControls: {} })).signupControls;
|
|
536
|
+
return Array.isArray(rows2) && rows2.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
537
|
+
}
|
|
538
|
+
async function loadLatestSignupControl(db, targetId, environment) {
|
|
539
|
+
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
540
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
541
|
+
where: { targetId, environment },
|
|
542
|
+
order: { revision: "desc" },
|
|
543
|
+
limit: 1
|
|
544
|
+
} } })).signupControls;
|
|
545
|
+
const latest = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
546
|
+
return asStored(latest);
|
|
547
|
+
}
|
|
548
|
+
async function loadActiveSignupControl(db, targetId, environment) {
|
|
549
|
+
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
550
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
551
|
+
where: { targetId, environment, active: true },
|
|
552
|
+
order: { revision: "desc" },
|
|
553
|
+
limit: 1
|
|
554
|
+
} } })).signupControls;
|
|
555
|
+
const active = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
556
|
+
return asStored(active);
|
|
557
|
+
}
|
|
558
|
+
function controlledApplication(base, document) {
|
|
559
|
+
const delegated = document?.application;
|
|
560
|
+
return delegated ? { ...base, required: delegated.required, optional: delegated.optional, conditions: { ...delegated.conditions ?? {} } } : base;
|
|
561
|
+
}
|
|
562
|
+
function validateControlledApplication(chapter, document) {
|
|
563
|
+
if (!document.application) return [];
|
|
564
|
+
const declared = /* @__PURE__ */ new Set([...chapter.application.required, ...chapter.application.optional]);
|
|
565
|
+
return [...document.application.required, ...document.application.optional].filter((field) => !declared.has(field)).map((field) => `document.application field "${field}" is not declared by this chapter schema`);
|
|
566
|
+
}
|
|
567
|
+
function groupAttrs(document) {
|
|
568
|
+
const first = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
569
|
+
const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
|
|
570
|
+
return {
|
|
571
|
+
...first ? { standardPriceCents: first.priceCents } : {},
|
|
572
|
+
foundingDiscountCents: 0,
|
|
573
|
+
stripePriceId: firstPaid?.stripePriceId ?? null,
|
|
574
|
+
...document.policy ?? {},
|
|
575
|
+
...document.scheduling ? { schedulingJson: document.scheduling } : {}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
579
|
+
const { document, digest } = envelope;
|
|
580
|
+
const currentRows = (await db.query({ tiers: { $: {
|
|
581
|
+
where: { groupId: document.target.chapterId },
|
|
582
|
+
limit: 100
|
|
583
|
+
} } })).tiers ?? [];
|
|
584
|
+
const incoming = new Set(document.tiers.map((tier) => tier.id));
|
|
585
|
+
const ops = [];
|
|
586
|
+
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
587
|
+
for (const row of currentRows) {
|
|
588
|
+
const id2 = String(row.id ?? "");
|
|
589
|
+
if (id2 && !incoming.has(id2)) ops.push({ t: "update", ns: "tiers", id: id2, attrs: { active: false } });
|
|
590
|
+
}
|
|
591
|
+
for (const tier of document.tiers) {
|
|
592
|
+
ops.push({ t: "update", ns: "tiers", id: tier.id, attrs: {
|
|
593
|
+
id: tier.id,
|
|
594
|
+
groupId: document.target.chapterId,
|
|
595
|
+
name: tier.name,
|
|
596
|
+
priceCents: tier.priceCents,
|
|
597
|
+
stripePriceId: tier.stripePriceId ?? null,
|
|
598
|
+
blurb: tier.blurb ?? null,
|
|
599
|
+
sortOrder: tier.sortOrder,
|
|
600
|
+
active: tier.available,
|
|
601
|
+
createdAt: receivedAt
|
|
602
|
+
} });
|
|
603
|
+
}
|
|
604
|
+
ops.push({ t: "update", ns: "groups", id: document.target.chapterId, attrs: groupAttrs(document) });
|
|
605
|
+
const id = `signup-control:${document.target.environment}:${document.revision}`;
|
|
606
|
+
ops.push({ t: "update", ns: "signupControls", id, attrs: {
|
|
607
|
+
id,
|
|
608
|
+
sourceId: document.source.appId,
|
|
609
|
+
targetId: document.target.chapterId,
|
|
610
|
+
environment: document.target.environment,
|
|
611
|
+
revision: document.revision,
|
|
612
|
+
digest,
|
|
613
|
+
document,
|
|
614
|
+
active: true,
|
|
615
|
+
receivedAt
|
|
616
|
+
} });
|
|
617
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest}` });
|
|
618
|
+
}
|
|
619
|
+
|
|
474
620
|
// src/session.ts
|
|
475
621
|
function applicationSummary(app) {
|
|
476
622
|
return {
|
|
@@ -813,10 +959,10 @@ function planDelivery(input) {
|
|
|
813
959
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
814
960
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
815
961
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
816
|
-
const
|
|
962
|
+
const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
817
963
|
|
|
818
964
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
819
|
-
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
965
|
+
return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
|
|
820
966
|
}
|
|
821
967
|
|
|
822
968
|
// src/notify.ts
|
|
@@ -1105,9 +1251,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1105
1251
|
group,
|
|
1106
1252
|
Boolean(stripeKey)
|
|
1107
1253
|
);
|
|
1254
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1255
|
+
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1108
1256
|
return json({
|
|
1109
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered),
|
|
1110
|
-
copy: chapter.copy.join
|
|
1257
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1258
|
+
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1259
|
+
...activeControl ? { signupControl: {
|
|
1260
|
+
revision: activeControl.revision,
|
|
1261
|
+
digest: activeControl.digest,
|
|
1262
|
+
membershipPage: activeControl.document.membershipPage,
|
|
1263
|
+
application: activeControl.document.application,
|
|
1264
|
+
theme: activeControl.document.theme
|
|
1265
|
+
} } : {}
|
|
1111
1266
|
});
|
|
1112
1267
|
}
|
|
1113
1268
|
if (req.method === "GET" && url.pathname === "/api/join/resume") {
|
|
@@ -1170,8 +1325,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1170
1325
|
}
|
|
1171
1326
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
1172
1327
|
const db = ctx.makeDb(env);
|
|
1328
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1329
|
+
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
1173
1330
|
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
1174
|
-
const result = await submitApplication(db,
|
|
1331
|
+
const result = await submitApplication(db, effectiveChapter, parsed, {
|
|
1175
1332
|
submissionId,
|
|
1176
1333
|
groupId: chapter.id,
|
|
1177
1334
|
now: Date.now(),
|
|
@@ -1180,10 +1337,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1180
1337
|
});
|
|
1181
1338
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
1182
1339
|
if (!result.duplicate) {
|
|
1183
|
-
if (
|
|
1340
|
+
if (effectiveChapter.sends.adminNotification === "submit") {
|
|
1184
1341
|
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
1185
1342
|
}
|
|
1186
|
-
await provisionApplicant(db,
|
|
1343
|
+
await provisionApplicant(db, effectiveChapter, result.id, parsed);
|
|
1187
1344
|
}
|
|
1188
1345
|
return json({
|
|
1189
1346
|
id: result.id,
|
|
@@ -1253,6 +1410,433 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
1253
1410
|
}
|
|
1254
1411
|
};
|
|
1255
1412
|
|
|
1413
|
+
// src/worker-routes-signup-control.ts
|
|
1414
|
+
var import_db3 = require("@odla-ai/db");
|
|
1415
|
+
|
|
1416
|
+
// src/signup-control-canonical.ts
|
|
1417
|
+
var encoder = new TextEncoder();
|
|
1418
|
+
function canonicalValue(value) {
|
|
1419
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
1420
|
+
return JSON.stringify(value);
|
|
1421
|
+
}
|
|
1422
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
1423
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
1424
|
+
const record = value;
|
|
1425
|
+
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
1426
|
+
const item = record[key];
|
|
1427
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
1428
|
+
});
|
|
1429
|
+
return `{${entries.join(",")}}`;
|
|
1430
|
+
}
|
|
1431
|
+
function canonicalSignupControl(value) {
|
|
1432
|
+
return canonicalValue(value);
|
|
1433
|
+
}
|
|
1434
|
+
async function signupControlDigest(document) {
|
|
1435
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
1436
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/signup-control-contract.ts
|
|
1440
|
+
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
1441
|
+
|
|
1442
|
+
// src/membership-page.ts
|
|
1443
|
+
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
1444
|
+
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1445
|
+
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
1446
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
1447
|
+
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
1448
|
+
"tierId",
|
|
1449
|
+
"variant",
|
|
1450
|
+
"badge",
|
|
1451
|
+
"summaryMarkdown",
|
|
1452
|
+
"priceEyebrow",
|
|
1453
|
+
"compareAtPriceCents",
|
|
1454
|
+
"billingLabel",
|
|
1455
|
+
"priceNoteMarkdown",
|
|
1456
|
+
"capacity",
|
|
1457
|
+
"benefits",
|
|
1458
|
+
"callout",
|
|
1459
|
+
"ctaLabel"
|
|
1460
|
+
]);
|
|
1461
|
+
var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
1462
|
+
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
1463
|
+
var MAX_TIERS = 20;
|
|
1464
|
+
var MAX_BENEFITS = 24;
|
|
1465
|
+
var MAX_MARKDOWN = 65536;
|
|
1466
|
+
var MAX_LABEL = 160;
|
|
1467
|
+
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1468
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
1469
|
+
for (const key of Object.keys(value)) {
|
|
1470
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function boundedString(value, path, errors, max, required = false) {
|
|
1474
|
+
if (value === void 0) {
|
|
1475
|
+
if (required) errors.push(`${path} is required`);
|
|
1476
|
+
return void 0;
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof value !== "string") {
|
|
1479
|
+
errors.push(`${path} must be a string`);
|
|
1480
|
+
return void 0;
|
|
1481
|
+
}
|
|
1482
|
+
const text2 = value.trim();
|
|
1483
|
+
if (required && !text2) errors.push(`${path} is required`);
|
|
1484
|
+
if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
1485
|
+
return text2;
|
|
1486
|
+
}
|
|
1487
|
+
function validateMembershipPageDocument(input, options = {}) {
|
|
1488
|
+
const errors = [];
|
|
1489
|
+
const root = objectRecord(input);
|
|
1490
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1491
|
+
rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
|
|
1492
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1493
|
+
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
1494
|
+
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
1495
|
+
}
|
|
1496
|
+
if (!Array.isArray(root.tiers)) errors.push("document.tiers must be an array");
|
|
1497
|
+
const rawTiers = Array.isArray(root.tiers) ? root.tiers : [];
|
|
1498
|
+
if (Array.isArray(root.tiers) && rawTiers.length === 0) errors.push("document.tiers must include at least one tier");
|
|
1499
|
+
if (rawTiers.length > MAX_TIERS) errors.push(`document.tiers supports at most ${MAX_TIERS} entries`);
|
|
1500
|
+
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
1501
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1502
|
+
const tiers = [];
|
|
1503
|
+
for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
1504
|
+
const path = `document.tiers[${index}]`;
|
|
1505
|
+
const item = objectRecord(raw);
|
|
1506
|
+
if (!item) {
|
|
1507
|
+
errors.push(`${path} must be an object`);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
rejectUnknownKeys(item, TIER_KEYS, path, errors);
|
|
1511
|
+
const tierId = boundedString(item.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
1512
|
+
if (tierId && !TIER_ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
1513
|
+
if (seen.has(tierId)) errors.push(`${path}.tierId duplicates "${tierId}"`);
|
|
1514
|
+
seen.add(tierId);
|
|
1515
|
+
if (authority.size && !authority.has(tierId)) errors.push(`${path}.tierId "${tierId}" is not an offered tier`);
|
|
1516
|
+
const variant = item.variant;
|
|
1517
|
+
if (variant !== void 0 && (typeof variant !== "string" || !VARIANTS.has(variant))) {
|
|
1518
|
+
errors.push(`${path}.variant must be standard, featured, or supporting`);
|
|
1519
|
+
}
|
|
1520
|
+
const compareAt = item.compareAtPriceCents;
|
|
1521
|
+
if (compareAt !== void 0 && (!Number.isInteger(compareAt) || Number(compareAt) < 0 || Number(compareAt) > 1e8)) {
|
|
1522
|
+
errors.push(`${path}.compareAtPriceCents must be an integer from 0 to 100,000,000`);
|
|
1523
|
+
}
|
|
1524
|
+
const actual = authority.get(tierId)?.priceCents;
|
|
1525
|
+
if (Number.isInteger(compareAt) && actual !== void 0 && Number(compareAt) <= actual) {
|
|
1526
|
+
errors.push(`${path}.compareAtPriceCents must be greater than the authoritative tier price`);
|
|
1527
|
+
}
|
|
1528
|
+
let benefits;
|
|
1529
|
+
if (item.benefits !== void 0) {
|
|
1530
|
+
if (!Array.isArray(item.benefits)) errors.push(`${path}.benefits must be an array`);
|
|
1531
|
+
else {
|
|
1532
|
+
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
1533
|
+
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
1534
|
+
const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
1535
|
+
return text2 ? [text2] : [];
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
let callout;
|
|
1540
|
+
if (item.callout !== void 0) {
|
|
1541
|
+
const rawCallout = objectRecord(item.callout);
|
|
1542
|
+
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
1543
|
+
else {
|
|
1544
|
+
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
1545
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
|
|
1546
|
+
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
1547
|
+
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
let capacity;
|
|
1551
|
+
if (item.capacity !== void 0) {
|
|
1552
|
+
const rawCapacity = objectRecord(item.capacity);
|
|
1553
|
+
if (!rawCapacity) errors.push(`${path}.capacity must be an object`);
|
|
1554
|
+
else {
|
|
1555
|
+
rejectUnknownKeys(rawCapacity, CAPACITY_KEYS, `${path}.capacity`, errors);
|
|
1556
|
+
const total = rawCapacity.total;
|
|
1557
|
+
if (!Number.isInteger(total) || Number(total) < 1 || Number(total) > 1e6) {
|
|
1558
|
+
errors.push(`${path}.capacity.total must be an integer from 1 to 1,000,000`);
|
|
1559
|
+
}
|
|
1560
|
+
const label = boundedString(rawCapacity.label, `${path}.capacity.label`, errors, MAX_LABEL);
|
|
1561
|
+
if (Number.isInteger(total) && Number(total) >= 1 && Number(total) <= 1e6) {
|
|
1562
|
+
capacity = { total: Number(total), ...label ? { label } : {} };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
1567
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
|
|
1568
|
+
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
1569
|
+
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
1570
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
|
|
1571
|
+
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
1572
|
+
tiers.push({
|
|
1573
|
+
tierId,
|
|
1574
|
+
...typeof variant === "string" && VARIANTS.has(variant) ? { variant } : {},
|
|
1575
|
+
...badge ? { badge } : {},
|
|
1576
|
+
...summaryMarkdown ? { summaryMarkdown } : {},
|
|
1577
|
+
...priceEyebrow ? { priceEyebrow } : {},
|
|
1578
|
+
...Number.isInteger(compareAt) ? { compareAtPriceCents: Number(compareAt) } : {},
|
|
1579
|
+
...billingLabel ? { billingLabel } : {},
|
|
1580
|
+
...priceNoteMarkdown ? { priceNoteMarkdown } : {},
|
|
1581
|
+
...capacity ? { capacity } : {},
|
|
1582
|
+
...benefits ? { benefits } : {},
|
|
1583
|
+
...callout ? { callout } : {},
|
|
1584
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
if (authority.size) {
|
|
1588
|
+
for (const tierId of authority.keys()) {
|
|
1589
|
+
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
|
|
1593
|
+
if (errors.length) return { ok: false, errors };
|
|
1594
|
+
return {
|
|
1595
|
+
ok: true,
|
|
1596
|
+
value: {
|
|
1597
|
+
schemaVersion: 1,
|
|
1598
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
1599
|
+
tiers,
|
|
1600
|
+
...footnoteMarkdown ? { footnoteMarkdown } : {}
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/signup-control-validate.ts
|
|
1606
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1607
|
+
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
1608
|
+
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1609
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
1610
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
1611
|
+
"schemaVersion",
|
|
1612
|
+
"revision",
|
|
1613
|
+
"generatedAt",
|
|
1614
|
+
"source",
|
|
1615
|
+
"target",
|
|
1616
|
+
"tiers",
|
|
1617
|
+
"membershipPage",
|
|
1618
|
+
"application",
|
|
1619
|
+
"policy",
|
|
1620
|
+
"scheduling",
|
|
1621
|
+
"theme"
|
|
1622
|
+
]);
|
|
1623
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
1624
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1625
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
1626
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1627
|
+
}
|
|
1628
|
+
function text(value, path, errors, max = 65536) {
|
|
1629
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1630
|
+
errors.push(`${path} is required`);
|
|
1631
|
+
return "";
|
|
1632
|
+
}
|
|
1633
|
+
if (value.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
1634
|
+
return value.trim();
|
|
1635
|
+
}
|
|
1636
|
+
function tiersOf(value, errors) {
|
|
1637
|
+
if (!Array.isArray(value) || !value.length) {
|
|
1638
|
+
errors.push("document.tiers must be a non-empty array");
|
|
1639
|
+
return [];
|
|
1640
|
+
}
|
|
1641
|
+
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
1642
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1643
|
+
return value.slice(0, 20).flatMap((raw, index) => {
|
|
1644
|
+
const path = `document.tiers[${index}]`;
|
|
1645
|
+
const item = recordOf(raw);
|
|
1646
|
+
if (!item) {
|
|
1647
|
+
errors.push(`${path} must be an object`);
|
|
1648
|
+
return [];
|
|
1649
|
+
}
|
|
1650
|
+
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
1651
|
+
const id = text(item.id, `${path}.id`, errors, 80);
|
|
1652
|
+
if (id && !TIER.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
1653
|
+
if (seen.has(id)) errors.push(`${path}.id duplicates "${id}"`);
|
|
1654
|
+
seen.add(id);
|
|
1655
|
+
const name = text(item.name, `${path}.name`, errors, 160);
|
|
1656
|
+
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
1657
|
+
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
1658
|
+
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
1659
|
+
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
1660
|
+
}
|
|
1661
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
|
|
1662
|
+
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
1663
|
+
}
|
|
1664
|
+
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
1665
|
+
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
1666
|
+
}
|
|
1667
|
+
const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
1668
|
+
return [{
|
|
1669
|
+
id,
|
|
1670
|
+
name,
|
|
1671
|
+
available: item.available === true,
|
|
1672
|
+
sortOrder: Number(item.sortOrder) || 0,
|
|
1673
|
+
priceCents: Number(item.priceCents) || 0,
|
|
1674
|
+
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
1675
|
+
...blurb ? { blurb } : {}
|
|
1676
|
+
}];
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function applicationOf(value, errors) {
|
|
1680
|
+
if (value === void 0) return void 0;
|
|
1681
|
+
const input = recordOf(value);
|
|
1682
|
+
if (!input) {
|
|
1683
|
+
errors.push("document.application must be an object");
|
|
1684
|
+
return void 0;
|
|
1685
|
+
}
|
|
1686
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
1687
|
+
try {
|
|
1688
|
+
const resolved = resolveApplication(input);
|
|
1689
|
+
const copy = recordOf(input.copy);
|
|
1690
|
+
if (input.copy !== void 0 && (!copy || Object.values(copy).some((item) => typeof item !== "string"))) {
|
|
1691
|
+
errors.push("document.application.copy must contain only string values");
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
required: resolved.required,
|
|
1695
|
+
optional: resolved.optional,
|
|
1696
|
+
...Object.keys(resolved.conditions).length ? { conditions: resolved.conditions } : {},
|
|
1697
|
+
...copy ? { copy } : {}
|
|
1698
|
+
};
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
errors.push(error instanceof Error ? error.message : "document.application is invalid");
|
|
1701
|
+
return void 0;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function policyOf(value, errors) {
|
|
1705
|
+
if (value === void 0) return void 0;
|
|
1706
|
+
const input = recordOf(value);
|
|
1707
|
+
if (!input) {
|
|
1708
|
+
errors.push("document.policy must be an object");
|
|
1709
|
+
return void 0;
|
|
1710
|
+
}
|
|
1711
|
+
const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
1712
|
+
unknownKeys(input, allowed, "document.policy", errors);
|
|
1713
|
+
const result = {};
|
|
1714
|
+
for (const key of allowed) {
|
|
1715
|
+
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
1716
|
+
result[key] = text(input[key], `document.policy.${key}`, errors);
|
|
1717
|
+
}
|
|
1718
|
+
return result;
|
|
1719
|
+
}
|
|
1720
|
+
function themeOf(value, errors) {
|
|
1721
|
+
if (value === void 0) return void 0;
|
|
1722
|
+
const input = recordOf(value);
|
|
1723
|
+
const tokens = recordOf(input?.tokens);
|
|
1724
|
+
if (!input || !tokens) {
|
|
1725
|
+
errors.push("document.theme.tokens must be an object");
|
|
1726
|
+
return void 0;
|
|
1727
|
+
}
|
|
1728
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
1729
|
+
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
1730
|
+
for (const [key, token] of Object.entries(tokens)) {
|
|
1731
|
+
if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
|
|
1732
|
+
if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
|
|
1733
|
+
errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return { tokens };
|
|
1737
|
+
}
|
|
1738
|
+
function validateSignupControlDocument(input) {
|
|
1739
|
+
const errors = [];
|
|
1740
|
+
const root = recordOf(input);
|
|
1741
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1742
|
+
unknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
1743
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1744
|
+
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
1745
|
+
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
1746
|
+
const source = recordOf(root.source);
|
|
1747
|
+
const target = recordOf(root.target);
|
|
1748
|
+
const appId = text(source?.appId, "document.source.appId", errors, 63);
|
|
1749
|
+
const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
1750
|
+
const environment = text(target?.environment, "document.target.environment", errors, 63);
|
|
1751
|
+
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
1752
|
+
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
1753
|
+
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
1754
|
+
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
1755
|
+
const tiers = tiersOf(root.tiers, errors);
|
|
1756
|
+
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1757
|
+
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
1758
|
+
if (!page.ok) errors.push(...page.errors);
|
|
1759
|
+
else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
|
|
1760
|
+
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
1761
|
+
}
|
|
1762
|
+
const application = applicationOf(root.application, errors);
|
|
1763
|
+
const policy = policyOf(root.policy, errors);
|
|
1764
|
+
let scheduling;
|
|
1765
|
+
if (root.scheduling !== void 0) {
|
|
1766
|
+
const checked = validateScheduling(root.scheduling);
|
|
1767
|
+
if (!checked.ok) errors.push(...Object.entries(checked.errors).map(([key, value]) => `document.scheduling.${key}: ${value}`));
|
|
1768
|
+
else scheduling = root.scheduling;
|
|
1769
|
+
}
|
|
1770
|
+
const theme = themeOf(root.theme, errors);
|
|
1771
|
+
if (errors.length || !page.ok) return { ok: false, errors };
|
|
1772
|
+
return { ok: true, value: {
|
|
1773
|
+
schemaVersion: 1,
|
|
1774
|
+
revision: Number(root.revision),
|
|
1775
|
+
generatedAt: Number(root.generatedAt),
|
|
1776
|
+
source: { appId },
|
|
1777
|
+
target: { chapterId, environment, stripeMode: target?.stripeMode },
|
|
1778
|
+
tiers,
|
|
1779
|
+
membershipPage: page.value,
|
|
1780
|
+
...application ? { application } : {},
|
|
1781
|
+
...policy ? { policy } : {},
|
|
1782
|
+
...scheduling ? { scheduling } : {},
|
|
1783
|
+
...theme ? { theme } : {}
|
|
1784
|
+
} };
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/worker-routes-signup-control.ts
|
|
1788
|
+
var reject = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
1789
|
+
var handleSignupControl = async (req, url, env, ctx) => {
|
|
1790
|
+
if (req.method !== "POST" || url.pathname !== SIGNUP_CONTROL_PATH) return null;
|
|
1791
|
+
const authority = ctx.chapter.signupControl;
|
|
1792
|
+
if (!authority) return json({ error: "not found" }, 404);
|
|
1793
|
+
const db = ctx.makeDb(env);
|
|
1794
|
+
const secret = await getVaultSecret(db, authority.secretName);
|
|
1795
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
1796
|
+
const verified = await (0, import_db3.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
|
|
1797
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
1798
|
+
let input;
|
|
1799
|
+
try {
|
|
1800
|
+
input = JSON.parse(verified.body);
|
|
1801
|
+
} catch {
|
|
1802
|
+
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
1803
|
+
}
|
|
1804
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
1805
|
+
const checked = validateSignupControlDocument(input.document);
|
|
1806
|
+
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
1807
|
+
const document = checked.value;
|
|
1808
|
+
if (document.source.appId !== authority.sourceId) return reject("wrong_source", 403);
|
|
1809
|
+
if (document.target.chapterId !== ctx.chapter.id) return reject("wrong_target", 409);
|
|
1810
|
+
if (document.target.environment !== env.ODLA_ENV) return reject("wrong_environment", 409);
|
|
1811
|
+
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
1812
|
+
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
1813
|
+
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
1814
|
+
if (!digest || await signupControlDigest(document) !== digest) return reject("invalid_digest", 400);
|
|
1815
|
+
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
1816
|
+
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
1817
|
+
if (latest && document.revision === latest.revision) {
|
|
1818
|
+
if (latest.digest !== digest) return reject("conflicting_revision", 409);
|
|
1819
|
+
return json({
|
|
1820
|
+
ok: true,
|
|
1821
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1822
|
+
revision: latest.revision,
|
|
1823
|
+
digest: latest.digest,
|
|
1824
|
+
status: "replayed",
|
|
1825
|
+
acknowledgedAt: latest.receivedAt
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
const receivedAt = Date.now();
|
|
1829
|
+
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
1830
|
+
return json({
|
|
1831
|
+
ok: true,
|
|
1832
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1833
|
+
revision: document.revision,
|
|
1834
|
+
digest,
|
|
1835
|
+
status: "accepted",
|
|
1836
|
+
acknowledgedAt: receivedAt
|
|
1837
|
+
});
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1256
1840
|
// src/crm-sync.ts
|
|
1257
1841
|
var import_crm4 = require("@odla-ai/crm");
|
|
1258
1842
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
@@ -2600,11 +3184,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
2600
3184
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
2601
3185
|
const t = templates[key];
|
|
2602
3186
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
2603
|
-
const
|
|
2604
|
-
if (!subject || !
|
|
3187
|
+
const text2 = typeof t?.text === "string" ? t.text : "";
|
|
3188
|
+
if (!subject || !text2.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
2605
3189
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
2606
|
-
if (
|
|
2607
|
-
clean[key] = { subject, text, enabled: t?.enabled !== false };
|
|
3190
|
+
if (text2.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
3191
|
+
clean[key] = { subject, text: text2, enabled: t?.enabled !== false };
|
|
2608
3192
|
}
|
|
2609
3193
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2610
3194
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -2699,7 +3283,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2699
3283
|
|
|
2700
3284
|
// src/worker-routes-network.ts
|
|
2701
3285
|
var import_crm6 = require("@odla-ai/crm");
|
|
2702
|
-
var
|
|
3286
|
+
var import_db5 = require("@odla-ai/db");
|
|
2703
3287
|
|
|
2704
3288
|
// src/worker-network-fetch.ts
|
|
2705
3289
|
function targetFetcher(env, target) {
|
|
@@ -2717,7 +3301,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
2717
3301
|
|
|
2718
3302
|
// src/worker-network-push.ts
|
|
2719
3303
|
var import_crm5 = require("@odla-ai/crm");
|
|
2720
|
-
var
|
|
3304
|
+
var import_db4 = require("@odla-ai/db");
|
|
2721
3305
|
async function pushNetworkRecord(db, env, ctx, target, record) {
|
|
2722
3306
|
const secret = await getVaultSecret(db, target.secretName);
|
|
2723
3307
|
const crmDeps3 = {
|
|
@@ -2751,7 +3335,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
|
|
|
2751
3335
|
const payloadBody = JSON.stringify(payload);
|
|
2752
3336
|
const destination = new URL("/api/network/shared", target.url);
|
|
2753
3337
|
try {
|
|
2754
|
-
const signed = await (0,
|
|
3338
|
+
const signed = await (0, import_db4.signFederatedRequest)({
|
|
2755
3339
|
secret,
|
|
2756
3340
|
sender: ctx.chapter.id,
|
|
2757
3341
|
method: "POST",
|
|
@@ -2836,7 +3420,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2836
3420
|
const db = ctx.makeDb(env);
|
|
2837
3421
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2838
3422
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2839
|
-
const verified = await (0,
|
|
3423
|
+
const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret });
|
|
2840
3424
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2841
3425
|
const types = await Promise.all(
|
|
2842
3426
|
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
@@ -2854,7 +3438,7 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
2854
3438
|
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
2855
3439
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
2856
3440
|
try {
|
|
2857
|
-
const headers = await (0,
|
|
3441
|
+
const headers = await (0, import_db5.signFederatedRequest)({
|
|
2858
3442
|
secret,
|
|
2859
3443
|
sender: ctx.chapter.id,
|
|
2860
3444
|
method: "GET",
|
|
@@ -2947,7 +3531,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2947
3531
|
|
|
2948
3532
|
// src/worker-routes-network-records.ts
|
|
2949
3533
|
var import_crm7 = require("@odla-ai/crm");
|
|
2950
|
-
var
|
|
3534
|
+
var import_db6 = require("@odla-ai/db");
|
|
2951
3535
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2952
3536
|
var MAX_PAGE = 50;
|
|
2953
3537
|
var MAX_OFFSET = 1e4;
|
|
@@ -2985,7 +3569,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
2985
3569
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2986
3570
|
const readers = ctx.chapter.network.readers;
|
|
2987
3571
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
2988
|
-
const verified = await (0,
|
|
3572
|
+
const verified = await (0, import_db6.verifyFederatedRequest)(req, {
|
|
2989
3573
|
secret,
|
|
2990
3574
|
senders: readers.map((reader2) => reader2.id)
|
|
2991
3575
|
});
|
|
@@ -3030,7 +3614,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
3030
3614
|
const destination = new URL("/api/network/records", target.url);
|
|
3031
3615
|
destination.search = query.toString();
|
|
3032
3616
|
try {
|
|
3033
|
-
const headers = await (0,
|
|
3617
|
+
const headers = await (0, import_db6.signFederatedRequest)({
|
|
3034
3618
|
secret,
|
|
3035
3619
|
sender: ctx.chapter.id,
|
|
3036
3620
|
method: "GET",
|
|
@@ -3165,7 +3749,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
3165
3749
|
|
|
3166
3750
|
// src/worker-routes-network-shared-notes.ts
|
|
3167
3751
|
var import_crm8 = require("@odla-ai/crm");
|
|
3168
|
-
var
|
|
3752
|
+
var import_db7 = require("@odla-ai/db");
|
|
3169
3753
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3170
3754
|
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3171
3755
|
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -3224,7 +3808,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
3224
3808
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
3225
3809
|
const readers = ctx.chapter.network.readers;
|
|
3226
3810
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
3227
|
-
const verified = await (0,
|
|
3811
|
+
const verified = await (0, import_db7.verifyFederatedRequest)(req, {
|
|
3228
3812
|
secret,
|
|
3229
3813
|
senders: readers.map((reader2) => reader2.id)
|
|
3230
3814
|
});
|
|
@@ -3321,7 +3905,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
3321
3905
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
3322
3906
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
3323
3907
|
try {
|
|
3324
|
-
const headers = await (0,
|
|
3908
|
+
const headers = await (0, import_db7.signFederatedRequest)({
|
|
3325
3909
|
secret,
|
|
3326
3910
|
sender: ctx.chapter.id,
|
|
3327
3911
|
method: req.method,
|
|
@@ -3441,6 +4025,31 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
3441
4025
|
}
|
|
3442
4026
|
};
|
|
3443
4027
|
|
|
4028
|
+
// src/signup-control-delivery.ts
|
|
4029
|
+
var import_db8 = require("@odla-ai/db");
|
|
4030
|
+
async function deliverSignupControl(options) {
|
|
4031
|
+
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
4032
|
+
const body = JSON.stringify(options.envelope);
|
|
4033
|
+
const signed = await (0, import_db8.signFederatedRequest)({
|
|
4034
|
+
secret: options.secret,
|
|
4035
|
+
sender: options.sender,
|
|
4036
|
+
method: "POST",
|
|
4037
|
+
url: destination,
|
|
4038
|
+
body
|
|
4039
|
+
});
|
|
4040
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
4041
|
+
method: "POST",
|
|
4042
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
4043
|
+
body
|
|
4044
|
+
});
|
|
4045
|
+
const result = await response.json();
|
|
4046
|
+
if (!response.ok || (!("ok" in result) || result.ok !== true)) {
|
|
4047
|
+
const rejection = result;
|
|
4048
|
+
throw new Error(`signup control rejected (${response.status}): ${rejection.reason ?? rejection.error ?? "unknown"}`);
|
|
4049
|
+
}
|
|
4050
|
+
return result;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
3444
4053
|
// src/worker.ts
|
|
3445
4054
|
var BUILTIN_ROUTES = [
|
|
3446
4055
|
handleHealth,
|
|
@@ -3448,6 +4057,7 @@ var BUILTIN_ROUTES = [
|
|
|
3448
4057
|
handleMe,
|
|
3449
4058
|
handleCrm,
|
|
3450
4059
|
handleNetworkShared,
|
|
4060
|
+
handleSignupControl,
|
|
3451
4061
|
handleNetworkSnapshot,
|
|
3452
4062
|
handleNetworkRecords,
|
|
3453
4063
|
handleNetworkSharedNotes,
|