@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/index.js
CHANGED
|
@@ -188,6 +188,19 @@ var networkNotes = {
|
|
|
188
188
|
createdAt: attr("number", { indexed: true })
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
|
+
var signupControls = {
|
|
192
|
+
attrs: {
|
|
193
|
+
id: id(),
|
|
194
|
+
sourceId: attr("string", { indexed: true }),
|
|
195
|
+
targetId: attr("string", { indexed: true }),
|
|
196
|
+
environment: attr("string", { indexed: true }),
|
|
197
|
+
revision: attr("number", { indexed: true }),
|
|
198
|
+
digest: attr("string", { indexed: true }),
|
|
199
|
+
document: attr("json"),
|
|
200
|
+
active: attr("boolean", { indexed: true }),
|
|
201
|
+
receivedAt: attr("number", { indexed: true })
|
|
202
|
+
}
|
|
203
|
+
};
|
|
191
204
|
var applications = {
|
|
192
205
|
attrs: {
|
|
193
206
|
id: id(),
|
|
@@ -335,7 +348,7 @@ var emailLog = {
|
|
|
335
348
|
sentAt: attr("number", { indexed: true })
|
|
336
349
|
}
|
|
337
350
|
};
|
|
338
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0)) {
|
|
351
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
339
352
|
const entities = {};
|
|
340
353
|
if (mode === "chapter") {
|
|
341
354
|
entities.applications = applicationsFor(application);
|
|
@@ -343,6 +356,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
343
356
|
entities.tiers = tiers;
|
|
344
357
|
entities.meetings = meetings;
|
|
345
358
|
entities.emailLog = emailLog;
|
|
359
|
+
if (includeSignupControl) entities.signupControls = signupControls;
|
|
346
360
|
}
|
|
347
361
|
if (auth.source === "table") entities.admins = admins;
|
|
348
362
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -1116,15 +1130,33 @@ function resolveNetwork(config, crm) {
|
|
|
1116
1130
|
return { targets, readers };
|
|
1117
1131
|
}
|
|
1118
1132
|
|
|
1119
|
-
// src/config.ts
|
|
1133
|
+
// src/config-signup-control.ts
|
|
1120
1134
|
var SLUG2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1135
|
+
var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1136
|
+
function resolveSignupControl(input) {
|
|
1137
|
+
if (!input) return null;
|
|
1138
|
+
if (!SLUG2.test(input.sourceId)) {
|
|
1139
|
+
throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
|
|
1140
|
+
}
|
|
1141
|
+
if (input.stripeMode !== "test" && input.stripeMode !== "live") {
|
|
1142
|
+
throw new Error('defineChapter.signupControl.stripeMode: must be "test" or "live"');
|
|
1143
|
+
}
|
|
1144
|
+
const secretName = input.secretName?.trim() || "signup_control_secret";
|
|
1145
|
+
if (!SECRET_NAME.test(secretName)) {
|
|
1146
|
+
throw new Error("defineChapter.signupControl.secretName: must be a vault-key identifier");
|
|
1147
|
+
}
|
|
1148
|
+
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/config.ts
|
|
1152
|
+
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1121
1153
|
function isResolvedCrm(x) {
|
|
1122
1154
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1123
1155
|
}
|
|
1124
1156
|
function defineChapter(config) {
|
|
1125
1157
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1126
1158
|
const { id: id2, name } = config;
|
|
1127
|
-
if (typeof id2 !== "string" || !
|
|
1159
|
+
if (typeof id2 !== "string" || !SLUG3.test(id2)) {
|
|
1128
1160
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
|
|
1129
1161
|
}
|
|
1130
1162
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1148,8 +1180,18 @@ function defineChapter(config) {
|
|
|
1148
1180
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
1149
1181
|
const copy = resolveChapterCopy(config.copy);
|
|
1150
1182
|
const network = resolveNetwork(config, crm);
|
|
1183
|
+
const signupControl = resolveSignupControl(config.signupControl);
|
|
1151
1184
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1152
|
-
|
|
1185
|
+
if (signupControl && mode !== "chapter") {
|
|
1186
|
+
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1187
|
+
}
|
|
1188
|
+
const { schema, rules } = chapterDb(
|
|
1189
|
+
mode,
|
|
1190
|
+
auth,
|
|
1191
|
+
network.targets.length > 0,
|
|
1192
|
+
application,
|
|
1193
|
+
Boolean(signupControl)
|
|
1194
|
+
);
|
|
1153
1195
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1154
1196
|
const account = config.account ?? "none";
|
|
1155
1197
|
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
@@ -1204,6 +1246,7 @@ function defineChapter(config) {
|
|
|
1204
1246
|
brand,
|
|
1205
1247
|
copy,
|
|
1206
1248
|
network,
|
|
1249
|
+
signupControl,
|
|
1207
1250
|
formation,
|
|
1208
1251
|
crm,
|
|
1209
1252
|
auth,
|
|
@@ -1534,10 +1577,10 @@ function planDelivery(input) {
|
|
|
1534
1577
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1535
1578
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1536
1579
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1537
|
-
const
|
|
1580
|
+
const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1538
1581
|
|
|
1539
1582
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1540
|
-
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
1583
|
+
return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
|
|
1541
1584
|
}
|
|
1542
1585
|
|
|
1543
1586
|
// src/notify.ts
|
|
@@ -2528,6 +2571,36 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
|
2528
2571
|
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
2529
2572
|
}
|
|
2530
2573
|
|
|
2574
|
+
// src/signup-control-contract.ts
|
|
2575
|
+
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
2576
|
+
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
2577
|
+
|
|
2578
|
+
// src/signup-control-canonical.ts
|
|
2579
|
+
var encoder = new TextEncoder();
|
|
2580
|
+
function canonicalValue(value) {
|
|
2581
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2582
|
+
return JSON.stringify(value);
|
|
2583
|
+
}
|
|
2584
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2585
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2586
|
+
const record = value;
|
|
2587
|
+
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
2588
|
+
const item = record[key];
|
|
2589
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2590
|
+
});
|
|
2591
|
+
return `{${entries.join(",")}}`;
|
|
2592
|
+
}
|
|
2593
|
+
function canonicalSignupControl(value) {
|
|
2594
|
+
return canonicalValue(value);
|
|
2595
|
+
}
|
|
2596
|
+
async function signupControlDigest(document) {
|
|
2597
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
2598
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2599
|
+
}
|
|
2600
|
+
async function createSignupControlEnvelope(document) {
|
|
2601
|
+
return { document, digest: await signupControlDigest(document) };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2531
2604
|
// src/membership-page.ts
|
|
2532
2605
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2533
2606
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
@@ -2568,10 +2641,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2568
2641
|
errors.push(`${path} must be a string`);
|
|
2569
2642
|
return void 0;
|
|
2570
2643
|
}
|
|
2571
|
-
const
|
|
2572
|
-
if (required && !
|
|
2573
|
-
if (
|
|
2574
|
-
return
|
|
2644
|
+
const text2 = value.trim();
|
|
2645
|
+
if (required && !text2) errors.push(`${path} is required`);
|
|
2646
|
+
if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2647
|
+
return text2;
|
|
2575
2648
|
}
|
|
2576
2649
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2577
2650
|
const errors = [];
|
|
@@ -2620,8 +2693,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2620
2693
|
else {
|
|
2621
2694
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2622
2695
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2623
|
-
const
|
|
2624
|
-
return
|
|
2696
|
+
const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2697
|
+
return text2 ? [text2] : [];
|
|
2625
2698
|
});
|
|
2626
2699
|
}
|
|
2627
2700
|
}
|
|
@@ -2696,6 +2769,188 @@ function membershipPageDocument(input, options = {}) {
|
|
|
2696
2769
|
return result.value;
|
|
2697
2770
|
}
|
|
2698
2771
|
|
|
2772
|
+
// src/signup-control-validate.ts
|
|
2773
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2774
|
+
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
2775
|
+
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2776
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2777
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
2778
|
+
"schemaVersion",
|
|
2779
|
+
"revision",
|
|
2780
|
+
"generatedAt",
|
|
2781
|
+
"source",
|
|
2782
|
+
"target",
|
|
2783
|
+
"tiers",
|
|
2784
|
+
"membershipPage",
|
|
2785
|
+
"application",
|
|
2786
|
+
"policy",
|
|
2787
|
+
"scheduling",
|
|
2788
|
+
"theme"
|
|
2789
|
+
]);
|
|
2790
|
+
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2791
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2792
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
2793
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2794
|
+
}
|
|
2795
|
+
function text(value, path, errors, max = 65536) {
|
|
2796
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2797
|
+
errors.push(`${path} is required`);
|
|
2798
|
+
return "";
|
|
2799
|
+
}
|
|
2800
|
+
if (value.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2801
|
+
return value.trim();
|
|
2802
|
+
}
|
|
2803
|
+
function tiersOf(value, errors) {
|
|
2804
|
+
if (!Array.isArray(value) || !value.length) {
|
|
2805
|
+
errors.push("document.tiers must be a non-empty array");
|
|
2806
|
+
return [];
|
|
2807
|
+
}
|
|
2808
|
+
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
2809
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2810
|
+
return value.slice(0, 20).flatMap((raw, index) => {
|
|
2811
|
+
const path = `document.tiers[${index}]`;
|
|
2812
|
+
const item = recordOf(raw);
|
|
2813
|
+
if (!item) {
|
|
2814
|
+
errors.push(`${path} must be an object`);
|
|
2815
|
+
return [];
|
|
2816
|
+
}
|
|
2817
|
+
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
2818
|
+
const id2 = text(item.id, `${path}.id`, errors, 80);
|
|
2819
|
+
if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2820
|
+
if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
|
|
2821
|
+
seen.add(id2);
|
|
2822
|
+
const name = text(item.name, `${path}.name`, errors, 160);
|
|
2823
|
+
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
2824
|
+
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
2825
|
+
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
2826
|
+
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
2827
|
+
}
|
|
2828
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
|
|
2829
|
+
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2830
|
+
}
|
|
2831
|
+
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2832
|
+
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2833
|
+
}
|
|
2834
|
+
const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2835
|
+
return [{
|
|
2836
|
+
id: id2,
|
|
2837
|
+
name,
|
|
2838
|
+
available: item.available === true,
|
|
2839
|
+
sortOrder: Number(item.sortOrder) || 0,
|
|
2840
|
+
priceCents: Number(item.priceCents) || 0,
|
|
2841
|
+
...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
|
|
2842
|
+
...blurb ? { blurb } : {}
|
|
2843
|
+
}];
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
function applicationOf(value, errors) {
|
|
2847
|
+
if (value === void 0) return void 0;
|
|
2848
|
+
const input = recordOf(value);
|
|
2849
|
+
if (!input) {
|
|
2850
|
+
errors.push("document.application must be an object");
|
|
2851
|
+
return void 0;
|
|
2852
|
+
}
|
|
2853
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
2854
|
+
try {
|
|
2855
|
+
const resolved = resolveApplication(input);
|
|
2856
|
+
const copy = recordOf(input.copy);
|
|
2857
|
+
if (input.copy !== void 0 && (!copy || Object.values(copy).some((item) => typeof item !== "string"))) {
|
|
2858
|
+
errors.push("document.application.copy must contain only string values");
|
|
2859
|
+
}
|
|
2860
|
+
return {
|
|
2861
|
+
required: resolved.required,
|
|
2862
|
+
optional: resolved.optional,
|
|
2863
|
+
...Object.keys(resolved.conditions).length ? { conditions: resolved.conditions } : {},
|
|
2864
|
+
...copy ? { copy } : {}
|
|
2865
|
+
};
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
errors.push(error instanceof Error ? error.message : "document.application is invalid");
|
|
2868
|
+
return void 0;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
function policyOf(value, errors) {
|
|
2872
|
+
if (value === void 0) return void 0;
|
|
2873
|
+
const input = recordOf(value);
|
|
2874
|
+
if (!input) {
|
|
2875
|
+
errors.push("document.policy must be an object");
|
|
2876
|
+
return void 0;
|
|
2877
|
+
}
|
|
2878
|
+
const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
2879
|
+
unknownKeys(input, allowed, "document.policy", errors);
|
|
2880
|
+
const result = {};
|
|
2881
|
+
for (const key of allowed) {
|
|
2882
|
+
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
2883
|
+
result[key] = text(input[key], `document.policy.${key}`, errors);
|
|
2884
|
+
}
|
|
2885
|
+
return result;
|
|
2886
|
+
}
|
|
2887
|
+
function themeOf(value, errors) {
|
|
2888
|
+
if (value === void 0) return void 0;
|
|
2889
|
+
const input = recordOf(value);
|
|
2890
|
+
const tokens = recordOf(input?.tokens);
|
|
2891
|
+
if (!input || !tokens) {
|
|
2892
|
+
errors.push("document.theme.tokens must be an object");
|
|
2893
|
+
return void 0;
|
|
2894
|
+
}
|
|
2895
|
+
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
2896
|
+
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
2897
|
+
for (const [key, token] of Object.entries(tokens)) {
|
|
2898
|
+
if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
|
|
2899
|
+
if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
|
|
2900
|
+
errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
return { tokens };
|
|
2904
|
+
}
|
|
2905
|
+
function validateSignupControlDocument(input) {
|
|
2906
|
+
const errors = [];
|
|
2907
|
+
const root = recordOf(input);
|
|
2908
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2909
|
+
unknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
2910
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
2911
|
+
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2912
|
+
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
2913
|
+
const source = recordOf(root.source);
|
|
2914
|
+
const target = recordOf(root.target);
|
|
2915
|
+
const appId = text(source?.appId, "document.source.appId", errors, 63);
|
|
2916
|
+
const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
2917
|
+
const environment = text(target?.environment, "document.target.environment", errors, 63);
|
|
2918
|
+
if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2919
|
+
if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
2920
|
+
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
2921
|
+
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
2922
|
+
const tiers2 = tiersOf(root.tiers, errors);
|
|
2923
|
+
const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
2924
|
+
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
2925
|
+
if (!page.ok) errors.push(...page.errors);
|
|
2926
|
+
else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
|
|
2927
|
+
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
2928
|
+
}
|
|
2929
|
+
const application = applicationOf(root.application, errors);
|
|
2930
|
+
const policy = policyOf(root.policy, errors);
|
|
2931
|
+
let scheduling;
|
|
2932
|
+
if (root.scheduling !== void 0) {
|
|
2933
|
+
const checked = validateScheduling(root.scheduling);
|
|
2934
|
+
if (!checked.ok) errors.push(...Object.entries(checked.errors).map(([key, value]) => `document.scheduling.${key}: ${value}`));
|
|
2935
|
+
else scheduling = root.scheduling;
|
|
2936
|
+
}
|
|
2937
|
+
const theme = themeOf(root.theme, errors);
|
|
2938
|
+
if (errors.length || !page.ok) return { ok: false, errors };
|
|
2939
|
+
return { ok: true, value: {
|
|
2940
|
+
schemaVersion: 1,
|
|
2941
|
+
revision: Number(root.revision),
|
|
2942
|
+
generatedAt: Number(root.generatedAt),
|
|
2943
|
+
source: { appId },
|
|
2944
|
+
target: { chapterId, environment, stripeMode: target?.stripeMode },
|
|
2945
|
+
tiers: tiers2,
|
|
2946
|
+
membershipPage: page.value,
|
|
2947
|
+
...application ? { application } : {},
|
|
2948
|
+
...policy ? { policy } : {},
|
|
2949
|
+
...scheduling ? { scheduling } : {},
|
|
2950
|
+
...theme ? { theme } : {}
|
|
2951
|
+
} };
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2699
2954
|
// src/membership-markdown.ts
|
|
2700
2955
|
var MAX_BENEFITS2 = 24;
|
|
2701
2956
|
var MAX_MARKDOWN2 = 65536;
|
|
@@ -2723,9 +2978,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
2723
2978
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
2724
2979
|
continue;
|
|
2725
2980
|
}
|
|
2726
|
-
const
|
|
2727
|
-
if (
|
|
2728
|
-
if (
|
|
2981
|
+
const text2 = (match[1] ?? "").trim();
|
|
2982
|
+
if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
2983
|
+
if (text2) benefits.push(text2);
|
|
2729
2984
|
}
|
|
2730
2985
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
2731
2986
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -2820,6 +3075,8 @@ export {
|
|
|
2820
3075
|
DEFAULT_SHARE_FIELDS,
|
|
2821
3076
|
MEMBERSHIP_PAGE_RENDERER,
|
|
2822
3077
|
SCHEDULING_DEFAULTS,
|
|
3078
|
+
SIGNUP_CONTROL_PATH,
|
|
3079
|
+
SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
2823
3080
|
applicantProfile,
|
|
2824
3081
|
applicationBookingUpdate,
|
|
2825
3082
|
applicationSummary,
|
|
@@ -2835,6 +3092,7 @@ export {
|
|
|
2835
3092
|
canChangeRole,
|
|
2836
3093
|
canTransition,
|
|
2837
3094
|
canceledPatch,
|
|
3095
|
+
canonicalSignupControl,
|
|
2838
3096
|
chapterBrandFromTokens,
|
|
2839
3097
|
chapterDb,
|
|
2840
3098
|
clampArray,
|
|
@@ -2848,6 +3106,7 @@ export {
|
|
|
2848
3106
|
createChapterIntegration,
|
|
2849
3107
|
createClerkInvitation,
|
|
2850
3108
|
createClerkUser,
|
|
3109
|
+
createSignupControlEnvelope,
|
|
2851
3110
|
dashboardMetricData,
|
|
2852
3111
|
defaultCrm,
|
|
2853
3112
|
defineChapter,
|
|
@@ -2898,11 +3157,13 @@ export {
|
|
|
2898
3157
|
resolveLeaderFormation,
|
|
2899
3158
|
resolvePipeline,
|
|
2900
3159
|
resolveScheduling,
|
|
3160
|
+
resolveSignupControl,
|
|
2901
3161
|
resolveTiers,
|
|
2902
3162
|
roleFromClaim,
|
|
2903
3163
|
sendTemplated,
|
|
2904
3164
|
sharedPersonInput,
|
|
2905
3165
|
sharedRecordFromCrm,
|
|
3166
|
+
signupControlDigest,
|
|
2906
3167
|
slotWindow,
|
|
2907
3168
|
stageIndex,
|
|
2908
3169
|
stripeCall,
|
|
@@ -2917,6 +3178,7 @@ export {
|
|
|
2917
3178
|
updateClerkUserMetadataByEmail,
|
|
2918
3179
|
validateMembershipPageDocument,
|
|
2919
3180
|
validateScheduling,
|
|
3181
|
+
validateSignupControlDocument,
|
|
2920
3182
|
verifyStripeSignature,
|
|
2921
3183
|
webhookMutationId
|
|
2922
3184
|
};
|