@odla-ai/chapter 0.35.0 → 0.37.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 +106 -0
- package/dist/{chunk-UJIVBGL5.js → chunk-BIYWPPH6.js} +535 -240
- package/dist/chunk-BIYWPPH6.js.map +1 -0
- package/dist/{chunk-JAWTIROV.js → chunk-IAWBE5PX.js} +55 -1
- package/dist/chunk-IAWBE5PX.js.map +1 -0
- package/dist/{chunk-TXE5ZDI4.js → chunk-SI3JHSEX.js} +319 -65
- package/dist/chunk-SI3JHSEX.js.map +1 -0
- package/dist/{copy-context-c1tqPXOR.d.ts → copy-context-Pbs3keLx.d.ts} +179 -29
- package/dist/index.cjs +1262 -326
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +439 -30
- package/dist/index.d.ts +439 -30
- package/dist/index.js +1262 -326
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +34 -3
- package/dist/ui/admin/index.js +10 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +17 -3
- package/dist/ui/member/index.d.ts +111 -7
- package/dist/ui/member/index.js +8 -2
- package/dist/worker/index.cjs +2623 -917
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +251 -29
- package/dist/worker/index.d.ts +251 -29
- package/dist/worker/index.js +2657 -951
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +27 -0
- package/dist/chunk-JAWTIROV.js.map +0 -1
- package/dist/chunk-TXE5ZDI4.js.map +0 -1
- package/dist/chunk-UJIVBGL5.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,213 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
15
15
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/admission-grants-contract.ts
|
|
19
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
20
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
21
|
+
|
|
22
|
+
// src/signup-control-canonical.ts
|
|
23
|
+
var encoder = new TextEncoder();
|
|
24
|
+
function canonicalValue(value) {
|
|
25
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
29
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
30
|
+
const record3 = value;
|
|
31
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
32
|
+
const item = record3[key];
|
|
33
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
34
|
+
});
|
|
35
|
+
return `{${entries.join(",")}}`;
|
|
36
|
+
}
|
|
37
|
+
function canonicalSignupControl(value) {
|
|
38
|
+
return canonicalValue(value);
|
|
39
|
+
}
|
|
40
|
+
async function signupControlDigest(document) {
|
|
41
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
42
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
43
|
+
}
|
|
44
|
+
async function createSignupControlEnvelope(document) {
|
|
45
|
+
return { document, digest: await signupControlDigest(document) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/admission-grants.ts
|
|
49
|
+
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
50
|
+
var MAX_GRANTS = 2e3;
|
|
51
|
+
var MAX_NOTE = 2e3;
|
|
52
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
53
|
+
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
54
|
+
function normalizeAdmissionEmail(email) {
|
|
55
|
+
return email.trim().toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
function validateAdmissionGrantDocument(input) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const root = input && typeof input === "object" ? input : {};
|
|
60
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
61
|
+
const revision = Number(root.revision);
|
|
62
|
+
const generatedAt = Number(root.generatedAt);
|
|
63
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
64
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
65
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
66
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
67
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
68
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
69
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
70
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
71
|
+
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
72
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
73
|
+
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
74
|
+
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
75
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
76
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
77
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
78
|
+
const grants = [];
|
|
79
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
80
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
82
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
83
|
+
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
84
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
85
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
86
|
+
const decision = grant.decision;
|
|
87
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
88
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
89
|
+
if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
90
|
+
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
91
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
92
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
93
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
94
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
95
|
+
}
|
|
96
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
97
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
98
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
99
|
+
}
|
|
100
|
+
seenIds.add(id4);
|
|
101
|
+
seenEmails.add(normalized);
|
|
102
|
+
grants.push({
|
|
103
|
+
id: id4,
|
|
104
|
+
email,
|
|
105
|
+
decision,
|
|
106
|
+
...note ? { note } : {},
|
|
107
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (errors.length) return { ok: false, errors };
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
value: {
|
|
114
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
115
|
+
revision,
|
|
116
|
+
generatedAt,
|
|
117
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
118
|
+
target: { chapterId, environment },
|
|
119
|
+
grants
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
124
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
125
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
126
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
127
|
+
}
|
|
128
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
129
|
+
function asGrant(row) {
|
|
130
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
131
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
132
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
133
|
+
return row;
|
|
134
|
+
}
|
|
135
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
136
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
137
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
138
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
139
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
140
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
141
|
+
)).sort((a, b) => {
|
|
142
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
143
|
+
return scope || b.createdAt - a.createdAt;
|
|
144
|
+
});
|
|
145
|
+
return active[0] ?? null;
|
|
146
|
+
}
|
|
147
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
148
|
+
const where = { sourceId, targetId, environment };
|
|
149
|
+
if (db.aggregate) {
|
|
150
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
151
|
+
}
|
|
152
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
153
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
154
|
+
}
|
|
155
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
156
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
157
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
158
|
+
where: { sourceId, targetId, environment },
|
|
159
|
+
order: { revision: "desc" },
|
|
160
|
+
limit: 1
|
|
161
|
+
} } });
|
|
162
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
163
|
+
return row ? row : null;
|
|
164
|
+
}
|
|
165
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
166
|
+
const { document, digest } = envelope;
|
|
167
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
168
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
169
|
+
limit: MAX_GRANTS
|
|
170
|
+
} } })).admissionGrants);
|
|
171
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
172
|
+
const ops = [];
|
|
173
|
+
for (const row of current) {
|
|
174
|
+
const id4 = String(row.id ?? "");
|
|
175
|
+
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
176
|
+
active: false,
|
|
177
|
+
revokedAt: receivedAt,
|
|
178
|
+
revokedBy: document.source.actor,
|
|
179
|
+
updatedAt: receivedAt,
|
|
180
|
+
updatedBy: document.source.actor
|
|
181
|
+
} });
|
|
182
|
+
}
|
|
183
|
+
for (const grant of document.grants) {
|
|
184
|
+
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
185
|
+
const existing = current.find((row) => row.id === id4);
|
|
186
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
187
|
+
id: id4,
|
|
188
|
+
sourceGrantId: grant.id,
|
|
189
|
+
sourceScope: "parent",
|
|
190
|
+
sourceId: document.source.appId,
|
|
191
|
+
targetId: document.target.chapterId,
|
|
192
|
+
email: grant.email,
|
|
193
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
194
|
+
decision: grant.decision,
|
|
195
|
+
...grant.note ? { note: grant.note } : {},
|
|
196
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
197
|
+
active: true,
|
|
198
|
+
sourceRevision: document.revision,
|
|
199
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
200
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
201
|
+
updatedAt: receivedAt,
|
|
202
|
+
updatedBy: document.source.actor
|
|
203
|
+
} });
|
|
204
|
+
const retract = [
|
|
205
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
206
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
207
|
+
];
|
|
208
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
209
|
+
}
|
|
210
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
211
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
212
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
213
|
+
id: revisionId,
|
|
214
|
+
sourceId: document.source.appId,
|
|
215
|
+
targetId: document.target.chapterId,
|
|
216
|
+
environment: document.target.environment,
|
|
217
|
+
revision: document.revision,
|
|
218
|
+
digest,
|
|
219
|
+
active: true,
|
|
220
|
+
receivedAt
|
|
221
|
+
} });
|
|
222
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
223
|
+
}
|
|
224
|
+
|
|
18
225
|
// src/member.ts
|
|
19
226
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
20
227
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
@@ -66,9 +273,9 @@ function resolveApplication(a) {
|
|
|
66
273
|
validateEmail: a?.validateEmail ?? true
|
|
67
274
|
};
|
|
68
275
|
}
|
|
69
|
-
var
|
|
276
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
70
277
|
function isValidEmail(value) {
|
|
71
|
-
return typeof value === "string" &&
|
|
278
|
+
return typeof value === "string" && EMAIL_RE2.test(value);
|
|
72
279
|
}
|
|
73
280
|
function clampArray(value, max) {
|
|
74
281
|
if (!Array.isArray(value)) return value;
|
|
@@ -80,14 +287,14 @@ function hasDisclaimerAck(fields) {
|
|
|
80
287
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
81
288
|
function applicantProfile(chapter, fields) {
|
|
82
289
|
const app = chapter.application;
|
|
83
|
-
const
|
|
290
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
84
291
|
const profile = {};
|
|
85
292
|
for (const f of [...app.required, ...app.optional]) {
|
|
86
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
293
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
87
294
|
const v = fields[f];
|
|
88
295
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
89
296
|
}
|
|
90
|
-
if (fields.focus !== void 0 &&
|
|
297
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
91
298
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
92
299
|
}
|
|
93
300
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -110,14 +317,23 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
110
317
|
if (app.requireDisclaimerAck && !acked) {
|
|
111
318
|
return { ok: false, error: "disclaimerAck is required" };
|
|
112
319
|
}
|
|
113
|
-
const
|
|
114
|
-
const row = { id:
|
|
320
|
+
const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
321
|
+
const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
115
322
|
for (const f of [...app.required, ...app.optional]) {
|
|
116
323
|
if (fieldStates[f]?.visible === false) continue;
|
|
117
324
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
118
325
|
}
|
|
119
326
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
120
327
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
328
|
+
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
329
|
+
row.emailNormalized = normalizeAdmissionEmail(fields.email);
|
|
330
|
+
}
|
|
331
|
+
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
332
|
+
row.admissionGrantId = opts.admissionGrant.id;
|
|
333
|
+
row.admissionSource = opts.admissionGrant.sourceScope;
|
|
334
|
+
row.interviewWaivedAt = opts.now;
|
|
335
|
+
}
|
|
336
|
+
if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
|
|
121
337
|
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
122
338
|
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
123
339
|
return { ok: false, error: "tierId is not an offered tier" };
|
|
@@ -125,11 +341,20 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
125
341
|
row.tierId = fields.tierId;
|
|
126
342
|
}
|
|
127
343
|
if (acked) row.disclaimerAckAt = opts.now;
|
|
344
|
+
const ops = [
|
|
345
|
+
{ t: "update", ns: "applications", id: id4, attrs: row }
|
|
346
|
+
];
|
|
347
|
+
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
348
|
+
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
349
|
+
consumedAt: opts.now,
|
|
350
|
+
consumedApplicationId: id4
|
|
351
|
+
} });
|
|
352
|
+
}
|
|
128
353
|
const { duplicate } = await db.transact(
|
|
129
|
-
|
|
354
|
+
ops,
|
|
130
355
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
131
356
|
);
|
|
132
|
-
return { ok: true, id:
|
|
357
|
+
return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
133
358
|
}
|
|
134
359
|
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
135
360
|
return {
|
|
@@ -149,93 +374,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
|
149
374
|
};
|
|
150
375
|
}
|
|
151
376
|
|
|
152
|
-
// src/schema.ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
var id = () => attr("string", { unique: true, indexed: true });
|
|
162
|
-
var admins = {
|
|
163
|
-
attrs: {
|
|
164
|
-
id: id(),
|
|
165
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
166
|
-
name: attr("string", { optional: true }),
|
|
167
|
-
note: attr("string", { optional: true })
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
var superAdmins = {
|
|
171
|
-
attrs: {
|
|
172
|
-
id: id(),
|
|
173
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
174
|
-
note: attr("string", { optional: true }),
|
|
175
|
-
createdAt: attr("number", { indexed: true })
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
var networkNotes = {
|
|
179
|
-
attrs: {
|
|
180
|
-
id: id(),
|
|
181
|
-
recordKey: attr("string", { indexed: true }),
|
|
182
|
-
targetId: attr("string", { indexed: true }),
|
|
183
|
-
recordType: attr("string", { indexed: true }),
|
|
184
|
-
recordId: attr("string", { indexed: true }),
|
|
185
|
-
body: attr("string"),
|
|
186
|
-
authorId: attr("string", { indexed: true }),
|
|
187
|
-
authorEmail: attr("string", { optional: true }),
|
|
188
|
-
createdAt: attr("number", { indexed: true })
|
|
189
|
-
}
|
|
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
|
-
};
|
|
204
|
-
var applications = {
|
|
205
|
-
attrs: {
|
|
206
|
-
id: id(),
|
|
207
|
-
firstName: attr("string"),
|
|
208
|
-
lastName: attr("string"),
|
|
209
|
-
email: attr("string", { indexed: true }),
|
|
210
|
-
referral: attr("string"),
|
|
211
|
-
referralName: attr("string", { optional: true }),
|
|
212
|
-
whoYouAre: attr("string"),
|
|
213
|
-
focus: attr("json"),
|
|
214
|
-
linkedin: attr("string", { optional: true }),
|
|
215
|
-
message: attr("string"),
|
|
216
|
-
status: attr("string", { indexed: true }),
|
|
217
|
-
createdAt: attr("number", { indexed: true }),
|
|
218
|
-
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
219
|
-
meetingLink: attr("string", { optional: true }),
|
|
220
|
-
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
221
|
-
// One-time/effect-repair marker. Absent means /api/me should copy the
|
|
222
|
-
// allowlisted application snapshot into Clerk private_metadata and remove
|
|
223
|
-
// the former public_metadata copy.
|
|
224
|
-
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
225
|
-
phone: attr("string", { optional: true }),
|
|
226
|
-
state: attr("string", { optional: true }),
|
|
227
|
-
groupId: attr("string", { indexed: true, optional: true }),
|
|
228
|
-
// The tier this application joined on. Absent on rows that predate tiers.
|
|
229
|
-
tierId: attr("string", { indexed: true, optional: true }),
|
|
230
|
-
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
231
|
-
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
232
|
-
renewalAt: attr("number", { optional: true }),
|
|
233
|
-
disclaimerAckAt: attr("number", { optional: true }),
|
|
234
|
-
refundPolicyAckAt: attr("number", { optional: true }),
|
|
235
|
-
prepEmailSentAt: attr("number", { optional: true }),
|
|
236
|
-
canceled: attr("boolean", { optional: true })
|
|
237
|
-
}
|
|
238
|
-
};
|
|
377
|
+
// src/schema-applications.ts
|
|
378
|
+
var attr = (type, flags = {}) => ({
|
|
379
|
+
type,
|
|
380
|
+
unique: flags.unique ?? false,
|
|
381
|
+
indexed: flags.indexed ?? false,
|
|
382
|
+
optional: flags.optional ?? false
|
|
383
|
+
});
|
|
239
384
|
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
240
385
|
"firstName",
|
|
241
386
|
"lastName",
|
|
@@ -249,25 +394,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
249
394
|
"phone",
|
|
250
395
|
"state"
|
|
251
396
|
]);
|
|
397
|
+
var applicationAttrs = {
|
|
398
|
+
id: attr("string", { unique: true, indexed: true }),
|
|
399
|
+
firstName: attr("string"),
|
|
400
|
+
lastName: attr("string"),
|
|
401
|
+
email: attr("string", { indexed: true }),
|
|
402
|
+
referral: attr("string"),
|
|
403
|
+
referralName: attr("string", { optional: true }),
|
|
404
|
+
whoYouAre: attr("string"),
|
|
405
|
+
focus: attr("json"),
|
|
406
|
+
linkedin: attr("string", { optional: true }),
|
|
407
|
+
message: attr("string"),
|
|
408
|
+
status: attr("string", { indexed: true }),
|
|
409
|
+
createdAt: attr("number", { indexed: true }),
|
|
410
|
+
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
411
|
+
meetingLink: attr("string", { optional: true }),
|
|
412
|
+
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
413
|
+
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
414
|
+
phone: attr("string", { optional: true }),
|
|
415
|
+
state: attr("string", { optional: true }),
|
|
416
|
+
groupId: attr("string", { indexed: true, optional: true }),
|
|
417
|
+
tierId: attr("string", { indexed: true, optional: true }),
|
|
418
|
+
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
419
|
+
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
420
|
+
renewalAt: attr("number", { optional: true }),
|
|
421
|
+
disclaimerAckAt: attr("number", { optional: true }),
|
|
422
|
+
refundPolicyAckAt: attr("number", { optional: true }),
|
|
423
|
+
prepEmailSentAt: attr("number", { optional: true }),
|
|
424
|
+
canceled: attr("boolean", { optional: true }),
|
|
425
|
+
emailNormalized: attr("string", { indexed: true, optional: true }),
|
|
426
|
+
admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
|
|
427
|
+
admissionSource: attr("string", { indexed: true, optional: true }),
|
|
428
|
+
interviewWaivedAt: attr("number", { optional: true }),
|
|
429
|
+
giftOfferId: attr("string", { optional: true, indexed: true }),
|
|
430
|
+
giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
431
|
+
giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
432
|
+
giftOrderId: attr("string", { optional: true, indexed: true }),
|
|
433
|
+
giftPaidAt: attr("number", { optional: true, indexed: true }),
|
|
434
|
+
giftRevokedAt: attr("number", { optional: true, indexed: true }),
|
|
435
|
+
giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
|
|
436
|
+
};
|
|
252
437
|
function applicationsFor(application) {
|
|
253
438
|
const attrs = {};
|
|
254
|
-
for (const [name, spec] of Object.entries(
|
|
255
|
-
attrs[name] = {
|
|
256
|
-
...spec,
|
|
257
|
-
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
258
|
-
};
|
|
439
|
+
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
440
|
+
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
259
441
|
}
|
|
260
442
|
const apply = (field, optional) => {
|
|
261
443
|
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
262
|
-
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
if (existing && existing.type !== "string") {
|
|
266
|
-
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
267
|
-
}
|
|
268
|
-
const spec = existing ? { ...existing, optional } : attr("string", { optional });
|
|
444
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
|
|
445
|
+
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
269
446
|
Object.defineProperty(attrs, field, {
|
|
270
|
-
value:
|
|
447
|
+
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
271
448
|
enumerable: true,
|
|
272
449
|
configurable: true,
|
|
273
450
|
writable: true
|
|
@@ -277,78 +454,297 @@ function applicationsFor(application) {
|
|
|
277
454
|
for (const field of application.optional) apply(field, true);
|
|
278
455
|
return { attrs };
|
|
279
456
|
}
|
|
280
|
-
|
|
457
|
+
|
|
458
|
+
// src/schema-admission.ts
|
|
459
|
+
var attr2 = (type, flags = {}) => ({
|
|
460
|
+
type,
|
|
461
|
+
unique: flags.unique ?? false,
|
|
462
|
+
indexed: flags.indexed ?? false,
|
|
463
|
+
optional: flags.optional ?? false
|
|
464
|
+
});
|
|
465
|
+
var id = () => attr2("string", { unique: true, indexed: true });
|
|
466
|
+
var admissionGrantsEntity = {
|
|
281
467
|
attrs: {
|
|
282
468
|
id: id(),
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
469
|
+
sourceGrantId: attr2("string", { indexed: true }),
|
|
470
|
+
sourceScope: attr2("string", { indexed: true }),
|
|
471
|
+
sourceId: attr2("string", { indexed: true }),
|
|
472
|
+
targetId: attr2("string", { indexed: true }),
|
|
473
|
+
email: attr2("string"),
|
|
474
|
+
emailNormalized: attr2("string", { indexed: true }),
|
|
475
|
+
decision: attr2("string", { indexed: true }),
|
|
476
|
+
note: attr2("string", { optional: true }),
|
|
477
|
+
active: attr2("boolean", { indexed: true }),
|
|
478
|
+
sourceRevision: attr2("number", { optional: true, indexed: true }),
|
|
479
|
+
createdAt: attr2("number", { indexed: true }),
|
|
480
|
+
createdBy: attr2("string", { optional: true, indexed: true }),
|
|
481
|
+
updatedAt: attr2("number", { optional: true, indexed: true }),
|
|
482
|
+
updatedBy: attr2("string", { optional: true, indexed: true }),
|
|
483
|
+
expiresAt: attr2("number", { optional: true, indexed: true }),
|
|
484
|
+
revokedAt: attr2("number", { optional: true, indexed: true }),
|
|
485
|
+
revokedBy: attr2("string", { optional: true, indexed: true }),
|
|
486
|
+
consumedAt: attr2("number", { optional: true, indexed: true }),
|
|
487
|
+
consumedApplicationId: attr2("string", { optional: true, indexed: true })
|
|
300
488
|
}
|
|
301
489
|
};
|
|
302
|
-
var
|
|
490
|
+
var admissionGrantRevisionsEntity = {
|
|
303
491
|
attrs: {
|
|
304
492
|
id: id(),
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
493
|
+
sourceId: attr2("string", { indexed: true }),
|
|
494
|
+
targetId: attr2("string", { indexed: true }),
|
|
495
|
+
environment: attr2("string", { indexed: true }),
|
|
496
|
+
revision: attr2("number", { indexed: true }),
|
|
497
|
+
digest: attr2("string", { indexed: true }),
|
|
498
|
+
active: attr2("boolean", { indexed: true }),
|
|
499
|
+
receivedAt: attr2("number", { indexed: true })
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// src/schema-gifts.ts
|
|
504
|
+
var attr3 = (type, flags = {}) => ({
|
|
505
|
+
type,
|
|
506
|
+
unique: flags.unique ?? false,
|
|
507
|
+
indexed: flags.indexed ?? false,
|
|
508
|
+
optional: flags.optional ?? false
|
|
509
|
+
});
|
|
510
|
+
var id2 = () => attr3("string", { unique: true, indexed: true });
|
|
511
|
+
var giftPurchaseIntentsEntity = { attrs: {
|
|
512
|
+
id: id2(),
|
|
513
|
+
chapterId: attr3("string", { indexed: true }),
|
|
514
|
+
environment: attr3("string", { indexed: true }),
|
|
515
|
+
purchaserType: attr3("string", { indexed: true }),
|
|
516
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
517
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
518
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
519
|
+
offerId: attr3("string", { indexed: true }),
|
|
520
|
+
offerSnapshot: attr3("json"),
|
|
521
|
+
status: attr3("string", { indexed: true }),
|
|
522
|
+
codeHash: attr3("string", { indexed: true }),
|
|
523
|
+
codeVersion: attr3("number"),
|
|
524
|
+
codeIssuedAt: attr3("number"),
|
|
525
|
+
providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
526
|
+
providerCustomerId: attr3("string", { optional: true, indexed: true }),
|
|
527
|
+
checkoutStartedAt: attr3("number", { optional: true }),
|
|
528
|
+
paidAt: attr3("number", { optional: true, indexed: true }),
|
|
529
|
+
claimId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
530
|
+
createdAt: attr3("number", { indexed: true }),
|
|
531
|
+
updatedAt: attr3("number", { indexed: true })
|
|
532
|
+
} };
|
|
533
|
+
var giftOrdersEntity = { attrs: {
|
|
534
|
+
id: id2(),
|
|
535
|
+
intentId: attr3("string", { unique: true, indexed: true }),
|
|
536
|
+
chapterId: attr3("string", { indexed: true }),
|
|
537
|
+
environment: attr3("string", { indexed: true }),
|
|
538
|
+
offerId: attr3("string", { indexed: true }),
|
|
539
|
+
offerSnapshot: attr3("json"),
|
|
540
|
+
purchaserType: attr3("string", { indexed: true }),
|
|
541
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
542
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
543
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
544
|
+
providerEventId: attr3("string", { unique: true, indexed: true }),
|
|
545
|
+
providerObjectId: attr3("string", { indexed: true }),
|
|
546
|
+
amountCents: attr3("number"),
|
|
547
|
+
currency: attr3("string"),
|
|
548
|
+
billingMode: attr3("string"),
|
|
549
|
+
createdAt: attr3("number", { indexed: true })
|
|
550
|
+
} };
|
|
551
|
+
var giftClaimsEntity = { attrs: {
|
|
552
|
+
id: id2(),
|
|
553
|
+
orderId: attr3("string", { unique: true, indexed: true }),
|
|
554
|
+
intentId: attr3("string", { unique: true, indexed: true }),
|
|
555
|
+
chapterId: attr3("string", { indexed: true }),
|
|
556
|
+
offerId: attr3("string", { indexed: true }),
|
|
557
|
+
target: attr3("json"),
|
|
558
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
559
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
560
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
561
|
+
codeHash: attr3("string", { unique: true, indexed: true }),
|
|
562
|
+
codeVersion: attr3("number"),
|
|
563
|
+
rotationCount: attr3("number"),
|
|
564
|
+
codeIssuedAt: attr3("number"),
|
|
565
|
+
codeExpiresAt: attr3("number", { indexed: true }),
|
|
566
|
+
attemptCount: attr3("number"),
|
|
567
|
+
attemptWindowStartedAt: attr3("number"),
|
|
568
|
+
status: attr3("string", { indexed: true }),
|
|
569
|
+
redeemedAt: attr3("number", { optional: true, indexed: true }),
|
|
570
|
+
beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
|
|
571
|
+
beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
|
|
572
|
+
applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
573
|
+
entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
574
|
+
rotatedAt: attr3("number", { optional: true }),
|
|
575
|
+
revokedAt: attr3("number", { optional: true, indexed: true }),
|
|
576
|
+
revokedReason: attr3("string", { optional: true }),
|
|
577
|
+
createdAt: attr3("number", { indexed: true })
|
|
578
|
+
} };
|
|
579
|
+
var giftEntitlementsEntity = { attrs: {
|
|
580
|
+
id: id2(),
|
|
581
|
+
claimId: attr3("string", { unique: true, indexed: true }),
|
|
582
|
+
orderId: attr3("string", { indexed: true }),
|
|
583
|
+
chapterId: attr3("string", { indexed: true }),
|
|
584
|
+
entitlementKey: attr3("string", { indexed: true }),
|
|
585
|
+
beneficiaryEmailNormalized: attr3("string", { indexed: true }),
|
|
586
|
+
beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
|
|
587
|
+
active: attr3("boolean", { indexed: true }),
|
|
588
|
+
createdAt: attr3("number", { indexed: true }),
|
|
589
|
+
revokedAt: attr3("number", { optional: true })
|
|
590
|
+
} };
|
|
591
|
+
var giftRedemptionAttemptsEntity = { attrs: {
|
|
592
|
+
id: id2(),
|
|
593
|
+
chapterId: attr3("string", { indexed: true }),
|
|
594
|
+
bucket: attr3("string", { indexed: true }),
|
|
595
|
+
attemptCount: attr3("number"),
|
|
596
|
+
windowStartedAt: attr3("number", { indexed: true }),
|
|
597
|
+
updatedAt: attr3("number")
|
|
598
|
+
} };
|
|
599
|
+
|
|
600
|
+
// src/schema.ts
|
|
601
|
+
function attr4(type, flags = {}) {
|
|
602
|
+
return {
|
|
603
|
+
type,
|
|
604
|
+
unique: flags.unique ?? false,
|
|
605
|
+
indexed: flags.indexed ?? false,
|
|
606
|
+
optional: flags.optional ?? false
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var id3 = () => attr4("string", { unique: true, indexed: true });
|
|
610
|
+
var admins = {
|
|
611
|
+
attrs: {
|
|
612
|
+
id: id3(),
|
|
613
|
+
email: attr4("string", { unique: true, indexed: true }),
|
|
614
|
+
name: attr4("string", { optional: true }),
|
|
615
|
+
note: attr4("string", { optional: true })
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
var superAdmins = {
|
|
619
|
+
attrs: {
|
|
620
|
+
id: id3(),
|
|
621
|
+
email: attr4("string", { unique: true, indexed: true }),
|
|
622
|
+
note: attr4("string", { optional: true }),
|
|
623
|
+
createdAt: attr4("number", { indexed: true })
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var networkNotes = {
|
|
627
|
+
attrs: {
|
|
628
|
+
id: id3(),
|
|
629
|
+
recordKey: attr4("string", { indexed: true }),
|
|
630
|
+
targetId: attr4("string", { indexed: true }),
|
|
631
|
+
recordType: attr4("string", { indexed: true }),
|
|
632
|
+
recordId: attr4("string", { indexed: true }),
|
|
633
|
+
body: attr4("string"),
|
|
634
|
+
authorId: attr4("string", { indexed: true }),
|
|
635
|
+
authorEmail: attr4("string", { optional: true }),
|
|
636
|
+
createdAt: attr4("number", { indexed: true })
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
var signupControls = {
|
|
640
|
+
attrs: {
|
|
641
|
+
id: id3(),
|
|
642
|
+
sourceId: attr4("string", { indexed: true }),
|
|
643
|
+
targetId: attr4("string", { indexed: true }),
|
|
644
|
+
environment: attr4("string", { indexed: true }),
|
|
645
|
+
revision: attr4("number", { indexed: true }),
|
|
646
|
+
digest: attr4("string", { indexed: true }),
|
|
647
|
+
document: attr4("json"),
|
|
648
|
+
active: attr4("boolean", { indexed: true }),
|
|
649
|
+
receivedAt: attr4("number", { indexed: true })
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
var memberContentControls = {
|
|
653
|
+
attrs: {
|
|
654
|
+
id: id3(),
|
|
655
|
+
sourceId: attr4("string", { indexed: true }),
|
|
656
|
+
targetId: attr4("string", { indexed: true }),
|
|
657
|
+
environment: attr4("string", { indexed: true }),
|
|
658
|
+
revision: attr4("number", { indexed: true }),
|
|
659
|
+
digest: attr4("string", { indexed: true }),
|
|
660
|
+
document: attr4("json"),
|
|
661
|
+
active: attr4("boolean", { indexed: true }),
|
|
662
|
+
receivedAt: attr4("number", { indexed: true })
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var memberContentLocal = {
|
|
666
|
+
attrs: {
|
|
667
|
+
id: id3(),
|
|
668
|
+
chapterId: attr4("string", { indexed: true }),
|
|
669
|
+
environment: attr4("string", { indexed: true }),
|
|
670
|
+
revision: attr4("number", { indexed: true }),
|
|
671
|
+
document: attr4("json"),
|
|
672
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
673
|
+
updatedBy: attr4("string", { optional: true })
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
var groups = {
|
|
677
|
+
attrs: {
|
|
678
|
+
id: id3(),
|
|
679
|
+
name: attr4("string"),
|
|
680
|
+
standardPriceCents: attr4("number"),
|
|
681
|
+
foundingDiscountCents: attr4("number"),
|
|
682
|
+
stripePriceId: attr4("string", { optional: true }),
|
|
683
|
+
stripePublishableKey: attr4("string", { optional: true }),
|
|
684
|
+
notificationEmail: attr4("string"),
|
|
685
|
+
replyTo: attr4("string"),
|
|
686
|
+
debugEmail: attr4("string", { optional: true }),
|
|
687
|
+
calendarLink: attr4("string", { optional: true }),
|
|
688
|
+
disclaimerText: attr4("string"),
|
|
689
|
+
refundPolicyText: attr4("string"),
|
|
690
|
+
trustCopy: attr4("string"),
|
|
691
|
+
commitmentText: attr4("string", { optional: true }),
|
|
692
|
+
normsText: attr4("string", { optional: true }),
|
|
693
|
+
emailTemplates: attr4("json"),
|
|
694
|
+
schedulingJson: attr4("json", { optional: true }),
|
|
695
|
+
createdAt: attr4("number", { indexed: true })
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var tiers = {
|
|
699
|
+
attrs: {
|
|
700
|
+
id: id3(),
|
|
701
|
+
groupId: attr4("string", { indexed: true }),
|
|
702
|
+
name: attr4("string"),
|
|
703
|
+
priceCents: attr4("number"),
|
|
704
|
+
stripePriceId: attr4("string", { optional: true }),
|
|
705
|
+
blurb: attr4("string", { optional: true }),
|
|
706
|
+
sortOrder: attr4("number", { indexed: true }),
|
|
707
|
+
active: attr4("boolean", { indexed: true }),
|
|
708
|
+
createdAt: attr4("number", { indexed: true })
|
|
313
709
|
}
|
|
314
710
|
};
|
|
315
711
|
var meetings = {
|
|
316
712
|
attrs: {
|
|
317
|
-
id:
|
|
318
|
-
applicationId:
|
|
319
|
-
groupId:
|
|
320
|
-
startAt:
|
|
321
|
-
endAt:
|
|
322
|
-
timezone:
|
|
323
|
-
status:
|
|
324
|
-
googleEventId:
|
|
325
|
-
meetUrl:
|
|
326
|
-
htmlLink:
|
|
327
|
-
drift:
|
|
328
|
-
driftGoogleStartAt:
|
|
329
|
-
driftDetectedAt:
|
|
330
|
-
adoptedFromGoogleAt:
|
|
331
|
-
createdAt:
|
|
713
|
+
id: id3(),
|
|
714
|
+
applicationId: attr4("string", { indexed: true }),
|
|
715
|
+
groupId: attr4("string", { indexed: true }),
|
|
716
|
+
startAt: attr4("number", { indexed: true }),
|
|
717
|
+
endAt: attr4("number"),
|
|
718
|
+
timezone: attr4("string"),
|
|
719
|
+
status: attr4("string", { indexed: true }),
|
|
720
|
+
googleEventId: attr4("string", { indexed: true, optional: true }),
|
|
721
|
+
meetUrl: attr4("string", { optional: true }),
|
|
722
|
+
htmlLink: attr4("string", { optional: true }),
|
|
723
|
+
drift: attr4("string", { indexed: true, optional: true }),
|
|
724
|
+
driftGoogleStartAt: attr4("number", { optional: true }),
|
|
725
|
+
driftDetectedAt: attr4("number", { optional: true }),
|
|
726
|
+
adoptedFromGoogleAt: attr4("number", { optional: true }),
|
|
727
|
+
createdAt: attr4("number", { indexed: true })
|
|
332
728
|
}
|
|
333
729
|
};
|
|
334
730
|
var emailLog = {
|
|
335
731
|
attrs: {
|
|
336
|
-
id:
|
|
337
|
-
groupId:
|
|
338
|
-
applicationId:
|
|
339
|
-
to:
|
|
340
|
-
template:
|
|
341
|
-
subject:
|
|
342
|
-
body:
|
|
343
|
-
transport:
|
|
344
|
-
messageId:
|
|
345
|
-
redirected:
|
|
346
|
-
dedupeKey:
|
|
347
|
-
error:
|
|
348
|
-
sentAt:
|
|
732
|
+
id: id3(),
|
|
733
|
+
groupId: attr4("string", { indexed: true }),
|
|
734
|
+
applicationId: attr4("string", { indexed: true, optional: true }),
|
|
735
|
+
to: attr4("string", { indexed: true }),
|
|
736
|
+
template: attr4("string", { indexed: true }),
|
|
737
|
+
subject: attr4("string"),
|
|
738
|
+
body: attr4("string", { optional: true }),
|
|
739
|
+
transport: attr4("string"),
|
|
740
|
+
messageId: attr4("string", { optional: true }),
|
|
741
|
+
redirected: attr4("boolean", { optional: true }),
|
|
742
|
+
dedupeKey: attr4("string", { indexed: true, optional: true }),
|
|
743
|
+
error: attr4("string", { optional: true }),
|
|
744
|
+
sentAt: attr4("number", { indexed: true })
|
|
349
745
|
}
|
|
350
746
|
};
|
|
351
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
747
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
352
748
|
const entities = {};
|
|
353
749
|
if (mode === "chapter") {
|
|
354
750
|
entities.applications = applicationsFor(application);
|
|
@@ -356,7 +752,18 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
356
752
|
entities.tiers = tiers;
|
|
357
753
|
entities.meetings = meetings;
|
|
358
754
|
entities.emailLog = emailLog;
|
|
755
|
+
entities.admissionGrants = admissionGrantsEntity;
|
|
756
|
+
entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
|
|
757
|
+
entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
|
|
758
|
+
entities.giftOrders = giftOrdersEntity;
|
|
759
|
+
entities.giftClaims = giftClaimsEntity;
|
|
760
|
+
entities.giftEntitlements = giftEntitlementsEntity;
|
|
761
|
+
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
359
762
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
763
|
+
if (includeMemberContent) {
|
|
764
|
+
entities.memberContentControls = memberContentControls;
|
|
765
|
+
entities.memberContentLocal = memberContentLocal;
|
|
766
|
+
}
|
|
360
767
|
}
|
|
361
768
|
if (auth.source === "table") entities.admins = admins;
|
|
362
769
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -640,6 +1047,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
640
1047
|
overview: "Overview",
|
|
641
1048
|
billing: "Billing",
|
|
642
1049
|
people: "People",
|
|
1050
|
+
admission: "Admission",
|
|
643
1051
|
network: "Network",
|
|
644
1052
|
portfolio: "Portfolio",
|
|
645
1053
|
settings: "Settings",
|
|
@@ -750,6 +1158,26 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
750
1158
|
reschedule: "Reschedule",
|
|
751
1159
|
cancel: "Cancel"
|
|
752
1160
|
},
|
|
1161
|
+
admission: {
|
|
1162
|
+
title: "Interview decisions",
|
|
1163
|
+
description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
|
|
1164
|
+
email: "Email",
|
|
1165
|
+
decision: "Decision",
|
|
1166
|
+
expires: "Expires",
|
|
1167
|
+
note: "Internal note",
|
|
1168
|
+
waive: "Waive interview",
|
|
1169
|
+
require: "Require interview",
|
|
1170
|
+
add: "Add decision",
|
|
1171
|
+
ledger: "Effective grant ledger",
|
|
1172
|
+
loading: "Loading\u2026",
|
|
1173
|
+
consumed: "consumed",
|
|
1174
|
+
revoked: "revoked",
|
|
1175
|
+
expired: "expired",
|
|
1176
|
+
active: "active",
|
|
1177
|
+
local: "local",
|
|
1178
|
+
parent: "Parent",
|
|
1179
|
+
revoke: "Revoke"
|
|
1180
|
+
},
|
|
753
1181
|
network: {
|
|
754
1182
|
title: "Share to a follower",
|
|
755
1183
|
allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
|
|
@@ -848,6 +1276,19 @@ var DEFAULT_JOIN_COPY = {
|
|
|
848
1276
|
label: "You're booked",
|
|
849
1277
|
calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
|
|
850
1278
|
memberArea: "Go to your member area"
|
|
1279
|
+
},
|
|
1280
|
+
accepted: {
|
|
1281
|
+
label: "You're approved",
|
|
1282
|
+
body: "Your membership is approved. No introduction call is required."
|
|
1283
|
+
},
|
|
1284
|
+
gift: {
|
|
1285
|
+
chooseSelf: "Buy membership for myself",
|
|
1286
|
+
optionsLabel: "Or give access to someone else",
|
|
1287
|
+
label: "Your gift code",
|
|
1288
|
+
body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
|
|
1289
|
+
save: "Save it now. For security, this code is shown only once.",
|
|
1290
|
+
rotate: "Generate a new code",
|
|
1291
|
+
rotateFailed: "A new code could not be generated."
|
|
851
1292
|
}
|
|
852
1293
|
};
|
|
853
1294
|
var DEFAULT_MEMBERS_COPY = {
|
|
@@ -875,6 +1316,26 @@ var DEFAULT_MEMBERS_COPY = {
|
|
|
875
1316
|
full: {
|
|
876
1317
|
welcome: "Welcome back."
|
|
877
1318
|
},
|
|
1319
|
+
gifts: {
|
|
1320
|
+
title: "Give access",
|
|
1321
|
+
body: "Purchase access for someone else and share the one-time claim code.",
|
|
1322
|
+
noneAvailable: "No gift options are available right now.",
|
|
1323
|
+
paid: "Payment submitted. Save the code below while it is visible.",
|
|
1324
|
+
saveCode: "This code is shown once. Save it before leaving this page.",
|
|
1325
|
+
purchases: "Your gifts",
|
|
1326
|
+
noPurchases: "No gift purchases yet.",
|
|
1327
|
+
pending: "Pending",
|
|
1328
|
+
redeemed: "Redeemed",
|
|
1329
|
+
expired: "Expired",
|
|
1330
|
+
revoked: "Revoked",
|
|
1331
|
+
rotate: "Rotate code",
|
|
1332
|
+
rotated: "A new code was generated. Save it now.",
|
|
1333
|
+
rotateFailed: "The code could not be rotated.",
|
|
1334
|
+
redeemTitle: "Redeem a gift",
|
|
1335
|
+
code: "One-time code",
|
|
1336
|
+
redeem: "Redeem",
|
|
1337
|
+
redeemFailed: "The code is invalid or unavailable."
|
|
1338
|
+
},
|
|
878
1339
|
reschedule: {
|
|
879
1340
|
changeTime: "Change your time",
|
|
880
1341
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
@@ -990,13 +1451,13 @@ function resolveLeaderFormation(input, crm) {
|
|
|
990
1451
|
}
|
|
991
1452
|
function formationFields(crm, formation) {
|
|
992
1453
|
const def = crm.type(formation.type);
|
|
993
|
-
return [...formation.required, ...formation.optional].map((
|
|
994
|
-
const field = def.fields[
|
|
1454
|
+
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1455
|
+
const field = def.fields[id4];
|
|
995
1456
|
return {
|
|
996
|
-
id:
|
|
997
|
-
label: field.label ??
|
|
1457
|
+
id: id4,
|
|
1458
|
+
label: field.label ?? id4,
|
|
998
1459
|
type: field.type,
|
|
999
|
-
required: formation.required.includes(
|
|
1460
|
+
required: formation.required.includes(id4),
|
|
1000
1461
|
...field.options ? { options: field.options } : {}
|
|
1001
1462
|
};
|
|
1002
1463
|
});
|
|
@@ -1148,16 +1609,36 @@ function resolveSignupControl(input) {
|
|
|
1148
1609
|
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1149
1610
|
}
|
|
1150
1611
|
|
|
1151
|
-
// src/config.ts
|
|
1612
|
+
// src/config-member-content.ts
|
|
1152
1613
|
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1614
|
+
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1615
|
+
function resolveMemberContent(input) {
|
|
1616
|
+
if (!input) return null;
|
|
1617
|
+
const parent = input.parent;
|
|
1618
|
+
if (parent && !SLUG3.test(parent.sourceId)) {
|
|
1619
|
+
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1620
|
+
}
|
|
1621
|
+
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
1622
|
+
if (parent && !SECRET_NAME2.test(secretName)) {
|
|
1623
|
+
throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
enabled: true,
|
|
1627
|
+
localAuthoring: input.localAuthoring !== false,
|
|
1628
|
+
parent: parent ? { sourceId: parent.sourceId, secretName } : null
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/config.ts
|
|
1633
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1153
1634
|
function isResolvedCrm(x) {
|
|
1154
1635
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1155
1636
|
}
|
|
1156
1637
|
function defineChapter(config) {
|
|
1157
1638
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1158
|
-
const { id:
|
|
1159
|
-
if (typeof
|
|
1160
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
1639
|
+
const { id: id4, name } = config;
|
|
1640
|
+
if (typeof id4 !== "string" || !SLUG4.test(id4)) {
|
|
1641
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1161
1642
|
}
|
|
1162
1643
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1163
1644
|
const mode = config.mode ?? "chapter";
|
|
@@ -1181,16 +1662,21 @@ function defineChapter(config) {
|
|
|
1181
1662
|
const copy = resolveChapterCopy(config.copy);
|
|
1182
1663
|
const network = resolveNetwork(config, crm);
|
|
1183
1664
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1665
|
+
const memberContent = resolveMemberContent(config.memberContent);
|
|
1184
1666
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1185
1667
|
if (signupControl && mode !== "chapter") {
|
|
1186
1668
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1187
1669
|
}
|
|
1670
|
+
if (memberContent && mode !== "chapter") {
|
|
1671
|
+
throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
|
|
1672
|
+
}
|
|
1188
1673
|
const { schema, rules } = chapterDb(
|
|
1189
1674
|
mode,
|
|
1190
1675
|
auth,
|
|
1191
1676
|
network.targets.length > 0,
|
|
1192
1677
|
application,
|
|
1193
|
-
Boolean(signupControl)
|
|
1678
|
+
Boolean(signupControl),
|
|
1679
|
+
Boolean(memberContent)
|
|
1194
1680
|
);
|
|
1195
1681
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1196
1682
|
const account = config.account ?? "none";
|
|
@@ -1240,13 +1726,14 @@ function defineChapter(config) {
|
|
|
1240
1726
|
};
|
|
1241
1727
|
const chapter = {
|
|
1242
1728
|
config,
|
|
1243
|
-
id:
|
|
1729
|
+
id: id4,
|
|
1244
1730
|
name,
|
|
1245
1731
|
mode,
|
|
1246
1732
|
brand,
|
|
1247
1733
|
copy,
|
|
1248
1734
|
network,
|
|
1249
1735
|
signupControl,
|
|
1736
|
+
memberContent,
|
|
1250
1737
|
formation,
|
|
1251
1738
|
crm,
|
|
1252
1739
|
auth,
|
|
@@ -1454,8 +1941,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
1454
1941
|
function byOrder(a, b) {
|
|
1455
1942
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1456
1943
|
}
|
|
1457
|
-
function resolveTiers(group,
|
|
1458
|
-
const stored =
|
|
1944
|
+
function resolveTiers(group, rows2 = []) {
|
|
1945
|
+
const stored = rows2.filter((tier) => tier.active);
|
|
1459
1946
|
if (stored.length) return [...stored].sort(byOrder);
|
|
1460
1947
|
return [{
|
|
1461
1948
|
id: "standard",
|
|
@@ -1541,12 +2028,332 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1541
2028
|
}] : [],
|
|
1542
2029
|
// Formation metadata is public only when the host explicitly enables
|
|
1543
2030
|
// the bounded intake contract.
|
|
1544
|
-
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
2031
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
2032
|
+
...chapter.memberContent ? [
|
|
2033
|
+
{ path: "/api/member-content", expectedStatus: 401 },
|
|
2034
|
+
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
2035
|
+
] : []
|
|
1545
2036
|
],
|
|
1546
2037
|
runbooks
|
|
1547
2038
|
};
|
|
1548
2039
|
}
|
|
1549
2040
|
|
|
2041
|
+
// src/member-content-validation-helpers.ts
|
|
2042
|
+
var ID2 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2043
|
+
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2044
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2045
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2046
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
2047
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
2048
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
2049
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
2050
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
2051
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
2052
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
2053
|
+
var MAX_PAGES = 100;
|
|
2054
|
+
var MAX_MARKDOWN = 65536;
|
|
2055
|
+
var MAX_TEXT = 240;
|
|
2056
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2057
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
2058
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2059
|
+
}
|
|
2060
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
2061
|
+
if (value === void 0) {
|
|
2062
|
+
if (required) errors.push(`${path} is required`);
|
|
2063
|
+
return void 0;
|
|
2064
|
+
}
|
|
2065
|
+
if (typeof value !== "string") {
|
|
2066
|
+
errors.push(`${path} must be a string`);
|
|
2067
|
+
return void 0;
|
|
2068
|
+
}
|
|
2069
|
+
const valueText = value.trim();
|
|
2070
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
2071
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2072
|
+
return valueText;
|
|
2073
|
+
}
|
|
2074
|
+
function offerValue(value, path, errors, tiers2, audience) {
|
|
2075
|
+
const raw = record(value);
|
|
2076
|
+
if (!raw) {
|
|
2077
|
+
errors.push(`${path} must be an object`);
|
|
2078
|
+
return void 0;
|
|
2079
|
+
}
|
|
2080
|
+
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
2081
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2082
|
+
if (tierId && !ID2.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2083
|
+
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2084
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2085
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
2086
|
+
}
|
|
2087
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
2088
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2089
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2090
|
+
return {
|
|
2091
|
+
tierId,
|
|
2092
|
+
...label ? { label } : {},
|
|
2093
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
2094
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/member-content-validation.ts
|
|
2099
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
2100
|
+
const errors = [];
|
|
2101
|
+
const root = record(input);
|
|
2102
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2103
|
+
unknownKeys(root, ROOT_KEYS, "document", errors);
|
|
2104
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2105
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2106
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
2107
|
+
const sourceRaw = record(root.source);
|
|
2108
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
2109
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2110
|
+
else {
|
|
2111
|
+
unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
2112
|
+
const type = sourceRaw.type;
|
|
2113
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2114
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2115
|
+
if (appId && !ID2.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2116
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2117
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2118
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
2119
|
+
}
|
|
2120
|
+
let target;
|
|
2121
|
+
if (root.target !== void 0) {
|
|
2122
|
+
const targetRaw = record(root.target);
|
|
2123
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
2124
|
+
else {
|
|
2125
|
+
unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
2126
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
2127
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
2128
|
+
target = { chapterId, environment };
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
2132
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
2133
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
2134
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
2135
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2136
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2137
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2138
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2139
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2140
|
+
const pages = [];
|
|
2141
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2142
|
+
const path = `document.pages[${index}]`;
|
|
2143
|
+
const raw = record(value);
|
|
2144
|
+
if (!raw) {
|
|
2145
|
+
errors.push(`${path} must be an object`);
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
unknownKeys(raw, PAGE_KEYS, path, errors);
|
|
2149
|
+
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2150
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2151
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2152
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2153
|
+
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2154
|
+
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2155
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2156
|
+
ids.add(id4);
|
|
2157
|
+
slugs.add(slug);
|
|
2158
|
+
const navRaw = record(raw.navigation);
|
|
2159
|
+
let navigation = { section: "General", order: index };
|
|
2160
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2161
|
+
else {
|
|
2162
|
+
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2163
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2164
|
+
const order = navRaw.order;
|
|
2165
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2166
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2167
|
+
}
|
|
2168
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2169
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2170
|
+
}
|
|
2171
|
+
const audienceRaw = record(raw.audience);
|
|
2172
|
+
let audience = { kind: "all-members" };
|
|
2173
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2174
|
+
else {
|
|
2175
|
+
unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2176
|
+
if (audienceRaw.kind === "all-members") {
|
|
2177
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2178
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2179
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2180
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2181
|
+
}
|
|
2182
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2183
|
+
const normalized = [];
|
|
2184
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2185
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2186
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2187
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2188
|
+
normalized.push(tier);
|
|
2189
|
+
}
|
|
2190
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2191
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2192
|
+
}
|
|
2193
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2194
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2195
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2196
|
+
let upsell;
|
|
2197
|
+
if (raw.upsell !== void 0) {
|
|
2198
|
+
const upsellRaw = record(raw.upsell);
|
|
2199
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2200
|
+
else {
|
|
2201
|
+
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2202
|
+
const owner = upsellRaw.owner;
|
|
2203
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2204
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2205
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2206
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2207
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2208
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2209
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2213
|
+
}
|
|
2214
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2215
|
+
for (const [index, page] of pages.entries()) {
|
|
2216
|
+
const parentId = page.navigation.parentId;
|
|
2217
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2218
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2219
|
+
}
|
|
2220
|
+
for (const page of pages) {
|
|
2221
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2222
|
+
let parentId = page.navigation.parentId;
|
|
2223
|
+
while (parentId) {
|
|
2224
|
+
if (seen.has(parentId)) {
|
|
2225
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
seen.add(parentId);
|
|
2229
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
let upsellOverrides;
|
|
2233
|
+
if (root.upsellOverrides !== void 0) {
|
|
2234
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2235
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2236
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2238
|
+
upsellOverrides = [];
|
|
2239
|
+
for (const [index, value] of values.entries()) {
|
|
2240
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2241
|
+
const raw = record(value);
|
|
2242
|
+
if (!raw) {
|
|
2243
|
+
errors.push(`${path} must be an object`);
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
unknownKeys(raw, OVERRIDE_KEYS, path, errors);
|
|
2247
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2248
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2249
|
+
seen.add(pageId);
|
|
2250
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2251
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2252
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2253
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2254
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (errors.length) return { ok: false, errors };
|
|
2258
|
+
return { ok: true, value: {
|
|
2259
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2260
|
+
revision: Number(root.revision),
|
|
2261
|
+
generatedAt: Number(root.generatedAt),
|
|
2262
|
+
source,
|
|
2263
|
+
...target ? { target } : {},
|
|
2264
|
+
pages,
|
|
2265
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2266
|
+
} };
|
|
2267
|
+
}
|
|
2268
|
+
function memberContentDocument(input, options = {}) {
|
|
2269
|
+
const result = validateMemberContentDocument(input, options);
|
|
2270
|
+
if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
|
|
2271
|
+
return result.value;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/member-content.ts
|
|
2275
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2276
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2277
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2278
|
+
var pageOrder = (a, b) => a.navigation.section.localeCompare(b.navigation.section) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
|
|
2279
|
+
function composeMemberContent(parent, chapter) {
|
|
2280
|
+
const pages = [];
|
|
2281
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2282
|
+
for (const page of parent?.pages ?? []) {
|
|
2283
|
+
const effective = { ...page, source: parent.source };
|
|
2284
|
+
parentPages.set(page.id, effective);
|
|
2285
|
+
pages.push(effective);
|
|
2286
|
+
}
|
|
2287
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2288
|
+
const page = parentPages.get(override.pageId);
|
|
2289
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2290
|
+
}
|
|
2291
|
+
for (const page of chapter?.pages ?? []) {
|
|
2292
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2293
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2294
|
+
}
|
|
2295
|
+
pages.push({ ...page, source: chapter.source });
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
pages: pages.sort(pageOrder),
|
|
2299
|
+
parentRevision: parent?.revision ?? null,
|
|
2300
|
+
chapterRevision: chapter?.revision ?? null
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2304
|
+
function projectMemberContent(content, viewer, tiers2) {
|
|
2305
|
+
const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
2306
|
+
return {
|
|
2307
|
+
viewer: { ...viewer },
|
|
2308
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2309
|
+
pages: content.pages.map((page) => {
|
|
2310
|
+
const canRead = allowed(page, viewer);
|
|
2311
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2312
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2313
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2314
|
+
return {
|
|
2315
|
+
id: page.id,
|
|
2316
|
+
slug: page.slug,
|
|
2317
|
+
title: page.title,
|
|
2318
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2319
|
+
navigation: page.navigation,
|
|
2320
|
+
audience: page.audience,
|
|
2321
|
+
source: page.source,
|
|
2322
|
+
access: canRead ? "allowed" : "locked",
|
|
2323
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2324
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2325
|
+
...upsell ? { upsell } : {}
|
|
2326
|
+
};
|
|
2327
|
+
})
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/member-content-canonical.ts
|
|
2332
|
+
var encoder2 = new TextEncoder();
|
|
2333
|
+
function canonicalValue2(value) {
|
|
2334
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2335
|
+
return JSON.stringify(value);
|
|
2336
|
+
}
|
|
2337
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2338
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2339
|
+
const record3 = value;
|
|
2340
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2341
|
+
const item = record3[key];
|
|
2342
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2343
|
+
});
|
|
2344
|
+
return `{${entries.join(",")}}`;
|
|
2345
|
+
}
|
|
2346
|
+
function canonicalMemberContent(value) {
|
|
2347
|
+
return canonicalValue2(value);
|
|
2348
|
+
}
|
|
2349
|
+
async function memberContentDigest(document) {
|
|
2350
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2351
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2352
|
+
}
|
|
2353
|
+
async function createMemberContentEnvelope(document) {
|
|
2354
|
+
return { document, digest: await memberContentDigest(document) };
|
|
2355
|
+
}
|
|
2356
|
+
|
|
1550
2357
|
// src/email.ts
|
|
1551
2358
|
function render(template, vars) {
|
|
1552
2359
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -1577,10 +2384,10 @@ function planDelivery(input) {
|
|
|
1577
2384
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1578
2385
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1579
2386
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1580
|
-
const
|
|
2387
|
+
const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1581
2388
|
|
|
1582
2389
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1583
|
-
return { deliver: true, transport, to, subject, text:
|
|
2390
|
+
return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
|
|
1584
2391
|
}
|
|
1585
2392
|
|
|
1586
2393
|
// src/notify.ts
|
|
@@ -1615,9 +2422,9 @@ async function sendTemplated(deps, input) {
|
|
|
1615
2422
|
error = e instanceof Error ? e.message : String(e);
|
|
1616
2423
|
}
|
|
1617
2424
|
}
|
|
1618
|
-
const
|
|
2425
|
+
const id4 = deps.newId();
|
|
1619
2426
|
const row = {
|
|
1620
|
-
id:
|
|
2427
|
+
id: id4,
|
|
1621
2428
|
groupId: input.group.id,
|
|
1622
2429
|
to: decision.to,
|
|
1623
2430
|
template: input.template,
|
|
@@ -1631,7 +2438,7 @@ async function sendTemplated(deps, input) {
|
|
|
1631
2438
|
...messageId ? { messageId } : {},
|
|
1632
2439
|
...error ? { error } : {}
|
|
1633
2440
|
};
|
|
1634
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2441
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
1635
2442
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
1636
2443
|
}
|
|
1637
2444
|
function emailGroupFrom(row) {
|
|
@@ -1667,8 +2474,26 @@ function findApplicationRef(obj) {
|
|
|
1667
2474
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
1668
2475
|
return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
|
|
1669
2476
|
}
|
|
2477
|
+
function findGiftRef(obj) {
|
|
2478
|
+
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
2479
|
+
const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
|
|
2480
|
+
const parent = obj.parent;
|
|
2481
|
+
const subscription = obj.subscription_details ?? parent?.subscription_details;
|
|
2482
|
+
const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
|
|
2483
|
+
const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
|
|
2484
|
+
return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
|
|
2485
|
+
}
|
|
1670
2486
|
function normalizeWebhookEvent(event) {
|
|
1671
2487
|
const obj = event.data?.object ?? {};
|
|
2488
|
+
const gift = findGiftRef(obj);
|
|
2489
|
+
if (gift.giftIntentId) {
|
|
2490
|
+
if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
2491
|
+
return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
|
|
2492
|
+
}
|
|
2493
|
+
if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
|
|
2494
|
+
if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
|
|
2495
|
+
return { kind: "ignored", type: event.type };
|
|
2496
|
+
}
|
|
1672
2497
|
const ref = findApplicationRef(obj);
|
|
1673
2498
|
switch (event.type) {
|
|
1674
2499
|
case "invoice.paid": {
|
|
@@ -1702,6 +2527,112 @@ function canceledPatch() {
|
|
|
1702
2527
|
return { canceled: true };
|
|
1703
2528
|
}
|
|
1704
2529
|
|
|
2530
|
+
// src/join-continuation.ts
|
|
2531
|
+
function resolveJoinContinuation(input) {
|
|
2532
|
+
if (input.status === "approved") return "accepted";
|
|
2533
|
+
if (input.interviewWaived) {
|
|
2534
|
+
if (!input.payable) return "approve";
|
|
2535
|
+
if (input.status === input.initialStatus) {
|
|
2536
|
+
return input.subscriptionStarted ? "paymentPending" : "payment";
|
|
2537
|
+
}
|
|
2538
|
+
return "approve";
|
|
2539
|
+
}
|
|
2540
|
+
if (!input.payable) return "booking";
|
|
2541
|
+
if (input.status === input.initialStatus) {
|
|
2542
|
+
return input.subscriptionStarted ? "paymentPending" : "payment";
|
|
2543
|
+
}
|
|
2544
|
+
if (canBook(input.status, input.pipeline)) return "booking";
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// src/gift-offers.ts
|
|
2549
|
+
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2550
|
+
var MAX_GIFT_OFFERS = 20;
|
|
2551
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2552
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2553
|
+
var CURRENCY = /^[a-z]{3}$/;
|
|
2554
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2555
|
+
var text = (value, path, errors, max) => {
|
|
2556
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2557
|
+
errors.push(`${path} is required`);
|
|
2558
|
+
return "";
|
|
2559
|
+
}
|
|
2560
|
+
if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2561
|
+
return value.trim();
|
|
2562
|
+
};
|
|
2563
|
+
function validateGiftOfferCatalog(input, tierIds) {
|
|
2564
|
+
const errors = [];
|
|
2565
|
+
const root = record2(input);
|
|
2566
|
+
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2567
|
+
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2568
|
+
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
2569
|
+
if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
|
|
2570
|
+
const rawOffers = Array.isArray(root.offers) ? root.offers : [];
|
|
2571
|
+
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
2572
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2573
|
+
const offers = [];
|
|
2574
|
+
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2575
|
+
const path = `document.giftOffers.offers[${index}]`;
|
|
2576
|
+
const item = record2(raw);
|
|
2577
|
+
if (!item) {
|
|
2578
|
+
errors.push(`${path} must be an object`);
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2582
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2583
|
+
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2584
|
+
if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2585
|
+
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2586
|
+
ids.add(id4);
|
|
2587
|
+
const targetRaw = record2(item.target);
|
|
2588
|
+
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2589
|
+
if (targetRaw?.type === "tier") {
|
|
2590
|
+
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
2591
|
+
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
2592
|
+
target = { type: "tier", tierId };
|
|
2593
|
+
} else if (targetRaw?.type === "entitlement") {
|
|
2594
|
+
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2595
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2596
|
+
target = { type: "entitlement", entitlementKey };
|
|
2597
|
+
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2598
|
+
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
2599
|
+
const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
2600
|
+
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2601
|
+
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
2602
|
+
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
2603
|
+
if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
|
|
2604
|
+
const lifetime = Number(item.redemptionLifetimeHours);
|
|
2605
|
+
if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
|
|
2606
|
+
const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
|
|
2607
|
+
const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
|
|
2608
|
+
if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
|
|
2609
|
+
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
2610
|
+
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
2611
|
+
offers.push({
|
|
2612
|
+
id: id4,
|
|
2613
|
+
target,
|
|
2614
|
+
billingMode: item.billingMode,
|
|
2615
|
+
stripePriceId,
|
|
2616
|
+
priceCents: Number(item.priceCents),
|
|
2617
|
+
currency,
|
|
2618
|
+
redemptionLifetimeHours: lifetime,
|
|
2619
|
+
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
2620
|
+
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
2621
|
+
title: text(item.title, `${path}.title`, errors, 160),
|
|
2622
|
+
description: text(item.description, `${path}.description`, errors, 2e3),
|
|
2623
|
+
ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
2627
|
+
}
|
|
2628
|
+
var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
|
|
2629
|
+
function publicGiftOffers(catalog, now) {
|
|
2630
|
+
return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
|
|
2631
|
+
}
|
|
2632
|
+
function findGiftOffer(catalog, offerId, now) {
|
|
2633
|
+
return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
1705
2636
|
// src/network.ts
|
|
1706
2637
|
import {
|
|
1707
2638
|
createRecord,
|
|
@@ -1746,34 +2677,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1746
2677
|
if (readable && raw.length <= 64) return raw;
|
|
1747
2678
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1748
2679
|
}
|
|
1749
|
-
function normalizeSharedRecord(
|
|
1750
|
-
if ("input" in
|
|
2680
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
2681
|
+
if ("input" in record3 && record3.version === 2) {
|
|
1751
2682
|
return {
|
|
1752
2683
|
version: 2,
|
|
1753
|
-
sourceId:
|
|
1754
|
-
sourceRecordId:
|
|
1755
|
-
type:
|
|
1756
|
-
input:
|
|
2684
|
+
sourceId: record3.source.siteId,
|
|
2685
|
+
sourceRecordId: record3.source.recordId,
|
|
2686
|
+
type: record3.type,
|
|
2687
|
+
input: record3.input
|
|
1757
2688
|
};
|
|
1758
2689
|
}
|
|
1759
|
-
if ("input" in
|
|
2690
|
+
if ("input" in record3) {
|
|
1760
2691
|
return {
|
|
1761
2692
|
version: 1,
|
|
1762
2693
|
sourceId: fallbackSourceId,
|
|
1763
|
-
sourceRecordId:
|
|
1764
|
-
type:
|
|
1765
|
-
input:
|
|
2694
|
+
sourceRecordId: record3.hubRecordId,
|
|
2695
|
+
type: record3.type,
|
|
2696
|
+
input: record3.input
|
|
1766
2697
|
};
|
|
1767
2698
|
}
|
|
1768
|
-
if ("type" in
|
|
1769
|
-
const input = { name:
|
|
2699
|
+
if ("type" in record3 && record3.type === "company") {
|
|
2700
|
+
const input = { name: record3.name };
|
|
1770
2701
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1771
|
-
if (
|
|
2702
|
+
if (record3[key]) input[key] = record3[key];
|
|
1772
2703
|
}
|
|
1773
2704
|
return {
|
|
1774
2705
|
version: 1,
|
|
1775
2706
|
sourceId: fallbackSourceId,
|
|
1776
|
-
sourceRecordId:
|
|
2707
|
+
sourceRecordId: record3.hubRecordId,
|
|
1777
2708
|
type: "company",
|
|
1778
2709
|
input
|
|
1779
2710
|
};
|
|
@@ -1781,28 +2712,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
|
1781
2712
|
return {
|
|
1782
2713
|
version: 1,
|
|
1783
2714
|
sourceId: fallbackSourceId,
|
|
1784
|
-
sourceRecordId:
|
|
2715
|
+
sourceRecordId: record3.hubRecordId,
|
|
1785
2716
|
type: "person",
|
|
1786
|
-
input: sharedPersonInput(
|
|
2717
|
+
input: sharedPersonInput(record3)
|
|
1787
2718
|
};
|
|
1788
2719
|
}
|
|
1789
|
-
function sharedRecordFromCrm(crm,
|
|
1790
|
-
if (target.fields && !target.fields[
|
|
1791
|
-
throw new Error(`${target.name} does not accept "${
|
|
2720
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
2721
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
2722
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
1792
2723
|
}
|
|
1793
|
-
const def = crm.type(
|
|
1794
|
-
const fields = target.fields?.[
|
|
2724
|
+
const def = crm.type(record3.type);
|
|
2725
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
1795
2726
|
if (!fields) {
|
|
1796
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2727
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
1797
2728
|
}
|
|
1798
2729
|
const nameField = def.nameField ?? "name";
|
|
1799
2730
|
const input = {};
|
|
1800
2731
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
1801
|
-
const value =
|
|
2732
|
+
const value = record3.fields?.[field];
|
|
1802
2733
|
if (value !== void 0) input[field] = value;
|
|
1803
2734
|
}
|
|
1804
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
1805
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2735
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
2736
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
1806
2737
|
}
|
|
1807
2738
|
async function upsertPerson(deps, opts) {
|
|
1808
2739
|
const email = opts.email.toLowerCase();
|
|
@@ -1816,11 +2747,11 @@ async function upsertPerson(deps, opts) {
|
|
|
1816
2747
|
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1817
2748
|
return { recordId: created.id };
|
|
1818
2749
|
}
|
|
1819
|
-
async function findSharedRecord(deps,
|
|
2750
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
1820
2751
|
const structured = await getRecordByOrigin(
|
|
1821
2752
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1822
|
-
|
|
1823
|
-
|
|
2753
|
+
record3.sourceId,
|
|
2754
|
+
record3.sourceRecordId
|
|
1824
2755
|
);
|
|
1825
2756
|
if (structured) return structured;
|
|
1826
2757
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1829,53 +2760,53 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1829
2760
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1830
2761
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1831
2762
|
}
|
|
1832
|
-
const def = deps.crm.type(
|
|
2763
|
+
const def = deps.crm.type(record3.type);
|
|
1833
2764
|
const emailField = def.emailField;
|
|
1834
|
-
if (emailField && typeof
|
|
1835
|
-
const primaryEmail =
|
|
2765
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
2766
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
1836
2767
|
const found = await deps.db.query({
|
|
1837
|
-
crm_record: { $: { where: { type:
|
|
2768
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
1838
2769
|
});
|
|
1839
2770
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1840
2771
|
}
|
|
1841
|
-
const domain =
|
|
2772
|
+
const domain = record3.input.domain;
|
|
1842
2773
|
const domainSlot = def.fields.domain?.slot;
|
|
1843
2774
|
if (typeof domain === "string" && domainSlot) {
|
|
1844
2775
|
const found = await deps.db.query({
|
|
1845
|
-
crm_record: { $: { where: { type:
|
|
2776
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
1846
2777
|
});
|
|
1847
2778
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1848
2779
|
}
|
|
1849
2780
|
const nameField = def.nameField ?? "name";
|
|
1850
|
-
const name =
|
|
1851
|
-
if (
|
|
2781
|
+
const name = record3.input[nameField];
|
|
2782
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
1852
2783
|
const found = await deps.db.query({
|
|
1853
|
-
crm_record: { $: { where: { type:
|
|
2784
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
1854
2785
|
});
|
|
1855
2786
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1856
2787
|
}
|
|
1857
2788
|
return void 0;
|
|
1858
2789
|
}
|
|
1859
2790
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1860
|
-
const
|
|
1861
|
-
if (!
|
|
2791
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
2792
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
1862
2793
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1863
2794
|
}
|
|
1864
|
-
if (options.sourceId && options.sourceId !==
|
|
2795
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
1865
2796
|
throw new Error("signed sender does not match payload source");
|
|
1866
2797
|
}
|
|
1867
|
-
const tag = networkSourceTag(
|
|
2798
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
1868
2799
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1869
|
-
const existing = await findSharedRecord(deps,
|
|
2800
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
1870
2801
|
let recordId;
|
|
1871
2802
|
if (existing && typeof existing.id === "string") {
|
|
1872
|
-
await updateRecord(crmDeps, { id: existing.id, input:
|
|
2803
|
+
await updateRecord(crmDeps, { id: existing.id, input: record3.input });
|
|
1873
2804
|
recordId = existing.id;
|
|
1874
2805
|
} else {
|
|
1875
|
-
recordId = `network_${shortHash(`${
|
|
2806
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
1876
2807
|
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
1877
|
-
type:
|
|
1878
|
-
input:
|
|
2808
|
+
type: record3.type,
|
|
2809
|
+
input: record3.input,
|
|
1879
2810
|
mutationId: `share-create:${tag}`
|
|
1880
2811
|
});
|
|
1881
2812
|
}
|
|
@@ -1885,10 +2816,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1885
2816
|
);
|
|
1886
2817
|
await upsertRecordOrigin(crmDeps, {
|
|
1887
2818
|
recordId,
|
|
1888
|
-
sourceId:
|
|
1889
|
-
sourceRecordId:
|
|
2819
|
+
sourceId: record3.sourceId,
|
|
2820
|
+
sourceRecordId: record3.sourceRecordId,
|
|
1890
2821
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1891
|
-
payloadVersion:
|
|
2822
|
+
payloadVersion: record3.version
|
|
1892
2823
|
});
|
|
1893
2824
|
return { recordId };
|
|
1894
2825
|
}
|
|
@@ -2104,9 +3035,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
2104
3035
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
2105
3036
|
if (!found.ok) return false;
|
|
2106
3037
|
const users = await found.json().catch(() => null);
|
|
2107
|
-
const
|
|
2108
|
-
if (!
|
|
2109
|
-
return updateClerkUserMetadata(secretKey,
|
|
3038
|
+
const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
3039
|
+
if (!id4) return false;
|
|
3040
|
+
return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
|
|
2110
3041
|
}
|
|
2111
3042
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
2112
3043
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -2162,8 +3093,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
2162
3093
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
2163
3094
|
return user ? toRecord(user) : null;
|
|
2164
3095
|
}
|
|
2165
|
-
async function clerkGetUser(secretKey,
|
|
2166
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
3096
|
+
async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
|
|
3097
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
|
|
2167
3098
|
return data ? toRecord(data) : null;
|
|
2168
3099
|
}
|
|
2169
3100
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -2172,15 +3103,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2172
3103
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2173
3104
|
const page = Array.isArray(data) ? data : [];
|
|
2174
3105
|
for (const u of page) {
|
|
2175
|
-
const
|
|
2176
|
-
if (
|
|
3106
|
+
const record3 = toRecord(u);
|
|
3107
|
+
if (record3) out.push(record3);
|
|
2177
3108
|
}
|
|
2178
3109
|
if (page.length < PAGE) break;
|
|
2179
3110
|
}
|
|
2180
3111
|
return out;
|
|
2181
3112
|
}
|
|
2182
|
-
async function clerkSetRole(secretKey,
|
|
2183
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
3113
|
+
async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
|
|
3114
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
|
|
2184
3115
|
method: "PATCH",
|
|
2185
3116
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
2186
3117
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -2275,7 +3206,8 @@ function applicationSummary(app) {
|
|
|
2275
3206
|
meetingLink: app.meetingLink ?? null,
|
|
2276
3207
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
2277
3208
|
renewalAt: app.renewalAt ?? null,
|
|
2278
|
-
canceled: app.canceled === true
|
|
3209
|
+
canceled: app.canceled === true,
|
|
3210
|
+
tierId: app.tierId ?? null
|
|
2279
3211
|
};
|
|
2280
3212
|
}
|
|
2281
3213
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -2556,15 +3488,15 @@ function rowToTier(row, groupId) {
|
|
|
2556
3488
|
}
|
|
2557
3489
|
async function loadTiers(db, group) {
|
|
2558
3490
|
const groupId = String(group.id ?? "");
|
|
2559
|
-
let
|
|
3491
|
+
let rows2;
|
|
2560
3492
|
try {
|
|
2561
|
-
|
|
3493
|
+
rows2 = (await db.query({
|
|
2562
3494
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2563
3495
|
})).tiers;
|
|
2564
3496
|
} catch {
|
|
2565
|
-
|
|
3497
|
+
rows2 = void 0;
|
|
2566
3498
|
}
|
|
2567
|
-
const list = Array.isArray(
|
|
3499
|
+
const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
|
|
2568
3500
|
return resolveTiers(group, list);
|
|
2569
3501
|
}
|
|
2570
3502
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -2575,37 +3507,11 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
|
2575
3507
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
2576
3508
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
2577
3509
|
|
|
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
|
-
|
|
2604
3510
|
// src/membership-page.ts
|
|
2605
3511
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2606
3512
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2607
3513
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
2608
|
-
var
|
|
3514
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
2609
3515
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
2610
3516
|
"tierId",
|
|
2611
3517
|
"variant",
|
|
@@ -2624,12 +3530,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
2624
3530
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
2625
3531
|
var MAX_TIERS = 20;
|
|
2626
3532
|
var MAX_BENEFITS = 24;
|
|
2627
|
-
var
|
|
3533
|
+
var MAX_MARKDOWN2 = 65536;
|
|
2628
3534
|
var MAX_LABEL = 160;
|
|
2629
3535
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2630
|
-
function rejectUnknownKeys(value,
|
|
3536
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
2631
3537
|
for (const key of Object.keys(value)) {
|
|
2632
|
-
if (!
|
|
3538
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2633
3539
|
}
|
|
2634
3540
|
}
|
|
2635
3541
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -2641,16 +3547,16 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2641
3547
|
errors.push(`${path} must be a string`);
|
|
2642
3548
|
return void 0;
|
|
2643
3549
|
}
|
|
2644
|
-
const
|
|
2645
|
-
if (required && !
|
|
2646
|
-
if (
|
|
2647
|
-
return
|
|
3550
|
+
const text3 = value.trim();
|
|
3551
|
+
if (required && !text3) errors.push(`${path} is required`);
|
|
3552
|
+
if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
3553
|
+
return text3;
|
|
2648
3554
|
}
|
|
2649
3555
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2650
3556
|
const errors = [];
|
|
2651
3557
|
const root = objectRecord(input);
|
|
2652
3558
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2653
|
-
rejectUnknownKeys(root,
|
|
3559
|
+
rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
2654
3560
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
2655
3561
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
2656
3562
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -2693,8 +3599,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2693
3599
|
else {
|
|
2694
3600
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2695
3601
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2696
|
-
const
|
|
2697
|
-
return
|
|
3602
|
+
const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
3603
|
+
return text3 ? [text3] : [];
|
|
2698
3604
|
});
|
|
2699
3605
|
}
|
|
2700
3606
|
}
|
|
@@ -2704,7 +3610,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2704
3610
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
2705
3611
|
else {
|
|
2706
3612
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
2707
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
3613
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
|
|
2708
3614
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
2709
3615
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
2710
3616
|
}
|
|
@@ -2726,10 +3632,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2726
3632
|
}
|
|
2727
3633
|
}
|
|
2728
3634
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
2729
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
3635
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
|
|
2730
3636
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
2731
3637
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
2732
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
3638
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
|
|
2733
3639
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
2734
3640
|
tiers2.push({
|
|
2735
3641
|
tierId,
|
|
@@ -2751,7 +3657,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2751
3657
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
2752
3658
|
}
|
|
2753
3659
|
}
|
|
2754
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
3660
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
|
|
2755
3661
|
if (errors.length) return { ok: false, errors };
|
|
2756
3662
|
return {
|
|
2757
3663
|
ok: true,
|
|
@@ -2770,11 +3676,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
2770
3676
|
}
|
|
2771
3677
|
|
|
2772
3678
|
// src/signup-control-validate.ts
|
|
2773
|
-
var
|
|
3679
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2774
3680
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
2775
3681
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2776
|
-
var
|
|
2777
|
-
var
|
|
3682
|
+
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3683
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
2778
3684
|
"schemaVersion",
|
|
2779
3685
|
"revision",
|
|
2780
3686
|
"generatedAt",
|
|
@@ -2785,14 +3691,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
2785
3691
|
"application",
|
|
2786
3692
|
"policy",
|
|
2787
3693
|
"scheduling",
|
|
2788
|
-
"theme"
|
|
3694
|
+
"theme",
|
|
3695
|
+
"giftOffers"
|
|
2789
3696
|
]);
|
|
2790
3697
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2791
3698
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2792
|
-
function
|
|
2793
|
-
for (const key of Object.keys(value)) if (!
|
|
3699
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
3700
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2794
3701
|
}
|
|
2795
|
-
function
|
|
3702
|
+
function text2(value, path, errors, max = 65536) {
|
|
2796
3703
|
if (typeof value !== "string" || !value.trim()) {
|
|
2797
3704
|
errors.push(`${path} is required`);
|
|
2798
3705
|
return "";
|
|
@@ -2814,26 +3721,26 @@ function tiersOf(value, errors) {
|
|
|
2814
3721
|
errors.push(`${path} must be an object`);
|
|
2815
3722
|
return [];
|
|
2816
3723
|
}
|
|
2817
|
-
|
|
2818
|
-
const
|
|
2819
|
-
if (
|
|
2820
|
-
if (seen.has(
|
|
2821
|
-
seen.add(
|
|
2822
|
-
const name =
|
|
3724
|
+
unknownKeys2(item, TIER_KEYS2, path, errors);
|
|
3725
|
+
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3726
|
+
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3727
|
+
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
3728
|
+
seen.add(id4);
|
|
3729
|
+
const name = text2(item.name, `${path}.name`, errors, 160);
|
|
2823
3730
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
2824
3731
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
2825
3732
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
2826
3733
|
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
2827
3734
|
}
|
|
2828
|
-
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !
|
|
3735
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
|
|
2829
3736
|
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2830
3737
|
}
|
|
2831
3738
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2832
3739
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2833
3740
|
}
|
|
2834
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
3741
|
+
const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2835
3742
|
return [{
|
|
2836
|
-
id:
|
|
3743
|
+
id: id4,
|
|
2837
3744
|
name,
|
|
2838
3745
|
available: item.available === true,
|
|
2839
3746
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -2850,7 +3757,7 @@ function applicationOf(value, errors) {
|
|
|
2850
3757
|
errors.push("document.application must be an object");
|
|
2851
3758
|
return void 0;
|
|
2852
3759
|
}
|
|
2853
|
-
|
|
3760
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
2854
3761
|
try {
|
|
2855
3762
|
const resolved = resolveApplication(input);
|
|
2856
3763
|
const copy = recordOf(input.copy);
|
|
@@ -2875,12 +3782,12 @@ function policyOf(value, errors) {
|
|
|
2875
3782
|
errors.push("document.policy must be an object");
|
|
2876
3783
|
return void 0;
|
|
2877
3784
|
}
|
|
2878
|
-
const
|
|
2879
|
-
|
|
3785
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3786
|
+
unknownKeys2(input, allowed2, "document.policy", errors);
|
|
2880
3787
|
const result = {};
|
|
2881
|
-
for (const key of
|
|
3788
|
+
for (const key of allowed2) {
|
|
2882
3789
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
2883
|
-
result[key] =
|
|
3790
|
+
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
2884
3791
|
}
|
|
2885
3792
|
return result;
|
|
2886
3793
|
}
|
|
@@ -2892,7 +3799,7 @@ function themeOf(value, errors) {
|
|
|
2892
3799
|
errors.push("document.theme.tokens must be an object");
|
|
2893
3800
|
return void 0;
|
|
2894
3801
|
}
|
|
2895
|
-
|
|
3802
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
2896
3803
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
2897
3804
|
let serializedLength = 0;
|
|
2898
3805
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -2912,17 +3819,17 @@ function validateSignupControlDocument(input) {
|
|
|
2912
3819
|
const errors = [];
|
|
2913
3820
|
const root = recordOf(input);
|
|
2914
3821
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2915
|
-
|
|
3822
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
2916
3823
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
2917
3824
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2918
3825
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
2919
3826
|
const source = recordOf(root.source);
|
|
2920
3827
|
const target = recordOf(root.target);
|
|
2921
|
-
const appId =
|
|
2922
|
-
const chapterId =
|
|
2923
|
-
const environment =
|
|
2924
|
-
if (appId && !
|
|
2925
|
-
if (chapterId && !
|
|
3828
|
+
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3829
|
+
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3830
|
+
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3831
|
+
if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3832
|
+
if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
2926
3833
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
2927
3834
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
2928
3835
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -2941,6 +3848,8 @@ function validateSignupControlDocument(input) {
|
|
|
2941
3848
|
else scheduling = root.scheduling;
|
|
2942
3849
|
}
|
|
2943
3850
|
const theme = themeOf(root.theme, errors);
|
|
3851
|
+
const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
|
|
3852
|
+
if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
|
|
2944
3853
|
if (errors.length || !page.ok) return { ok: false, errors };
|
|
2945
3854
|
return { ok: true, value: {
|
|
2946
3855
|
schemaVersion: 1,
|
|
@@ -2953,7 +3862,8 @@ function validateSignupControlDocument(input) {
|
|
|
2953
3862
|
...application ? { application } : {},
|
|
2954
3863
|
...policy ? { policy } : {},
|
|
2955
3864
|
...scheduling ? { scheduling } : {},
|
|
2956
|
-
...theme ? { theme } : {}
|
|
3865
|
+
...theme ? { theme } : {},
|
|
3866
|
+
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
2957
3867
|
} };
|
|
2958
3868
|
}
|
|
2959
3869
|
|
|
@@ -3069,7 +3979,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
3069
3979
|
|
|
3070
3980
|
// src/membership-markdown.ts
|
|
3071
3981
|
var MAX_BENEFITS2 = 24;
|
|
3072
|
-
var
|
|
3982
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3073
3983
|
var MAX_LABEL2 = 160;
|
|
3074
3984
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
3075
3985
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -3094,17 +4004,17 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3094
4004
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
3095
4005
|
continue;
|
|
3096
4006
|
}
|
|
3097
|
-
const
|
|
3098
|
-
if (
|
|
3099
|
-
if (
|
|
4007
|
+
const text3 = (match[1] ?? "").trim();
|
|
4008
|
+
if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
4009
|
+
if (text3) benefits.push(text3);
|
|
3100
4010
|
}
|
|
3101
4011
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
3102
4012
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
3103
4013
|
}
|
|
3104
4014
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
3105
4015
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
3106
|
-
if (source.length >
|
|
3107
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4016
|
+
if (source.length > MAX_MARKDOWN3) {
|
|
4017
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
|
|
3108
4018
|
}
|
|
3109
4019
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
3110
4020
|
const errors = [];
|
|
@@ -3198,7 +4108,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
3198
4108
|
id: chapterId,
|
|
3199
4109
|
reason: "provider_identity",
|
|
3200
4110
|
paths: [
|
|
3201
|
-
...paidTierIds.map((
|
|
4111
|
+
...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
|
|
3202
4112
|
"groups.stripePublishableKey",
|
|
3203
4113
|
"applications.stripeCustomerId",
|
|
3204
4114
|
"applications.stripeSubscriptionId",
|
|
@@ -3240,9 +4150,9 @@ async function exportChapterPromotion(input) {
|
|
|
3240
4150
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
3241
4151
|
receiptDigests: [input.digest]
|
|
3242
4152
|
};
|
|
3243
|
-
const artifact = (kind,
|
|
4153
|
+
const artifact = (kind, id4, content) => ({
|
|
3244
4154
|
kind,
|
|
3245
|
-
id:
|
|
4155
|
+
id: id4,
|
|
3246
4156
|
sourceRevision: revision,
|
|
3247
4157
|
provenance: promotionProvenance,
|
|
3248
4158
|
content: canonicalPromotionValue(content)
|
|
@@ -3280,10 +4190,17 @@ async function exportChapterPromotion(input) {
|
|
|
3280
4190
|
};
|
|
3281
4191
|
}
|
|
3282
4192
|
export {
|
|
4193
|
+
ADMISSION_GRANTS_PATH,
|
|
4194
|
+
ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
3283
4195
|
CHAPTER_PROMOTION_ADAPTER,
|
|
3284
4196
|
DEFAULT_CHAPTER_COPY,
|
|
3285
4197
|
DEFAULT_SHARE_FIELDS,
|
|
4198
|
+
GIFT_OFFER_CATALOG_VERSION,
|
|
4199
|
+
MAX_GIFT_OFFERS,
|
|
3286
4200
|
MEMBERSHIP_PAGE_RENDERER,
|
|
4201
|
+
MEMBER_CONTENT_CONTROL_PATH,
|
|
4202
|
+
MEMBER_CONTENT_PATH,
|
|
4203
|
+
MEMBER_CONTENT_SCHEMA_VERSION,
|
|
3287
4204
|
SCHEDULING_DEFAULTS,
|
|
3288
4205
|
SIGNUP_CONTROL_PATH,
|
|
3289
4206
|
SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
@@ -3304,6 +4221,7 @@ export {
|
|
|
3304
4221
|
canChangeRole,
|
|
3305
4222
|
canTransition,
|
|
3306
4223
|
canceledPatch,
|
|
4224
|
+
canonicalMemberContent,
|
|
3307
4225
|
canonicalSignupControl,
|
|
3308
4226
|
chapterBrandFromTokens,
|
|
3309
4227
|
chapterDb,
|
|
@@ -3315,9 +4233,12 @@ export {
|
|
|
3315
4233
|
clerkListUsers,
|
|
3316
4234
|
clerkSetRole,
|
|
3317
4235
|
clerkUserRequest,
|
|
4236
|
+
composeMemberContent,
|
|
4237
|
+
createAdmissionGrantEnvelope,
|
|
3318
4238
|
createChapterIntegration,
|
|
3319
4239
|
createClerkInvitation,
|
|
3320
4240
|
createClerkUser,
|
|
4241
|
+
createMemberContentEnvelope,
|
|
3321
4242
|
createSignupControlEnvelope,
|
|
3322
4243
|
dashboardMetricData,
|
|
3323
4244
|
defaultCrm,
|
|
@@ -3325,12 +4246,15 @@ export {
|
|
|
3325
4246
|
emailGroupFrom,
|
|
3326
4247
|
endForSlot,
|
|
3327
4248
|
exportChapterPromotion,
|
|
4249
|
+
findAdmissionGrant,
|
|
3328
4250
|
findApplicationRef,
|
|
4251
|
+
findGiftOffer,
|
|
3329
4252
|
findTier,
|
|
3330
4253
|
firstPaymentPatch,
|
|
3331
4254
|
formatChapterCopy,
|
|
3332
4255
|
formationFields,
|
|
3333
4256
|
getVaultSecret,
|
|
4257
|
+
giftOfferActive,
|
|
3334
4258
|
hasDisclaimerAck,
|
|
3335
4259
|
hydrateSignupControlTheme,
|
|
3336
4260
|
injectSignupControlThemeHead,
|
|
@@ -3343,15 +4267,20 @@ export {
|
|
|
3343
4267
|
joinConfig,
|
|
3344
4268
|
joinConfigTiers,
|
|
3345
4269
|
leaderCrmConfig,
|
|
4270
|
+
loadLatestAdmissionRevision,
|
|
3346
4271
|
loadOfferableTiers,
|
|
3347
4272
|
loadTiers,
|
|
4273
|
+
materializeAdmissionGrants,
|
|
3348
4274
|
meetingCreateRow,
|
|
3349
4275
|
meetingRescheduleUpdate,
|
|
3350
4276
|
memberApplication,
|
|
4277
|
+
memberContentDigest,
|
|
4278
|
+
memberContentDocument,
|
|
3351
4279
|
memberSession,
|
|
3352
4280
|
membershipPageDocument,
|
|
3353
4281
|
membershipTierPresentationFromMarkdown,
|
|
3354
4282
|
networkSourceTag,
|
|
4283
|
+
normalizeAdmissionEmail,
|
|
3355
4284
|
normalizeSharedRecord,
|
|
3356
4285
|
normalizeWebhookEvent,
|
|
3357
4286
|
offerableTiers,
|
|
@@ -3359,7 +4288,9 @@ export {
|
|
|
3359
4288
|
personInputFromApp,
|
|
3360
4289
|
planDelivery,
|
|
3361
4290
|
projectApplicant,
|
|
4291
|
+
projectMemberContent,
|
|
3362
4292
|
projectSharedRecord,
|
|
4293
|
+
publicGiftOffers,
|
|
3363
4294
|
reconcileMeetings,
|
|
3364
4295
|
refundedPatch,
|
|
3365
4296
|
render,
|
|
@@ -3370,7 +4301,9 @@ export {
|
|
|
3370
4301
|
resolveApplication,
|
|
3371
4302
|
resolveAuth,
|
|
3372
4303
|
resolveChapterCopy,
|
|
4304
|
+
resolveJoinContinuation,
|
|
3373
4305
|
resolveLeaderFormation,
|
|
4306
|
+
resolveMemberContent,
|
|
3374
4307
|
resolvePipeline,
|
|
3375
4308
|
resolveScheduling,
|
|
3376
4309
|
resolveSignupControl,
|
|
@@ -3392,6 +4325,9 @@ export {
|
|
|
3392
4325
|
tierPayable,
|
|
3393
4326
|
updateClerkUserMetadata,
|
|
3394
4327
|
updateClerkUserMetadataByEmail,
|
|
4328
|
+
validateAdmissionGrantDocument,
|
|
4329
|
+
validateGiftOfferCatalog,
|
|
4330
|
+
validateMemberContentDocument,
|
|
3395
4331
|
validateMembershipPageDocument,
|
|
3396
4332
|
validateScheduling,
|
|
3397
4333
|
validateSignupControlDocument,
|