@odla-ai/chapter 0.47.6 → 0.47.7

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.
@@ -0,0 +1,280 @@
1
+ // src/testing-probe.ts
2
+ function activeMemberships(site) {
3
+ return site.skus.filter((sku) => sku.active && sku.kind === "membership");
4
+ }
5
+ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
6
+ const validation = validateChapterAcceptanceManifest(manifest);
7
+ if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
8
+ const findings = [];
9
+ for (const site of manifest.targets) {
10
+ let health;
11
+ let configResponse;
12
+ try {
13
+ [health, configResponse] = await Promise.all([
14
+ fetcher(`${site.origin}/api/health`, { headers: { "cache-control": "no-cache" } }),
15
+ fetcher(`${site.origin}/api/join-config`, { headers: { "cache-control": "no-cache" } })
16
+ ]);
17
+ } catch (error) {
18
+ findings.push({ targetId: site.targetId, siteId: site.siteId, path: "fetch", expected: "reachable", actual: error instanceof Error ? error.message : String(error) });
19
+ continue;
20
+ }
21
+ const targetNote = (path, expected, actual) => {
22
+ if (JSON.stringify(expected) !== JSON.stringify(actual)) findings.push({ targetId: site.targetId, siteId: site.siteId, path, expected, actual });
23
+ };
24
+ targetNote("/api/health status", 200, health.status);
25
+ targetNote("/api/join-config status", 200, configResponse.status);
26
+ if (!configResponse.ok) continue;
27
+ let config;
28
+ try {
29
+ config = await configResponse.json();
30
+ } catch {
31
+ targetNote("/api/join-config JSON", "object", "invalid JSON");
32
+ continue;
33
+ }
34
+ targetNote("id", site.target.chapterId, config.id);
35
+ const actualTiers = Array.isArray(config.tiers) ? config.tiers : [];
36
+ const expectedTiers = activeMemberships(site).map((sku) => ({
37
+ id: sku.id,
38
+ priceCents: sku.priceCents,
39
+ currency: sku.currency,
40
+ interval: sku.billing,
41
+ free: sku.priceCents === 0
42
+ }));
43
+ const projected = actualTiers.map((tier) => {
44
+ const value = tier;
45
+ return {
46
+ id: value.id,
47
+ priceCents: value.priceCents,
48
+ currency: value.currency,
49
+ interval: value.interval,
50
+ free: value.free
51
+ };
52
+ });
53
+ targetNote("tiers", expectedTiers, projected);
54
+ targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), config.paymentsReady);
55
+ }
56
+ return { ok: findings.length === 0, findings };
57
+ }
58
+
59
+ // src/testing.ts
60
+ var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
61
+ var CURRENCY = /^[a-z]{3}$/;
62
+ var STRIPE_PRICE = /^price_[A-Za-z0-9]{8,}$/;
63
+ function activeMemberships2(site) {
64
+ return site.skus.filter((sku) => sku.active && sku.kind === "membership");
65
+ }
66
+ function representativePaidMembership(site) {
67
+ return activeMemberships2(site).find((sku) => sku.priceCents > 0);
68
+ }
69
+ function record(value) {
70
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
71
+ }
72
+ function validateChapterAcceptanceManifest(value) {
73
+ const errors = [];
74
+ const input = record(value);
75
+ if (!input) return { ok: false, errors: ["manifest must be an object"] };
76
+ if (input.schemaVersion !== 1) errors.push("schemaVersion must be 1");
77
+ const inputTargets = Array.isArray(input.targets) ? input.targets : [];
78
+ if (inputTargets.length < 2) errors.push("targets must contain at least two deployed Chapter targets");
79
+ const targetIds = /* @__PURE__ */ new Set();
80
+ const siteIds = /* @__PURE__ */ new Set();
81
+ const targets = /* @__PURE__ */ new Set();
82
+ const priceOwners = /* @__PURE__ */ new Map();
83
+ for (const [siteIndex, rawSite] of inputTargets.entries()) {
84
+ const path = `targets[${siteIndex}]`;
85
+ const site = record(rawSite);
86
+ if (!site) {
87
+ errors.push(`${path} must be an object`);
88
+ continue;
89
+ }
90
+ const targetId = typeof site.targetId === "string" ? site.targetId : "";
91
+ const siteId = typeof site.siteId === "string" ? site.siteId : "";
92
+ if (!SLUG.test(targetId)) errors.push(`${path}.targetId must be a stable lowercase slug`);
93
+ if (targetIds.has(targetId)) errors.push(`${path}.targetId duplicates ${JSON.stringify(targetId)}`);
94
+ if (targetId) targetIds.add(targetId);
95
+ if (!SLUG.test(siteId)) errors.push(`${path}.siteId must be a stable lowercase slug`);
96
+ if (siteId) siteIds.add(siteId);
97
+ try {
98
+ if (typeof site.origin !== "string") throw new TypeError("origin");
99
+ const origin = new URL(site.origin);
100
+ if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash) {
101
+ errors.push(`${path}.origin must be an HTTPS origin without a path, query, or fragment`);
102
+ }
103
+ } catch {
104
+ errors.push(`${path}.origin must be a valid HTTPS origin`);
105
+ }
106
+ const target = record(site.target);
107
+ const targetValues = {};
108
+ for (const key of ["appId", "environment", "dataEnvironment", "runtime", "chapterId"]) {
109
+ targetValues[key] = typeof target?.[key] === "string" ? String(target[key]) : "";
110
+ if (!targetValues[key]) errors.push(`${path}.target.${key} is required`);
111
+ }
112
+ const partition = Object.values(targetValues).join("\0");
113
+ if (targets.has(partition)) errors.push(`${path}.target duplicates another site partition`);
114
+ if (Object.values(targetValues).every(Boolean)) targets.add(partition);
115
+ if (site.stripeMode !== "test" && site.stripeMode !== "live") {
116
+ errors.push(`${path}.stripeMode must be test or live`);
117
+ }
118
+ if (site.stripeMode === "live" && targetValues.environment !== "prod" && targetValues.environment !== "production") {
119
+ errors.push(`${path}.stripeMode cannot be live outside a production environment`);
120
+ }
121
+ const skuIds = /* @__PURE__ */ new Set();
122
+ const inputSkus = Array.isArray(site.skus) ? site.skus : [];
123
+ if (!Array.isArray(site.skus) || inputSkus.length === 0) errors.push(`${path}.skus must be a non-empty array`);
124
+ const skus = [];
125
+ for (const [skuIndex, rawSku] of inputSkus.entries()) {
126
+ const skuPath = `${path}.skus[${skuIndex}]`;
127
+ const sku = record(rawSku);
128
+ if (!sku) {
129
+ errors.push(`${skuPath} must be an object`);
130
+ continue;
131
+ }
132
+ const id = typeof sku.id === "string" ? sku.id : "";
133
+ if (!SLUG.test(id)) errors.push(`${skuPath}.id must be a stable lowercase slug`);
134
+ if (skuIds.has(id)) errors.push(`${skuPath}.id duplicates ${JSON.stringify(id)}`);
135
+ if (id) skuIds.add(id);
136
+ if (sku.kind !== "membership" && sku.kind !== "add_on") errors.push(`${skuPath}.kind must be membership or add_on`);
137
+ if (typeof sku.active !== "boolean") errors.push(`${skuPath}.active must be boolean`);
138
+ const priceCents = typeof sku.priceCents === "number" ? sku.priceCents : Number.NaN;
139
+ if (!Number.isSafeInteger(priceCents) || priceCents < 0) errors.push(`${skuPath}.priceCents must be a non-negative integer`);
140
+ if (typeof sku.currency !== "string" || !CURRENCY.test(sku.currency)) errors.push(`${skuPath}.currency must be a lowercase ISO currency code`);
141
+ if (sku.billing !== "month" && sku.billing !== "year" && sku.billing !== "once") {
142
+ errors.push(`${skuPath}.billing must be month, year, or once`);
143
+ }
144
+ if (sku.kind === "membership" && sku.billing === "once") {
145
+ errors.push(`${skuPath}.billing must be month or year for a membership SKU`);
146
+ }
147
+ if (sku.active === true && priceCents > 0 && (typeof sku.stripePriceId !== "string" || !STRIPE_PRICE.test(sku.stripePriceId))) {
148
+ errors.push(`${skuPath}.stripePriceId is required for an active paid SKU`);
149
+ }
150
+ if (priceCents === 0 && sku.stripePriceId) errors.push(`${skuPath}.stripePriceId must be absent for a free SKU`);
151
+ if (typeof sku.stripePriceId === "string" && sku.stripePriceId) {
152
+ const owner = `${siteId}/${id}`;
153
+ const previous = priceOwners.get(sku.stripePriceId);
154
+ if (previous && previous !== owner) errors.push(`${skuPath}.stripePriceId is already owned by ${previous}`);
155
+ else priceOwners.set(sku.stripePriceId, owner);
156
+ }
157
+ if (sku.compareAtPriceCents !== void 0) {
158
+ if (!Number.isSafeInteger(sku.compareAtPriceCents) || Number(sku.compareAtPriceCents) <= priceCents) {
159
+ errors.push(`${skuPath}.compareAtPriceCents must be an integer greater than priceCents`);
160
+ }
161
+ if (sku.discountDuration !== "first_term" && sku.discountDuration !== "while_active") {
162
+ errors.push(`${skuPath}.discountDuration is required with compareAtPriceCents`);
163
+ }
164
+ } else if (sku.discountDuration) {
165
+ errors.push(`${skuPath}.compareAtPriceCents is required with discountDuration`);
166
+ }
167
+ skus.push(sku);
168
+ }
169
+ if (!skus.some((sku) => sku.active && sku.kind === "membership")) errors.push(`${path}.skus must contain an active membership SKU`);
170
+ const options = record(site.options);
171
+ if (!options) errors.push(`${path}.options must be an object`);
172
+ if (options?.ordinaryInterview !== true) errors.push(`${path}.options.ordinaryInterview must be true`);
173
+ if (typeof options?.invitationInterviewWaiver !== "boolean") errors.push(`${path}.options.invitationInterviewWaiver must be boolean`);
174
+ if (options?.autoRenew !== true) errors.push(`${path}.options.autoRenew must be true`);
175
+ const seat = record(options?.additionalSeat);
176
+ if (options?.additionalSeat !== void 0 && !seat) errors.push(`${path}.options.additionalSeat must be an object`);
177
+ if (seat) {
178
+ const seatSku = skus.find((sku) => sku.id === seat.skuId);
179
+ if (!seatSku || !seatSku.active || seatSku.kind !== "add_on") {
180
+ errors.push(`${path}.options.additionalSeat.skuId must name an active add_on SKU`);
181
+ } else if (seatSku.billing === "once") {
182
+ errors.push(`${path}.options.additionalSeat.skuId must name a recurring add_on SKU`);
183
+ }
184
+ if (typeof seat.purchaseWindowCalendarMonths !== "number" || !Number.isInteger(seat.purchaseWindowCalendarMonths) || seat.purchaseWindowCalendarMonths < 1) {
185
+ errors.push(`${path}.options.additionalSeat.purchaseWindowCalendarMonths must be a positive integer`);
186
+ }
187
+ if (seat.named !== true || seat.uniqueEmail !== true || seat.renewal !== "membership_aligned" || seat.chargeTiming !== "start") {
188
+ errors.push(`${path}.options.additionalSeat must require a named unique-email seat, membership-aligned renewal, and payment at start`);
189
+ }
190
+ }
191
+ }
192
+ if (siteIds.size < 2) errors.push("targets must cover at least two distinct logical sites");
193
+ return errors.length ? { ok: false, errors } : { ok: true, value };
194
+ }
195
+ function scenario(input) {
196
+ return {
197
+ ...input,
198
+ id: [input.suite, input.targetId, input.skuId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
199
+ };
200
+ }
201
+ function planChapterAcceptanceMatrix(manifest) {
202
+ const validation = validateChapterAcceptanceManifest(manifest);
203
+ if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
204
+ if (manifest.targets.some((site) => site.stripeMode !== "test")) {
205
+ throw new TypeError("provider acceptance runs are development-only and require Stripe test mode on every target");
206
+ }
207
+ const scenarios = [];
208
+ for (const site of manifest.targets) {
209
+ for (const sku of site.skus.filter((candidate) => candidate.active)) {
210
+ scenarios.push(scenario({ suite: "hermetic", kind: "catalog_contract", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
211
+ if (sku.priceCents > 0) {
212
+ scenarios.push(scenario({ suite: "hermetic", kind: "amount_tamper", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
213
+ scenarios.push(scenario({ suite: "hermetic", kind: "wrong_environment_event", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
214
+ scenarios.push(scenario({ suite: "hermetic", kind: "post_stripe_compensation", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
215
+ for (const sibling of manifest.targets.filter((candidate) => candidate.targetId !== site.targetId)) {
216
+ scenarios.push(scenario({
217
+ suite: "hermetic",
218
+ kind: "sibling_event_isolation",
219
+ targetId: site.targetId,
220
+ siteId: site.siteId,
221
+ skuId: sku.id,
222
+ siblingTargetId: sibling.targetId
223
+ }));
224
+ }
225
+ }
226
+ }
227
+ for (const sku of activeMemberships2(site)) {
228
+ scenarios.push(scenario({ suite: "hermetic", kind: "submit_replay", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
229
+ scenarios.push(scenario({
230
+ suite: "provider_dev",
231
+ kind: sku.priceCents > 0 ? "paid_signup" : "free_signup",
232
+ targetId: site.targetId,
233
+ siteId: site.siteId,
234
+ skuId: sku.id
235
+ }));
236
+ }
237
+ const paid = representativePaidMembership(site);
238
+ if (paid) {
239
+ scenarios.push(scenario({ suite: "provider_dev", kind: "ordinary_interview", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
240
+ scenarios.push(scenario({ suite: "provider_dev", kind: "renewal", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
241
+ scenarios.push(scenario({ suite: "provider_dev", kind: "refund_cancel", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
242
+ if (site.options.invitationInterviewWaiver) {
243
+ scenarios.push(scenario({ suite: "provider_dev", kind: "invitation_interview_waiver", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
244
+ scenarios.push(scenario({ suite: "hermetic", kind: "expired_interview_waiver", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
245
+ scenarios.push(scenario({ suite: "hermetic", kind: "replayed_interview_waiver", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
246
+ }
247
+ if (site.options.additionalSeat) {
248
+ scenarios.push(scenario({
249
+ suite: "provider_dev",
250
+ kind: "named_seat",
251
+ targetId: site.targetId,
252
+ siteId: site.siteId,
253
+ skuId: site.options.additionalSeat.skuId
254
+ }));
255
+ scenarios.push(scenario({
256
+ suite: "hermetic",
257
+ kind: "duplicate_seat_email",
258
+ targetId: site.targetId,
259
+ siteId: site.siteId,
260
+ skuId: site.options.additionalSeat.skuId
261
+ }));
262
+ scenarios.push(scenario({
263
+ suite: "hermetic",
264
+ kind: "expired_seat_window",
265
+ targetId: site.targetId,
266
+ siteId: site.siteId,
267
+ skuId: site.options.additionalSeat.skuId
268
+ }));
269
+ }
270
+ scenarios.push(scenario({ suite: "hermetic", kind: "canceled_membership", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
271
+ }
272
+ }
273
+ return scenarios;
274
+ }
275
+ export {
276
+ planChapterAcceptanceMatrix,
277
+ probeChapterJoinContracts,
278
+ validateChapterAcceptanceManifest
279
+ };
280
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing-probe.ts","../src/testing.ts"],"sourcesContent":["import type {\n ChapterAcceptanceManifest,\n ChapterAcceptanceTarget,\n ChapterJoinConfigProbe,\n ChapterJoinContractFinding,\n ChapterJoinContractProbeResult,\n} from \"./testing-contract\";\nimport { validateChapterAcceptanceManifest } from \"./testing\";\n\nfunction activeMemberships(site: ChapterAcceptanceTarget) {\n return site.skus.filter((sku) => sku.active && sku.kind === \"membership\");\n}\n\n/** Probe public deployed join contracts without creating applications or provider objects. */\nexport async function probeChapterJoinContracts(\n manifest: ChapterAcceptanceManifest,\n fetcher: typeof fetch = fetch,\n): Promise<ChapterJoinContractProbeResult> {\n const validation = validateChapterAcceptanceManifest(manifest);\n if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join(\"; \")}`);\n const findings: ChapterJoinContractFinding[] = [];\n for (const site of manifest.targets) {\n let health: Response;\n let configResponse: Response;\n try {\n [health, configResponse] = await Promise.all([\n fetcher(`${site.origin}/api/health`, { headers: { \"cache-control\": \"no-cache\" } }),\n fetcher(`${site.origin}/api/join-config`, { headers: { \"cache-control\": \"no-cache\" } }),\n ]);\n } catch (error) {\n findings.push({ targetId: site.targetId, siteId: site.siteId, path: \"fetch\", expected: \"reachable\", actual: error instanceof Error ? error.message : String(error) });\n continue;\n }\n const targetNote = (path: string, expected: unknown, actual: unknown): void => {\n if (JSON.stringify(expected) !== JSON.stringify(actual)) findings.push({ targetId: site.targetId, siteId: site.siteId, path, expected, actual });\n };\n targetNote(\"/api/health status\", 200, health.status);\n targetNote(\"/api/join-config status\", 200, configResponse.status);\n if (!configResponse.ok) continue;\n let config: ChapterJoinConfigProbe;\n try {\n config = await configResponse.json() as ChapterJoinConfigProbe;\n } catch {\n targetNote(\"/api/join-config JSON\", \"object\", \"invalid JSON\");\n continue;\n }\n targetNote(\"id\", site.target.chapterId, config.id);\n const actualTiers = Array.isArray(config.tiers) ? config.tiers : [];\n const expectedTiers = activeMemberships(site).map((sku) => ({\n id: sku.id, priceCents: sku.priceCents, currency: sku.currency,\n interval: sku.billing, free: sku.priceCents === 0,\n }));\n const projected = actualTiers.map((tier) => {\n const value = tier as Record<string, unknown>;\n return {\n id: value.id, priceCents: value.priceCents, currency: value.currency,\n interval: value.interval, free: value.free,\n };\n });\n targetNote(\"tiers\", expectedTiers, projected);\n targetNote(\"paymentsReady\", expectedTiers.some((tier) => !tier.free), config.paymentsReady);\n }\n return { ok: findings.length === 0, findings };\n}\n","import type {\n ChapterAcceptanceManifest, ChapterAcceptanceScenario, ChapterAcceptanceSku, ChapterAcceptanceTarget, ChapterAcceptanceValidation,\n} from \"./testing-contract\";\nexport type * from \"./testing-contract\";\nexport { probeChapterJoinContracts } from \"./testing-probe\";\n\nconst SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;\nconst CURRENCY = /^[a-z]{3}$/;\nconst STRIPE_PRICE = /^price_[A-Za-z0-9]{8,}$/;\n\nfunction activeMemberships(site: ChapterAcceptanceTarget): ChapterAcceptanceSku[] {\n return site.skus.filter((sku) => sku.active && sku.kind === \"membership\");\n}\n\nfunction representativePaidMembership(site: ChapterAcceptanceTarget): ChapterAcceptanceSku | undefined {\n return activeMemberships(site).find((sku) => sku.priceCents > 0);\n}\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return value !== null && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : null;\n}\n\n/** Validate the exact facts a multi-site acceptance run needs before it writes anywhere. */\nexport function validateChapterAcceptanceManifest(value: unknown): ChapterAcceptanceValidation {\n const errors: string[] = [];\n const input = record(value);\n if (!input) return { ok: false, errors: [\"manifest must be an object\"] };\n if (input.schemaVersion !== 1) errors.push(\"schemaVersion must be 1\");\n const inputTargets = Array.isArray(input.targets) ? input.targets : [];\n if (inputTargets.length < 2) errors.push(\"targets must contain at least two deployed Chapter targets\");\n\n const targetIds = new Set<string>();\n const siteIds = new Set<string>();\n const targets = new Set<string>();\n const priceOwners = new Map<string, string>();\n for (const [siteIndex, rawSite] of inputTargets.entries()) {\n const path = `targets[${siteIndex}]`;\n const site = record(rawSite);\n if (!site) {\n errors.push(`${path} must be an object`);\n continue;\n }\n const targetId = typeof site.targetId === \"string\" ? site.targetId : \"\";\n const siteId = typeof site.siteId === \"string\" ? site.siteId : \"\";\n if (!SLUG.test(targetId)) errors.push(`${path}.targetId must be a stable lowercase slug`);\n if (targetIds.has(targetId)) errors.push(`${path}.targetId duplicates ${JSON.stringify(targetId)}`);\n if (targetId) targetIds.add(targetId);\n if (!SLUG.test(siteId)) errors.push(`${path}.siteId must be a stable lowercase slug`);\n if (siteId) siteIds.add(siteId);\n try {\n if (typeof site.origin !== \"string\") throw new TypeError(\"origin\");\n const origin = new URL(site.origin);\n if (origin.protocol !== \"https:\" || origin.pathname !== \"/\" || origin.search || origin.hash) {\n errors.push(`${path}.origin must be an HTTPS origin without a path, query, or fragment`);\n }\n } catch {\n errors.push(`${path}.origin must be a valid HTTPS origin`);\n }\n const target = record(site.target);\n const targetValues = {} as Record<\"appId\" | \"environment\" | \"dataEnvironment\" | \"runtime\" | \"chapterId\", string>;\n for (const key of [\"appId\", \"environment\", \"dataEnvironment\", \"runtime\", \"chapterId\"] as const) {\n targetValues[key] = typeof target?.[key] === \"string\" ? String(target[key]) : \"\";\n if (!targetValues[key]) errors.push(`${path}.target.${key} is required`);\n }\n const partition = Object.values(targetValues).join(\"\\u0000\");\n if (targets.has(partition)) errors.push(`${path}.target duplicates another site partition`);\n if (Object.values(targetValues).every(Boolean)) targets.add(partition);\n if (site.stripeMode !== \"test\" && site.stripeMode !== \"live\") {\n errors.push(`${path}.stripeMode must be test or live`);\n }\n if (site.stripeMode === \"live\" && targetValues.environment !== \"prod\" && targetValues.environment !== \"production\") {\n errors.push(`${path}.stripeMode cannot be live outside a production environment`);\n }\n\n const skuIds = new Set<string>();\n const inputSkus = Array.isArray(site.skus) ? site.skus : [];\n if (!Array.isArray(site.skus) || inputSkus.length === 0) errors.push(`${path}.skus must be a non-empty array`);\n const skus: ChapterAcceptanceSku[] = [];\n for (const [skuIndex, rawSku] of inputSkus.entries()) {\n const skuPath = `${path}.skus[${skuIndex}]`;\n const sku = record(rawSku);\n if (!sku) {\n errors.push(`${skuPath} must be an object`);\n continue;\n }\n const id = typeof sku.id === \"string\" ? sku.id : \"\";\n if (!SLUG.test(id)) errors.push(`${skuPath}.id must be a stable lowercase slug`);\n if (skuIds.has(id)) errors.push(`${skuPath}.id duplicates ${JSON.stringify(id)}`);\n if (id) skuIds.add(id);\n if (sku.kind !== \"membership\" && sku.kind !== \"add_on\") errors.push(`${skuPath}.kind must be membership or add_on`);\n if (typeof sku.active !== \"boolean\") errors.push(`${skuPath}.active must be boolean`);\n const priceCents = typeof sku.priceCents === \"number\" ? sku.priceCents : Number.NaN;\n if (!Number.isSafeInteger(priceCents) || priceCents < 0) errors.push(`${skuPath}.priceCents must be a non-negative integer`);\n if (typeof sku.currency !== \"string\" || !CURRENCY.test(sku.currency)) errors.push(`${skuPath}.currency must be a lowercase ISO currency code`);\n if (sku.billing !== \"month\" && sku.billing !== \"year\" && sku.billing !== \"once\") {\n errors.push(`${skuPath}.billing must be month, year, or once`);\n }\n if (sku.kind === \"membership\" && sku.billing === \"once\") {\n errors.push(`${skuPath}.billing must be month or year for a membership SKU`);\n }\n if (sku.active === true && priceCents > 0 && (typeof sku.stripePriceId !== \"string\" || !STRIPE_PRICE.test(sku.stripePriceId))) {\n errors.push(`${skuPath}.stripePriceId is required for an active paid SKU`);\n }\n if (priceCents === 0 && sku.stripePriceId) errors.push(`${skuPath}.stripePriceId must be absent for a free SKU`);\n if (typeof sku.stripePriceId === \"string\" && sku.stripePriceId) {\n const owner = `${siteId}/${id}`;\n const previous = priceOwners.get(sku.stripePriceId);\n if (previous && previous !== owner) errors.push(`${skuPath}.stripePriceId is already owned by ${previous}`);\n else priceOwners.set(sku.stripePriceId, owner);\n }\n if (sku.compareAtPriceCents !== undefined) {\n if (!Number.isSafeInteger(sku.compareAtPriceCents) || Number(sku.compareAtPriceCents) <= priceCents) {\n errors.push(`${skuPath}.compareAtPriceCents must be an integer greater than priceCents`);\n }\n if (sku.discountDuration !== \"first_term\" && sku.discountDuration !== \"while_active\") {\n errors.push(`${skuPath}.discountDuration is required with compareAtPriceCents`);\n }\n } else if (sku.discountDuration) {\n errors.push(`${skuPath}.compareAtPriceCents is required with discountDuration`);\n }\n skus.push(sku as unknown as ChapterAcceptanceSku);\n }\n if (!skus.some((sku) => sku.active && sku.kind === \"membership\")) errors.push(`${path}.skus must contain an active membership SKU`);\n const options = record(site.options);\n if (!options) errors.push(`${path}.options must be an object`);\n if (options?.ordinaryInterview !== true) errors.push(`${path}.options.ordinaryInterview must be true`);\n if (typeof options?.invitationInterviewWaiver !== \"boolean\") errors.push(`${path}.options.invitationInterviewWaiver must be boolean`);\n if (options?.autoRenew !== true) errors.push(`${path}.options.autoRenew must be true`);\n const seat = record(options?.additionalSeat);\n if (options?.additionalSeat !== undefined && !seat) errors.push(`${path}.options.additionalSeat must be an object`);\n if (seat) {\n const seatSku = skus.find((sku) => sku.id === seat.skuId);\n if (!seatSku || !seatSku.active || seatSku.kind !== \"add_on\") {\n errors.push(`${path}.options.additionalSeat.skuId must name an active add_on SKU`);\n } else if (seatSku.billing === \"once\") {\n errors.push(`${path}.options.additionalSeat.skuId must name a recurring add_on SKU`);\n }\n if (typeof seat.purchaseWindowCalendarMonths !== \"number\" ||\n !Number.isInteger(seat.purchaseWindowCalendarMonths) || seat.purchaseWindowCalendarMonths < 1) {\n errors.push(`${path}.options.additionalSeat.purchaseWindowCalendarMonths must be a positive integer`);\n }\n if (seat.named !== true || seat.uniqueEmail !== true || seat.renewal !== \"membership_aligned\" || seat.chargeTiming !== \"start\") {\n errors.push(`${path}.options.additionalSeat must require a named unique-email seat, membership-aligned renewal, and payment at start`);\n }\n }\n }\n if (siteIds.size < 2) errors.push(\"targets must cover at least two distinct logical sites\");\n return errors.length ? { ok: false, errors } : { ok: true, value: value as ChapterAcceptanceManifest };\n}\n\nfunction scenario(input: Omit<ChapterAcceptanceScenario, \"id\">): ChapterAcceptanceScenario {\n return {\n ...input,\n id: [input.suite, input.targetId, input.skuId, input.siblingTargetId, input.kind].filter(Boolean).join(\":\"),\n };\n}\n\n/**\n * Build exhaustive pure cases plus a bounded provider-backed development matrix.\n * Throws before returning provider work when the manifest is invalid or names live Stripe.\n */\nexport function planChapterAcceptanceMatrix(manifest: ChapterAcceptanceManifest): ChapterAcceptanceScenario[] {\n const validation = validateChapterAcceptanceManifest(manifest);\n if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join(\"; \")}`);\n if (manifest.targets.some((site) => site.stripeMode !== \"test\")) {\n throw new TypeError(\"provider acceptance runs are development-only and require Stripe test mode on every target\");\n }\n const scenarios: ChapterAcceptanceScenario[] = [];\n for (const site of manifest.targets) {\n for (const sku of site.skus.filter((candidate) => candidate.active)) {\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"catalog_contract\", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));\n if (sku.priceCents > 0) {\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"amount_tamper\", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"wrong_environment_event\", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"post_stripe_compensation\", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));\n for (const sibling of manifest.targets.filter((candidate) => candidate.targetId !== site.targetId)) {\n scenarios.push(scenario({\n suite: \"hermetic\", kind: \"sibling_event_isolation\", targetId: site.targetId, siteId: site.siteId,\n skuId: sku.id, siblingTargetId: sibling.targetId,\n }));\n }\n }\n }\n for (const sku of activeMemberships(site)) {\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"submit_replay\", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));\n scenarios.push(scenario({\n suite: \"provider_dev\", kind: sku.priceCents > 0 ? \"paid_signup\" : \"free_signup\",\n targetId: site.targetId, siteId: site.siteId, skuId: sku.id,\n }));\n }\n const paid = representativePaidMembership(site);\n if (paid) {\n scenarios.push(scenario({ suite: \"provider_dev\", kind: \"ordinary_interview\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n scenarios.push(scenario({ suite: \"provider_dev\", kind: \"renewal\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n scenarios.push(scenario({ suite: \"provider_dev\", kind: \"refund_cancel\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n if (site.options.invitationInterviewWaiver) {\n scenarios.push(scenario({ suite: \"provider_dev\", kind: \"invitation_interview_waiver\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"expired_interview_waiver\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"replayed_interview_waiver\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n }\n if (site.options.additionalSeat) {\n scenarios.push(scenario({\n suite: \"provider_dev\", kind: \"named_seat\", targetId: site.targetId, siteId: site.siteId,\n skuId: site.options.additionalSeat.skuId,\n }));\n scenarios.push(scenario({\n suite: \"hermetic\", kind: \"duplicate_seat_email\", targetId: site.targetId, siteId: site.siteId,\n skuId: site.options.additionalSeat.skuId,\n }));\n scenarios.push(scenario({\n suite: \"hermetic\", kind: \"expired_seat_window\", targetId: site.targetId, siteId: site.siteId,\n skuId: site.options.additionalSeat.skuId,\n }));\n }\n scenarios.push(scenario({ suite: \"hermetic\", kind: \"canceled_membership\", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));\n }\n }\n return scenarios;\n}\n"],"mappings":"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","names":["activeMemberships"]}
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-20WFI5TI.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-20WFI5TI.js';
4
+ import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-Dorxp-WK.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dorxp-WK.js';
6
6
  import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
7
7
 
8
8
  /** Normalized public auth configuration consumed by the Clerk gate. */
@@ -1,5 +1,5 @@
1
1
  export { AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderAdmissionAction, LeaderAdmissionActionProps, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-20WFI5TI.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-Dorxp-WK.js';
3
3
  export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-20WFI5TI.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-20WFI5TI.js';
3
+ import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-Dorxp-WK.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dorxp-WK.js';
5
5
  import { FieldType, FieldConditions } from '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -351,6 +351,8 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
351
351
  name: group.name,
352
352
  standardPriceCents: group.standardPriceCents ?? 0,
353
353
  foundingDiscountCents: group.foundingDiscountCents ?? 0,
354
+ currency: group.currency ?? "usd",
355
+ interval: group.interval ?? "year",
354
356
  tiers: [...tiers],
355
357
  // The browser evaluates the same conditions the server enforces.
356
358
  conditions,
@@ -383,6 +385,8 @@ function resolveTiers(group, rows5 = []) {
383
385
  groupId: String(group.id ?? ""),
384
386
  name: "Membership",
385
387
  priceCents: group.standardPriceCents ?? 0,
388
+ currency: group.currency ?? "usd",
389
+ interval: group.interval ?? "year",
386
390
  ...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
387
391
  sortOrder: 0,
388
392
  active: true
@@ -395,11 +399,13 @@ function findTier(tiers, tierId) {
395
399
  if (!tierId) return tiers.length === 1 ? tiers[0] : null;
396
400
  return tiers.find((tier) => tier.id === tierId) ?? null;
397
401
  }
398
- function joinConfigTiers(tiers) {
402
+ function joinConfigTiers(tiers, defaults = {}) {
399
403
  return tiers.map((tier) => ({
400
404
  id: tier.id,
401
405
  name: tier.name,
402
406
  priceCents: tier.priceCents,
407
+ currency: tier.currency ?? defaults.currency ?? "usd",
408
+ interval: tier.interval ?? defaults.interval ?? "year",
403
409
  blurb: tier.blurb ?? "",
404
410
  free: tierIsFree(tier)
405
411
  }));
@@ -412,6 +418,8 @@ function rowToTier(row, groupId) {
412
418
  groupId: String(row.groupId ?? groupId),
413
419
  name: String(row.name ?? ""),
414
420
  priceCents: typeof row.priceCents === "number" ? row.priceCents : 0,
421
+ ...typeof row.currency === "string" && row.currency ? { currency: row.currency } : {},
422
+ ...row.interval === "year" || row.interval === "month" ? { interval: row.interval } : {},
415
423
  ...typeof row.stripePriceId === "string" && row.stripePriceId ? { stripePriceId: row.stripePriceId } : {},
416
424
  ...typeof row.blurb === "string" ? { blurb: row.blurb } : {},
417
425
  sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
@@ -1521,6 +1529,8 @@ function groupAttrs(document) {
1521
1529
  const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1522
1530
  return {
1523
1531
  ...first4 ? { standardPriceCents: first4.priceCents } : {},
1532
+ currency: first4?.currency ?? "usd",
1533
+ interval: first4?.interval ?? "year",
1524
1534
  foundingDiscountCents: 0,
1525
1535
  ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1526
1536
  ...document.policy ?? {},
@@ -1554,6 +1564,8 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
1554
1564
  groupId: document.target.chapterId,
1555
1565
  name: tier.name,
1556
1566
  priceCents: tier.priceCents,
1567
+ currency: tier.currency ?? "usd",
1568
+ interval: tier.interval ?? "year",
1557
1569
  ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
1558
1570
  ...tier.blurb ? { blurb: tier.blurb } : {},
1559
1571
  sortOrder: tier.sortOrder,
@@ -1970,7 +1982,7 @@ var handleMember = async (req, url, env, ctx) => {
1970
1982
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
1971
1983
  const application = controlledApplication(chapter.application, activeControl?.document);
1972
1984
  return json({
1973
- ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1985
+ ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
1974
1986
  fieldContract: chapterFlowFieldContract(chapter, "join", application),
1975
1987
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1976
1988
  ...activeControl ? { signupControl: {
@@ -2327,7 +2339,7 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2327
2339
  "theme",
2328
2340
  "giftOffers"
2329
2341
  ]);
2330
- var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2342
+ var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
2331
2343
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2332
2344
  function unknownKeys(value, allowed2, path, errors) {
2333
2345
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
@@ -2371,6 +2383,12 @@ function tiersOf(value, errors) {
2371
2383
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2372
2384
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2373
2385
  }
2386
+ if (item.currency !== void 0 && (typeof item.currency !== "string" || !/^[a-z]{3}$/.test(item.currency))) {
2387
+ errors.push(`${path}.currency must be a lowercase ISO currency code`);
2388
+ }
2389
+ if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
2390
+ errors.push(`${path}.interval must be year or month`);
2391
+ }
2374
2392
  const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2375
2393
  return [{
2376
2394
  id,
@@ -2378,6 +2396,8 @@ function tiersOf(value, errors) {
2378
2396
  available: item.available === true,
2379
2397
  sortOrder: Number(item.sortOrder) || 0,
2380
2398
  priceCents: Number(item.priceCents) || 0,
2399
+ ...typeof item.currency === "string" ? { currency: item.currency } : {},
2400
+ ...item.interval === "year" || item.interval === "month" ? { interval: item.interval } : {},
2381
2401
  ...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
2382
2402
  ...blurb ? { blurb } : {}
2383
2403
  }];