@odla-ai/chapter 0.32.3 → 0.33.0
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 +79 -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 +120 -2
- package/dist/index.d.ts +120 -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 +623 -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 +618 -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,91 @@ 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 loadLatestSignupControl(db, targetId, environment) {
|
|
531
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
532
|
+
where: { targetId, environment },
|
|
533
|
+
order: { revision: "desc" },
|
|
534
|
+
limit: 1
|
|
535
|
+
} } })).signupControls;
|
|
536
|
+
const latest = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
537
|
+
return asStored(latest);
|
|
538
|
+
}
|
|
539
|
+
async function loadActiveSignupControl(db, targetId, environment) {
|
|
540
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
541
|
+
where: { targetId, environment, active: true },
|
|
542
|
+
order: { revision: "desc" },
|
|
543
|
+
limit: 1
|
|
544
|
+
} } })).signupControls;
|
|
545
|
+
const active = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
546
|
+
return asStored(active);
|
|
547
|
+
}
|
|
548
|
+
function controlledApplication(base, document) {
|
|
549
|
+
const delegated = document?.application;
|
|
550
|
+
return delegated ? { ...base, required: delegated.required, optional: delegated.optional, conditions: { ...delegated.conditions ?? {} } } : base;
|
|
551
|
+
}
|
|
552
|
+
function validateControlledApplication(chapter, document) {
|
|
553
|
+
if (!document.application) return [];
|
|
554
|
+
const declared = /* @__PURE__ */ new Set([...chapter.application.required, ...chapter.application.optional]);
|
|
555
|
+
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`);
|
|
556
|
+
}
|
|
557
|
+
function groupAttrs(document) {
|
|
558
|
+
const first = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
559
|
+
const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
|
|
560
|
+
return {
|
|
561
|
+
...first ? { standardPriceCents: first.priceCents } : {},
|
|
562
|
+
foundingDiscountCents: 0,
|
|
563
|
+
stripePriceId: firstPaid?.stripePriceId ?? null,
|
|
564
|
+
...document.policy ?? {},
|
|
565
|
+
...document.scheduling ? { schedulingJson: document.scheduling } : {}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
569
|
+
const { document, digest } = envelope;
|
|
570
|
+
const currentRows = (await db.query({ tiers: { $: {
|
|
571
|
+
where: { groupId: document.target.chapterId },
|
|
572
|
+
limit: 100
|
|
573
|
+
} } })).tiers ?? [];
|
|
574
|
+
const incoming = new Set(document.tiers.map((tier) => tier.id));
|
|
575
|
+
const ops = [];
|
|
576
|
+
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
577
|
+
for (const row of currentRows) {
|
|
578
|
+
const id2 = String(row.id ?? "");
|
|
579
|
+
if (id2 && !incoming.has(id2)) ops.push({ t: "update", ns: "tiers", id: id2, attrs: { active: false } });
|
|
580
|
+
}
|
|
581
|
+
for (const tier of document.tiers) {
|
|
582
|
+
ops.push({ t: "update", ns: "tiers", id: tier.id, attrs: {
|
|
583
|
+
id: tier.id,
|
|
584
|
+
groupId: document.target.chapterId,
|
|
585
|
+
name: tier.name,
|
|
586
|
+
priceCents: tier.priceCents,
|
|
587
|
+
stripePriceId: tier.stripePriceId ?? null,
|
|
588
|
+
blurb: tier.blurb ?? null,
|
|
589
|
+
sortOrder: tier.sortOrder,
|
|
590
|
+
active: tier.available,
|
|
591
|
+
createdAt: receivedAt
|
|
592
|
+
} });
|
|
593
|
+
}
|
|
594
|
+
ops.push({ t: "update", ns: "groups", id: document.target.chapterId, attrs: groupAttrs(document) });
|
|
595
|
+
const id = `signup-control:${document.target.environment}:${document.revision}`;
|
|
596
|
+
ops.push({ t: "update", ns: "signupControls", id, attrs: {
|
|
597
|
+
id,
|
|
598
|
+
sourceId: document.source.appId,
|
|
599
|
+
targetId: document.target.chapterId,
|
|
600
|
+
environment: document.target.environment,
|
|
601
|
+
revision: document.revision,
|
|
602
|
+
digest,
|
|
603
|
+
document,
|
|
604
|
+
active: true,
|
|
605
|
+
receivedAt
|
|
606
|
+
} });
|
|
607
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest}` });
|
|
608
|
+
}
|
|
609
|
+
|
|
474
610
|
// src/session.ts
|
|
475
611
|
function applicationSummary(app) {
|
|
476
612
|
return {
|
|
@@ -813,10 +949,10 @@ function planDelivery(input) {
|
|
|
813
949
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
814
950
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
815
951
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
816
|
-
const
|
|
952
|
+
const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
817
953
|
|
|
818
954
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
819
|
-
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
955
|
+
return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
|
|
820
956
|
}
|
|
821
957
|
|
|
822
958
|
// src/notify.ts
|
|
@@ -1105,9 +1241,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1105
1241
|
group,
|
|
1106
1242
|
Boolean(stripeKey)
|
|
1107
1243
|
);
|
|
1244
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1245
|
+
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1108
1246
|
return json({
|
|
1109
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered),
|
|
1110
|
-
copy: chapter.copy.join
|
|
1247
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1248
|
+
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1249
|
+
...activeControl ? { signupControl: {
|
|
1250
|
+
revision: activeControl.revision,
|
|
1251
|
+
digest: activeControl.digest,
|
|
1252
|
+
membershipPage: activeControl.document.membershipPage,
|
|
1253
|
+
application: activeControl.document.application,
|
|
1254
|
+
theme: activeControl.document.theme
|
|
1255
|
+
} } : {}
|
|
1111
1256
|
});
|
|
1112
1257
|
}
|
|
1113
1258
|
if (req.method === "GET" && url.pathname === "/api/join/resume") {
|
|
@@ -1170,8 +1315,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1170
1315
|
}
|
|
1171
1316
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
1172
1317
|
const db = ctx.makeDb(env);
|
|
1318
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1319
|
+
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
1173
1320
|
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,
|
|
1321
|
+
const result = await submitApplication(db, effectiveChapter, parsed, {
|
|
1175
1322
|
submissionId,
|
|
1176
1323
|
groupId: chapter.id,
|
|
1177
1324
|
now: Date.now(),
|
|
@@ -1180,10 +1327,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1180
1327
|
});
|
|
1181
1328
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
1182
1329
|
if (!result.duplicate) {
|
|
1183
|
-
if (
|
|
1330
|
+
if (effectiveChapter.sends.adminNotification === "submit") {
|
|
1184
1331
|
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
1185
1332
|
}
|
|
1186
|
-
await provisionApplicant(db,
|
|
1333
|
+
await provisionApplicant(db, effectiveChapter, result.id, parsed);
|
|
1187
1334
|
}
|
|
1188
1335
|
return json({
|
|
1189
1336
|
id: result.id,
|
|
@@ -1253,6 +1400,433 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
1253
1400
|
}
|
|
1254
1401
|
};
|
|
1255
1402
|
|
|
1403
|
+
// src/worker-routes-signup-control.ts
|
|
1404
|
+
var import_db3 = require("@odla-ai/db");
|
|
1405
|
+
|
|
1406
|
+
// src/signup-control-canonical.ts
|
|
1407
|
+
var encoder = new TextEncoder();
|
|
1408
|
+
function canonicalValue(value) {
|
|
1409
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
1410
|
+
return JSON.stringify(value);
|
|
1411
|
+
}
|
|
1412
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
1413
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
1414
|
+
const record = value;
|
|
1415
|
+
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
1416
|
+
const item = record[key];
|
|
1417
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
1418
|
+
});
|
|
1419
|
+
return `{${entries.join(",")}}`;
|
|
1420
|
+
}
|
|
1421
|
+
function canonicalSignupControl(value) {
|
|
1422
|
+
return canonicalValue(value);
|
|
1423
|
+
}
|
|
1424
|
+
async function signupControlDigest(document) {
|
|
1425
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
1426
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/signup-control-contract.ts
|
|
1430
|
+
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
1431
|
+
|
|
1432
|
+
// src/membership-page.ts
|
|
1433
|
+
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
1434
|
+
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1435
|
+
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
1436
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
1437
|
+
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
1438
|
+
"tierId",
|
|
1439
|
+
"variant",
|
|
1440
|
+
"badge",
|
|
1441
|
+
"summaryMarkdown",
|
|
1442
|
+
"priceEyebrow",
|
|
1443
|
+
"compareAtPriceCents",
|
|
1444
|
+
"billingLabel",
|
|
1445
|
+
"priceNoteMarkdown",
|
|
1446
|
+
"capacity",
|
|
1447
|
+
"benefits",
|
|
1448
|
+
"callout",
|
|
1449
|
+
"ctaLabel"
|
|
1450
|
+
]);
|
|
1451
|
+
var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
1452
|
+
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
1453
|
+
var MAX_TIERS = 20;
|
|
1454
|
+
var MAX_BENEFITS = 24;
|
|
1455
|
+
var MAX_MARKDOWN = 65536;
|
|
1456
|
+
var MAX_LABEL = 160;
|
|
1457
|
+
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1458
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
1459
|
+
for (const key of Object.keys(value)) {
|
|
1460
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
function boundedString(value, path, errors, max, required = false) {
|
|
1464
|
+
if (value === void 0) {
|
|
1465
|
+
if (required) errors.push(`${path} is required`);
|
|
1466
|
+
return void 0;
|
|
1467
|
+
}
|
|
1468
|
+
if (typeof value !== "string") {
|
|
1469
|
+
errors.push(`${path} must be a string`);
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
const text2 = value.trim();
|
|
1473
|
+
if (required && !text2) errors.push(`${path} is required`);
|
|
1474
|
+
if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
1475
|
+
return text2;
|
|
1476
|
+
}
|
|
1477
|
+
function validateMembershipPageDocument(input, options = {}) {
|
|
1478
|
+
const errors = [];
|
|
1479
|
+
const root = objectRecord(input);
|
|
1480
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1481
|
+
rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
|
|
1482
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1483
|
+
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
1484
|
+
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
1485
|
+
}
|
|
1486
|
+
if (!Array.isArray(root.tiers)) errors.push("document.tiers must be an array");
|
|
1487
|
+
const rawTiers = Array.isArray(root.tiers) ? root.tiers : [];
|
|
1488
|
+
if (Array.isArray(root.tiers) && rawTiers.length === 0) errors.push("document.tiers must include at least one tier");
|
|
1489
|
+
if (rawTiers.length > MAX_TIERS) errors.push(`document.tiers supports at most ${MAX_TIERS} entries`);
|
|
1490
|
+
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
1491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1492
|
+
const tiers = [];
|
|
1493
|
+
for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
1494
|
+
const path = `document.tiers[${index}]`;
|
|
1495
|
+
const item = objectRecord(raw);
|
|
1496
|
+
if (!item) {
|
|
1497
|
+
errors.push(`${path} must be an object`);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
rejectUnknownKeys(item, TIER_KEYS, path, errors);
|
|
1501
|
+
const tierId = boundedString(item.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
1502
|
+
if (tierId && !TIER_ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
1503
|
+
if (seen.has(tierId)) errors.push(`${path}.tierId duplicates "${tierId}"`);
|
|
1504
|
+
seen.add(tierId);
|
|
1505
|
+
if (authority.size && !authority.has(tierId)) errors.push(`${path}.tierId "${tierId}" is not an offered tier`);
|
|
1506
|
+
const variant = item.variant;
|
|
1507
|
+
if (variant !== void 0 && (typeof variant !== "string" || !VARIANTS.has(variant))) {
|
|
1508
|
+
errors.push(`${path}.variant must be standard, featured, or supporting`);
|
|
1509
|
+
}
|
|
1510
|
+
const compareAt = item.compareAtPriceCents;
|
|
1511
|
+
if (compareAt !== void 0 && (!Number.isInteger(compareAt) || Number(compareAt) < 0 || Number(compareAt) > 1e8)) {
|
|
1512
|
+
errors.push(`${path}.compareAtPriceCents must be an integer from 0 to 100,000,000`);
|
|
1513
|
+
}
|
|
1514
|
+
const actual = authority.get(tierId)?.priceCents;
|
|
1515
|
+
if (Number.isInteger(compareAt) && actual !== void 0 && Number(compareAt) <= actual) {
|
|
1516
|
+
errors.push(`${path}.compareAtPriceCents must be greater than the authoritative tier price`);
|
|
1517
|
+
}
|
|
1518
|
+
let benefits;
|
|
1519
|
+
if (item.benefits !== void 0) {
|
|
1520
|
+
if (!Array.isArray(item.benefits)) errors.push(`${path}.benefits must be an array`);
|
|
1521
|
+
else {
|
|
1522
|
+
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
1523
|
+
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
1524
|
+
const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
1525
|
+
return text2 ? [text2] : [];
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
let callout;
|
|
1530
|
+
if (item.callout !== void 0) {
|
|
1531
|
+
const rawCallout = objectRecord(item.callout);
|
|
1532
|
+
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
1533
|
+
else {
|
|
1534
|
+
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
1535
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
|
|
1536
|
+
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
1537
|
+
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
let capacity;
|
|
1541
|
+
if (item.capacity !== void 0) {
|
|
1542
|
+
const rawCapacity = objectRecord(item.capacity);
|
|
1543
|
+
if (!rawCapacity) errors.push(`${path}.capacity must be an object`);
|
|
1544
|
+
else {
|
|
1545
|
+
rejectUnknownKeys(rawCapacity, CAPACITY_KEYS, `${path}.capacity`, errors);
|
|
1546
|
+
const total = rawCapacity.total;
|
|
1547
|
+
if (!Number.isInteger(total) || Number(total) < 1 || Number(total) > 1e6) {
|
|
1548
|
+
errors.push(`${path}.capacity.total must be an integer from 1 to 1,000,000`);
|
|
1549
|
+
}
|
|
1550
|
+
const label = boundedString(rawCapacity.label, `${path}.capacity.label`, errors, MAX_LABEL);
|
|
1551
|
+
if (Number.isInteger(total) && Number(total) >= 1 && Number(total) <= 1e6) {
|
|
1552
|
+
capacity = { total: Number(total), ...label ? { label } : {} };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
1557
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
|
|
1558
|
+
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
1559
|
+
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
1560
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
|
|
1561
|
+
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
1562
|
+
tiers.push({
|
|
1563
|
+
tierId,
|
|
1564
|
+
...typeof variant === "string" && VARIANTS.has(variant) ? { variant } : {},
|
|
1565
|
+
...badge ? { badge } : {},
|
|
1566
|
+
...summaryMarkdown ? { summaryMarkdown } : {},
|
|
1567
|
+
...priceEyebrow ? { priceEyebrow } : {},
|
|
1568
|
+
...Number.isInteger(compareAt) ? { compareAtPriceCents: Number(compareAt) } : {},
|
|
1569
|
+
...billingLabel ? { billingLabel } : {},
|
|
1570
|
+
...priceNoteMarkdown ? { priceNoteMarkdown } : {},
|
|
1571
|
+
...capacity ? { capacity } : {},
|
|
1572
|
+
...benefits ? { benefits } : {},
|
|
1573
|
+
...callout ? { callout } : {},
|
|
1574
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
if (authority.size) {
|
|
1578
|
+
for (const tierId of authority.keys()) {
|
|
1579
|
+
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
|
|
1583
|
+
if (errors.length) return { ok: false, errors };
|
|
1584
|
+
return {
|
|
1585
|
+
ok: true,
|
|
1586
|
+
value: {
|
|
1587
|
+
schemaVersion: 1,
|
|
1588
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
1589
|
+
tiers,
|
|
1590
|
+
...footnoteMarkdown ? { footnoteMarkdown } : {}
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/signup-control-validate.ts
|
|
1596
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1597
|
+
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
1598
|
+
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1599
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
1600
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
1601
|
+
"schemaVersion",
|
|
1602
|
+
"revision",
|
|
1603
|
+
"generatedAt",
|
|
1604
|
+
"source",
|
|
1605
|
+
"target",
|
|
1606
|
+
"tiers",
|
|
1607
|
+
"membershipPage",
|
|
1608
|
+
"application",
|
|
1609
|
+
"policy",
|
|
1610
|
+
"scheduling",
|
|
1611
|
+
"theme"
|
|
1612
|
+
]);
|
|
1613
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
1614
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1615
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
1616
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1617
|
+
}
|
|
1618
|
+
function text(value, path, errors, max = 65536) {
|
|
1619
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1620
|
+
errors.push(`${path} is required`);
|
|
1621
|
+
return "";
|
|
1622
|
+
}
|
|
1623
|
+
if (value.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
1624
|
+
return value.trim();
|
|
1625
|
+
}
|
|
1626
|
+
function tiersOf(value, errors) {
|
|
1627
|
+
if (!Array.isArray(value) || !value.length) {
|
|
1628
|
+
errors.push("document.tiers must be a non-empty array");
|
|
1629
|
+
return [];
|
|
1630
|
+
}
|
|
1631
|
+
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
1632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1633
|
+
return value.slice(0, 20).flatMap((raw, index) => {
|
|
1634
|
+
const path = `document.tiers[${index}]`;
|
|
1635
|
+
const item = recordOf(raw);
|
|
1636
|
+
if (!item) {
|
|
1637
|
+
errors.push(`${path} must be an object`);
|
|
1638
|
+
return [];
|
|
1639
|
+
}
|
|
1640
|
+
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
1641
|
+
const id = text(item.id, `${path}.id`, errors, 80);
|
|
1642
|
+
if (id && !TIER.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
1643
|
+
if (seen.has(id)) errors.push(`${path}.id duplicates "${id}"`);
|
|
1644
|
+
seen.add(id);
|
|
1645
|
+
const name = text(item.name, `${path}.name`, errors, 160);
|
|
1646
|
+
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
1647
|
+
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
1648
|
+
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
1649
|
+
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
1650
|
+
}
|
|
1651
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
|
|
1652
|
+
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
1653
|
+
}
|
|
1654
|
+
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
1655
|
+
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
1656
|
+
}
|
|
1657
|
+
const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
1658
|
+
return [{
|
|
1659
|
+
id,
|
|
1660
|
+
name,
|
|
1661
|
+
available: item.available === true,
|
|
1662
|
+
sortOrder: Number(item.sortOrder) || 0,
|
|
1663
|
+
priceCents: Number(item.priceCents) || 0,
|
|
1664
|
+
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
1665
|
+
...blurb ? { blurb } : {}
|
|
1666
|
+
}];
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
function applicationOf(value, errors) {
|
|
1670
|
+
if (value === void 0) return void 0;
|
|
1671
|
+
const input = recordOf(value);
|
|
1672
|
+
if (!input) {
|
|
1673
|
+
errors.push("document.application must be an object");
|
|
1674
|
+
return void 0;
|
|
1675
|
+
}
|
|
1676
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
1677
|
+
try {
|
|
1678
|
+
const resolved = resolveApplication(input);
|
|
1679
|
+
const copy = recordOf(input.copy);
|
|
1680
|
+
if (input.copy !== void 0 && (!copy || Object.values(copy).some((item) => typeof item !== "string"))) {
|
|
1681
|
+
errors.push("document.application.copy must contain only string values");
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
required: resolved.required,
|
|
1685
|
+
optional: resolved.optional,
|
|
1686
|
+
...Object.keys(resolved.conditions).length ? { conditions: resolved.conditions } : {},
|
|
1687
|
+
...copy ? { copy } : {}
|
|
1688
|
+
};
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
errors.push(error instanceof Error ? error.message : "document.application is invalid");
|
|
1691
|
+
return void 0;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
function policyOf(value, errors) {
|
|
1695
|
+
if (value === void 0) return void 0;
|
|
1696
|
+
const input = recordOf(value);
|
|
1697
|
+
if (!input) {
|
|
1698
|
+
errors.push("document.policy must be an object");
|
|
1699
|
+
return void 0;
|
|
1700
|
+
}
|
|
1701
|
+
const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
1702
|
+
unknownKeys(input, allowed, "document.policy", errors);
|
|
1703
|
+
const result = {};
|
|
1704
|
+
for (const key of allowed) {
|
|
1705
|
+
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
1706
|
+
result[key] = text(input[key], `document.policy.${key}`, errors);
|
|
1707
|
+
}
|
|
1708
|
+
return result;
|
|
1709
|
+
}
|
|
1710
|
+
function themeOf(value, errors) {
|
|
1711
|
+
if (value === void 0) return void 0;
|
|
1712
|
+
const input = recordOf(value);
|
|
1713
|
+
const tokens = recordOf(input?.tokens);
|
|
1714
|
+
if (!input || !tokens) {
|
|
1715
|
+
errors.push("document.theme.tokens must be an object");
|
|
1716
|
+
return void 0;
|
|
1717
|
+
}
|
|
1718
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
1719
|
+
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
1720
|
+
for (const [key, token] of Object.entries(tokens)) {
|
|
1721
|
+
if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
|
|
1722
|
+
if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
|
|
1723
|
+
errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return { tokens };
|
|
1727
|
+
}
|
|
1728
|
+
function validateSignupControlDocument(input) {
|
|
1729
|
+
const errors = [];
|
|
1730
|
+
const root = recordOf(input);
|
|
1731
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1732
|
+
unknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
1733
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1734
|
+
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
1735
|
+
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
1736
|
+
const source = recordOf(root.source);
|
|
1737
|
+
const target = recordOf(root.target);
|
|
1738
|
+
const appId = text(source?.appId, "document.source.appId", errors, 63);
|
|
1739
|
+
const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
1740
|
+
const environment = text(target?.environment, "document.target.environment", errors, 63);
|
|
1741
|
+
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
1742
|
+
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
1743
|
+
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
1744
|
+
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
1745
|
+
const tiers = tiersOf(root.tiers, errors);
|
|
1746
|
+
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1747
|
+
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
1748
|
+
if (!page.ok) errors.push(...page.errors);
|
|
1749
|
+
else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
|
|
1750
|
+
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
1751
|
+
}
|
|
1752
|
+
const application = applicationOf(root.application, errors);
|
|
1753
|
+
const policy = policyOf(root.policy, errors);
|
|
1754
|
+
let scheduling;
|
|
1755
|
+
if (root.scheduling !== void 0) {
|
|
1756
|
+
const checked = validateScheduling(root.scheduling);
|
|
1757
|
+
if (!checked.ok) errors.push(...Object.entries(checked.errors).map(([key, value]) => `document.scheduling.${key}: ${value}`));
|
|
1758
|
+
else scheduling = root.scheduling;
|
|
1759
|
+
}
|
|
1760
|
+
const theme = themeOf(root.theme, errors);
|
|
1761
|
+
if (errors.length || !page.ok) return { ok: false, errors };
|
|
1762
|
+
return { ok: true, value: {
|
|
1763
|
+
schemaVersion: 1,
|
|
1764
|
+
revision: Number(root.revision),
|
|
1765
|
+
generatedAt: Number(root.generatedAt),
|
|
1766
|
+
source: { appId },
|
|
1767
|
+
target: { chapterId, environment, stripeMode: target?.stripeMode },
|
|
1768
|
+
tiers,
|
|
1769
|
+
membershipPage: page.value,
|
|
1770
|
+
...application ? { application } : {},
|
|
1771
|
+
...policy ? { policy } : {},
|
|
1772
|
+
...scheduling ? { scheduling } : {},
|
|
1773
|
+
...theme ? { theme } : {}
|
|
1774
|
+
} };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/worker-routes-signup-control.ts
|
|
1778
|
+
var reject = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
1779
|
+
var handleSignupControl = async (req, url, env, ctx) => {
|
|
1780
|
+
if (req.method !== "POST" || url.pathname !== SIGNUP_CONTROL_PATH) return null;
|
|
1781
|
+
const authority = ctx.chapter.signupControl;
|
|
1782
|
+
if (!authority) return json({ error: "not found" }, 404);
|
|
1783
|
+
const db = ctx.makeDb(env);
|
|
1784
|
+
const secret = await getVaultSecret(db, authority.secretName);
|
|
1785
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
1786
|
+
const verified = await (0, import_db3.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
|
|
1787
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
1788
|
+
let input;
|
|
1789
|
+
try {
|
|
1790
|
+
input = JSON.parse(verified.body);
|
|
1791
|
+
} catch {
|
|
1792
|
+
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
1793
|
+
}
|
|
1794
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
1795
|
+
const checked = validateSignupControlDocument(input.document);
|
|
1796
|
+
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
1797
|
+
const document = checked.value;
|
|
1798
|
+
if (document.source.appId !== authority.sourceId) return reject("wrong_source", 403);
|
|
1799
|
+
if (document.target.chapterId !== ctx.chapter.id) return reject("wrong_target", 409);
|
|
1800
|
+
if (document.target.environment !== env.ODLA_ENV) return reject("wrong_environment", 409);
|
|
1801
|
+
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
1802
|
+
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
1803
|
+
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
1804
|
+
if (!digest || await signupControlDigest(document) !== digest) return reject("invalid_digest", 400);
|
|
1805
|
+
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
1806
|
+
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
1807
|
+
if (latest && document.revision === latest.revision) {
|
|
1808
|
+
if (latest.digest !== digest) return reject("conflicting_revision", 409);
|
|
1809
|
+
return json({
|
|
1810
|
+
ok: true,
|
|
1811
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1812
|
+
revision: latest.revision,
|
|
1813
|
+
digest: latest.digest,
|
|
1814
|
+
status: "replayed",
|
|
1815
|
+
acknowledgedAt: latest.receivedAt
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
const receivedAt = Date.now();
|
|
1819
|
+
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
1820
|
+
return json({
|
|
1821
|
+
ok: true,
|
|
1822
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1823
|
+
revision: document.revision,
|
|
1824
|
+
digest,
|
|
1825
|
+
status: "accepted",
|
|
1826
|
+
acknowledgedAt: receivedAt
|
|
1827
|
+
});
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1256
1830
|
// src/crm-sync.ts
|
|
1257
1831
|
var import_crm4 = require("@odla-ai/crm");
|
|
1258
1832
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
@@ -2600,11 +3174,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
2600
3174
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
2601
3175
|
const t = templates[key];
|
|
2602
3176
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
2603
|
-
const
|
|
2604
|
-
if (!subject || !
|
|
3177
|
+
const text2 = typeof t?.text === "string" ? t.text : "";
|
|
3178
|
+
if (!subject || !text2.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
2605
3179
|
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 };
|
|
3180
|
+
if (text2.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
3181
|
+
clean[key] = { subject, text: text2, enabled: t?.enabled !== false };
|
|
2608
3182
|
}
|
|
2609
3183
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2610
3184
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -2699,7 +3273,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2699
3273
|
|
|
2700
3274
|
// src/worker-routes-network.ts
|
|
2701
3275
|
var import_crm6 = require("@odla-ai/crm");
|
|
2702
|
-
var
|
|
3276
|
+
var import_db5 = require("@odla-ai/db");
|
|
2703
3277
|
|
|
2704
3278
|
// src/worker-network-fetch.ts
|
|
2705
3279
|
function targetFetcher(env, target) {
|
|
@@ -2717,7 +3291,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
2717
3291
|
|
|
2718
3292
|
// src/worker-network-push.ts
|
|
2719
3293
|
var import_crm5 = require("@odla-ai/crm");
|
|
2720
|
-
var
|
|
3294
|
+
var import_db4 = require("@odla-ai/db");
|
|
2721
3295
|
async function pushNetworkRecord(db, env, ctx, target, record) {
|
|
2722
3296
|
const secret = await getVaultSecret(db, target.secretName);
|
|
2723
3297
|
const crmDeps3 = {
|
|
@@ -2751,7 +3325,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
|
|
|
2751
3325
|
const payloadBody = JSON.stringify(payload);
|
|
2752
3326
|
const destination = new URL("/api/network/shared", target.url);
|
|
2753
3327
|
try {
|
|
2754
|
-
const signed = await (0,
|
|
3328
|
+
const signed = await (0, import_db4.signFederatedRequest)({
|
|
2755
3329
|
secret,
|
|
2756
3330
|
sender: ctx.chapter.id,
|
|
2757
3331
|
method: "POST",
|
|
@@ -2836,7 +3410,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2836
3410
|
const db = ctx.makeDb(env);
|
|
2837
3411
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2838
3412
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2839
|
-
const verified = await (0,
|
|
3413
|
+
const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret });
|
|
2840
3414
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2841
3415
|
const types = await Promise.all(
|
|
2842
3416
|
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
@@ -2854,7 +3428,7 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
2854
3428
|
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
2855
3429
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
2856
3430
|
try {
|
|
2857
|
-
const headers = await (0,
|
|
3431
|
+
const headers = await (0, import_db5.signFederatedRequest)({
|
|
2858
3432
|
secret,
|
|
2859
3433
|
sender: ctx.chapter.id,
|
|
2860
3434
|
method: "GET",
|
|
@@ -2947,7 +3521,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2947
3521
|
|
|
2948
3522
|
// src/worker-routes-network-records.ts
|
|
2949
3523
|
var import_crm7 = require("@odla-ai/crm");
|
|
2950
|
-
var
|
|
3524
|
+
var import_db6 = require("@odla-ai/db");
|
|
2951
3525
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2952
3526
|
var MAX_PAGE = 50;
|
|
2953
3527
|
var MAX_OFFSET = 1e4;
|
|
@@ -2985,7 +3559,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
2985
3559
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2986
3560
|
const readers = ctx.chapter.network.readers;
|
|
2987
3561
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
2988
|
-
const verified = await (0,
|
|
3562
|
+
const verified = await (0, import_db6.verifyFederatedRequest)(req, {
|
|
2989
3563
|
secret,
|
|
2990
3564
|
senders: readers.map((reader2) => reader2.id)
|
|
2991
3565
|
});
|
|
@@ -3030,7 +3604,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
3030
3604
|
const destination = new URL("/api/network/records", target.url);
|
|
3031
3605
|
destination.search = query.toString();
|
|
3032
3606
|
try {
|
|
3033
|
-
const headers = await (0,
|
|
3607
|
+
const headers = await (0, import_db6.signFederatedRequest)({
|
|
3034
3608
|
secret,
|
|
3035
3609
|
sender: ctx.chapter.id,
|
|
3036
3610
|
method: "GET",
|
|
@@ -3165,7 +3739,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
3165
3739
|
|
|
3166
3740
|
// src/worker-routes-network-shared-notes.ts
|
|
3167
3741
|
var import_crm8 = require("@odla-ai/crm");
|
|
3168
|
-
var
|
|
3742
|
+
var import_db7 = require("@odla-ai/db");
|
|
3169
3743
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3170
3744
|
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3171
3745
|
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -3224,7 +3798,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
3224
3798
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
3225
3799
|
const readers = ctx.chapter.network.readers;
|
|
3226
3800
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
3227
|
-
const verified = await (0,
|
|
3801
|
+
const verified = await (0, import_db7.verifyFederatedRequest)(req, {
|
|
3228
3802
|
secret,
|
|
3229
3803
|
senders: readers.map((reader2) => reader2.id)
|
|
3230
3804
|
});
|
|
@@ -3321,7 +3895,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
3321
3895
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
3322
3896
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
3323
3897
|
try {
|
|
3324
|
-
const headers = await (0,
|
|
3898
|
+
const headers = await (0, import_db7.signFederatedRequest)({
|
|
3325
3899
|
secret,
|
|
3326
3900
|
sender: ctx.chapter.id,
|
|
3327
3901
|
method: req.method,
|
|
@@ -3441,6 +4015,31 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
3441
4015
|
}
|
|
3442
4016
|
};
|
|
3443
4017
|
|
|
4018
|
+
// src/signup-control-delivery.ts
|
|
4019
|
+
var import_db8 = require("@odla-ai/db");
|
|
4020
|
+
async function deliverSignupControl(options) {
|
|
4021
|
+
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
4022
|
+
const body = JSON.stringify(options.envelope);
|
|
4023
|
+
const signed = await (0, import_db8.signFederatedRequest)({
|
|
4024
|
+
secret: options.secret,
|
|
4025
|
+
sender: options.sender,
|
|
4026
|
+
method: "POST",
|
|
4027
|
+
url: destination,
|
|
4028
|
+
body
|
|
4029
|
+
});
|
|
4030
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
4031
|
+
method: "POST",
|
|
4032
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
4033
|
+
body
|
|
4034
|
+
});
|
|
4035
|
+
const result = await response.json();
|
|
4036
|
+
if (!response.ok || (!("ok" in result) || result.ok !== true)) {
|
|
4037
|
+
const rejection = result;
|
|
4038
|
+
throw new Error(`signup control rejected (${response.status}): ${rejection.reason ?? rejection.error ?? "unknown"}`);
|
|
4039
|
+
}
|
|
4040
|
+
return result;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
3444
4043
|
// src/worker.ts
|
|
3445
4044
|
var BUILTIN_ROUTES = [
|
|
3446
4045
|
handleHealth,
|
|
@@ -3448,6 +4047,7 @@ var BUILTIN_ROUTES = [
|
|
|
3448
4047
|
handleMe,
|
|
3449
4048
|
handleCrm,
|
|
3450
4049
|
handleNetworkShared,
|
|
4050
|
+
handleSignupControl,
|
|
3451
4051
|
handleNetworkSnapshot,
|
|
3452
4052
|
handleNetworkRecords,
|
|
3453
4053
|
handleNetworkSharedNotes,
|