@odla-ai/chapter 0.36.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-I45VYBUV.js → chunk-BIYWPPH6.js} +233 -140
- package/dist/chunk-BIYWPPH6.js.map +1 -0
- package/dist/{chunk-LWM32REV.js → chunk-SI3JHSEX.js} +211 -64
- package/dist/chunk-SI3JHSEX.js.map +1 -0
- package/dist/{copy-context-0k0-yWHL.d.ts → copy-context-Pbs3keLx.d.ts} +174 -28
- package/dist/index.cjs +483 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +241 -29
- package/dist/index.d.ts +241 -29
- package/dist/index.js +483 -97
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +27 -3
- package/dist/ui/admin/index.js +5 -1
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +8 -2
- package/dist/ui/member/index.d.ts +28 -3
- package/dist/ui/member/index.js +3 -1
- package/dist/worker/index.cjs +736 -126
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +151 -28
- package/dist/worker/index.d.ts +151 -28
- package/dist/worker/index.js +729 -119
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +27 -0
- package/dist/chunk-I45VYBUV.js.map +0 -1
- package/dist/chunk-LWM32REV.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(worker_exports, {
|
|
|
23
23
|
chapterWorker: () => chapterWorker,
|
|
24
24
|
createWorkerContext: () => createWorkerContext,
|
|
25
25
|
deliverAdmissionGrants: () => deliverAdmissionGrants,
|
|
26
|
+
deliverMemberContent: () => deliverMemberContent,
|
|
26
27
|
deliverSignupControl: () => deliverSignupControl,
|
|
27
28
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
28
29
|
withActiveSignupControlTheme: () => withActiveSignupControlTheme
|
|
@@ -210,9 +211,9 @@ function canonicalValue(value) {
|
|
|
210
211
|
}
|
|
211
212
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
212
213
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
213
|
-
const
|
|
214
|
-
const entries = Object.keys(
|
|
215
|
-
const item =
|
|
214
|
+
const record3 = value;
|
|
215
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
216
|
+
const item = record3[key];
|
|
216
217
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
217
218
|
});
|
|
218
219
|
return `{${entries.join(",")}}`;
|
|
@@ -462,14 +463,14 @@ function hasDisclaimerAck(fields) {
|
|
|
462
463
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
463
464
|
function applicantProfile(chapter, fields) {
|
|
464
465
|
const app = chapter.application;
|
|
465
|
-
const
|
|
466
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
466
467
|
const profile = {};
|
|
467
468
|
for (const f of [...app.required, ...app.optional]) {
|
|
468
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
469
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
469
470
|
const v = fields[f];
|
|
470
471
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
471
472
|
}
|
|
472
|
-
if (fields.focus !== void 0 &&
|
|
473
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
473
474
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
474
475
|
}
|
|
475
476
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -762,8 +763,8 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
762
763
|
errors.push(`${path} must be an object`);
|
|
763
764
|
continue;
|
|
764
765
|
}
|
|
765
|
-
const
|
|
766
|
-
for (const key of Object.keys(item)) if (!
|
|
766
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
767
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
767
768
|
const id = text(item.id, `${path}.id`, errors, 80);
|
|
768
769
|
if (id && !ID2.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
769
770
|
if (ids.has(id)) errors.push(`${path}.id duplicates "${id}"`);
|
|
@@ -860,8 +861,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
860
861
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
861
862
|
const page = Array.isArray(data) ? data : [];
|
|
862
863
|
for (const u of page) {
|
|
863
|
-
const
|
|
864
|
-
if (
|
|
864
|
+
const record3 = toRecord(u);
|
|
865
|
+
if (record3) out.push(record3);
|
|
865
866
|
}
|
|
866
867
|
if (page.length < PAGE) break;
|
|
867
868
|
}
|
|
@@ -918,34 +919,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
918
919
|
if (readable && raw.length <= 64) return raw;
|
|
919
920
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
920
921
|
}
|
|
921
|
-
function normalizeSharedRecord(
|
|
922
|
-
if ("input" in
|
|
922
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
923
|
+
if ("input" in record3 && record3.version === 2) {
|
|
923
924
|
return {
|
|
924
925
|
version: 2,
|
|
925
|
-
sourceId:
|
|
926
|
-
sourceRecordId:
|
|
927
|
-
type:
|
|
928
|
-
input:
|
|
926
|
+
sourceId: record3.source.siteId,
|
|
927
|
+
sourceRecordId: record3.source.recordId,
|
|
928
|
+
type: record3.type,
|
|
929
|
+
input: record3.input
|
|
929
930
|
};
|
|
930
931
|
}
|
|
931
|
-
if ("input" in
|
|
932
|
+
if ("input" in record3) {
|
|
932
933
|
return {
|
|
933
934
|
version: 1,
|
|
934
935
|
sourceId: fallbackSourceId,
|
|
935
|
-
sourceRecordId:
|
|
936
|
-
type:
|
|
937
|
-
input:
|
|
936
|
+
sourceRecordId: record3.hubRecordId,
|
|
937
|
+
type: record3.type,
|
|
938
|
+
input: record3.input
|
|
938
939
|
};
|
|
939
940
|
}
|
|
940
|
-
if ("type" in
|
|
941
|
-
const input = { name:
|
|
941
|
+
if ("type" in record3 && record3.type === "company") {
|
|
942
|
+
const input = { name: record3.name };
|
|
942
943
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
943
|
-
if (
|
|
944
|
+
if (record3[key]) input[key] = record3[key];
|
|
944
945
|
}
|
|
945
946
|
return {
|
|
946
947
|
version: 1,
|
|
947
948
|
sourceId: fallbackSourceId,
|
|
948
|
-
sourceRecordId:
|
|
949
|
+
sourceRecordId: record3.hubRecordId,
|
|
949
950
|
type: "company",
|
|
950
951
|
input
|
|
951
952
|
};
|
|
@@ -953,28 +954,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
|
953
954
|
return {
|
|
954
955
|
version: 1,
|
|
955
956
|
sourceId: fallbackSourceId,
|
|
956
|
-
sourceRecordId:
|
|
957
|
+
sourceRecordId: record3.hubRecordId,
|
|
957
958
|
type: "person",
|
|
958
|
-
input: sharedPersonInput(
|
|
959
|
+
input: sharedPersonInput(record3)
|
|
959
960
|
};
|
|
960
961
|
}
|
|
961
|
-
function sharedRecordFromCrm(crm,
|
|
962
|
-
if (target.fields && !target.fields[
|
|
963
|
-
throw new Error(`${target.name} does not accept "${
|
|
962
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
963
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
964
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
964
965
|
}
|
|
965
|
-
const def = crm.type(
|
|
966
|
-
const fields = target.fields?.[
|
|
966
|
+
const def = crm.type(record3.type);
|
|
967
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
967
968
|
if (!fields) {
|
|
968
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
969
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
969
970
|
}
|
|
970
971
|
const nameField = def.nameField ?? "name";
|
|
971
972
|
const input = {};
|
|
972
973
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
973
|
-
const value =
|
|
974
|
+
const value = record3.fields?.[field];
|
|
974
975
|
if (value !== void 0) input[field] = value;
|
|
975
976
|
}
|
|
976
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
977
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
977
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
978
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
978
979
|
}
|
|
979
980
|
async function upsertPerson(deps, opts) {
|
|
980
981
|
const email = opts.email.toLowerCase();
|
|
@@ -988,11 +989,11 @@ async function upsertPerson(deps, opts) {
|
|
|
988
989
|
const created = await (0, import_crm2.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
989
990
|
return { recordId: created.id };
|
|
990
991
|
}
|
|
991
|
-
async function findSharedRecord(deps,
|
|
992
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
992
993
|
const structured = await (0, import_crm2.getRecordByOrigin)(
|
|
993
994
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
994
|
-
|
|
995
|
-
|
|
995
|
+
record3.sourceId,
|
|
996
|
+
record3.sourceRecordId
|
|
996
997
|
);
|
|
997
998
|
if (structured) return structured;
|
|
998
999
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1001,53 +1002,53 @@ async function findSharedRecord(deps, record2, tag) {
|
|
|
1001
1002
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1002
1003
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1003
1004
|
}
|
|
1004
|
-
const def = deps.crm.type(
|
|
1005
|
+
const def = deps.crm.type(record3.type);
|
|
1005
1006
|
const emailField = def.emailField;
|
|
1006
|
-
if (emailField && typeof
|
|
1007
|
-
const primaryEmail =
|
|
1007
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
1008
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
1008
1009
|
const found = await deps.db.query({
|
|
1009
|
-
crm_record: { $: { where: { type:
|
|
1010
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
1010
1011
|
});
|
|
1011
1012
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1012
1013
|
}
|
|
1013
|
-
const domain =
|
|
1014
|
+
const domain = record3.input.domain;
|
|
1014
1015
|
const domainSlot = def.fields.domain?.slot;
|
|
1015
1016
|
if (typeof domain === "string" && domainSlot) {
|
|
1016
1017
|
const found = await deps.db.query({
|
|
1017
|
-
crm_record: { $: { where: { type:
|
|
1018
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
1018
1019
|
});
|
|
1019
1020
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1020
1021
|
}
|
|
1021
1022
|
const nameField = def.nameField ?? "name";
|
|
1022
|
-
const name =
|
|
1023
|
-
if (
|
|
1023
|
+
const name = record3.input[nameField];
|
|
1024
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
1024
1025
|
const found = await deps.db.query({
|
|
1025
|
-
crm_record: { $: { where: { type:
|
|
1026
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
1026
1027
|
});
|
|
1027
1028
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1028
1029
|
}
|
|
1029
1030
|
return void 0;
|
|
1030
1031
|
}
|
|
1031
1032
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1032
|
-
const
|
|
1033
|
-
if (!
|
|
1033
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
1034
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
1034
1035
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1035
1036
|
}
|
|
1036
|
-
if (options.sourceId && options.sourceId !==
|
|
1037
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
1037
1038
|
throw new Error("signed sender does not match payload source");
|
|
1038
1039
|
}
|
|
1039
|
-
const tag = networkSourceTag(
|
|
1040
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
1040
1041
|
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1041
|
-
const existing = await findSharedRecord(deps,
|
|
1042
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
1042
1043
|
let recordId;
|
|
1043
1044
|
if (existing && typeof existing.id === "string") {
|
|
1044
|
-
await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input:
|
|
1045
|
+
await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record3.input });
|
|
1045
1046
|
recordId = existing.id;
|
|
1046
1047
|
} else {
|
|
1047
|
-
recordId = `network_${shortHash(`${
|
|
1048
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
1048
1049
|
await (0, import_crm2.createRecord)({ ...crmDeps3, newId: () => recordId }, {
|
|
1049
|
-
type:
|
|
1050
|
-
input:
|
|
1050
|
+
type: record3.type,
|
|
1051
|
+
input: record3.input,
|
|
1051
1052
|
mutationId: `share-create:${tag}`
|
|
1052
1053
|
});
|
|
1053
1054
|
}
|
|
@@ -1057,10 +1058,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1057
1058
|
);
|
|
1058
1059
|
await (0, import_crm2.upsertRecordOrigin)(crmDeps3, {
|
|
1059
1060
|
recordId,
|
|
1060
|
-
sourceId:
|
|
1061
|
-
sourceRecordId:
|
|
1061
|
+
sourceId: record3.sourceId,
|
|
1062
|
+
sourceRecordId: record3.sourceRecordId,
|
|
1062
1063
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
1063
|
-
payloadVersion:
|
|
1064
|
+
payloadVersion: record3.version
|
|
1064
1065
|
});
|
|
1065
1066
|
return { recordId };
|
|
1066
1067
|
}
|
|
@@ -1509,7 +1510,8 @@ function applicationSummary(app) {
|
|
|
1509
1510
|
meetingLink: app.meetingLink ?? null,
|
|
1510
1511
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
1511
1512
|
renewalAt: app.renewalAt ?? null,
|
|
1512
|
-
canceled: app.canceled === true
|
|
1513
|
+
canceled: app.canceled === true,
|
|
1514
|
+
tierId: app.tierId ?? null
|
|
1513
1515
|
};
|
|
1514
1516
|
}
|
|
1515
1517
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -1983,8 +1985,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
1983
1985
|
payload,
|
|
1984
1986
|
sourceId ? { sourceId } : {}
|
|
1985
1987
|
);
|
|
1986
|
-
const
|
|
1987
|
-
return json({ recordId, type:
|
|
1988
|
+
const record3 = normalizeSharedRecord(payload, sourceId);
|
|
1989
|
+
return json({ recordId, type: record3.type });
|
|
1988
1990
|
} catch (error) {
|
|
1989
1991
|
return json({
|
|
1990
1992
|
error: error instanceof Error ? error.message : "invalid shared record"
|
|
@@ -2024,9 +2026,9 @@ var MAX_BENEFITS = 24;
|
|
|
2024
2026
|
var MAX_MARKDOWN = 65536;
|
|
2025
2027
|
var MAX_LABEL = 160;
|
|
2026
2028
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2027
|
-
function rejectUnknownKeys(value,
|
|
2029
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
2028
2030
|
for (const key of Object.keys(value)) {
|
|
2029
|
-
if (!
|
|
2031
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2030
2032
|
}
|
|
2031
2033
|
}
|
|
2032
2034
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -2182,8 +2184,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
2182
2184
|
]);
|
|
2183
2185
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2184
2186
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2185
|
-
function unknownKeys(value,
|
|
2186
|
-
for (const key of Object.keys(value)) if (!
|
|
2187
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
2188
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2187
2189
|
}
|
|
2188
2190
|
function text2(value, path, errors, max = 65536) {
|
|
2189
2191
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -2268,10 +2270,10 @@ function policyOf(value, errors) {
|
|
|
2268
2270
|
errors.push("document.policy must be an object");
|
|
2269
2271
|
return void 0;
|
|
2270
2272
|
}
|
|
2271
|
-
const
|
|
2272
|
-
unknownKeys(input,
|
|
2273
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
2274
|
+
unknownKeys(input, allowed2, "document.policy", errors);
|
|
2273
2275
|
const result = {};
|
|
2274
|
-
for (const key of
|
|
2276
|
+
for (const key of allowed2) {
|
|
2275
2277
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
2276
2278
|
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
2277
2279
|
}
|
|
@@ -2410,9 +2412,589 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2410
2412
|
});
|
|
2411
2413
|
};
|
|
2412
2414
|
|
|
2413
|
-
// src/worker-routes-
|
|
2415
|
+
// src/worker-routes-member-content.ts
|
|
2414
2416
|
var import_db4 = require("@odla-ai/db");
|
|
2417
|
+
|
|
2418
|
+
// src/member-content-canonical.ts
|
|
2419
|
+
var encoder2 = new TextEncoder();
|
|
2420
|
+
function canonicalValue2(value) {
|
|
2421
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2422
|
+
return JSON.stringify(value);
|
|
2423
|
+
}
|
|
2424
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2425
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2426
|
+
const record3 = value;
|
|
2427
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2428
|
+
const item = record3[key];
|
|
2429
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2430
|
+
});
|
|
2431
|
+
return `{${entries.join(",")}}`;
|
|
2432
|
+
}
|
|
2433
|
+
function canonicalMemberContent(value) {
|
|
2434
|
+
return canonicalValue2(value);
|
|
2435
|
+
}
|
|
2436
|
+
async function memberContentDigest(document) {
|
|
2437
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2438
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// src/member-content-validation-helpers.ts
|
|
2442
|
+
var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2443
|
+
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2444
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2445
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2446
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
2447
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
2448
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
2449
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
2450
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
2451
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
2452
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
2453
|
+
var MAX_PAGES = 100;
|
|
2454
|
+
var MAX_MARKDOWN2 = 65536;
|
|
2455
|
+
var MAX_TEXT = 240;
|
|
2456
|
+
var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2457
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
2458
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2459
|
+
}
|
|
2460
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
2461
|
+
if (value === void 0) {
|
|
2462
|
+
if (required) errors.push(`${path} is required`);
|
|
2463
|
+
return void 0;
|
|
2464
|
+
}
|
|
2465
|
+
if (typeof value !== "string") {
|
|
2466
|
+
errors.push(`${path} must be a string`);
|
|
2467
|
+
return void 0;
|
|
2468
|
+
}
|
|
2469
|
+
const valueText = value.trim();
|
|
2470
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
2471
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2472
|
+
return valueText;
|
|
2473
|
+
}
|
|
2474
|
+
function offerValue(value, path, errors, tiers, audience) {
|
|
2475
|
+
const raw = record2(value);
|
|
2476
|
+
if (!raw) {
|
|
2477
|
+
errors.push(`${path} must be an object`);
|
|
2478
|
+
return void 0;
|
|
2479
|
+
}
|
|
2480
|
+
unknownKeys2(raw, OFFER_KEYS, path, errors);
|
|
2481
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2482
|
+
if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2483
|
+
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2484
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2485
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
2486
|
+
}
|
|
2487
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
2488
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
|
|
2489
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2490
|
+
return {
|
|
2491
|
+
tierId,
|
|
2492
|
+
...label ? { label } : {},
|
|
2493
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
2494
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// src/member-content-validation.ts
|
|
2499
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
2500
|
+
const errors = [];
|
|
2501
|
+
const root = record2(input);
|
|
2502
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2503
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
2504
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2505
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2506
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
2507
|
+
const sourceRaw = record2(root.source);
|
|
2508
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
2509
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2510
|
+
else {
|
|
2511
|
+
unknownKeys2(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
2512
|
+
const type = sourceRaw.type;
|
|
2513
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2514
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2515
|
+
if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2516
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2517
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2518
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
2519
|
+
}
|
|
2520
|
+
let target;
|
|
2521
|
+
if (root.target !== void 0) {
|
|
2522
|
+
const targetRaw = record2(root.target);
|
|
2523
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
2524
|
+
else {
|
|
2525
|
+
unknownKeys2(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
2526
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
2527
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
2528
|
+
target = { chapterId, environment };
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
2532
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
2533
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
2534
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
2535
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2536
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2537
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2538
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2539
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2540
|
+
const pages = [];
|
|
2541
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2542
|
+
const path = `document.pages[${index}]`;
|
|
2543
|
+
const raw = record2(value);
|
|
2544
|
+
if (!raw) {
|
|
2545
|
+
errors.push(`${path} must be an object`);
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
unknownKeys2(raw, PAGE_KEYS, path, errors);
|
|
2549
|
+
const id = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2550
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2551
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2552
|
+
if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2553
|
+
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2554
|
+
if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
|
|
2555
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2556
|
+
ids.add(id);
|
|
2557
|
+
slugs.add(slug);
|
|
2558
|
+
const navRaw = record2(raw.navigation);
|
|
2559
|
+
let navigation = { section: "General", order: index };
|
|
2560
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2561
|
+
else {
|
|
2562
|
+
unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2563
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2564
|
+
const order = navRaw.order;
|
|
2565
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2566
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2567
|
+
}
|
|
2568
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2569
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2570
|
+
}
|
|
2571
|
+
const audienceRaw = record2(raw.audience);
|
|
2572
|
+
let audience = { kind: "all-members" };
|
|
2573
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2574
|
+
else {
|
|
2575
|
+
unknownKeys2(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2576
|
+
if (audienceRaw.kind === "all-members") {
|
|
2577
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2578
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2579
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2580
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2581
|
+
}
|
|
2582
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2583
|
+
const normalized = [];
|
|
2584
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2585
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2586
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2587
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2588
|
+
normalized.push(tier);
|
|
2589
|
+
}
|
|
2590
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2591
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2592
|
+
}
|
|
2593
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2594
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
|
|
2595
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
|
|
2596
|
+
let upsell;
|
|
2597
|
+
if (raw.upsell !== void 0) {
|
|
2598
|
+
const upsellRaw = record2(raw.upsell);
|
|
2599
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2600
|
+
else {
|
|
2601
|
+
unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2602
|
+
const owner = upsellRaw.owner;
|
|
2603
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2604
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2605
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2606
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2607
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2608
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2609
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2613
|
+
}
|
|
2614
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2615
|
+
for (const [index, page] of pages.entries()) {
|
|
2616
|
+
const parentId = page.navigation.parentId;
|
|
2617
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2618
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2619
|
+
}
|
|
2620
|
+
for (const page of pages) {
|
|
2621
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2622
|
+
let parentId = page.navigation.parentId;
|
|
2623
|
+
while (parentId) {
|
|
2624
|
+
if (seen.has(parentId)) {
|
|
2625
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2626
|
+
break;
|
|
2627
|
+
}
|
|
2628
|
+
seen.add(parentId);
|
|
2629
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
let upsellOverrides;
|
|
2633
|
+
if (root.upsellOverrides !== void 0) {
|
|
2634
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2635
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2636
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2637
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2638
|
+
upsellOverrides = [];
|
|
2639
|
+
for (const [index, value] of values.entries()) {
|
|
2640
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2641
|
+
const raw = record2(value);
|
|
2642
|
+
if (!raw) {
|
|
2643
|
+
errors.push(`${path} must be an object`);
|
|
2644
|
+
continue;
|
|
2645
|
+
}
|
|
2646
|
+
unknownKeys2(raw, OVERRIDE_KEYS, path, errors);
|
|
2647
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2648
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2649
|
+
seen.add(pageId);
|
|
2650
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2651
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2652
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2653
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2654
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (errors.length) return { ok: false, errors };
|
|
2658
|
+
return { ok: true, value: {
|
|
2659
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2660
|
+
revision: Number(root.revision),
|
|
2661
|
+
generatedAt: Number(root.generatedAt),
|
|
2662
|
+
source,
|
|
2663
|
+
...target ? { target } : {},
|
|
2664
|
+
pages,
|
|
2665
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2666
|
+
} };
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// src/member-content.ts
|
|
2670
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2671
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2672
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2673
|
+
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);
|
|
2674
|
+
function composeMemberContent(parent, chapter) {
|
|
2675
|
+
const pages = [];
|
|
2676
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2677
|
+
for (const page of parent?.pages ?? []) {
|
|
2678
|
+
const effective = { ...page, source: parent.source };
|
|
2679
|
+
parentPages.set(page.id, effective);
|
|
2680
|
+
pages.push(effective);
|
|
2681
|
+
}
|
|
2682
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2683
|
+
const page = parentPages.get(override.pageId);
|
|
2684
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2685
|
+
}
|
|
2686
|
+
for (const page of chapter?.pages ?? []) {
|
|
2687
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2688
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2689
|
+
}
|
|
2690
|
+
pages.push({ ...page, source: chapter.source });
|
|
2691
|
+
}
|
|
2692
|
+
return {
|
|
2693
|
+
pages: pages.sort(pageOrder),
|
|
2694
|
+
parentRevision: parent?.revision ?? null,
|
|
2695
|
+
chapterRevision: chapter?.revision ?? null
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2699
|
+
function projectMemberContent(content, viewer, tiers) {
|
|
2700
|
+
const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
2701
|
+
return {
|
|
2702
|
+
viewer: { ...viewer },
|
|
2703
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2704
|
+
pages: content.pages.map((page) => {
|
|
2705
|
+
const canRead = allowed(page, viewer);
|
|
2706
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2707
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2708
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2709
|
+
return {
|
|
2710
|
+
id: page.id,
|
|
2711
|
+
slug: page.slug,
|
|
2712
|
+
title: page.title,
|
|
2713
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2714
|
+
navigation: page.navigation,
|
|
2715
|
+
audience: page.audience,
|
|
2716
|
+
source: page.source,
|
|
2717
|
+
access: canRead ? "allowed" : "locked",
|
|
2718
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2719
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2720
|
+
...upsell ? { upsell } : {}
|
|
2721
|
+
};
|
|
2722
|
+
})
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/member-content-store.ts
|
|
2727
|
+
var control = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2728
|
+
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2729
|
+
async function exists(db, ns, where) {
|
|
2730
|
+
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2731
|
+
const rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2732
|
+
return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2733
|
+
}
|
|
2734
|
+
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2735
|
+
const where = { targetId, environment, active: true };
|
|
2736
|
+
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2737
|
+
const rows4 = (await db.query({ memberContentControls: { $: {
|
|
2738
|
+
where,
|
|
2739
|
+
order: { revision: "desc" },
|
|
2740
|
+
limit: 1
|
|
2741
|
+
} } })).memberContentControls;
|
|
2742
|
+
return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2743
|
+
}
|
|
2744
|
+
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2745
|
+
const where = { targetId, environment };
|
|
2746
|
+
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2747
|
+
const rows4 = (await db.query({ memberContentControls: { $: {
|
|
2748
|
+
where,
|
|
2749
|
+
order: { revision: "desc" },
|
|
2750
|
+
limit: 1
|
|
2751
|
+
} } })).memberContentControls;
|
|
2752
|
+
return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2753
|
+
}
|
|
2754
|
+
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2755
|
+
const id = `member-content-local:${environment}`;
|
|
2756
|
+
const rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2757
|
+
return local(Array.isArray(rows4) ? rows4[0] : void 0);
|
|
2758
|
+
}
|
|
2759
|
+
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2760
|
+
const { document, digest } = envelope;
|
|
2761
|
+
const target = document.target;
|
|
2762
|
+
const id = `member-content-control:${target.environment}:${document.revision}`;
|
|
2763
|
+
const ops = [];
|
|
2764
|
+
if (previous?.active) {
|
|
2765
|
+
ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
|
|
2766
|
+
}
|
|
2767
|
+
ops.push({ t: "update", ns: "memberContentControls", id, attrs: {
|
|
2768
|
+
id,
|
|
2769
|
+
sourceId: document.source.appId,
|
|
2770
|
+
targetId: target.chapterId,
|
|
2771
|
+
environment: target.environment,
|
|
2772
|
+
revision: document.revision,
|
|
2773
|
+
digest,
|
|
2774
|
+
document,
|
|
2775
|
+
active: true,
|
|
2776
|
+
receivedAt
|
|
2777
|
+
} });
|
|
2778
|
+
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
|
|
2779
|
+
}
|
|
2780
|
+
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
2781
|
+
const id = `member-content-local:${environment}`;
|
|
2782
|
+
const current = await loadLocalMemberContent(db, document.source.appId, environment);
|
|
2783
|
+
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
|
|
2784
|
+
const row = {
|
|
2785
|
+
id,
|
|
2786
|
+
chapterId: document.source.appId,
|
|
2787
|
+
environment,
|
|
2788
|
+
revision: document.revision,
|
|
2789
|
+
document,
|
|
2790
|
+
updatedAt,
|
|
2791
|
+
...updatedBy ? { updatedBy } : {}
|
|
2792
|
+
};
|
|
2793
|
+
await db.transact([{ t: "update", ns: "memberContentLocal", id, attrs: row }], {
|
|
2794
|
+
mutationId: `member-content-local:${environment}:${document.revision}`,
|
|
2795
|
+
guards: current ? [{ ns: "memberContentLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id, exists: false }]
|
|
2796
|
+
});
|
|
2797
|
+
return row;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// src/worker-routes-member-content.ts
|
|
2801
|
+
var ADMIN_PATH = "/api/admin/member-content";
|
|
2802
|
+
var BODY_CAP = 1e6;
|
|
2415
2803
|
var reject2 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
2804
|
+
async function groupAndTiers(db, chapterId) {
|
|
2805
|
+
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
2806
|
+
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
2807
|
+
if (!group) return { group: null, tiers: [] };
|
|
2808
|
+
return { group, tiers: await loadTiers(db, group) };
|
|
2809
|
+
}
|
|
2810
|
+
async function memberTierId(db, chapterId, email) {
|
|
2811
|
+
if (!email) return null;
|
|
2812
|
+
let rows4 = (await db.query({ applications: { $: {
|
|
2813
|
+
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2814
|
+
limit: 20
|
|
2815
|
+
} } })).applications;
|
|
2816
|
+
if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2817
|
+
const latest = Array.isArray(rows4) ? [...rows4].filter((row) => String(row.groupId ?? chapterId) === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
|
|
2818
|
+
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2819
|
+
}
|
|
2820
|
+
async function activeDocuments(db, chapterId, environment) {
|
|
2821
|
+
const [parent, chapter] = await Promise.all([
|
|
2822
|
+
loadActiveMemberContentControl(db, chapterId, environment),
|
|
2823
|
+
loadLocalMemberContent(db, chapterId, environment)
|
|
2824
|
+
]);
|
|
2825
|
+
return { parent, chapter };
|
|
2826
|
+
}
|
|
2827
|
+
var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
2828
|
+
if (req.method !== "POST" || url.pathname !== MEMBER_CONTENT_CONTROL_PATH) return null;
|
|
2829
|
+
const settings = ctx.chapter.memberContent?.parent;
|
|
2830
|
+
if (!settings) return json({ error: "not found" }, 404);
|
|
2831
|
+
const db = ctx.makeDb(env);
|
|
2832
|
+
const secret = await getVaultSecret(db, settings.secretName);
|
|
2833
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2834
|
+
const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret, senders: [settings.sourceId] });
|
|
2835
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2836
|
+
let input;
|
|
2837
|
+
try {
|
|
2838
|
+
input = JSON.parse(verified.body);
|
|
2839
|
+
} catch {
|
|
2840
|
+
return reject2("invalid_document", 400, ["request body must be JSON"]);
|
|
2841
|
+
}
|
|
2842
|
+
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
2843
|
+
const initial = validateMemberContentDocument(input.document, {
|
|
2844
|
+
sourceType: "parent"
|
|
2845
|
+
});
|
|
2846
|
+
if (!initial.ok) return reject2("invalid_document", 400, initial.errors);
|
|
2847
|
+
const document = initial.value;
|
|
2848
|
+
if (document.source.appId !== settings.sourceId) return reject2("wrong_source", 403);
|
|
2849
|
+
if (document.target?.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
|
|
2850
|
+
if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
|
|
2851
|
+
const { tiers } = await groupAndTiers(db, ctx.chapter.id);
|
|
2852
|
+
const checked = validateMemberContentDocument(document, {
|
|
2853
|
+
sourceType: "parent",
|
|
2854
|
+
sourceAppId: settings.sourceId,
|
|
2855
|
+
tiers
|
|
2856
|
+
});
|
|
2857
|
+
if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
|
|
2858
|
+
const local2 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2859
|
+
if (local2) {
|
|
2860
|
+
const localCheck = validateMemberContentDocument(local2.document, {
|
|
2861
|
+
sourceType: "chapter",
|
|
2862
|
+
sourceAppId: ctx.chapter.id,
|
|
2863
|
+
tiers,
|
|
2864
|
+
parentPages: document.pages
|
|
2865
|
+
});
|
|
2866
|
+
if (!localCheck.ok) {
|
|
2867
|
+
return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (!digest || await memberContentDigest(document) !== digest) return reject2("invalid_digest", 400);
|
|
2871
|
+
const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2872
|
+
if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
|
|
2873
|
+
if (latest && document.revision === latest.revision) {
|
|
2874
|
+
if (latest.digest !== digest) return reject2("conflicting_revision", 409);
|
|
2875
|
+
return json({
|
|
2876
|
+
ok: true,
|
|
2877
|
+
target: document.target,
|
|
2878
|
+
revision: latest.revision,
|
|
2879
|
+
digest: latest.digest,
|
|
2880
|
+
status: "replayed",
|
|
2881
|
+
acknowledgedAt: latest.receivedAt
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
const receivedAt = Date.now();
|
|
2885
|
+
try {
|
|
2886
|
+
await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
|
|
2887
|
+
} catch {
|
|
2888
|
+
return reject2("materialization_failed", 503);
|
|
2889
|
+
}
|
|
2890
|
+
return json({
|
|
2891
|
+
ok: true,
|
|
2892
|
+
target: document.target,
|
|
2893
|
+
revision: document.revision,
|
|
2894
|
+
digest,
|
|
2895
|
+
status: "accepted",
|
|
2896
|
+
acknowledgedAt: receivedAt
|
|
2897
|
+
});
|
|
2898
|
+
};
|
|
2899
|
+
var handleMemberContent = async (req, url, env, ctx) => {
|
|
2900
|
+
if (req.method !== "GET" || url.pathname !== MEMBER_CONTENT_PATH) return null;
|
|
2901
|
+
if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
|
|
2902
|
+
const user = await ctx.verifyUser(req, env);
|
|
2903
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
2904
|
+
const db = ctx.makeDb(env);
|
|
2905
|
+
const role = await ctx.roleFor(db, user);
|
|
2906
|
+
const admin = await ctx.isAdmin(db, user);
|
|
2907
|
+
const roleIndex = ctx.chapter.auth.ladder.indexOf(role);
|
|
2908
|
+
const memberIndex = Math.max(0, ctx.chapter.auth.ladder.length - 2);
|
|
2909
|
+
if (!admin && roleIndex < memberIndex) return json({ error: "member access required" }, 403);
|
|
2910
|
+
const [{ parent, chapter }, { group, tiers }, tierId] = await Promise.all([
|
|
2911
|
+
activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
|
|
2912
|
+
groupAndTiers(db, ctx.chapter.id),
|
|
2913
|
+
memberTierId(db, ctx.chapter.id, user.email)
|
|
2914
|
+
]);
|
|
2915
|
+
const effective = composeMemberContent(parent?.document, chapter?.document);
|
|
2916
|
+
let offerable = tiers;
|
|
2917
|
+
if (group) {
|
|
2918
|
+
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
2919
|
+
offerable = await loadOfferableTiers(db, group, Boolean(stripeSecret));
|
|
2920
|
+
}
|
|
2921
|
+
return json(projectMemberContent(effective, { tierId, admin }, offerable));
|
|
2922
|
+
};
|
|
2923
|
+
var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
2924
|
+
if (url.pathname !== ADMIN_PATH) return null;
|
|
2925
|
+
if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
|
|
2926
|
+
const user = await ctx.verifyUser(req, env);
|
|
2927
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
2928
|
+
const db = ctx.makeDb(env);
|
|
2929
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
2930
|
+
const [{ parent, chapter }, { tiers }] = await Promise.all([
|
|
2931
|
+
activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
|
|
2932
|
+
groupAndTiers(db, ctx.chapter.id)
|
|
2933
|
+
]);
|
|
2934
|
+
if (req.method === "GET") {
|
|
2935
|
+
const effective = composeMemberContent(parent?.document, chapter?.document);
|
|
2936
|
+
return json({
|
|
2937
|
+
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
2938
|
+
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
2939
|
+
effective,
|
|
2940
|
+
tiers: tiers.map(({ id, name, priceCents, active }) => ({ id, name, priceCents, active })),
|
|
2941
|
+
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
2945
|
+
if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
2946
|
+
const raw = await req.text();
|
|
2947
|
+
if (raw.length > BODY_CAP) return json({ error: "request body too large" }, 413);
|
|
2948
|
+
let input;
|
|
2949
|
+
try {
|
|
2950
|
+
input = JSON.parse(raw);
|
|
2951
|
+
} catch {
|
|
2952
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
2953
|
+
}
|
|
2954
|
+
const expectedRevision = Number(input.expectedRevision);
|
|
2955
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
|
|
2956
|
+
return json({ error: "expectedRevision must be a non-negative integer" }, 400);
|
|
2957
|
+
}
|
|
2958
|
+
const now = Date.now();
|
|
2959
|
+
const documentInput = withLocalAuthority(input.document, expectedRevision + 1, now, ctx.chapter.id);
|
|
2960
|
+
const checked = validateMemberContentDocument(documentInput, {
|
|
2961
|
+
sourceType: "chapter",
|
|
2962
|
+
sourceAppId: ctx.chapter.id,
|
|
2963
|
+
tiers,
|
|
2964
|
+
parentPages: parent?.document.pages ?? []
|
|
2965
|
+
});
|
|
2966
|
+
if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
|
|
2967
|
+
try {
|
|
2968
|
+
const saved = await saveLocalMemberContent(
|
|
2969
|
+
db,
|
|
2970
|
+
checked.value,
|
|
2971
|
+
env.ODLA_ENV,
|
|
2972
|
+
expectedRevision,
|
|
2973
|
+
now,
|
|
2974
|
+
user.email ?? user.userId
|
|
2975
|
+
);
|
|
2976
|
+
return json({ ok: true, revision: saved.revision, document: saved.document });
|
|
2977
|
+
} catch (error) {
|
|
2978
|
+
if (error instanceof Error && error.message === "member_content_revision_conflict") {
|
|
2979
|
+
return json({ error: "member content changed; reload before saving" }, 409);
|
|
2980
|
+
}
|
|
2981
|
+
return json({ error: "member content could not be saved" }, 503);
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
function withLocalAuthority(value, revision, generatedAt, appId) {
|
|
2985
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
2986
|
+
return {
|
|
2987
|
+
...value,
|
|
2988
|
+
schemaVersion: 1,
|
|
2989
|
+
revision,
|
|
2990
|
+
generatedAt,
|
|
2991
|
+
source: { type: "chapter", appId }
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// src/worker-routes-admission-grants.ts
|
|
2996
|
+
var import_db5 = require("@odla-ai/db");
|
|
2997
|
+
var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
2416
2998
|
var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
2417
2999
|
if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
|
|
2418
3000
|
const authority = ctx.chapter.signupControl;
|
|
@@ -2420,26 +3002,26 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
2420
3002
|
const db = ctx.makeDb(env);
|
|
2421
3003
|
const secret = await getVaultSecret(db, authority.secretName);
|
|
2422
3004
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2423
|
-
const verified = await (0,
|
|
3005
|
+
const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
|
|
2424
3006
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2425
3007
|
let input;
|
|
2426
3008
|
try {
|
|
2427
3009
|
input = JSON.parse(verified.body);
|
|
2428
3010
|
} catch {
|
|
2429
|
-
return
|
|
3011
|
+
return reject3("invalid_document", 400, ["request body must be JSON"]);
|
|
2430
3012
|
}
|
|
2431
3013
|
const digest = typeof input.digest === "string" ? input.digest : "";
|
|
2432
3014
|
const checked = validateAdmissionGrantDocument(input.document);
|
|
2433
|
-
if (!checked.ok) return
|
|
3015
|
+
if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
|
|
2434
3016
|
const document = checked.value;
|
|
2435
|
-
if (document.source.appId !== authority.sourceId) return
|
|
2436
|
-
if (document.target.chapterId !== ctx.chapter.id) return
|
|
2437
|
-
if (document.target.environment !== env.ODLA_ENV) return
|
|
2438
|
-
if (!digest || await signupControlDigest(document) !== digest) return
|
|
3017
|
+
if (document.source.appId !== authority.sourceId) return reject3("wrong_source", 403);
|
|
3018
|
+
if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
|
|
3019
|
+
if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
|
|
3020
|
+
if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
|
|
2439
3021
|
const latest = await loadLatestAdmissionRevision(db, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
|
|
2440
|
-
if (latest && document.revision < latest.revision) return
|
|
3022
|
+
if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
|
|
2441
3023
|
if (latest && document.revision === latest.revision) {
|
|
2442
|
-
if (latest.digest !== digest) return
|
|
3024
|
+
if (latest.digest !== digest) return reject3("conflicting_revision", 409);
|
|
2443
3025
|
return json({
|
|
2444
3026
|
ok: true,
|
|
2445
3027
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -2453,7 +3035,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
2453
3035
|
try {
|
|
2454
3036
|
await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
|
|
2455
3037
|
} catch {
|
|
2456
|
-
return
|
|
3038
|
+
return reject3("materialization_failed", 503);
|
|
2457
3039
|
}
|
|
2458
3040
|
return json({
|
|
2459
3041
|
ok: true,
|
|
@@ -3281,8 +3863,8 @@ async function startSubscription(req, env, ctx) {
|
|
|
3281
3863
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
3282
3864
|
if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
|
|
3283
3865
|
if (typeof app.giftOfferId === "string") {
|
|
3284
|
-
const
|
|
3285
|
-
const offer = findGiftOffer(
|
|
3866
|
+
const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
3867
|
+
const offer = findGiftOffer(control2?.document.giftOffers, app.giftOfferId, Date.now());
|
|
3286
3868
|
if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
|
|
3287
3869
|
return json({ error: "gift offer is unavailable" }, 409);
|
|
3288
3870
|
}
|
|
@@ -3441,14 +4023,14 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
3441
4023
|
if (req.method === "GET" && url.pathname === "/api/gifts") {
|
|
3442
4024
|
const user = await userFor(req, env, ctx);
|
|
3443
4025
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3444
|
-
const
|
|
4026
|
+
const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
3445
4027
|
const found = await db.query({ giftClaims: { $: { where: {
|
|
3446
4028
|
purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
|
|
3447
4029
|
chapterId: ctx.chapter.id
|
|
3448
4030
|
}, limit: 200 } } });
|
|
3449
4031
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
3450
4032
|
return json({
|
|
3451
|
-
offers: publicGiftOffers(
|
|
4033
|
+
offers: publicGiftOffers(control2?.document.giftOffers, Date.now()),
|
|
3452
4034
|
claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
|
|
3453
4035
|
refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
|
|
3454
4036
|
});
|
|
@@ -3464,8 +4046,8 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
3464
4046
|
}
|
|
3465
4047
|
const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
|
|
3466
4048
|
if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
|
|
3467
|
-
const
|
|
3468
|
-
const offer = findGiftOffer(
|
|
4049
|
+
const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4050
|
+
const offer = findGiftOffer(control2?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
|
|
3469
4051
|
if (!offer) return json({ error: "gift offer is unavailable" }, 409);
|
|
3470
4052
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
3471
4053
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
@@ -4386,7 +4968,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
4386
4968
|
|
|
4387
4969
|
// src/worker-routes-network.ts
|
|
4388
4970
|
var import_crm6 = require("@odla-ai/crm");
|
|
4389
|
-
var
|
|
4971
|
+
var import_db7 = require("@odla-ai/db");
|
|
4390
4972
|
|
|
4391
4973
|
// src/worker-network-fetch.ts
|
|
4392
4974
|
function targetFetcher(env, target) {
|
|
@@ -4404,8 +4986,8 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
4404
4986
|
|
|
4405
4987
|
// src/worker-network-push.ts
|
|
4406
4988
|
var import_crm5 = require("@odla-ai/crm");
|
|
4407
|
-
var
|
|
4408
|
-
async function pushNetworkRecord(db, env, ctx, target,
|
|
4989
|
+
var import_db6 = require("@odla-ai/db");
|
|
4990
|
+
async function pushNetworkRecord(db, env, ctx, target, record3) {
|
|
4409
4991
|
const secret = await getVaultSecret(db, target.secretName);
|
|
4410
4992
|
const crmDeps3 = {
|
|
4411
4993
|
crm: ctx.chapter.crm,
|
|
@@ -4416,7 +4998,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
|
|
|
4416
4998
|
if (!secret) {
|
|
4417
4999
|
const error = `vault secret "${target.secretName}" is missing`;
|
|
4418
5000
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
4419
|
-
recordId:
|
|
5001
|
+
recordId: record3.id,
|
|
4420
5002
|
targetId: target.id,
|
|
4421
5003
|
status: "failed",
|
|
4422
5004
|
payloadVersion: 2,
|
|
@@ -4426,7 +5008,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
|
|
|
4426
5008
|
}
|
|
4427
5009
|
let payload;
|
|
4428
5010
|
try {
|
|
4429
|
-
payload = sharedRecordFromCrm(ctx.chapter.crm,
|
|
5011
|
+
payload = sharedRecordFromCrm(ctx.chapter.crm, record3, target, ctx.chapter.id);
|
|
4430
5012
|
} catch (err) {
|
|
4431
5013
|
return {
|
|
4432
5014
|
id: target.id,
|
|
@@ -4438,7 +5020,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
|
|
|
4438
5020
|
const payloadBody = JSON.stringify(payload);
|
|
4439
5021
|
const destination = new URL("/api/network/shared", target.url);
|
|
4440
5022
|
try {
|
|
4441
|
-
const signed = await (0,
|
|
5023
|
+
const signed = await (0, import_db6.signFederatedRequest)({
|
|
4442
5024
|
secret,
|
|
4443
5025
|
sender: ctx.chapter.id,
|
|
4444
5026
|
method: "POST",
|
|
@@ -4455,7 +5037,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
|
|
|
4455
5037
|
if (!res.ok) {
|
|
4456
5038
|
const error = responseBody.error ?? "follower rejected the record";
|
|
4457
5039
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
4458
|
-
recordId:
|
|
5040
|
+
recordId: record3.id,
|
|
4459
5041
|
targetId: target.id,
|
|
4460
5042
|
status: "failed",
|
|
4461
5043
|
payloadVersion: 2,
|
|
@@ -4470,7 +5052,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
|
|
|
4470
5052
|
};
|
|
4471
5053
|
}
|
|
4472
5054
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
4473
|
-
recordId:
|
|
5055
|
+
recordId: record3.id,
|
|
4474
5056
|
targetId: target.id,
|
|
4475
5057
|
status: "delivered",
|
|
4476
5058
|
payloadVersion: 2,
|
|
@@ -4486,7 +5068,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
|
|
|
4486
5068
|
} catch (err) {
|
|
4487
5069
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
4488
5070
|
await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
|
|
4489
|
-
recordId:
|
|
5071
|
+
recordId: record3.id,
|
|
4490
5072
|
targetId: target.id,
|
|
4491
5073
|
status: "failed",
|
|
4492
5074
|
payloadVersion: 2,
|
|
@@ -4523,7 +5105,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
4523
5105
|
const db = ctx.makeDb(env);
|
|
4524
5106
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
4525
5107
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
4526
|
-
const verified = await (0,
|
|
5108
|
+
const verified = await (0, import_db7.verifyFederatedRequest)(req, { secret });
|
|
4527
5109
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
4528
5110
|
const types = await Promise.all(
|
|
4529
5111
|
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
@@ -4541,7 +5123,7 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
4541
5123
|
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
4542
5124
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
4543
5125
|
try {
|
|
4544
|
-
const headers = await (0,
|
|
5126
|
+
const headers = await (0, import_db7.signFederatedRequest)({
|
|
4545
5127
|
secret,
|
|
4546
5128
|
sender: ctx.chapter.id,
|
|
4547
5129
|
method: "GET",
|
|
@@ -4624,17 +5206,17 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
4624
5206
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
4625
5207
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
4626
5208
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
4627
|
-
const
|
|
4628
|
-
if (!
|
|
5209
|
+
const record3 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
5210
|
+
if (!record3) return json({ error: "record not found" }, 404);
|
|
4629
5211
|
const results = await Promise.all(
|
|
4630
|
-
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target,
|
|
5212
|
+
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record3))
|
|
4631
5213
|
);
|
|
4632
5214
|
return json({ ok: results.every((result) => result.ok), results });
|
|
4633
5215
|
};
|
|
4634
5216
|
|
|
4635
5217
|
// src/worker-routes-network-records.ts
|
|
4636
5218
|
var import_crm7 = require("@odla-ai/crm");
|
|
4637
|
-
var
|
|
5219
|
+
var import_db8 = require("@odla-ai/db");
|
|
4638
5220
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
4639
5221
|
var MAX_PAGE = 50;
|
|
4640
5222
|
var MAX_OFFSET = 1e4;
|
|
@@ -4652,18 +5234,18 @@ function integer(value, fallback, min, max) {
|
|
|
4652
5234
|
const parsed = Number(value);
|
|
4653
5235
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
4654
5236
|
}
|
|
4655
|
-
function projectRecord(
|
|
5237
|
+
function projectRecord(record3, allowed2) {
|
|
4656
5238
|
const fields = Object.fromEntries(
|
|
4657
|
-
|
|
5239
|
+
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record3.fields, name) ? [[name, record3.fields[name]]] : [])
|
|
4658
5240
|
);
|
|
4659
5241
|
return {
|
|
4660
|
-
id:
|
|
4661
|
-
type:
|
|
4662
|
-
name:
|
|
4663
|
-
...
|
|
5242
|
+
id: record3.id,
|
|
5243
|
+
type: record3.type,
|
|
5244
|
+
name: record3.name,
|
|
5245
|
+
...record3.stage ? { stage: record3.stage } : {},
|
|
4664
5246
|
fields,
|
|
4665
|
-
createdAt:
|
|
4666
|
-
updatedAt:
|
|
5247
|
+
createdAt: record3.createdAt,
|
|
5248
|
+
updatedAt: record3.updatedAt
|
|
4667
5249
|
};
|
|
4668
5250
|
}
|
|
4669
5251
|
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
@@ -4672,15 +5254,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
4672
5254
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
4673
5255
|
const readers = ctx.chapter.network.readers;
|
|
4674
5256
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
4675
|
-
const verified = await (0,
|
|
5257
|
+
const verified = await (0, import_db8.verifyFederatedRequest)(req, {
|
|
4676
5258
|
secret,
|
|
4677
5259
|
senders: readers.map((reader2) => reader2.id)
|
|
4678
5260
|
});
|
|
4679
5261
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
4680
5262
|
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
4681
5263
|
const type = url.searchParams.get("type") ?? "";
|
|
4682
|
-
const
|
|
4683
|
-
if (!IDENTIFIER.test(type) || !
|
|
5264
|
+
const allowed2 = reader?.fields[type];
|
|
5265
|
+
if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
|
|
4684
5266
|
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
4685
5267
|
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
4686
5268
|
const search = (url.searchParams.get("search") ?? "").trim();
|
|
@@ -4701,7 +5283,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
4701
5283
|
version: 1,
|
|
4702
5284
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
4703
5285
|
type,
|
|
4704
|
-
records: page.records.map((
|
|
5286
|
+
records: page.records.map((record3) => projectRecord(record3, allowed2)),
|
|
4705
5287
|
total: page.total,
|
|
4706
5288
|
limit: page.limit,
|
|
4707
5289
|
offset: page.offset
|
|
@@ -4717,7 +5299,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
4717
5299
|
const destination = new URL("/api/network/records", target.url);
|
|
4718
5300
|
destination.search = query.toString();
|
|
4719
5301
|
try {
|
|
4720
|
-
const headers = await (0,
|
|
5302
|
+
const headers = await (0, import_db8.signFederatedRequest)({
|
|
4721
5303
|
secret,
|
|
4722
5304
|
sender: ctx.chapter.id,
|
|
4723
5305
|
method: "GET",
|
|
@@ -4852,7 +5434,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
4852
5434
|
|
|
4853
5435
|
// src/worker-routes-network-shared-notes.ts
|
|
4854
5436
|
var import_crm8 = require("@odla-ai/crm");
|
|
4855
|
-
var
|
|
5437
|
+
var import_db9 = require("@odla-ai/db");
|
|
4856
5438
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
4857
5439
|
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
4858
5440
|
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -4911,7 +5493,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
4911
5493
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
4912
5494
|
const readers = ctx.chapter.network.readers;
|
|
4913
5495
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
4914
|
-
const verified = await (0,
|
|
5496
|
+
const verified = await (0, import_db9.verifyFederatedRequest)(req, {
|
|
4915
5497
|
secret,
|
|
4916
5498
|
senders: readers.map((reader2) => reader2.id)
|
|
4917
5499
|
});
|
|
@@ -4922,8 +5504,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
4922
5504
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
4923
5505
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
4924
5506
|
}
|
|
4925
|
-
const
|
|
4926
|
-
if (!
|
|
5507
|
+
const record3 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
5508
|
+
if (!record3 || record3.type !== recordType) return json({ error: "record not found" }, 404);
|
|
4927
5509
|
if (req.method === "GET") {
|
|
4928
5510
|
return json({
|
|
4929
5511
|
version: 1,
|
|
@@ -5008,7 +5590,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5008
5590
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
5009
5591
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
5010
5592
|
try {
|
|
5011
|
-
const headers = await (0,
|
|
5593
|
+
const headers = await (0, import_db9.signFederatedRequest)({
|
|
5012
5594
|
secret,
|
|
5013
5595
|
sender: ctx.chapter.id,
|
|
5014
5596
|
method: req.method,
|
|
@@ -5077,15 +5659,15 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
5077
5659
|
if (submissionId && !SUBMISSION.test(submissionId)) {
|
|
5078
5660
|
return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
|
|
5079
5661
|
}
|
|
5080
|
-
const
|
|
5081
|
-
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !
|
|
5662
|
+
const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
|
|
5663
|
+
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
|
|
5082
5664
|
if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
|
|
5083
5665
|
for (const field of formation.required) {
|
|
5084
5666
|
const value = body[field];
|
|
5085
5667
|
if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
|
|
5086
5668
|
}
|
|
5087
5669
|
const input = {};
|
|
5088
|
-
for (const field of
|
|
5670
|
+
for (const field of allowed2) {
|
|
5089
5671
|
const value = body[field];
|
|
5090
5672
|
if (value === void 0) continue;
|
|
5091
5673
|
const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
|
|
@@ -5129,11 +5711,11 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
5129
5711
|
};
|
|
5130
5712
|
|
|
5131
5713
|
// src/signup-control-delivery.ts
|
|
5132
|
-
var
|
|
5714
|
+
var import_db10 = require("@odla-ai/db");
|
|
5133
5715
|
async function deliverSignupControl(options) {
|
|
5134
5716
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
5135
5717
|
const body = JSON.stringify(options.envelope);
|
|
5136
|
-
const signed = await (0,
|
|
5718
|
+
const signed = await (0, import_db10.signFederatedRequest)({
|
|
5137
5719
|
secret: options.secret,
|
|
5138
5720
|
sender: options.sender,
|
|
5139
5721
|
method: "POST",
|
|
@@ -5153,12 +5735,37 @@ async function deliverSignupControl(options) {
|
|
|
5153
5735
|
return result;
|
|
5154
5736
|
}
|
|
5155
5737
|
|
|
5738
|
+
// src/member-content-delivery.ts
|
|
5739
|
+
var import_db11 = require("@odla-ai/db");
|
|
5740
|
+
async function deliverMemberContent(options) {
|
|
5741
|
+
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
5742
|
+
const body = JSON.stringify(options.envelope);
|
|
5743
|
+
const signed = await (0, import_db11.signFederatedRequest)({
|
|
5744
|
+
secret: options.secret,
|
|
5745
|
+
sender: options.sender,
|
|
5746
|
+
method: "POST",
|
|
5747
|
+
url: destination,
|
|
5748
|
+
body
|
|
5749
|
+
});
|
|
5750
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
5751
|
+
method: "POST",
|
|
5752
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
5753
|
+
body
|
|
5754
|
+
});
|
|
5755
|
+
const result = await response.json();
|
|
5756
|
+
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
5757
|
+
const failure = result;
|
|
5758
|
+
throw new Error(`member content rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
|
|
5759
|
+
}
|
|
5760
|
+
return result;
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5156
5763
|
// src/admission-grants-delivery.ts
|
|
5157
|
-
var
|
|
5764
|
+
var import_db12 = require("@odla-ai/db");
|
|
5158
5765
|
async function deliverAdmissionGrants(options) {
|
|
5159
5766
|
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
5160
5767
|
const body = JSON.stringify(options.envelope);
|
|
5161
|
-
const signed = await (0,
|
|
5768
|
+
const signed = await (0, import_db12.signFederatedRequest)({
|
|
5162
5769
|
secret: options.secret,
|
|
5163
5770
|
sender: options.sender,
|
|
5164
5771
|
method: "POST",
|
|
@@ -5317,12 +5924,14 @@ var BUILTIN_ROUTES = [
|
|
|
5317
5924
|
handleCrm,
|
|
5318
5925
|
handleNetworkShared,
|
|
5319
5926
|
handleSignupControl,
|
|
5927
|
+
handleMemberContentControl,
|
|
5320
5928
|
handleAdmissionGrants,
|
|
5321
5929
|
handleNetworkSnapshot,
|
|
5322
5930
|
handleNetworkRecords,
|
|
5323
5931
|
handleNetworkSharedNotes,
|
|
5324
5932
|
handleFormation,
|
|
5325
5933
|
handleMember,
|
|
5934
|
+
handleMemberContent,
|
|
5326
5935
|
handleGifts,
|
|
5327
5936
|
handleSchedule,
|
|
5328
5937
|
handlePayments,
|
|
@@ -5338,6 +5947,7 @@ var BUILTIN_ROUTES = [
|
|
|
5338
5947
|
// Aggregation
|
|
5339
5948
|
handleAdminDashboard,
|
|
5340
5949
|
handleAdminBilling,
|
|
5950
|
+
handleAdminMemberContent,
|
|
5341
5951
|
// Lifecycle actions
|
|
5342
5952
|
handleAdminMeetingReschedule,
|
|
5343
5953
|
handleAdminMeetingCancel,
|