@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.js
CHANGED
|
@@ -168,6 +168,56 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// src/member.ts
|
|
171
|
+
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
172
|
+
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
173
|
+
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
174
|
+
function resolveApplication(a) {
|
|
175
|
+
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
176
|
+
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
177
|
+
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
178
|
+
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
179
|
+
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
180
|
+
}
|
|
181
|
+
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
182
|
+
if (unsafe) {
|
|
183
|
+
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
|
|
187
|
+
if (duplicate) {
|
|
188
|
+
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
189
|
+
}
|
|
190
|
+
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
191
|
+
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
192
|
+
}
|
|
193
|
+
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
194
|
+
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
195
|
+
}
|
|
196
|
+
const conditions = a?.conditions ?? {};
|
|
197
|
+
for (const [field, declared] of Object.entries(conditions)) {
|
|
198
|
+
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
199
|
+
const expression = declared?.[key];
|
|
200
|
+
if (expression === void 0) continue;
|
|
201
|
+
if (typeof expression !== "string" || !expression.trim()) {
|
|
202
|
+
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
203
|
+
}
|
|
204
|
+
assertFieldCondition(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
required,
|
|
209
|
+
optional,
|
|
210
|
+
conditions,
|
|
211
|
+
maxLen: a?.maxLen ?? {},
|
|
212
|
+
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
213
|
+
bodyCap: a?.bodyCap ?? 32768,
|
|
214
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
215
|
+
profileFields: a?.profileFields ?? [],
|
|
216
|
+
crmFields: a?.crmFields ?? [],
|
|
217
|
+
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
218
|
+
validateEmail: a?.validateEmail ?? true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
171
221
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
172
222
|
function isValidEmail(value) {
|
|
173
223
|
return typeof value === "string" && EMAIL_RE.test(value);
|
|
@@ -444,6 +494,101 @@ function canApprove(status, p) {
|
|
|
444
494
|
return p.approvableFrom.includes(status);
|
|
445
495
|
}
|
|
446
496
|
|
|
497
|
+
// src/signup-control-store.ts
|
|
498
|
+
function asStored(row) {
|
|
499
|
+
if (!row || typeof row.document !== "object" || row.document === null) return null;
|
|
500
|
+
return row;
|
|
501
|
+
}
|
|
502
|
+
async function hasSignupControl(db, targetId, environment, active) {
|
|
503
|
+
const where = { targetId, environment, ...active ? { active: true } : {} };
|
|
504
|
+
if (db.aggregate) {
|
|
505
|
+
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
506
|
+
}
|
|
507
|
+
const rows2 = (await db.query({ signupControls: {} })).signupControls;
|
|
508
|
+
return Array.isArray(rows2) && rows2.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
509
|
+
}
|
|
510
|
+
async function loadLatestSignupControl(db, targetId, environment) {
|
|
511
|
+
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
512
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
513
|
+
where: { targetId, environment },
|
|
514
|
+
order: { revision: "desc" },
|
|
515
|
+
limit: 1
|
|
516
|
+
} } })).signupControls;
|
|
517
|
+
const latest = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
518
|
+
return asStored(latest);
|
|
519
|
+
}
|
|
520
|
+
async function loadActiveSignupControl(db, targetId, environment) {
|
|
521
|
+
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
522
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
523
|
+
where: { targetId, environment, active: true },
|
|
524
|
+
order: { revision: "desc" },
|
|
525
|
+
limit: 1
|
|
526
|
+
} } })).signupControls;
|
|
527
|
+
const active = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
528
|
+
return asStored(active);
|
|
529
|
+
}
|
|
530
|
+
function controlledApplication(base, document) {
|
|
531
|
+
const delegated = document?.application;
|
|
532
|
+
return delegated ? { ...base, required: delegated.required, optional: delegated.optional, conditions: { ...delegated.conditions ?? {} } } : base;
|
|
533
|
+
}
|
|
534
|
+
function validateControlledApplication(chapter, document) {
|
|
535
|
+
if (!document.application) return [];
|
|
536
|
+
const declared = /* @__PURE__ */ new Set([...chapter.application.required, ...chapter.application.optional]);
|
|
537
|
+
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`);
|
|
538
|
+
}
|
|
539
|
+
function groupAttrs(document) {
|
|
540
|
+
const first = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
541
|
+
const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
|
|
542
|
+
return {
|
|
543
|
+
...first ? { standardPriceCents: first.priceCents } : {},
|
|
544
|
+
foundingDiscountCents: 0,
|
|
545
|
+
stripePriceId: firstPaid?.stripePriceId ?? null,
|
|
546
|
+
...document.policy ?? {},
|
|
547
|
+
...document.scheduling ? { schedulingJson: document.scheduling } : {}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
551
|
+
const { document, digest } = envelope;
|
|
552
|
+
const currentRows = (await db.query({ tiers: { $: {
|
|
553
|
+
where: { groupId: document.target.chapterId },
|
|
554
|
+
limit: 100
|
|
555
|
+
} } })).tiers ?? [];
|
|
556
|
+
const incoming = new Set(document.tiers.map((tier) => tier.id));
|
|
557
|
+
const ops = [];
|
|
558
|
+
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
559
|
+
for (const row of currentRows) {
|
|
560
|
+
const id2 = String(row.id ?? "");
|
|
561
|
+
if (id2 && !incoming.has(id2)) ops.push({ t: "update", ns: "tiers", id: id2, attrs: { active: false } });
|
|
562
|
+
}
|
|
563
|
+
for (const tier of document.tiers) {
|
|
564
|
+
ops.push({ t: "update", ns: "tiers", id: tier.id, attrs: {
|
|
565
|
+
id: tier.id,
|
|
566
|
+
groupId: document.target.chapterId,
|
|
567
|
+
name: tier.name,
|
|
568
|
+
priceCents: tier.priceCents,
|
|
569
|
+
stripePriceId: tier.stripePriceId ?? null,
|
|
570
|
+
blurb: tier.blurb ?? null,
|
|
571
|
+
sortOrder: tier.sortOrder,
|
|
572
|
+
active: tier.available,
|
|
573
|
+
createdAt: receivedAt
|
|
574
|
+
} });
|
|
575
|
+
}
|
|
576
|
+
ops.push({ t: "update", ns: "groups", id: document.target.chapterId, attrs: groupAttrs(document) });
|
|
577
|
+
const id = `signup-control:${document.target.environment}:${document.revision}`;
|
|
578
|
+
ops.push({ t: "update", ns: "signupControls", id, attrs: {
|
|
579
|
+
id,
|
|
580
|
+
sourceId: document.source.appId,
|
|
581
|
+
targetId: document.target.chapterId,
|
|
582
|
+
environment: document.target.environment,
|
|
583
|
+
revision: document.revision,
|
|
584
|
+
digest,
|
|
585
|
+
document,
|
|
586
|
+
active: true,
|
|
587
|
+
receivedAt
|
|
588
|
+
} });
|
|
589
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest}` });
|
|
590
|
+
}
|
|
591
|
+
|
|
447
592
|
// src/session.ts
|
|
448
593
|
function applicationSummary(app) {
|
|
449
594
|
return {
|
|
@@ -791,10 +936,10 @@ function planDelivery(input) {
|
|
|
791
936
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
792
937
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
793
938
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
794
|
-
const
|
|
939
|
+
const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
795
940
|
|
|
796
941
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
797
|
-
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
942
|
+
return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
|
|
798
943
|
}
|
|
799
944
|
|
|
800
945
|
// src/notify.ts
|
|
@@ -1083,9 +1228,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1083
1228
|
group,
|
|
1084
1229
|
Boolean(stripeKey)
|
|
1085
1230
|
);
|
|
1231
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1232
|
+
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1086
1233
|
return json({
|
|
1087
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered),
|
|
1088
|
-
copy: chapter.copy.join
|
|
1234
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1235
|
+
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1236
|
+
...activeControl ? { signupControl: {
|
|
1237
|
+
revision: activeControl.revision,
|
|
1238
|
+
digest: activeControl.digest,
|
|
1239
|
+
membershipPage: activeControl.document.membershipPage,
|
|
1240
|
+
application: activeControl.document.application,
|
|
1241
|
+
theme: activeControl.document.theme
|
|
1242
|
+
} } : {}
|
|
1089
1243
|
});
|
|
1090
1244
|
}
|
|
1091
1245
|
if (req.method === "GET" && url.pathname === "/api/join/resume") {
|
|
@@ -1148,8 +1302,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1148
1302
|
}
|
|
1149
1303
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
1150
1304
|
const db = ctx.makeDb(env);
|
|
1305
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1306
|
+
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
1151
1307
|
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
1152
|
-
const result = await submitApplication(db,
|
|
1308
|
+
const result = await submitApplication(db, effectiveChapter, parsed, {
|
|
1153
1309
|
submissionId,
|
|
1154
1310
|
groupId: chapter.id,
|
|
1155
1311
|
now: Date.now(),
|
|
@@ -1158,10 +1314,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1158
1314
|
});
|
|
1159
1315
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
1160
1316
|
if (!result.duplicate) {
|
|
1161
|
-
if (
|
|
1317
|
+
if (effectiveChapter.sends.adminNotification === "submit") {
|
|
1162
1318
|
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
1163
1319
|
}
|
|
1164
|
-
await provisionApplicant(db,
|
|
1320
|
+
await provisionApplicant(db, effectiveChapter, result.id, parsed);
|
|
1165
1321
|
}
|
|
1166
1322
|
return json({
|
|
1167
1323
|
id: result.id,
|
|
@@ -1231,6 +1387,433 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
1231
1387
|
}
|
|
1232
1388
|
};
|
|
1233
1389
|
|
|
1390
|
+
// src/worker-routes-signup-control.ts
|
|
1391
|
+
import { verifyFederatedRequest as verifyFederatedRequest2 } from "@odla-ai/db";
|
|
1392
|
+
|
|
1393
|
+
// src/signup-control-canonical.ts
|
|
1394
|
+
var encoder = new TextEncoder();
|
|
1395
|
+
function canonicalValue(value) {
|
|
1396
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
1397
|
+
return JSON.stringify(value);
|
|
1398
|
+
}
|
|
1399
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
1400
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
1401
|
+
const record = value;
|
|
1402
|
+
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
1403
|
+
const item = record[key];
|
|
1404
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
1405
|
+
});
|
|
1406
|
+
return `{${entries.join(",")}}`;
|
|
1407
|
+
}
|
|
1408
|
+
function canonicalSignupControl(value) {
|
|
1409
|
+
return canonicalValue(value);
|
|
1410
|
+
}
|
|
1411
|
+
async function signupControlDigest(document) {
|
|
1412
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
1413
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/signup-control-contract.ts
|
|
1417
|
+
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
1418
|
+
|
|
1419
|
+
// src/membership-page.ts
|
|
1420
|
+
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
1421
|
+
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1422
|
+
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
1423
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
1424
|
+
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
1425
|
+
"tierId",
|
|
1426
|
+
"variant",
|
|
1427
|
+
"badge",
|
|
1428
|
+
"summaryMarkdown",
|
|
1429
|
+
"priceEyebrow",
|
|
1430
|
+
"compareAtPriceCents",
|
|
1431
|
+
"billingLabel",
|
|
1432
|
+
"priceNoteMarkdown",
|
|
1433
|
+
"capacity",
|
|
1434
|
+
"benefits",
|
|
1435
|
+
"callout",
|
|
1436
|
+
"ctaLabel"
|
|
1437
|
+
]);
|
|
1438
|
+
var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
1439
|
+
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
1440
|
+
var MAX_TIERS = 20;
|
|
1441
|
+
var MAX_BENEFITS = 24;
|
|
1442
|
+
var MAX_MARKDOWN = 65536;
|
|
1443
|
+
var MAX_LABEL = 160;
|
|
1444
|
+
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1445
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
1446
|
+
for (const key of Object.keys(value)) {
|
|
1447
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function boundedString(value, path, errors, max, required = false) {
|
|
1451
|
+
if (value === void 0) {
|
|
1452
|
+
if (required) errors.push(`${path} is required`);
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof value !== "string") {
|
|
1456
|
+
errors.push(`${path} must be a string`);
|
|
1457
|
+
return void 0;
|
|
1458
|
+
}
|
|
1459
|
+
const text2 = value.trim();
|
|
1460
|
+
if (required && !text2) errors.push(`${path} is required`);
|
|
1461
|
+
if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
1462
|
+
return text2;
|
|
1463
|
+
}
|
|
1464
|
+
function validateMembershipPageDocument(input, options = {}) {
|
|
1465
|
+
const errors = [];
|
|
1466
|
+
const root = objectRecord(input);
|
|
1467
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1468
|
+
rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
|
|
1469
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1470
|
+
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
1471
|
+
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
1472
|
+
}
|
|
1473
|
+
if (!Array.isArray(root.tiers)) errors.push("document.tiers must be an array");
|
|
1474
|
+
const rawTiers = Array.isArray(root.tiers) ? root.tiers : [];
|
|
1475
|
+
if (Array.isArray(root.tiers) && rawTiers.length === 0) errors.push("document.tiers must include at least one tier");
|
|
1476
|
+
if (rawTiers.length > MAX_TIERS) errors.push(`document.tiers supports at most ${MAX_TIERS} entries`);
|
|
1477
|
+
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
1478
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1479
|
+
const tiers = [];
|
|
1480
|
+
for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
1481
|
+
const path = `document.tiers[${index}]`;
|
|
1482
|
+
const item = objectRecord(raw);
|
|
1483
|
+
if (!item) {
|
|
1484
|
+
errors.push(`${path} must be an object`);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
rejectUnknownKeys(item, TIER_KEYS, path, errors);
|
|
1488
|
+
const tierId = boundedString(item.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
1489
|
+
if (tierId && !TIER_ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
1490
|
+
if (seen.has(tierId)) errors.push(`${path}.tierId duplicates "${tierId}"`);
|
|
1491
|
+
seen.add(tierId);
|
|
1492
|
+
if (authority.size && !authority.has(tierId)) errors.push(`${path}.tierId "${tierId}" is not an offered tier`);
|
|
1493
|
+
const variant = item.variant;
|
|
1494
|
+
if (variant !== void 0 && (typeof variant !== "string" || !VARIANTS.has(variant))) {
|
|
1495
|
+
errors.push(`${path}.variant must be standard, featured, or supporting`);
|
|
1496
|
+
}
|
|
1497
|
+
const compareAt = item.compareAtPriceCents;
|
|
1498
|
+
if (compareAt !== void 0 && (!Number.isInteger(compareAt) || Number(compareAt) < 0 || Number(compareAt) > 1e8)) {
|
|
1499
|
+
errors.push(`${path}.compareAtPriceCents must be an integer from 0 to 100,000,000`);
|
|
1500
|
+
}
|
|
1501
|
+
const actual = authority.get(tierId)?.priceCents;
|
|
1502
|
+
if (Number.isInteger(compareAt) && actual !== void 0 && Number(compareAt) <= actual) {
|
|
1503
|
+
errors.push(`${path}.compareAtPriceCents must be greater than the authoritative tier price`);
|
|
1504
|
+
}
|
|
1505
|
+
let benefits;
|
|
1506
|
+
if (item.benefits !== void 0) {
|
|
1507
|
+
if (!Array.isArray(item.benefits)) errors.push(`${path}.benefits must be an array`);
|
|
1508
|
+
else {
|
|
1509
|
+
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
1510
|
+
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
1511
|
+
const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
1512
|
+
return text2 ? [text2] : [];
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
let callout;
|
|
1517
|
+
if (item.callout !== void 0) {
|
|
1518
|
+
const rawCallout = objectRecord(item.callout);
|
|
1519
|
+
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
1520
|
+
else {
|
|
1521
|
+
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
1522
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
|
|
1523
|
+
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
1524
|
+
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
let capacity;
|
|
1528
|
+
if (item.capacity !== void 0) {
|
|
1529
|
+
const rawCapacity = objectRecord(item.capacity);
|
|
1530
|
+
if (!rawCapacity) errors.push(`${path}.capacity must be an object`);
|
|
1531
|
+
else {
|
|
1532
|
+
rejectUnknownKeys(rawCapacity, CAPACITY_KEYS, `${path}.capacity`, errors);
|
|
1533
|
+
const total = rawCapacity.total;
|
|
1534
|
+
if (!Number.isInteger(total) || Number(total) < 1 || Number(total) > 1e6) {
|
|
1535
|
+
errors.push(`${path}.capacity.total must be an integer from 1 to 1,000,000`);
|
|
1536
|
+
}
|
|
1537
|
+
const label = boundedString(rawCapacity.label, `${path}.capacity.label`, errors, MAX_LABEL);
|
|
1538
|
+
if (Number.isInteger(total) && Number(total) >= 1 && Number(total) <= 1e6) {
|
|
1539
|
+
capacity = { total: Number(total), ...label ? { label } : {} };
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
1544
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
|
|
1545
|
+
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
1546
|
+
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
1547
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
|
|
1548
|
+
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
1549
|
+
tiers.push({
|
|
1550
|
+
tierId,
|
|
1551
|
+
...typeof variant === "string" && VARIANTS.has(variant) ? { variant } : {},
|
|
1552
|
+
...badge ? { badge } : {},
|
|
1553
|
+
...summaryMarkdown ? { summaryMarkdown } : {},
|
|
1554
|
+
...priceEyebrow ? { priceEyebrow } : {},
|
|
1555
|
+
...Number.isInteger(compareAt) ? { compareAtPriceCents: Number(compareAt) } : {},
|
|
1556
|
+
...billingLabel ? { billingLabel } : {},
|
|
1557
|
+
...priceNoteMarkdown ? { priceNoteMarkdown } : {},
|
|
1558
|
+
...capacity ? { capacity } : {},
|
|
1559
|
+
...benefits ? { benefits } : {},
|
|
1560
|
+
...callout ? { callout } : {},
|
|
1561
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
if (authority.size) {
|
|
1565
|
+
for (const tierId of authority.keys()) {
|
|
1566
|
+
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
|
|
1570
|
+
if (errors.length) return { ok: false, errors };
|
|
1571
|
+
return {
|
|
1572
|
+
ok: true,
|
|
1573
|
+
value: {
|
|
1574
|
+
schemaVersion: 1,
|
|
1575
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
1576
|
+
tiers,
|
|
1577
|
+
...footnoteMarkdown ? { footnoteMarkdown } : {}
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/signup-control-validate.ts
|
|
1583
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1584
|
+
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
1585
|
+
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1586
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
1587
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
1588
|
+
"schemaVersion",
|
|
1589
|
+
"revision",
|
|
1590
|
+
"generatedAt",
|
|
1591
|
+
"source",
|
|
1592
|
+
"target",
|
|
1593
|
+
"tiers",
|
|
1594
|
+
"membershipPage",
|
|
1595
|
+
"application",
|
|
1596
|
+
"policy",
|
|
1597
|
+
"scheduling",
|
|
1598
|
+
"theme"
|
|
1599
|
+
]);
|
|
1600
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
1601
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1602
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
1603
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1604
|
+
}
|
|
1605
|
+
function text(value, path, errors, max = 65536) {
|
|
1606
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1607
|
+
errors.push(`${path} is required`);
|
|
1608
|
+
return "";
|
|
1609
|
+
}
|
|
1610
|
+
if (value.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
1611
|
+
return value.trim();
|
|
1612
|
+
}
|
|
1613
|
+
function tiersOf(value, errors) {
|
|
1614
|
+
if (!Array.isArray(value) || !value.length) {
|
|
1615
|
+
errors.push("document.tiers must be a non-empty array");
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
1619
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1620
|
+
return value.slice(0, 20).flatMap((raw, index) => {
|
|
1621
|
+
const path = `document.tiers[${index}]`;
|
|
1622
|
+
const item = recordOf(raw);
|
|
1623
|
+
if (!item) {
|
|
1624
|
+
errors.push(`${path} must be an object`);
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
1628
|
+
const id = text(item.id, `${path}.id`, errors, 80);
|
|
1629
|
+
if (id && !TIER.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
1630
|
+
if (seen.has(id)) errors.push(`${path}.id duplicates "${id}"`);
|
|
1631
|
+
seen.add(id);
|
|
1632
|
+
const name = text(item.name, `${path}.name`, errors, 160);
|
|
1633
|
+
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
1634
|
+
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
1635
|
+
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
1636
|
+
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
1637
|
+
}
|
|
1638
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
|
|
1639
|
+
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
1640
|
+
}
|
|
1641
|
+
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
1642
|
+
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
1643
|
+
}
|
|
1644
|
+
const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
1645
|
+
return [{
|
|
1646
|
+
id,
|
|
1647
|
+
name,
|
|
1648
|
+
available: item.available === true,
|
|
1649
|
+
sortOrder: Number(item.sortOrder) || 0,
|
|
1650
|
+
priceCents: Number(item.priceCents) || 0,
|
|
1651
|
+
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
1652
|
+
...blurb ? { blurb } : {}
|
|
1653
|
+
}];
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function applicationOf(value, errors) {
|
|
1657
|
+
if (value === void 0) return void 0;
|
|
1658
|
+
const input = recordOf(value);
|
|
1659
|
+
if (!input) {
|
|
1660
|
+
errors.push("document.application must be an object");
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
1664
|
+
try {
|
|
1665
|
+
const resolved = resolveApplication(input);
|
|
1666
|
+
const copy = recordOf(input.copy);
|
|
1667
|
+
if (input.copy !== void 0 && (!copy || Object.values(copy).some((item) => typeof item !== "string"))) {
|
|
1668
|
+
errors.push("document.application.copy must contain only string values");
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
required: resolved.required,
|
|
1672
|
+
optional: resolved.optional,
|
|
1673
|
+
...Object.keys(resolved.conditions).length ? { conditions: resolved.conditions } : {},
|
|
1674
|
+
...copy ? { copy } : {}
|
|
1675
|
+
};
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
errors.push(error instanceof Error ? error.message : "document.application is invalid");
|
|
1678
|
+
return void 0;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function policyOf(value, errors) {
|
|
1682
|
+
if (value === void 0) return void 0;
|
|
1683
|
+
const input = recordOf(value);
|
|
1684
|
+
if (!input) {
|
|
1685
|
+
errors.push("document.policy must be an object");
|
|
1686
|
+
return void 0;
|
|
1687
|
+
}
|
|
1688
|
+
const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
1689
|
+
unknownKeys(input, allowed, "document.policy", errors);
|
|
1690
|
+
const result = {};
|
|
1691
|
+
for (const key of allowed) {
|
|
1692
|
+
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
1693
|
+
result[key] = text(input[key], `document.policy.${key}`, errors);
|
|
1694
|
+
}
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
function themeOf(value, errors) {
|
|
1698
|
+
if (value === void 0) return void 0;
|
|
1699
|
+
const input = recordOf(value);
|
|
1700
|
+
const tokens = recordOf(input?.tokens);
|
|
1701
|
+
if (!input || !tokens) {
|
|
1702
|
+
errors.push("document.theme.tokens must be an object");
|
|
1703
|
+
return void 0;
|
|
1704
|
+
}
|
|
1705
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
1706
|
+
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
1707
|
+
for (const [key, token] of Object.entries(tokens)) {
|
|
1708
|
+
if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
|
|
1709
|
+
if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
|
|
1710
|
+
errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return { tokens };
|
|
1714
|
+
}
|
|
1715
|
+
function validateSignupControlDocument(input) {
|
|
1716
|
+
const errors = [];
|
|
1717
|
+
const root = recordOf(input);
|
|
1718
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1719
|
+
unknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
1720
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1721
|
+
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
1722
|
+
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
1723
|
+
const source = recordOf(root.source);
|
|
1724
|
+
const target = recordOf(root.target);
|
|
1725
|
+
const appId = text(source?.appId, "document.source.appId", errors, 63);
|
|
1726
|
+
const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
1727
|
+
const environment = text(target?.environment, "document.target.environment", errors, 63);
|
|
1728
|
+
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
1729
|
+
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
1730
|
+
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
1731
|
+
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
1732
|
+
const tiers = tiersOf(root.tiers, errors);
|
|
1733
|
+
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1734
|
+
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
1735
|
+
if (!page.ok) errors.push(...page.errors);
|
|
1736
|
+
else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
|
|
1737
|
+
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
1738
|
+
}
|
|
1739
|
+
const application = applicationOf(root.application, errors);
|
|
1740
|
+
const policy = policyOf(root.policy, errors);
|
|
1741
|
+
let scheduling;
|
|
1742
|
+
if (root.scheduling !== void 0) {
|
|
1743
|
+
const checked = validateScheduling(root.scheduling);
|
|
1744
|
+
if (!checked.ok) errors.push(...Object.entries(checked.errors).map(([key, value]) => `document.scheduling.${key}: ${value}`));
|
|
1745
|
+
else scheduling = root.scheduling;
|
|
1746
|
+
}
|
|
1747
|
+
const theme = themeOf(root.theme, errors);
|
|
1748
|
+
if (errors.length || !page.ok) return { ok: false, errors };
|
|
1749
|
+
return { ok: true, value: {
|
|
1750
|
+
schemaVersion: 1,
|
|
1751
|
+
revision: Number(root.revision),
|
|
1752
|
+
generatedAt: Number(root.generatedAt),
|
|
1753
|
+
source: { appId },
|
|
1754
|
+
target: { chapterId, environment, stripeMode: target?.stripeMode },
|
|
1755
|
+
tiers,
|
|
1756
|
+
membershipPage: page.value,
|
|
1757
|
+
...application ? { application } : {},
|
|
1758
|
+
...policy ? { policy } : {},
|
|
1759
|
+
...scheduling ? { scheduling } : {},
|
|
1760
|
+
...theme ? { theme } : {}
|
|
1761
|
+
} };
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// src/worker-routes-signup-control.ts
|
|
1765
|
+
var reject = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
1766
|
+
var handleSignupControl = async (req, url, env, ctx) => {
|
|
1767
|
+
if (req.method !== "POST" || url.pathname !== SIGNUP_CONTROL_PATH) return null;
|
|
1768
|
+
const authority = ctx.chapter.signupControl;
|
|
1769
|
+
if (!authority) return json({ error: "not found" }, 404);
|
|
1770
|
+
const db = ctx.makeDb(env);
|
|
1771
|
+
const secret = await getVaultSecret(db, authority.secretName);
|
|
1772
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
1773
|
+
const verified = await verifyFederatedRequest2(req, { secret, senders: [authority.sourceId] });
|
|
1774
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
1775
|
+
let input;
|
|
1776
|
+
try {
|
|
1777
|
+
input = JSON.parse(verified.body);
|
|
1778
|
+
} catch {
|
|
1779
|
+
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
1780
|
+
}
|
|
1781
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
1782
|
+
const checked = validateSignupControlDocument(input.document);
|
|
1783
|
+
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
1784
|
+
const document = checked.value;
|
|
1785
|
+
if (document.source.appId !== authority.sourceId) return reject("wrong_source", 403);
|
|
1786
|
+
if (document.target.chapterId !== ctx.chapter.id) return reject("wrong_target", 409);
|
|
1787
|
+
if (document.target.environment !== env.ODLA_ENV) return reject("wrong_environment", 409);
|
|
1788
|
+
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
1789
|
+
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
1790
|
+
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
1791
|
+
if (!digest || await signupControlDigest(document) !== digest) return reject("invalid_digest", 400);
|
|
1792
|
+
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
1793
|
+
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
1794
|
+
if (latest && document.revision === latest.revision) {
|
|
1795
|
+
if (latest.digest !== digest) return reject("conflicting_revision", 409);
|
|
1796
|
+
return json({
|
|
1797
|
+
ok: true,
|
|
1798
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1799
|
+
revision: latest.revision,
|
|
1800
|
+
digest: latest.digest,
|
|
1801
|
+
status: "replayed",
|
|
1802
|
+
acknowledgedAt: latest.receivedAt
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
const receivedAt = Date.now();
|
|
1806
|
+
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
1807
|
+
return json({
|
|
1808
|
+
ok: true,
|
|
1809
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1810
|
+
revision: document.revision,
|
|
1811
|
+
digest,
|
|
1812
|
+
status: "accepted",
|
|
1813
|
+
acknowledgedAt: receivedAt
|
|
1814
|
+
});
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1234
1817
|
// src/crm-sync.ts
|
|
1235
1818
|
import { createRecord as createRecord2, updateRecord as updateRecord2, setStage, linkIdentity } from "@odla-ai/crm";
|
|
1236
1819
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
@@ -2578,11 +3161,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
2578
3161
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
2579
3162
|
const t = templates[key];
|
|
2580
3163
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
2581
|
-
const
|
|
2582
|
-
if (!subject || !
|
|
3164
|
+
const text2 = typeof t?.text === "string" ? t.text : "";
|
|
3165
|
+
if (!subject || !text2.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
2583
3166
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
2584
|
-
if (
|
|
2585
|
-
clean[key] = { subject, text, enabled: t?.enabled !== false };
|
|
3167
|
+
if (text2.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
3168
|
+
clean[key] = { subject, text: text2, enabled: t?.enabled !== false };
|
|
2586
3169
|
}
|
|
2587
3170
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2588
3171
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -2677,7 +3260,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2677
3260
|
|
|
2678
3261
|
// src/worker-routes-network.ts
|
|
2679
3262
|
import { getRecord, typeSummary } from "@odla-ai/crm";
|
|
2680
|
-
import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as
|
|
3263
|
+
import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest3 } from "@odla-ai/db";
|
|
2681
3264
|
|
|
2682
3265
|
// src/worker-network-fetch.ts
|
|
2683
3266
|
function targetFetcher(env, target) {
|
|
@@ -2814,7 +3397,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2814
3397
|
const db = ctx.makeDb(env);
|
|
2815
3398
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2816
3399
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2817
|
-
const verified = await
|
|
3400
|
+
const verified = await verifyFederatedRequest3(req, { secret });
|
|
2818
3401
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2819
3402
|
const types = await Promise.all(
|
|
2820
3403
|
Object.keys(ctx.chapter.crm.config.types).map((type) => typeSummary({ crm: ctx.chapter.crm, db }, type))
|
|
@@ -2925,7 +3508,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2925
3508
|
|
|
2926
3509
|
// src/worker-routes-network-records.ts
|
|
2927
3510
|
import { listRecords } from "@odla-ai/crm";
|
|
2928
|
-
import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as
|
|
3511
|
+
import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
|
|
2929
3512
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2930
3513
|
var MAX_PAGE = 50;
|
|
2931
3514
|
var MAX_OFFSET = 1e4;
|
|
@@ -2963,7 +3546,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
2963
3546
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2964
3547
|
const readers = ctx.chapter.network.readers;
|
|
2965
3548
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
2966
|
-
const verified = await
|
|
3549
|
+
const verified = await verifyFederatedRequest4(req, {
|
|
2967
3550
|
secret,
|
|
2968
3551
|
senders: readers.map((reader2) => reader2.id)
|
|
2969
3552
|
});
|
|
@@ -3147,7 +3730,7 @@ import {
|
|
|
3147
3730
|
CRM_NS,
|
|
3148
3731
|
getRecord as getRecord2
|
|
3149
3732
|
} from "@odla-ai/crm";
|
|
3150
|
-
import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as
|
|
3733
|
+
import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
|
|
3151
3734
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3152
3735
|
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3153
3736
|
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -3206,7 +3789,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
3206
3789
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
3207
3790
|
const readers = ctx.chapter.network.readers;
|
|
3208
3791
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
3209
|
-
const verified = await
|
|
3792
|
+
const verified = await verifyFederatedRequest5(req, {
|
|
3210
3793
|
secret,
|
|
3211
3794
|
senders: readers.map((reader2) => reader2.id)
|
|
3212
3795
|
});
|
|
@@ -3423,6 +4006,31 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
3423
4006
|
}
|
|
3424
4007
|
};
|
|
3425
4008
|
|
|
4009
|
+
// src/signup-control-delivery.ts
|
|
4010
|
+
import { signFederatedRequest as signFederatedRequest5 } from "@odla-ai/db";
|
|
4011
|
+
async function deliverSignupControl(options) {
|
|
4012
|
+
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
4013
|
+
const body = JSON.stringify(options.envelope);
|
|
4014
|
+
const signed = await signFederatedRequest5({
|
|
4015
|
+
secret: options.secret,
|
|
4016
|
+
sender: options.sender,
|
|
4017
|
+
method: "POST",
|
|
4018
|
+
url: destination,
|
|
4019
|
+
body
|
|
4020
|
+
});
|
|
4021
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
4022
|
+
method: "POST",
|
|
4023
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
4024
|
+
body
|
|
4025
|
+
});
|
|
4026
|
+
const result = await response.json();
|
|
4027
|
+
if (!response.ok || (!("ok" in result) || result.ok !== true)) {
|
|
4028
|
+
const rejection = result;
|
|
4029
|
+
throw new Error(`signup control rejected (${response.status}): ${rejection.reason ?? rejection.error ?? "unknown"}`);
|
|
4030
|
+
}
|
|
4031
|
+
return result;
|
|
4032
|
+
}
|
|
4033
|
+
|
|
3426
4034
|
// src/worker.ts
|
|
3427
4035
|
var BUILTIN_ROUTES = [
|
|
3428
4036
|
handleHealth,
|
|
@@ -3430,6 +4038,7 @@ var BUILTIN_ROUTES = [
|
|
|
3430
4038
|
handleMe,
|
|
3431
4039
|
handleCrm,
|
|
3432
4040
|
handleNetworkShared,
|
|
4041
|
+
handleSignupControl,
|
|
3433
4042
|
handleNetworkSnapshot,
|
|
3434
4043
|
handleNetworkRecords,
|
|
3435
4044
|
handleNetworkSharedNotes,
|
|
@@ -3487,6 +4096,7 @@ function chapterWorker(options) {
|
|
|
3487
4096
|
}
|
|
3488
4097
|
export {
|
|
3489
4098
|
chapterWorker,
|
|
3490
|
-
createWorkerContext
|
|
4099
|
+
createWorkerContext,
|
|
4100
|
+
deliverSignupControl
|
|
3491
4101
|
};
|
|
3492
4102
|
//# sourceMappingURL=index.js.map
|