@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/index.cjs
CHANGED
|
@@ -24,6 +24,8 @@ __export(index_exports, {
|
|
|
24
24
|
DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
|
|
25
25
|
MEMBERSHIP_PAGE_RENDERER: () => MEMBERSHIP_PAGE_RENDERER,
|
|
26
26
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
27
|
+
SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
|
|
28
|
+
SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
27
29
|
applicantProfile: () => applicantProfile,
|
|
28
30
|
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
29
31
|
applicationSummary: () => applicationSummary,
|
|
@@ -39,6 +41,7 @@ __export(index_exports, {
|
|
|
39
41
|
canChangeRole: () => canChangeRole,
|
|
40
42
|
canTransition: () => canTransition,
|
|
41
43
|
canceledPatch: () => canceledPatch,
|
|
44
|
+
canonicalSignupControl: () => canonicalSignupControl,
|
|
42
45
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
43
46
|
chapterDb: () => chapterDb,
|
|
44
47
|
clampArray: () => clampArray,
|
|
@@ -52,6 +55,7 @@ __export(index_exports, {
|
|
|
52
55
|
createChapterIntegration: () => createChapterIntegration,
|
|
53
56
|
createClerkInvitation: () => createClerkInvitation,
|
|
54
57
|
createClerkUser: () => createClerkUser,
|
|
58
|
+
createSignupControlEnvelope: () => createSignupControlEnvelope,
|
|
55
59
|
dashboardMetricData: () => dashboardMetricData,
|
|
56
60
|
defaultCrm: () => defaultCrm,
|
|
57
61
|
defineChapter: () => defineChapter,
|
|
@@ -102,11 +106,13 @@ __export(index_exports, {
|
|
|
102
106
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
103
107
|
resolvePipeline: () => resolvePipeline,
|
|
104
108
|
resolveScheduling: () => resolveScheduling,
|
|
109
|
+
resolveSignupControl: () => resolveSignupControl,
|
|
105
110
|
resolveTiers: () => resolveTiers,
|
|
106
111
|
roleFromClaim: () => roleFromClaim,
|
|
107
112
|
sendTemplated: () => sendTemplated,
|
|
108
113
|
sharedPersonInput: () => sharedPersonInput,
|
|
109
114
|
sharedRecordFromCrm: () => sharedRecordFromCrm,
|
|
115
|
+
signupControlDigest: () => signupControlDigest,
|
|
110
116
|
slotWindow: () => slotWindow,
|
|
111
117
|
stageIndex: () => stageIndex,
|
|
112
118
|
stripeCall: () => stripeCall,
|
|
@@ -121,6 +127,7 @@ __export(index_exports, {
|
|
|
121
127
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
122
128
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
123
129
|
validateScheduling: () => validateScheduling,
|
|
130
|
+
validateSignupControlDocument: () => validateSignupControlDocument,
|
|
124
131
|
verifyStripeSignature: () => import_stripe.verifyStripeSignature,
|
|
125
132
|
webhookMutationId: () => webhookMutationId
|
|
126
133
|
});
|
|
@@ -316,6 +323,19 @@ var networkNotes = {
|
|
|
316
323
|
createdAt: attr("number", { indexed: true })
|
|
317
324
|
}
|
|
318
325
|
};
|
|
326
|
+
var signupControls = {
|
|
327
|
+
attrs: {
|
|
328
|
+
id: id(),
|
|
329
|
+
sourceId: attr("string", { indexed: true }),
|
|
330
|
+
targetId: attr("string", { indexed: true }),
|
|
331
|
+
environment: attr("string", { indexed: true }),
|
|
332
|
+
revision: attr("number", { indexed: true }),
|
|
333
|
+
digest: attr("string", { indexed: true }),
|
|
334
|
+
document: attr("json"),
|
|
335
|
+
active: attr("boolean", { indexed: true }),
|
|
336
|
+
receivedAt: attr("number", { indexed: true })
|
|
337
|
+
}
|
|
338
|
+
};
|
|
319
339
|
var applications = {
|
|
320
340
|
attrs: {
|
|
321
341
|
id: id(),
|
|
@@ -463,7 +483,7 @@ var emailLog = {
|
|
|
463
483
|
sentAt: attr("number", { indexed: true })
|
|
464
484
|
}
|
|
465
485
|
};
|
|
466
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0)) {
|
|
486
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
467
487
|
const entities = {};
|
|
468
488
|
if (mode === "chapter") {
|
|
469
489
|
entities.applications = applicationsFor(application);
|
|
@@ -471,6 +491,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
471
491
|
entities.tiers = tiers;
|
|
472
492
|
entities.meetings = meetings;
|
|
473
493
|
entities.emailLog = emailLog;
|
|
494
|
+
if (includeSignupControl) entities.signupControls = signupControls;
|
|
474
495
|
}
|
|
475
496
|
if (auth.source === "table") entities.admins = admins;
|
|
476
497
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -1244,15 +1265,33 @@ function resolveNetwork(config, crm) {
|
|
|
1244
1265
|
return { targets, readers };
|
|
1245
1266
|
}
|
|
1246
1267
|
|
|
1247
|
-
// src/config.ts
|
|
1268
|
+
// src/config-signup-control.ts
|
|
1248
1269
|
var SLUG2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1270
|
+
var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1271
|
+
function resolveSignupControl(input) {
|
|
1272
|
+
if (!input) return null;
|
|
1273
|
+
if (!SLUG2.test(input.sourceId)) {
|
|
1274
|
+
throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
|
|
1275
|
+
}
|
|
1276
|
+
if (input.stripeMode !== "test" && input.stripeMode !== "live") {
|
|
1277
|
+
throw new Error('defineChapter.signupControl.stripeMode: must be "test" or "live"');
|
|
1278
|
+
}
|
|
1279
|
+
const secretName = input.secretName?.trim() || "signup_control_secret";
|
|
1280
|
+
if (!SECRET_NAME.test(secretName)) {
|
|
1281
|
+
throw new Error("defineChapter.signupControl.secretName: must be a vault-key identifier");
|
|
1282
|
+
}
|
|
1283
|
+
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/config.ts
|
|
1287
|
+
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1249
1288
|
function isResolvedCrm(x) {
|
|
1250
1289
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1251
1290
|
}
|
|
1252
1291
|
function defineChapter(config) {
|
|
1253
1292
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1254
1293
|
const { id: id2, name } = config;
|
|
1255
|
-
if (typeof id2 !== "string" || !
|
|
1294
|
+
if (typeof id2 !== "string" || !SLUG3.test(id2)) {
|
|
1256
1295
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
|
|
1257
1296
|
}
|
|
1258
1297
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1276,8 +1315,18 @@ function defineChapter(config) {
|
|
|
1276
1315
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
1277
1316
|
const copy = resolveChapterCopy(config.copy);
|
|
1278
1317
|
const network = resolveNetwork(config, crm);
|
|
1318
|
+
const signupControl = resolveSignupControl(config.signupControl);
|
|
1279
1319
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1280
|
-
|
|
1320
|
+
if (signupControl && mode !== "chapter") {
|
|
1321
|
+
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1322
|
+
}
|
|
1323
|
+
const { schema, rules } = chapterDb(
|
|
1324
|
+
mode,
|
|
1325
|
+
auth,
|
|
1326
|
+
network.targets.length > 0,
|
|
1327
|
+
application,
|
|
1328
|
+
Boolean(signupControl)
|
|
1329
|
+
);
|
|
1281
1330
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1282
1331
|
const account = config.account ?? "none";
|
|
1283
1332
|
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
@@ -1332,6 +1381,7 @@ function defineChapter(config) {
|
|
|
1332
1381
|
brand,
|
|
1333
1382
|
copy,
|
|
1334
1383
|
network,
|
|
1384
|
+
signupControl,
|
|
1335
1385
|
formation,
|
|
1336
1386
|
crm,
|
|
1337
1387
|
auth,
|
|
@@ -1662,10 +1712,10 @@ function planDelivery(input) {
|
|
|
1662
1712
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1663
1713
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1664
1714
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1665
|
-
const
|
|
1715
|
+
const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1666
1716
|
|
|
1667
1717
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1668
|
-
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
1718
|
+
return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
|
|
1669
1719
|
}
|
|
1670
1720
|
|
|
1671
1721
|
// src/notify.ts
|
|
@@ -2651,6 +2701,36 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
|
2651
2701
|
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
2652
2702
|
}
|
|
2653
2703
|
|
|
2704
|
+
// src/signup-control-contract.ts
|
|
2705
|
+
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
2706
|
+
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
2707
|
+
|
|
2708
|
+
// src/signup-control-canonical.ts
|
|
2709
|
+
var encoder = new TextEncoder();
|
|
2710
|
+
function canonicalValue(value) {
|
|
2711
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2712
|
+
return JSON.stringify(value);
|
|
2713
|
+
}
|
|
2714
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2715
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2716
|
+
const record = value;
|
|
2717
|
+
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
2718
|
+
const item = record[key];
|
|
2719
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2720
|
+
});
|
|
2721
|
+
return `{${entries.join(",")}}`;
|
|
2722
|
+
}
|
|
2723
|
+
function canonicalSignupControl(value) {
|
|
2724
|
+
return canonicalValue(value);
|
|
2725
|
+
}
|
|
2726
|
+
async function signupControlDigest(document) {
|
|
2727
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
2728
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2729
|
+
}
|
|
2730
|
+
async function createSignupControlEnvelope(document) {
|
|
2731
|
+
return { document, digest: await signupControlDigest(document) };
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2654
2734
|
// src/membership-page.ts
|
|
2655
2735
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2656
2736
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
@@ -2691,10 +2771,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2691
2771
|
errors.push(`${path} must be a string`);
|
|
2692
2772
|
return void 0;
|
|
2693
2773
|
}
|
|
2694
|
-
const
|
|
2695
|
-
if (required && !
|
|
2696
|
-
if (
|
|
2697
|
-
return
|
|
2774
|
+
const text2 = value.trim();
|
|
2775
|
+
if (required && !text2) errors.push(`${path} is required`);
|
|
2776
|
+
if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2777
|
+
return text2;
|
|
2698
2778
|
}
|
|
2699
2779
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2700
2780
|
const errors = [];
|
|
@@ -2743,8 +2823,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2743
2823
|
else {
|
|
2744
2824
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2745
2825
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2746
|
-
const
|
|
2747
|
-
return
|
|
2826
|
+
const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2827
|
+
return text2 ? [text2] : [];
|
|
2748
2828
|
});
|
|
2749
2829
|
}
|
|
2750
2830
|
}
|
|
@@ -2819,6 +2899,188 @@ function membershipPageDocument(input, options = {}) {
|
|
|
2819
2899
|
return result.value;
|
|
2820
2900
|
}
|
|
2821
2901
|
|
|
2902
|
+
// src/signup-control-validate.ts
|
|
2903
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2904
|
+
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
2905
|
+
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2906
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2907
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
2908
|
+
"schemaVersion",
|
|
2909
|
+
"revision",
|
|
2910
|
+
"generatedAt",
|
|
2911
|
+
"source",
|
|
2912
|
+
"target",
|
|
2913
|
+
"tiers",
|
|
2914
|
+
"membershipPage",
|
|
2915
|
+
"application",
|
|
2916
|
+
"policy",
|
|
2917
|
+
"scheduling",
|
|
2918
|
+
"theme"
|
|
2919
|
+
]);
|
|
2920
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2921
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2922
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
2923
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2924
|
+
}
|
|
2925
|
+
function text(value, path, errors, max = 65536) {
|
|
2926
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2927
|
+
errors.push(`${path} is required`);
|
|
2928
|
+
return "";
|
|
2929
|
+
}
|
|
2930
|
+
if (value.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2931
|
+
return value.trim();
|
|
2932
|
+
}
|
|
2933
|
+
function tiersOf(value, errors) {
|
|
2934
|
+
if (!Array.isArray(value) || !value.length) {
|
|
2935
|
+
errors.push("document.tiers must be a non-empty array");
|
|
2936
|
+
return [];
|
|
2937
|
+
}
|
|
2938
|
+
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
2939
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2940
|
+
return value.slice(0, 20).flatMap((raw, index) => {
|
|
2941
|
+
const path = `document.tiers[${index}]`;
|
|
2942
|
+
const item = recordOf(raw);
|
|
2943
|
+
if (!item) {
|
|
2944
|
+
errors.push(`${path} must be an object`);
|
|
2945
|
+
return [];
|
|
2946
|
+
}
|
|
2947
|
+
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
2948
|
+
const id2 = text(item.id, `${path}.id`, errors, 80);
|
|
2949
|
+
if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2950
|
+
if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
|
|
2951
|
+
seen.add(id2);
|
|
2952
|
+
const name = text(item.name, `${path}.name`, errors, 160);
|
|
2953
|
+
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
2954
|
+
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
2955
|
+
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
2956
|
+
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
2957
|
+
}
|
|
2958
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
|
|
2959
|
+
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2960
|
+
}
|
|
2961
|
+
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2962
|
+
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2963
|
+
}
|
|
2964
|
+
const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2965
|
+
return [{
|
|
2966
|
+
id: id2,
|
|
2967
|
+
name,
|
|
2968
|
+
available: item.available === true,
|
|
2969
|
+
sortOrder: Number(item.sortOrder) || 0,
|
|
2970
|
+
priceCents: Number(item.priceCents) || 0,
|
|
2971
|
+
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
2972
|
+
...blurb ? { blurb } : {}
|
|
2973
|
+
}];
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
function applicationOf(value, errors) {
|
|
2977
|
+
if (value === void 0) return void 0;
|
|
2978
|
+
const input = recordOf(value);
|
|
2979
|
+
if (!input) {
|
|
2980
|
+
errors.push("document.application must be an object");
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
2984
|
+
try {
|
|
2985
|
+
const resolved = resolveApplication(input);
|
|
2986
|
+
const copy = recordOf(input.copy);
|
|
2987
|
+
if (input.copy !== void 0 && (!copy || Object.values(copy).some((item) => typeof item !== "string"))) {
|
|
2988
|
+
errors.push("document.application.copy must contain only string values");
|
|
2989
|
+
}
|
|
2990
|
+
return {
|
|
2991
|
+
required: resolved.required,
|
|
2992
|
+
optional: resolved.optional,
|
|
2993
|
+
...Object.keys(resolved.conditions).length ? { conditions: resolved.conditions } : {},
|
|
2994
|
+
...copy ? { copy } : {}
|
|
2995
|
+
};
|
|
2996
|
+
} catch (error) {
|
|
2997
|
+
errors.push(error instanceof Error ? error.message : "document.application is invalid");
|
|
2998
|
+
return void 0;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
function policyOf(value, errors) {
|
|
3002
|
+
if (value === void 0) return void 0;
|
|
3003
|
+
const input = recordOf(value);
|
|
3004
|
+
if (!input) {
|
|
3005
|
+
errors.push("document.policy must be an object");
|
|
3006
|
+
return void 0;
|
|
3007
|
+
}
|
|
3008
|
+
const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3009
|
+
unknownKeys(input, allowed, "document.policy", errors);
|
|
3010
|
+
const result = {};
|
|
3011
|
+
for (const key of allowed) {
|
|
3012
|
+
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3013
|
+
result[key] = text(input[key], `document.policy.${key}`, errors);
|
|
3014
|
+
}
|
|
3015
|
+
return result;
|
|
3016
|
+
}
|
|
3017
|
+
function themeOf(value, errors) {
|
|
3018
|
+
if (value === void 0) return void 0;
|
|
3019
|
+
const input = recordOf(value);
|
|
3020
|
+
const tokens = recordOf(input?.tokens);
|
|
3021
|
+
if (!input || !tokens) {
|
|
3022
|
+
errors.push("document.theme.tokens must be an object");
|
|
3023
|
+
return void 0;
|
|
3024
|
+
}
|
|
3025
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3026
|
+
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3027
|
+
for (const [key, token] of Object.entries(tokens)) {
|
|
3028
|
+
if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
|
|
3029
|
+
if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
|
|
3030
|
+
errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
return { tokens };
|
|
3034
|
+
}
|
|
3035
|
+
function validateSignupControlDocument(input) {
|
|
3036
|
+
const errors = [];
|
|
3037
|
+
const root = recordOf(input);
|
|
3038
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3039
|
+
unknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
3040
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3041
|
+
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3042
|
+
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
3043
|
+
const source = recordOf(root.source);
|
|
3044
|
+
const target = recordOf(root.target);
|
|
3045
|
+
const appId = text(source?.appId, "document.source.appId", errors, 63);
|
|
3046
|
+
const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3047
|
+
const environment = text(target?.environment, "document.target.environment", errors, 63);
|
|
3048
|
+
if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3049
|
+
if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3050
|
+
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3051
|
+
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3052
|
+
const tiers2 = tiersOf(root.tiers, errors);
|
|
3053
|
+
const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
3054
|
+
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
3055
|
+
if (!page.ok) errors.push(...page.errors);
|
|
3056
|
+
else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
|
|
3057
|
+
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
3058
|
+
}
|
|
3059
|
+
const application = applicationOf(root.application, errors);
|
|
3060
|
+
const policy = policyOf(root.policy, errors);
|
|
3061
|
+
let scheduling;
|
|
3062
|
+
if (root.scheduling !== void 0) {
|
|
3063
|
+
const checked = validateScheduling(root.scheduling);
|
|
3064
|
+
if (!checked.ok) errors.push(...Object.entries(checked.errors).map(([key, value]) => `document.scheduling.${key}: ${value}`));
|
|
3065
|
+
else scheduling = root.scheduling;
|
|
3066
|
+
}
|
|
3067
|
+
const theme = themeOf(root.theme, errors);
|
|
3068
|
+
if (errors.length || !page.ok) return { ok: false, errors };
|
|
3069
|
+
return { ok: true, value: {
|
|
3070
|
+
schemaVersion: 1,
|
|
3071
|
+
revision: Number(root.revision),
|
|
3072
|
+
generatedAt: Number(root.generatedAt),
|
|
3073
|
+
source: { appId },
|
|
3074
|
+
target: { chapterId, environment, stripeMode: target?.stripeMode },
|
|
3075
|
+
tiers: tiers2,
|
|
3076
|
+
membershipPage: page.value,
|
|
3077
|
+
...application ? { application } : {},
|
|
3078
|
+
...policy ? { policy } : {},
|
|
3079
|
+
...scheduling ? { scheduling } : {},
|
|
3080
|
+
...theme ? { theme } : {}
|
|
3081
|
+
} };
|
|
3082
|
+
}
|
|
3083
|
+
|
|
2822
3084
|
// src/membership-markdown.ts
|
|
2823
3085
|
var MAX_BENEFITS2 = 24;
|
|
2824
3086
|
var MAX_MARKDOWN2 = 65536;
|
|
@@ -2846,9 +3108,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
2846
3108
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
2847
3109
|
continue;
|
|
2848
3110
|
}
|
|
2849
|
-
const
|
|
2850
|
-
if (
|
|
2851
|
-
if (
|
|
3111
|
+
const text2 = (match[1] ?? "").trim();
|
|
3112
|
+
if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
3113
|
+
if (text2) benefits.push(text2);
|
|
2852
3114
|
}
|
|
2853
3115
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
2854
3116
|
return benefits.slice(0, MAX_BENEFITS2);
|