@odla-ai/chapter 0.32.2 → 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-HJNPF5JD.js → chunk-UJIVBGL5.js} +89 -24
- 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-HJNPF5JD.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,91 @@ 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 loadLatestSignupControl(db, targetId, environment) {
|
|
503
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
504
|
+
where: { targetId, environment },
|
|
505
|
+
order: { revision: "desc" },
|
|
506
|
+
limit: 1
|
|
507
|
+
} } })).signupControls;
|
|
508
|
+
const latest = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
509
|
+
return asStored(latest);
|
|
510
|
+
}
|
|
511
|
+
async function loadActiveSignupControl(db, targetId, environment) {
|
|
512
|
+
const rows2 = (await db.query({ signupControls: { $: {
|
|
513
|
+
where: { targetId, environment, active: true },
|
|
514
|
+
order: { revision: "desc" },
|
|
515
|
+
limit: 1
|
|
516
|
+
} } })).signupControls;
|
|
517
|
+
const active = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
518
|
+
return asStored(active);
|
|
519
|
+
}
|
|
520
|
+
function controlledApplication(base, document) {
|
|
521
|
+
const delegated = document?.application;
|
|
522
|
+
return delegated ? { ...base, required: delegated.required, optional: delegated.optional, conditions: { ...delegated.conditions ?? {} } } : base;
|
|
523
|
+
}
|
|
524
|
+
function validateControlledApplication(chapter, document) {
|
|
525
|
+
if (!document.application) return [];
|
|
526
|
+
const declared = /* @__PURE__ */ new Set([...chapter.application.required, ...chapter.application.optional]);
|
|
527
|
+
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`);
|
|
528
|
+
}
|
|
529
|
+
function groupAttrs(document) {
|
|
530
|
+
const first = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
531
|
+
const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
|
|
532
|
+
return {
|
|
533
|
+
...first ? { standardPriceCents: first.priceCents } : {},
|
|
534
|
+
foundingDiscountCents: 0,
|
|
535
|
+
stripePriceId: firstPaid?.stripePriceId ?? null,
|
|
536
|
+
...document.policy ?? {},
|
|
537
|
+
...document.scheduling ? { schedulingJson: document.scheduling } : {}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
541
|
+
const { document, digest } = envelope;
|
|
542
|
+
const currentRows = (await db.query({ tiers: { $: {
|
|
543
|
+
where: { groupId: document.target.chapterId },
|
|
544
|
+
limit: 100
|
|
545
|
+
} } })).tiers ?? [];
|
|
546
|
+
const incoming = new Set(document.tiers.map((tier) => tier.id));
|
|
547
|
+
const ops = [];
|
|
548
|
+
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
549
|
+
for (const row of currentRows) {
|
|
550
|
+
const id2 = String(row.id ?? "");
|
|
551
|
+
if (id2 && !incoming.has(id2)) ops.push({ t: "update", ns: "tiers", id: id2, attrs: { active: false } });
|
|
552
|
+
}
|
|
553
|
+
for (const tier of document.tiers) {
|
|
554
|
+
ops.push({ t: "update", ns: "tiers", id: tier.id, attrs: {
|
|
555
|
+
id: tier.id,
|
|
556
|
+
groupId: document.target.chapterId,
|
|
557
|
+
name: tier.name,
|
|
558
|
+
priceCents: tier.priceCents,
|
|
559
|
+
stripePriceId: tier.stripePriceId ?? null,
|
|
560
|
+
blurb: tier.blurb ?? null,
|
|
561
|
+
sortOrder: tier.sortOrder,
|
|
562
|
+
active: tier.available,
|
|
563
|
+
createdAt: receivedAt
|
|
564
|
+
} });
|
|
565
|
+
}
|
|
566
|
+
ops.push({ t: "update", ns: "groups", id: document.target.chapterId, attrs: groupAttrs(document) });
|
|
567
|
+
const id = `signup-control:${document.target.environment}:${document.revision}`;
|
|
568
|
+
ops.push({ t: "update", ns: "signupControls", id, attrs: {
|
|
569
|
+
id,
|
|
570
|
+
sourceId: document.source.appId,
|
|
571
|
+
targetId: document.target.chapterId,
|
|
572
|
+
environment: document.target.environment,
|
|
573
|
+
revision: document.revision,
|
|
574
|
+
digest,
|
|
575
|
+
document,
|
|
576
|
+
active: true,
|
|
577
|
+
receivedAt
|
|
578
|
+
} });
|
|
579
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest}` });
|
|
580
|
+
}
|
|
581
|
+
|
|
447
582
|
// src/session.ts
|
|
448
583
|
function applicationSummary(app) {
|
|
449
584
|
return {
|
|
@@ -791,10 +926,10 @@ function planDelivery(input) {
|
|
|
791
926
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
792
927
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
793
928
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
794
|
-
const
|
|
929
|
+
const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
795
930
|
|
|
796
931
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
797
|
-
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
932
|
+
return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
|
|
798
933
|
}
|
|
799
934
|
|
|
800
935
|
// src/notify.ts
|
|
@@ -1083,9 +1218,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1083
1218
|
group,
|
|
1084
1219
|
Boolean(stripeKey)
|
|
1085
1220
|
);
|
|
1221
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1222
|
+
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1086
1223
|
return json({
|
|
1087
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered),
|
|
1088
|
-
copy: chapter.copy.join
|
|
1224
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1225
|
+
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1226
|
+
...activeControl ? { signupControl: {
|
|
1227
|
+
revision: activeControl.revision,
|
|
1228
|
+
digest: activeControl.digest,
|
|
1229
|
+
membershipPage: activeControl.document.membershipPage,
|
|
1230
|
+
application: activeControl.document.application,
|
|
1231
|
+
theme: activeControl.document.theme
|
|
1232
|
+
} } : {}
|
|
1089
1233
|
});
|
|
1090
1234
|
}
|
|
1091
1235
|
if (req.method === "GET" && url.pathname === "/api/join/resume") {
|
|
@@ -1148,8 +1292,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1148
1292
|
}
|
|
1149
1293
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
1150
1294
|
const db = ctx.makeDb(env);
|
|
1295
|
+
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
1296
|
+
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
1151
1297
|
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,
|
|
1298
|
+
const result = await submitApplication(db, effectiveChapter, parsed, {
|
|
1153
1299
|
submissionId,
|
|
1154
1300
|
groupId: chapter.id,
|
|
1155
1301
|
now: Date.now(),
|
|
@@ -1158,10 +1304,10 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1158
1304
|
});
|
|
1159
1305
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
1160
1306
|
if (!result.duplicate) {
|
|
1161
|
-
if (
|
|
1307
|
+
if (effectiveChapter.sends.adminNotification === "submit") {
|
|
1162
1308
|
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
1163
1309
|
}
|
|
1164
|
-
await provisionApplicant(db,
|
|
1310
|
+
await provisionApplicant(db, effectiveChapter, result.id, parsed);
|
|
1165
1311
|
}
|
|
1166
1312
|
return json({
|
|
1167
1313
|
id: result.id,
|
|
@@ -1231,6 +1377,433 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
1231
1377
|
}
|
|
1232
1378
|
};
|
|
1233
1379
|
|
|
1380
|
+
// src/worker-routes-signup-control.ts
|
|
1381
|
+
import { verifyFederatedRequest as verifyFederatedRequest2 } from "@odla-ai/db";
|
|
1382
|
+
|
|
1383
|
+
// src/signup-control-canonical.ts
|
|
1384
|
+
var encoder = new TextEncoder();
|
|
1385
|
+
function canonicalValue(value) {
|
|
1386
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
1387
|
+
return JSON.stringify(value);
|
|
1388
|
+
}
|
|
1389
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
1390
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
1391
|
+
const record = value;
|
|
1392
|
+
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
1393
|
+
const item = record[key];
|
|
1394
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
1395
|
+
});
|
|
1396
|
+
return `{${entries.join(",")}}`;
|
|
1397
|
+
}
|
|
1398
|
+
function canonicalSignupControl(value) {
|
|
1399
|
+
return canonicalValue(value);
|
|
1400
|
+
}
|
|
1401
|
+
async function signupControlDigest(document) {
|
|
1402
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
1403
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/signup-control-contract.ts
|
|
1407
|
+
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
1408
|
+
|
|
1409
|
+
// src/membership-page.ts
|
|
1410
|
+
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
1411
|
+
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1412
|
+
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
1413
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
1414
|
+
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
1415
|
+
"tierId",
|
|
1416
|
+
"variant",
|
|
1417
|
+
"badge",
|
|
1418
|
+
"summaryMarkdown",
|
|
1419
|
+
"priceEyebrow",
|
|
1420
|
+
"compareAtPriceCents",
|
|
1421
|
+
"billingLabel",
|
|
1422
|
+
"priceNoteMarkdown",
|
|
1423
|
+
"capacity",
|
|
1424
|
+
"benefits",
|
|
1425
|
+
"callout",
|
|
1426
|
+
"ctaLabel"
|
|
1427
|
+
]);
|
|
1428
|
+
var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
1429
|
+
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
1430
|
+
var MAX_TIERS = 20;
|
|
1431
|
+
var MAX_BENEFITS = 24;
|
|
1432
|
+
var MAX_MARKDOWN = 65536;
|
|
1433
|
+
var MAX_LABEL = 160;
|
|
1434
|
+
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1435
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
1436
|
+
for (const key of Object.keys(value)) {
|
|
1437
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
function boundedString(value, path, errors, max, required = false) {
|
|
1441
|
+
if (value === void 0) {
|
|
1442
|
+
if (required) errors.push(`${path} is required`);
|
|
1443
|
+
return void 0;
|
|
1444
|
+
}
|
|
1445
|
+
if (typeof value !== "string") {
|
|
1446
|
+
errors.push(`${path} must be a string`);
|
|
1447
|
+
return void 0;
|
|
1448
|
+
}
|
|
1449
|
+
const text2 = value.trim();
|
|
1450
|
+
if (required && !text2) errors.push(`${path} is required`);
|
|
1451
|
+
if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
1452
|
+
return text2;
|
|
1453
|
+
}
|
|
1454
|
+
function validateMembershipPageDocument(input, options = {}) {
|
|
1455
|
+
const errors = [];
|
|
1456
|
+
const root = objectRecord(input);
|
|
1457
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1458
|
+
rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
|
|
1459
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1460
|
+
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
1461
|
+
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
1462
|
+
}
|
|
1463
|
+
if (!Array.isArray(root.tiers)) errors.push("document.tiers must be an array");
|
|
1464
|
+
const rawTiers = Array.isArray(root.tiers) ? root.tiers : [];
|
|
1465
|
+
if (Array.isArray(root.tiers) && rawTiers.length === 0) errors.push("document.tiers must include at least one tier");
|
|
1466
|
+
if (rawTiers.length > MAX_TIERS) errors.push(`document.tiers supports at most ${MAX_TIERS} entries`);
|
|
1467
|
+
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
1468
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1469
|
+
const tiers = [];
|
|
1470
|
+
for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
1471
|
+
const path = `document.tiers[${index}]`;
|
|
1472
|
+
const item = objectRecord(raw);
|
|
1473
|
+
if (!item) {
|
|
1474
|
+
errors.push(`${path} must be an object`);
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
rejectUnknownKeys(item, TIER_KEYS, path, errors);
|
|
1478
|
+
const tierId = boundedString(item.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
1479
|
+
if (tierId && !TIER_ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
1480
|
+
if (seen.has(tierId)) errors.push(`${path}.tierId duplicates "${tierId}"`);
|
|
1481
|
+
seen.add(tierId);
|
|
1482
|
+
if (authority.size && !authority.has(tierId)) errors.push(`${path}.tierId "${tierId}" is not an offered tier`);
|
|
1483
|
+
const variant = item.variant;
|
|
1484
|
+
if (variant !== void 0 && (typeof variant !== "string" || !VARIANTS.has(variant))) {
|
|
1485
|
+
errors.push(`${path}.variant must be standard, featured, or supporting`);
|
|
1486
|
+
}
|
|
1487
|
+
const compareAt = item.compareAtPriceCents;
|
|
1488
|
+
if (compareAt !== void 0 && (!Number.isInteger(compareAt) || Number(compareAt) < 0 || Number(compareAt) > 1e8)) {
|
|
1489
|
+
errors.push(`${path}.compareAtPriceCents must be an integer from 0 to 100,000,000`);
|
|
1490
|
+
}
|
|
1491
|
+
const actual = authority.get(tierId)?.priceCents;
|
|
1492
|
+
if (Number.isInteger(compareAt) && actual !== void 0 && Number(compareAt) <= actual) {
|
|
1493
|
+
errors.push(`${path}.compareAtPriceCents must be greater than the authoritative tier price`);
|
|
1494
|
+
}
|
|
1495
|
+
let benefits;
|
|
1496
|
+
if (item.benefits !== void 0) {
|
|
1497
|
+
if (!Array.isArray(item.benefits)) errors.push(`${path}.benefits must be an array`);
|
|
1498
|
+
else {
|
|
1499
|
+
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
1500
|
+
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
1501
|
+
const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
1502
|
+
return text2 ? [text2] : [];
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
let callout;
|
|
1507
|
+
if (item.callout !== void 0) {
|
|
1508
|
+
const rawCallout = objectRecord(item.callout);
|
|
1509
|
+
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
1510
|
+
else {
|
|
1511
|
+
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
1512
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
|
|
1513
|
+
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
1514
|
+
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
let capacity;
|
|
1518
|
+
if (item.capacity !== void 0) {
|
|
1519
|
+
const rawCapacity = objectRecord(item.capacity);
|
|
1520
|
+
if (!rawCapacity) errors.push(`${path}.capacity must be an object`);
|
|
1521
|
+
else {
|
|
1522
|
+
rejectUnknownKeys(rawCapacity, CAPACITY_KEYS, `${path}.capacity`, errors);
|
|
1523
|
+
const total = rawCapacity.total;
|
|
1524
|
+
if (!Number.isInteger(total) || Number(total) < 1 || Number(total) > 1e6) {
|
|
1525
|
+
errors.push(`${path}.capacity.total must be an integer from 1 to 1,000,000`);
|
|
1526
|
+
}
|
|
1527
|
+
const label = boundedString(rawCapacity.label, `${path}.capacity.label`, errors, MAX_LABEL);
|
|
1528
|
+
if (Number.isInteger(total) && Number(total) >= 1 && Number(total) <= 1e6) {
|
|
1529
|
+
capacity = { total: Number(total), ...label ? { label } : {} };
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
1534
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
|
|
1535
|
+
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
1536
|
+
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
1537
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
|
|
1538
|
+
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
1539
|
+
tiers.push({
|
|
1540
|
+
tierId,
|
|
1541
|
+
...typeof variant === "string" && VARIANTS.has(variant) ? { variant } : {},
|
|
1542
|
+
...badge ? { badge } : {},
|
|
1543
|
+
...summaryMarkdown ? { summaryMarkdown } : {},
|
|
1544
|
+
...priceEyebrow ? { priceEyebrow } : {},
|
|
1545
|
+
...Number.isInteger(compareAt) ? { compareAtPriceCents: Number(compareAt) } : {},
|
|
1546
|
+
...billingLabel ? { billingLabel } : {},
|
|
1547
|
+
...priceNoteMarkdown ? { priceNoteMarkdown } : {},
|
|
1548
|
+
...capacity ? { capacity } : {},
|
|
1549
|
+
...benefits ? { benefits } : {},
|
|
1550
|
+
...callout ? { callout } : {},
|
|
1551
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
if (authority.size) {
|
|
1555
|
+
for (const tierId of authority.keys()) {
|
|
1556
|
+
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
|
|
1560
|
+
if (errors.length) return { ok: false, errors };
|
|
1561
|
+
return {
|
|
1562
|
+
ok: true,
|
|
1563
|
+
value: {
|
|
1564
|
+
schemaVersion: 1,
|
|
1565
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
1566
|
+
tiers,
|
|
1567
|
+
...footnoteMarkdown ? { footnoteMarkdown } : {}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/signup-control-validate.ts
|
|
1573
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1574
|
+
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
1575
|
+
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1576
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
1577
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
1578
|
+
"schemaVersion",
|
|
1579
|
+
"revision",
|
|
1580
|
+
"generatedAt",
|
|
1581
|
+
"source",
|
|
1582
|
+
"target",
|
|
1583
|
+
"tiers",
|
|
1584
|
+
"membershipPage",
|
|
1585
|
+
"application",
|
|
1586
|
+
"policy",
|
|
1587
|
+
"scheduling",
|
|
1588
|
+
"theme"
|
|
1589
|
+
]);
|
|
1590
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
1591
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1592
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
1593
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1594
|
+
}
|
|
1595
|
+
function text(value, path, errors, max = 65536) {
|
|
1596
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1597
|
+
errors.push(`${path} is required`);
|
|
1598
|
+
return "";
|
|
1599
|
+
}
|
|
1600
|
+
if (value.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
1601
|
+
return value.trim();
|
|
1602
|
+
}
|
|
1603
|
+
function tiersOf(value, errors) {
|
|
1604
|
+
if (!Array.isArray(value) || !value.length) {
|
|
1605
|
+
errors.push("document.tiers must be a non-empty array");
|
|
1606
|
+
return [];
|
|
1607
|
+
}
|
|
1608
|
+
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
1609
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1610
|
+
return value.slice(0, 20).flatMap((raw, index) => {
|
|
1611
|
+
const path = `document.tiers[${index}]`;
|
|
1612
|
+
const item = recordOf(raw);
|
|
1613
|
+
if (!item) {
|
|
1614
|
+
errors.push(`${path} must be an object`);
|
|
1615
|
+
return [];
|
|
1616
|
+
}
|
|
1617
|
+
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
1618
|
+
const id = text(item.id, `${path}.id`, errors, 80);
|
|
1619
|
+
if (id && !TIER.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
1620
|
+
if (seen.has(id)) errors.push(`${path}.id duplicates "${id}"`);
|
|
1621
|
+
seen.add(id);
|
|
1622
|
+
const name = text(item.name, `${path}.name`, errors, 160);
|
|
1623
|
+
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
1624
|
+
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
1625
|
+
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
1626
|
+
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
1627
|
+
}
|
|
1628
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
|
|
1629
|
+
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
1630
|
+
}
|
|
1631
|
+
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
1632
|
+
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
1633
|
+
}
|
|
1634
|
+
const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
1635
|
+
return [{
|
|
1636
|
+
id,
|
|
1637
|
+
name,
|
|
1638
|
+
available: item.available === true,
|
|
1639
|
+
sortOrder: Number(item.sortOrder) || 0,
|
|
1640
|
+
priceCents: Number(item.priceCents) || 0,
|
|
1641
|
+
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
1642
|
+
...blurb ? { blurb } : {}
|
|
1643
|
+
}];
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
function applicationOf(value, errors) {
|
|
1647
|
+
if (value === void 0) return void 0;
|
|
1648
|
+
const input = recordOf(value);
|
|
1649
|
+
if (!input) {
|
|
1650
|
+
errors.push("document.application must be an object");
|
|
1651
|
+
return void 0;
|
|
1652
|
+
}
|
|
1653
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
1654
|
+
try {
|
|
1655
|
+
const resolved = resolveApplication(input);
|
|
1656
|
+
const copy = recordOf(input.copy);
|
|
1657
|
+
if (input.copy !== void 0 && (!copy || Object.values(copy).some((item) => typeof item !== "string"))) {
|
|
1658
|
+
errors.push("document.application.copy must contain only string values");
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
required: resolved.required,
|
|
1662
|
+
optional: resolved.optional,
|
|
1663
|
+
...Object.keys(resolved.conditions).length ? { conditions: resolved.conditions } : {},
|
|
1664
|
+
...copy ? { copy } : {}
|
|
1665
|
+
};
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
errors.push(error instanceof Error ? error.message : "document.application is invalid");
|
|
1668
|
+
return void 0;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
function policyOf(value, errors) {
|
|
1672
|
+
if (value === void 0) return void 0;
|
|
1673
|
+
const input = recordOf(value);
|
|
1674
|
+
if (!input) {
|
|
1675
|
+
errors.push("document.policy must be an object");
|
|
1676
|
+
return void 0;
|
|
1677
|
+
}
|
|
1678
|
+
const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
1679
|
+
unknownKeys(input, allowed, "document.policy", errors);
|
|
1680
|
+
const result = {};
|
|
1681
|
+
for (const key of allowed) {
|
|
1682
|
+
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
1683
|
+
result[key] = text(input[key], `document.policy.${key}`, errors);
|
|
1684
|
+
}
|
|
1685
|
+
return result;
|
|
1686
|
+
}
|
|
1687
|
+
function themeOf(value, errors) {
|
|
1688
|
+
if (value === void 0) return void 0;
|
|
1689
|
+
const input = recordOf(value);
|
|
1690
|
+
const tokens = recordOf(input?.tokens);
|
|
1691
|
+
if (!input || !tokens) {
|
|
1692
|
+
errors.push("document.theme.tokens must be an object");
|
|
1693
|
+
return void 0;
|
|
1694
|
+
}
|
|
1695
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
1696
|
+
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
1697
|
+
for (const [key, token] of Object.entries(tokens)) {
|
|
1698
|
+
if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
|
|
1699
|
+
if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
|
|
1700
|
+
errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return { tokens };
|
|
1704
|
+
}
|
|
1705
|
+
function validateSignupControlDocument(input) {
|
|
1706
|
+
const errors = [];
|
|
1707
|
+
const root = recordOf(input);
|
|
1708
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1709
|
+
unknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
1710
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
1711
|
+
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
1712
|
+
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
1713
|
+
const source = recordOf(root.source);
|
|
1714
|
+
const target = recordOf(root.target);
|
|
1715
|
+
const appId = text(source?.appId, "document.source.appId", errors, 63);
|
|
1716
|
+
const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
1717
|
+
const environment = text(target?.environment, "document.target.environment", errors, 63);
|
|
1718
|
+
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
1719
|
+
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
1720
|
+
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
1721
|
+
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
1722
|
+
const tiers = tiersOf(root.tiers, errors);
|
|
1723
|
+
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1724
|
+
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
1725
|
+
if (!page.ok) errors.push(...page.errors);
|
|
1726
|
+
else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
|
|
1727
|
+
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
1728
|
+
}
|
|
1729
|
+
const application = applicationOf(root.application, errors);
|
|
1730
|
+
const policy = policyOf(root.policy, errors);
|
|
1731
|
+
let scheduling;
|
|
1732
|
+
if (root.scheduling !== void 0) {
|
|
1733
|
+
const checked = validateScheduling(root.scheduling);
|
|
1734
|
+
if (!checked.ok) errors.push(...Object.entries(checked.errors).map(([key, value]) => `document.scheduling.${key}: ${value}`));
|
|
1735
|
+
else scheduling = root.scheduling;
|
|
1736
|
+
}
|
|
1737
|
+
const theme = themeOf(root.theme, errors);
|
|
1738
|
+
if (errors.length || !page.ok) return { ok: false, errors };
|
|
1739
|
+
return { ok: true, value: {
|
|
1740
|
+
schemaVersion: 1,
|
|
1741
|
+
revision: Number(root.revision),
|
|
1742
|
+
generatedAt: Number(root.generatedAt),
|
|
1743
|
+
source: { appId },
|
|
1744
|
+
target: { chapterId, environment, stripeMode: target?.stripeMode },
|
|
1745
|
+
tiers,
|
|
1746
|
+
membershipPage: page.value,
|
|
1747
|
+
...application ? { application } : {},
|
|
1748
|
+
...policy ? { policy } : {},
|
|
1749
|
+
...scheduling ? { scheduling } : {},
|
|
1750
|
+
...theme ? { theme } : {}
|
|
1751
|
+
} };
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/worker-routes-signup-control.ts
|
|
1755
|
+
var reject = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
1756
|
+
var handleSignupControl = async (req, url, env, ctx) => {
|
|
1757
|
+
if (req.method !== "POST" || url.pathname !== SIGNUP_CONTROL_PATH) return null;
|
|
1758
|
+
const authority = ctx.chapter.signupControl;
|
|
1759
|
+
if (!authority) return json({ error: "not found" }, 404);
|
|
1760
|
+
const db = ctx.makeDb(env);
|
|
1761
|
+
const secret = await getVaultSecret(db, authority.secretName);
|
|
1762
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
1763
|
+
const verified = await verifyFederatedRequest2(req, { secret, senders: [authority.sourceId] });
|
|
1764
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
1765
|
+
let input;
|
|
1766
|
+
try {
|
|
1767
|
+
input = JSON.parse(verified.body);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
1770
|
+
}
|
|
1771
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
1772
|
+
const checked = validateSignupControlDocument(input.document);
|
|
1773
|
+
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
1774
|
+
const document = checked.value;
|
|
1775
|
+
if (document.source.appId !== authority.sourceId) return reject("wrong_source", 403);
|
|
1776
|
+
if (document.target.chapterId !== ctx.chapter.id) return reject("wrong_target", 409);
|
|
1777
|
+
if (document.target.environment !== env.ODLA_ENV) return reject("wrong_environment", 409);
|
|
1778
|
+
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
1779
|
+
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
1780
|
+
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
1781
|
+
if (!digest || await signupControlDigest(document) !== digest) return reject("invalid_digest", 400);
|
|
1782
|
+
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
1783
|
+
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
1784
|
+
if (latest && document.revision === latest.revision) {
|
|
1785
|
+
if (latest.digest !== digest) return reject("conflicting_revision", 409);
|
|
1786
|
+
return json({
|
|
1787
|
+
ok: true,
|
|
1788
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1789
|
+
revision: latest.revision,
|
|
1790
|
+
digest: latest.digest,
|
|
1791
|
+
status: "replayed",
|
|
1792
|
+
acknowledgedAt: latest.receivedAt
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
const receivedAt = Date.now();
|
|
1796
|
+
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
1797
|
+
return json({
|
|
1798
|
+
ok: true,
|
|
1799
|
+
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
1800
|
+
revision: document.revision,
|
|
1801
|
+
digest,
|
|
1802
|
+
status: "accepted",
|
|
1803
|
+
acknowledgedAt: receivedAt
|
|
1804
|
+
});
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1234
1807
|
// src/crm-sync.ts
|
|
1235
1808
|
import { createRecord as createRecord2, updateRecord as updateRecord2, setStage, linkIdentity } from "@odla-ai/crm";
|
|
1236
1809
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
@@ -2578,11 +3151,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
2578
3151
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
2579
3152
|
const t = templates[key];
|
|
2580
3153
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
2581
|
-
const
|
|
2582
|
-
if (!subject || !
|
|
3154
|
+
const text2 = typeof t?.text === "string" ? t.text : "";
|
|
3155
|
+
if (!subject || !text2.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
2583
3156
|
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 };
|
|
3157
|
+
if (text2.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
3158
|
+
clean[key] = { subject, text: text2, enabled: t?.enabled !== false };
|
|
2586
3159
|
}
|
|
2587
3160
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2588
3161
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -2677,7 +3250,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2677
3250
|
|
|
2678
3251
|
// src/worker-routes-network.ts
|
|
2679
3252
|
import { getRecord, typeSummary } from "@odla-ai/crm";
|
|
2680
|
-
import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as
|
|
3253
|
+
import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest3 } from "@odla-ai/db";
|
|
2681
3254
|
|
|
2682
3255
|
// src/worker-network-fetch.ts
|
|
2683
3256
|
function targetFetcher(env, target) {
|
|
@@ -2814,7 +3387,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2814
3387
|
const db = ctx.makeDb(env);
|
|
2815
3388
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2816
3389
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2817
|
-
const verified = await
|
|
3390
|
+
const verified = await verifyFederatedRequest3(req, { secret });
|
|
2818
3391
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2819
3392
|
const types = await Promise.all(
|
|
2820
3393
|
Object.keys(ctx.chapter.crm.config.types).map((type) => typeSummary({ crm: ctx.chapter.crm, db }, type))
|
|
@@ -2925,7 +3498,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2925
3498
|
|
|
2926
3499
|
// src/worker-routes-network-records.ts
|
|
2927
3500
|
import { listRecords } from "@odla-ai/crm";
|
|
2928
|
-
import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as
|
|
3501
|
+
import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
|
|
2929
3502
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2930
3503
|
var MAX_PAGE = 50;
|
|
2931
3504
|
var MAX_OFFSET = 1e4;
|
|
@@ -2963,7 +3536,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
2963
3536
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2964
3537
|
const readers = ctx.chapter.network.readers;
|
|
2965
3538
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
2966
|
-
const verified = await
|
|
3539
|
+
const verified = await verifyFederatedRequest4(req, {
|
|
2967
3540
|
secret,
|
|
2968
3541
|
senders: readers.map((reader2) => reader2.id)
|
|
2969
3542
|
});
|
|
@@ -3147,7 +3720,7 @@ import {
|
|
|
3147
3720
|
CRM_NS,
|
|
3148
3721
|
getRecord as getRecord2
|
|
3149
3722
|
} from "@odla-ai/crm";
|
|
3150
|
-
import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as
|
|
3723
|
+
import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
|
|
3151
3724
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3152
3725
|
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3153
3726
|
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -3206,7 +3779,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
3206
3779
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
3207
3780
|
const readers = ctx.chapter.network.readers;
|
|
3208
3781
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
3209
|
-
const verified = await
|
|
3782
|
+
const verified = await verifyFederatedRequest5(req, {
|
|
3210
3783
|
secret,
|
|
3211
3784
|
senders: readers.map((reader2) => reader2.id)
|
|
3212
3785
|
});
|
|
@@ -3423,6 +3996,31 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
3423
3996
|
}
|
|
3424
3997
|
};
|
|
3425
3998
|
|
|
3999
|
+
// src/signup-control-delivery.ts
|
|
4000
|
+
import { signFederatedRequest as signFederatedRequest5 } from "@odla-ai/db";
|
|
4001
|
+
async function deliverSignupControl(options) {
|
|
4002
|
+
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
4003
|
+
const body = JSON.stringify(options.envelope);
|
|
4004
|
+
const signed = await signFederatedRequest5({
|
|
4005
|
+
secret: options.secret,
|
|
4006
|
+
sender: options.sender,
|
|
4007
|
+
method: "POST",
|
|
4008
|
+
url: destination,
|
|
4009
|
+
body
|
|
4010
|
+
});
|
|
4011
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
4012
|
+
method: "POST",
|
|
4013
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
4014
|
+
body
|
|
4015
|
+
});
|
|
4016
|
+
const result = await response.json();
|
|
4017
|
+
if (!response.ok || (!("ok" in result) || result.ok !== true)) {
|
|
4018
|
+
const rejection = result;
|
|
4019
|
+
throw new Error(`signup control rejected (${response.status}): ${rejection.reason ?? rejection.error ?? "unknown"}`);
|
|
4020
|
+
}
|
|
4021
|
+
return result;
|
|
4022
|
+
}
|
|
4023
|
+
|
|
3426
4024
|
// src/worker.ts
|
|
3427
4025
|
var BUILTIN_ROUTES = [
|
|
3428
4026
|
handleHealth,
|
|
@@ -3430,6 +4028,7 @@ var BUILTIN_ROUTES = [
|
|
|
3430
4028
|
handleMe,
|
|
3431
4029
|
handleCrm,
|
|
3432
4030
|
handleNetworkShared,
|
|
4031
|
+
handleSignupControl,
|
|
3433
4032
|
handleNetworkSnapshot,
|
|
3434
4033
|
handleNetworkRecords,
|
|
3435
4034
|
handleNetworkSharedNotes,
|
|
@@ -3487,6 +4086,7 @@ function chapterWorker(options) {
|
|
|
3487
4086
|
}
|
|
3488
4087
|
export {
|
|
3489
4088
|
chapterWorker,
|
|
3490
|
-
createWorkerContext
|
|
4089
|
+
createWorkerContext,
|
|
4090
|
+
deliverSignupControl
|
|
3491
4091
|
};
|
|
3492
4092
|
//# sourceMappingURL=index.js.map
|