@odla-ai/chapter 0.47.9 → 0.48.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 +257 -17
- package/dist/{chunk-VWDEHUNI.js → chunk-KZ2AXEZW.js} +497 -204
- package/dist/chunk-KZ2AXEZW.js.map +1 -0
- package/dist/{chunk-7MEVZEL2.js → chunk-MQ4OPZCS.js} +969 -516
- package/dist/chunk-MQ4OPZCS.js.map +1 -0
- package/dist/{copy-context-Dorxp-WK.d.ts → copy-context-BveBZglM.d.ts} +18 -7
- package/dist/index.cjs +3789 -761
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +753 -70
- package/dist/index.d.ts +753 -70
- package/dist/index.js +3782 -754
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +308 -88
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +102 -4
- package/dist/testing.d.ts +102 -4
- package/dist/testing.js +308 -88
- package/dist/testing.js.map +1 -1
- package/dist/ui/admin/index.d.ts +75 -5
- package/dist/ui/admin/index.js +3 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +4 -2
- package/dist/ui/member/index.d.ts +9 -2
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +9698 -4695
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +509 -9
- package/dist/worker/index.d.ts +509 -9
- package/dist/worker/index.js +8385 -3383
- package/dist/worker/index.js.map +1 -1
- package/package.json +5 -5
- package/runbooks/adopt-existing.md +44 -8
- package/dist/chunk-7MEVZEL2.js.map +0 -1
- package/dist/chunk-VWDEHUNI.js.map +0 -1
package/dist/testing.cjs
CHANGED
|
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/testing.ts
|
|
21
21
|
var testing_exports = {};
|
|
22
22
|
__export(testing_exports, {
|
|
23
|
+
assertCommercialQuote: () => assertCommercialQuote,
|
|
24
|
+
assessChapterReleaseEvidence: () => assessChapterReleaseEvidence,
|
|
23
25
|
formatChapterAcceptanceRun: () => formatChapterAcceptanceRun,
|
|
24
26
|
planChapterAcceptanceMatrix: () => planChapterAcceptanceMatrix,
|
|
25
27
|
probeChapterJoinContracts: () => probeChapterJoinContracts,
|
|
@@ -28,7 +30,146 @@ __export(testing_exports, {
|
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(testing_exports);
|
|
30
32
|
|
|
33
|
+
// src/testing-commercial.ts
|
|
34
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
35
|
+
var id = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9_.:-]{1,159}$/.test(value);
|
|
36
|
+
var time = (value) => Number.isSafeInteger(value) && Number(value) > 0;
|
|
37
|
+
function validateCommercialCases(value, skus, path) {
|
|
38
|
+
if (value === void 0) return [];
|
|
39
|
+
if (!Array.isArray(value) || !value.length || value.length > 32) return [`${path} must be a non-empty array of at most 32 cases`];
|
|
40
|
+
const errors = [];
|
|
41
|
+
const ids = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const [index, raw] of value.entries()) {
|
|
43
|
+
const item = object(raw);
|
|
44
|
+
const at = `${path}[${index}]`;
|
|
45
|
+
if (!item) {
|
|
46
|
+
errors.push(`${at} must be an object`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!id(item.id) || ids.has(String(item.id))) errors.push(`${at}.id must be a unique stable id`);
|
|
50
|
+
ids.add(String(item.id));
|
|
51
|
+
const sku = skus.find((candidate) => candidate.id === item.skuId && candidate.active && candidate.kind === "membership" && candidate.priceCents > 0);
|
|
52
|
+
if (!sku) errors.push(`${at}.skuId must name an active paid base membership SKU`);
|
|
53
|
+
if (sku?.compareAtPriceCents !== void 0 || sku?.discountDuration !== void 0) {
|
|
54
|
+
errors.push(`${at} must not combine a discounted SKU with a separate commercial policy`);
|
|
55
|
+
}
|
|
56
|
+
if (!id(item.offeringVersionId)) errors.push(`${at}.offeringVersionId is required`);
|
|
57
|
+
if (typeof item.founding !== "boolean" || item.founding && !id(item.cohortPolicyVersionId) || item.cohortPolicyVersionId !== void 0 && !id(item.cohortPolicyVersionId)) errors.push(`${at}.founding requires a stable cohort policy identity`);
|
|
58
|
+
if (!time(item.purchasedAt) || !time(item.initialTermEndsAt) || item.initialTermEndsAt <= item.purchasedAt) {
|
|
59
|
+
errors.push(`${at} must specify a purchase instant and a later initial term end`);
|
|
60
|
+
}
|
|
61
|
+
const discount = object(item.discount);
|
|
62
|
+
if (item.discount !== void 0 && (!discount || !id(discount.policyVersionId) || !Number.isInteger(discount.percentOffBps) || Number(discount.percentOffBps) <= 0 || Number(discount.percentOffBps) >= 1e4 || !["first_term", "while_active", "fixed_end"].includes(String(discount.duration)) || (discount.duration === "fixed_end" ? !time(discount.endsAt) || Number(discount.endsAt) <= Number(item.purchasedAt) : discount.endsAt !== void 0))) {
|
|
63
|
+
errors.push(`${at}.discount must specify a version, percentage, and explicit expiry policy`);
|
|
64
|
+
}
|
|
65
|
+
if (sku) {
|
|
66
|
+
const due = Math.round(sku.priceCents * (1e4 - Number(discount?.percentOffBps ?? 0)) / 1e4);
|
|
67
|
+
const ongoing = discount?.duration === "while_active" || discount?.duration === "fixed_end" && Number(discount.endsAt) > Number(item.initialTermEndsAt);
|
|
68
|
+
if (item.dueTodayCents !== due || item.renewalCents !== (ongoing ? due : sku.priceCents)) {
|
|
69
|
+
errors.push(`${at} expected initial and renewal amounts must match the separate policy on the base price`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return errors;
|
|
74
|
+
}
|
|
75
|
+
function assertCommercialQuote(expected, sku, actual) {
|
|
76
|
+
const quote = object(actual), lines = object(quote?.lineItems), terms = object(quote?.commercial);
|
|
77
|
+
return Boolean(quote && terms && sku.id === expected.skuId && quote.tierId === expected.skuId && quote.currency === sku.currency && quote.interval === sku.billing && lines?.standardCents === sku.priceCents && lines.discountCents === sku.priceCents - expected.dueTodayCents && lines.dueTodayCents === expected.dueTodayCents && terms.offeringVersionId === expected.offeringVersionId && terms.initialTermEndsAt === expected.initialTermEndsAt && terms.renewalCents === expected.renewalCents && terms.autoRenew === true && terms.discountPolicyVersionId === expected.discount?.policyVersionId && terms.discountDuration === expected.discount?.duration);
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
// src/testing-probe.ts
|
|
81
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
82
|
+
var TIMEOUT_MS = 1e4;
|
|
83
|
+
function record(value) {
|
|
84
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
85
|
+
}
|
|
86
|
+
async function readBoundedJson(response, signal) {
|
|
87
|
+
const reader = response.body?.getReader();
|
|
88
|
+
if (!reader) throw new Error("invalid_json");
|
|
89
|
+
const chunks = [];
|
|
90
|
+
let size = 0;
|
|
91
|
+
const abort = () => {
|
|
92
|
+
void reader.cancel().catch(() => {
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
96
|
+
try {
|
|
97
|
+
if (signal.aborted) throw new Error("response_failed");
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done) break;
|
|
101
|
+
size += value.byteLength;
|
|
102
|
+
if (size > MAX_BODY_BYTES) throw new Error("response_too_large");
|
|
103
|
+
chunks.push(value);
|
|
104
|
+
}
|
|
105
|
+
const bytes = new Uint8Array(size);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const chunk of chunks) {
|
|
108
|
+
bytes.set(chunk, offset);
|
|
109
|
+
offset += chunk.byteLength;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error("invalid_json");
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
signal.removeEventListener("abort", abort);
|
|
118
|
+
await reader.cancel().catch(() => {
|
|
119
|
+
});
|
|
120
|
+
reader.releaseLock();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function probe(url, fetcher, json) {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
let timer;
|
|
126
|
+
const timeout = new Promise((resolve) => {
|
|
127
|
+
timer = setTimeout(() => {
|
|
128
|
+
controller.abort();
|
|
129
|
+
resolve({ status: 0, error: "request_timeout" });
|
|
130
|
+
}, TIMEOUT_MS);
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
return await Promise.race([timeout, (async () => {
|
|
134
|
+
let response;
|
|
135
|
+
try {
|
|
136
|
+
response = await fetcher(url, {
|
|
137
|
+
headers: { "cache-control": "no-cache" },
|
|
138
|
+
redirect: "manual",
|
|
139
|
+
signal: controller.signal
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
return { status: 0, error: "request_failed" };
|
|
143
|
+
}
|
|
144
|
+
if (!json || !response.ok) {
|
|
145
|
+
await response.body?.cancel().catch(() => {
|
|
146
|
+
});
|
|
147
|
+
return { status: response.status };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
return {
|
|
151
|
+
status: response.status,
|
|
152
|
+
json: await readBoundedJson(response, controller.signal),
|
|
153
|
+
identity: Object.fromEntries(["app-id", "chapter-id", "data-environment", "runtime"].map((name) => [name, slug(response.headers.get(`x-odla-${name}`) ?? void 0)]))
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const code = error instanceof Error && ["invalid_json", "response_too_large"].includes(error.message) ? error.message : "response_failed";
|
|
157
|
+
return { status: response.status, error: code };
|
|
158
|
+
}
|
|
159
|
+
})()]);
|
|
160
|
+
} finally {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function slug(value) {
|
|
165
|
+
return value === void 0 ? void 0 : typeof value === "string" && /^[a-z0-9][a-z0-9-]{1,62}$/.test(value) ? value : "invalid_value";
|
|
166
|
+
}
|
|
167
|
+
function amount(value) {
|
|
168
|
+
return value === void 0 ? void 0 : Number.isSafeInteger(value) && Number(value) >= 0 ? value : "invalid_value";
|
|
169
|
+
}
|
|
170
|
+
function boolean(value) {
|
|
171
|
+
return value === void 0 ? void 0 : typeof value === "boolean" ? value : "invalid_value";
|
|
172
|
+
}
|
|
32
173
|
function activeMemberships(site) {
|
|
33
174
|
return site.skus.filter((sku) => sku.active && sku.kind === "membership");
|
|
34
175
|
}
|
|
@@ -37,31 +178,35 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
|
|
|
37
178
|
if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
|
|
38
179
|
const findings = [];
|
|
39
180
|
for (const site of manifest.targets) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
fetcher(`${site.origin}/api/health`, { headers: { "cache-control": "no-cache" } }),
|
|
45
|
-
fetcher(`${site.origin}/api/join-config`, { headers: { "cache-control": "no-cache" } })
|
|
46
|
-
]);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
findings.push({ targetId: site.targetId, siteId: site.siteId, path: "fetch", expected: "reachable", actual: error instanceof Error ? error.message : String(error) });
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
181
|
+
const [health, configResponse] = await Promise.all([
|
|
182
|
+
probe(`${site.origin}/api/health`, fetcher, true),
|
|
183
|
+
probe(`${site.origin}/api/join-config`, fetcher, true)
|
|
184
|
+
]);
|
|
51
185
|
const targetNote = (path, expected, actual) => {
|
|
52
186
|
if (JSON.stringify(expected) !== JSON.stringify(actual)) findings.push({ targetId: site.targetId, siteId: site.siteId, path, expected, actual });
|
|
53
187
|
};
|
|
54
|
-
targetNote("/api/health status", 200, health.status);
|
|
55
|
-
targetNote("/api/
|
|
56
|
-
if (!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
188
|
+
targetNote("/api/health status", 200, health.error ?? health.status);
|
|
189
|
+
if (health.status === 200 && !health.error) targetNote("/api/health ok", true, boolean(record(health.json)?.ok));
|
|
190
|
+
if (health.status === 200 && !health.error) {
|
|
191
|
+
for (const [name, expected] of Object.entries({
|
|
192
|
+
"app-id": site.target.appId,
|
|
193
|
+
"chapter-id": site.target.chapterId,
|
|
194
|
+
"data-environment": site.target.dataEnvironment,
|
|
195
|
+
"runtime": site.target.runtime
|
|
196
|
+
})) targetNote(`/api/health x-odla-${name}`, expected, health.identity?.[name]);
|
|
197
|
+
}
|
|
198
|
+
targetNote("/api/join-config status", 200, configResponse.status || configResponse.error);
|
|
199
|
+
if (configResponse.status !== 200) continue;
|
|
200
|
+
if (configResponse.error) {
|
|
201
|
+
targetNote("/api/join-config JSON", "object", configResponse.error === "invalid_json" ? "invalid JSON" : configResponse.error);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const config = record(configResponse.json);
|
|
205
|
+
if (!config) {
|
|
206
|
+
targetNote("/api/join-config JSON", "object", "invalid shape");
|
|
62
207
|
continue;
|
|
63
208
|
}
|
|
64
|
-
targetNote("id", site.target.chapterId, config.id);
|
|
209
|
+
targetNote("id", site.target.chapterId, slug(config.id));
|
|
65
210
|
const actualTiers = Array.isArray(config.tiers) ? config.tiers : [];
|
|
66
211
|
const expectedTiers = activeMemberships(site).map((sku) => ({
|
|
67
212
|
id: sku.id,
|
|
@@ -71,17 +216,18 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
|
|
|
71
216
|
free: sku.priceCents === 0
|
|
72
217
|
}));
|
|
73
218
|
const projected = actualTiers.map((tier) => {
|
|
74
|
-
const value = tier;
|
|
219
|
+
const value = record(tier);
|
|
220
|
+
if (!value) return "invalid_tier";
|
|
75
221
|
return {
|
|
76
|
-
id: value.id,
|
|
77
|
-
priceCents: value.priceCents,
|
|
78
|
-
currency: value.currency,
|
|
79
|
-
interval: value.interval,
|
|
80
|
-
free: value.free
|
|
222
|
+
id: slug(value.id),
|
|
223
|
+
priceCents: amount(value.priceCents),
|
|
224
|
+
currency: value.currency === void 0 ? void 0 : typeof value.currency === "string" && /^[a-z]{3}$/.test(value.currency) ? value.currency : "invalid_value",
|
|
225
|
+
interval: value.interval === void 0 ? void 0 : ["month", "year", "once"].includes(String(value.interval)) ? value.interval : "invalid_value",
|
|
226
|
+
free: boolean(value.free)
|
|
81
227
|
};
|
|
82
228
|
});
|
|
83
229
|
targetNote("tiers", expectedTiers, projected);
|
|
84
|
-
targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), config.paymentsReady);
|
|
230
|
+
targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), boolean(config.paymentsReady));
|
|
85
231
|
}
|
|
86
232
|
return { ok: findings.length === 0, findings };
|
|
87
233
|
}
|
|
@@ -89,16 +235,21 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
|
|
|
89
235
|
// src/testing-runner.ts
|
|
90
236
|
var SAFE_LABEL = /^[a-z0-9][a-z0-9._:-]{0,159}$/;
|
|
91
237
|
var SAFE_REFERENCE = /^[A-Za-z0-9_:.=-]{3,240}$/;
|
|
238
|
+
var CREDENTIAL = /(?:^|[_.:-])(?:sk|rk|pk)_(?:test|live)_|whsec_|_secret_|eyJ[A-Za-z0-9_-]+\./i;
|
|
239
|
+
var ERROR_CODES = /* @__PURE__ */ new Set(["invalid_evidence", "assertion_failed", "preflight_failed", "unsupported_scenario", "execution_failed"]);
|
|
240
|
+
function safeValue(value, pattern) {
|
|
241
|
+
return pattern.test(value) && !CREDENTIAL.test(value);
|
|
242
|
+
}
|
|
92
243
|
function safeErrorCode(error) {
|
|
93
244
|
if (error && typeof error === "object") {
|
|
94
245
|
const code = error.code;
|
|
95
|
-
if (typeof code === "string" &&
|
|
246
|
+
if (typeof code === "string" && ERROR_CODES.has(code)) return code;
|
|
96
247
|
}
|
|
97
248
|
return "execution_failed";
|
|
98
249
|
}
|
|
99
250
|
function validateLabels(values, path) {
|
|
100
251
|
if (!Array.isArray(values) || values.length === 0) throw Object.assign(new TypeError(`${path} must be non-empty`), { code: "invalid_evidence" });
|
|
101
|
-
if (values.length > 64 || values.some((value) => typeof value !== "string" || !
|
|
252
|
+
if (values.length > 64 || values.some((value) => typeof value !== "string" || !safeValue(value, SAFE_LABEL))) {
|
|
102
253
|
throw Object.assign(new TypeError(`${path} contains an unsafe or unbounded label`), { code: "invalid_evidence" });
|
|
103
254
|
}
|
|
104
255
|
}
|
|
@@ -107,7 +258,7 @@ function validateReferences(values) {
|
|
|
107
258
|
if (!Array.isArray(values) || values.length > 64) throw Object.assign(new TypeError("references must be bounded"), { code: "invalid_evidence" });
|
|
108
259
|
const kinds = /* @__PURE__ */ new Set(["stripe_event", "stripe_object", "clerk_user", "calendar_event", "email_message", "event_digest"]);
|
|
109
260
|
for (const reference of values) {
|
|
110
|
-
if (!reference || !kinds.has(reference.kind) || typeof reference.value !== "string" || !
|
|
261
|
+
if (!reference || !kinds.has(reference.kind) || typeof reference.value !== "string" || !safeValue(reference.value, SAFE_REFERENCE) || reference.value.includes("@")) {
|
|
111
262
|
throw Object.assign(new TypeError("references may contain only opaque provider ids or digests"), { code: "invalid_evidence" });
|
|
112
263
|
}
|
|
113
264
|
}
|
|
@@ -116,52 +267,70 @@ function validateEvidence(evidence, provider) {
|
|
|
116
267
|
if (!evidence || typeof evidence !== "object") throw Object.assign(new TypeError("executor returned no evidence"), { code: "invalid_evidence" });
|
|
117
268
|
validateLabels(evidence.checks, "evidence.checks");
|
|
118
269
|
validateReferences(evidence.references);
|
|
119
|
-
if (provider && (typeof evidence.deploymentVersion !== "string" || !
|
|
270
|
+
if ((provider || evidence.deploymentVersion !== void 0) && (typeof evidence.deploymentVersion !== "string" || !safeValue(evidence.deploymentVersion, SAFE_REFERENCE))) {
|
|
120
271
|
throw Object.assign(new TypeError("provider evidence requires a safe deploymentVersion"), { code: "invalid_evidence" });
|
|
121
272
|
}
|
|
273
|
+
return {
|
|
274
|
+
checks: [...evidence.checks],
|
|
275
|
+
...evidence.deploymentVersion !== void 0 ? { deploymentVersion: evidence.deploymentVersion } : {},
|
|
276
|
+
...evidence.references !== void 0 ? {
|
|
277
|
+
references: evidence.references.map(({ kind, value }) => ({ kind, value }))
|
|
278
|
+
} : {}
|
|
279
|
+
};
|
|
122
280
|
}
|
|
123
281
|
function validateCleanup(cleanup) {
|
|
124
282
|
if (!cleanup || cleanup.status !== "complete" && cleanup.status !== "incomplete") {
|
|
125
283
|
throw Object.assign(new TypeError("executor returned invalid cleanup evidence"), { code: "cleanup_failed" });
|
|
126
284
|
}
|
|
127
285
|
validateLabels(cleanup.checks, "cleanup.checks");
|
|
286
|
+
return { status: cleanup.status, checks: [...cleanup.checks] };
|
|
128
287
|
}
|
|
129
|
-
function contextFor(manifest,
|
|
130
|
-
const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id ===
|
|
131
|
-
if (!scenario2) throw new TypeError(`unknown acceptance scenario ${
|
|
288
|
+
function contextFor(manifest, id2) {
|
|
289
|
+
const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id === id2);
|
|
290
|
+
if (!scenario2) throw new TypeError(`unknown acceptance scenario ${id2}`);
|
|
132
291
|
const target = manifest.targets.find((item) => item.targetId === scenario2.targetId);
|
|
133
292
|
return {
|
|
134
293
|
scenario: scenario2,
|
|
135
294
|
target,
|
|
136
295
|
...scenario2.skuId ? { sku: target.skus.find((item) => item.id === scenario2.skuId) } : {},
|
|
296
|
+
...scenario2.parentSkuId ? { parentSku: target.skus.find((item) => item.id === scenario2.parentSkuId) } : {},
|
|
297
|
+
...scenario2.commercialCaseId ? { commercialCase: target.commercialCases?.find((item) => item.id === scenario2.commercialCaseId) } : {},
|
|
137
298
|
...scenario2.siblingTargetId ? {
|
|
138
299
|
siblingTarget: manifest.targets.find((item) => item.targetId === scenario2.siblingTargetId)
|
|
139
300
|
} : {}
|
|
140
301
|
};
|
|
141
302
|
}
|
|
142
303
|
async function runChapterAcceptanceSuite(manifest, suite, executor) {
|
|
304
|
+
if (suite !== "hermetic" && suite !== "provider_dev") throw new TypeError("unknown acceptance suite");
|
|
143
305
|
const scenarios = planChapterAcceptanceMatrix(manifest).filter((item) => item.suite === suite);
|
|
144
306
|
if (suite === "provider_dev" && typeof executor.cleanup !== "function") {
|
|
145
307
|
throw new TypeError("provider_dev acceptance requires a cleanup executor before any case can run");
|
|
146
308
|
}
|
|
147
309
|
const cases = [];
|
|
310
|
+
let cleanupFailed = false;
|
|
148
311
|
for (const scenario2 of scenarios) {
|
|
312
|
+
if (cleanupFailed) {
|
|
313
|
+
cases.push({ id: scenario2.id, suite, outcome: "failed", errorCode: "not_run_cleanup_failed" });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
149
316
|
const context = contextFor(manifest, scenario2.id);
|
|
150
317
|
let evidence;
|
|
151
318
|
let errorCode;
|
|
152
319
|
let cleanup;
|
|
153
320
|
try {
|
|
154
|
-
evidence = await executor.execute(context);
|
|
155
|
-
validateEvidence(evidence, suite === "provider_dev");
|
|
321
|
+
evidence = validateEvidence(await executor.execute(context), suite === "provider_dev");
|
|
156
322
|
} catch (error) {
|
|
157
323
|
errorCode = safeErrorCode(error);
|
|
158
324
|
} finally {
|
|
159
325
|
if (suite === "provider_dev") {
|
|
160
326
|
try {
|
|
161
|
-
cleanup = await executor.cleanup(context);
|
|
162
|
-
|
|
163
|
-
|
|
327
|
+
cleanup = validateCleanup(await executor.cleanup(context));
|
|
328
|
+
if (cleanup.status !== "complete") {
|
|
329
|
+
cleanupFailed = true;
|
|
330
|
+
errorCode = "cleanup_incomplete";
|
|
331
|
+
}
|
|
164
332
|
} catch {
|
|
333
|
+
cleanupFailed = true;
|
|
165
334
|
errorCode = "cleanup_failed";
|
|
166
335
|
}
|
|
167
336
|
}
|
|
@@ -186,6 +355,66 @@ function formatChapterAcceptanceRun(result) {
|
|
|
186
355
|
return lines.join("\n");
|
|
187
356
|
}
|
|
188
357
|
|
|
358
|
+
// src/testing-release.ts
|
|
359
|
+
var ID = /^[a-z0-9][a-z0-9_.:-]{1,199}$/;
|
|
360
|
+
var validId = (value) => typeof value === "string" && ID.test(value);
|
|
361
|
+
var opaque = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_:-]{2,159}$/.test(value) && !/(?:sk|rk|pk|whsec)_(?:test_|live_)?|_secret_|bearer|password|token/i.test(value);
|
|
362
|
+
var keys = ["targetId", "appId", "chapterId", "runtime", "environment", "dataEnvironment", "stripeMode", "sourceCommit", "deploymentVersion"];
|
|
363
|
+
function assessChapterReleaseEvidence(plan, proofs) {
|
|
364
|
+
const blockers = [];
|
|
365
|
+
const block = (requirementId, code) => blockers.push({ requirementId, code });
|
|
366
|
+
const targets = /* @__PURE__ */ new Map();
|
|
367
|
+
if (!plan || plan.schemaVersion !== 1 || !Array.isArray(plan.targets) || !plan.targets.length || plan.targets.length > 64 || !Array.isArray(plan.requirements) || !plan.requirements.length || plan.requirements.length > 4096 || !Array.isArray(proofs) || proofs.length > 8192) {
|
|
368
|
+
return {
|
|
369
|
+
readyForReview: false,
|
|
370
|
+
productionAuthorized: false,
|
|
371
|
+
required: 0,
|
|
372
|
+
passed: 0,
|
|
373
|
+
blockers: [{ requirementId: "plan", code: "invalid_release_plan" }]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
for (const target of plan.targets) {
|
|
377
|
+
if (!target || !validId(target.targetId) || targets.has(target.targetId) || ![target.appId, target.chapterId, target.runtime].every(validId) || target.environment !== "dev" || target.dataEnvironment !== "dev" || target.stripeMode !== "test" || typeof target.sourceCommit !== "string" || !/^[0-9a-f]{40}$/.test(target.sourceCommit) || !opaque(target.deploymentVersion)) {
|
|
378
|
+
block("plan", "invalid_or_undeployed_target");
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
targets.set(target.targetId, target);
|
|
382
|
+
}
|
|
383
|
+
const requirementIds = /* @__PURE__ */ new Set();
|
|
384
|
+
let passed = 0;
|
|
385
|
+
for (const requirement of plan.requirements) {
|
|
386
|
+
if (!requirement || !validId(requirement.id) || requirementIds.has(requirement.id) || !["deterministic", "deployed_contract", "provider_dev", "browser_dev"].includes(requirement.phase) || !Array.isArray(requirement.assertions) || !requirement.assertions.length || !requirement.assertions.every(validId) || new Set(requirement.assertions).size !== requirement.assertions.length || !Array.isArray(requirement.providers) || requirement.providers.some((kind) => !["stripe", "clerk", "calendar", "email"].includes(kind)) || !Array.isArray(requirement.cleanup) || !requirement.cleanup.every(validId) || requirement.phase === "provider_dev" && (!requirement.providers.length || !requirement.cleanup.length)) {
|
|
387
|
+
block("plan", "invalid_requirement");
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
requirementIds.add(requirement.id);
|
|
391
|
+
const target = targets.get(requirement.targetId);
|
|
392
|
+
if (!target) {
|
|
393
|
+
block(requirement.id, "missing_candidate_target");
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const matching = proofs.filter((proof2) => proof2?.requirementId === requirement.id);
|
|
397
|
+
if (matching.length !== 1) {
|
|
398
|
+
block(requirement.id, matching.length ? "duplicate_proof" : "missing_proof");
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const proof = matching[0];
|
|
402
|
+
const before = blockers.length;
|
|
403
|
+
if (!proof.target || keys.some((key) => proof.target[key] !== target[key])) block(requirement.id, "candidate_mismatch");
|
|
404
|
+
if (proof.phase !== requirement.phase) block(requirement.id, "wrong_execution_phase");
|
|
405
|
+
if (proof.outcome !== "passed") block(requirement.id, "attempt_not_passed");
|
|
406
|
+
if (!proof.assertions || requirement.assertions.some((id2) => proof.assertions[id2] !== true)) block(requirement.id, "assertions_incomplete");
|
|
407
|
+
if (!Array.isArray(proof.references) || proof.references.some((reference) => !reference || !opaque(reference.id) || !["stripe", "clerk", "calendar", "email"].includes(reference.provider)) || requirement.providers.some((provider) => !proof.references.some((reference) => reference.provider === provider))) block(requirement.id, "provider_evidence_incomplete");
|
|
408
|
+
if (!proof.cleanup || requirement.cleanup.some((id2) => proof.cleanup[id2] !== true)) block(requirement.id, "cleanup_incomplete");
|
|
409
|
+
if (blockers.length === before) passed++;
|
|
410
|
+
}
|
|
411
|
+
if (proofs.some((proof) => !proof || !requirementIds.has(proof.requirementId))) block("plan", "unexpected_proof");
|
|
412
|
+
for (const target of targets.values()) {
|
|
413
|
+
if (!plan.requirements.some((requirement) => requirement?.targetId === target.targetId)) block("plan", "uncovered_target");
|
|
414
|
+
}
|
|
415
|
+
return { readyForReview: blockers.length === 0, productionAuthorized: false, required: plan.requirements.length, passed, blockers };
|
|
416
|
+
}
|
|
417
|
+
|
|
189
418
|
// src/testing.ts
|
|
190
419
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
191
420
|
var CURRENCY = /^[a-z]{3}$/;
|
|
@@ -193,15 +422,12 @@ var STRIPE_PRICE = /^price_[A-Za-z0-9]{8,}$/;
|
|
|
193
422
|
function activeMemberships2(site) {
|
|
194
423
|
return site.skus.filter((sku) => sku.active && sku.kind === "membership");
|
|
195
424
|
}
|
|
196
|
-
function
|
|
197
|
-
return activeMemberships2(site).find((sku) => sku.priceCents > 0);
|
|
198
|
-
}
|
|
199
|
-
function record(value) {
|
|
425
|
+
function record2(value) {
|
|
200
426
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
201
427
|
}
|
|
202
428
|
function validateChapterAcceptanceManifest(value) {
|
|
203
429
|
const errors = [];
|
|
204
|
-
const input =
|
|
430
|
+
const input = record2(value);
|
|
205
431
|
if (!input) return { ok: false, errors: ["manifest must be an object"] };
|
|
206
432
|
if (input.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
207
433
|
const inputTargets = Array.isArray(input.targets) ? input.targets : [];
|
|
@@ -212,7 +438,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
212
438
|
const priceOwners = /* @__PURE__ */ new Map();
|
|
213
439
|
for (const [siteIndex, rawSite] of inputTargets.entries()) {
|
|
214
440
|
const path = `targets[${siteIndex}]`;
|
|
215
|
-
const site =
|
|
441
|
+
const site = record2(rawSite);
|
|
216
442
|
if (!site) {
|
|
217
443
|
errors.push(`${path} must be an object`);
|
|
218
444
|
continue;
|
|
@@ -227,13 +453,13 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
227
453
|
try {
|
|
228
454
|
if (typeof site.origin !== "string") throw new TypeError("origin");
|
|
229
455
|
const origin = new URL(site.origin);
|
|
230
|
-
if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash) {
|
|
456
|
+
if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash || origin.username || origin.password) {
|
|
231
457
|
errors.push(`${path}.origin must be an HTTPS origin without a path, query, or fragment`);
|
|
232
458
|
}
|
|
233
459
|
} catch {
|
|
234
460
|
errors.push(`${path}.origin must be a valid HTTPS origin`);
|
|
235
461
|
}
|
|
236
|
-
const target =
|
|
462
|
+
const target = record2(site.target);
|
|
237
463
|
const targetValues = {};
|
|
238
464
|
for (const key of ["appId", "environment", "dataEnvironment", "runtime", "chapterId"]) {
|
|
239
465
|
targetValues[key] = typeof target?.[key] === "string" ? String(target[key]) : "";
|
|
@@ -254,15 +480,15 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
254
480
|
const skus = [];
|
|
255
481
|
for (const [skuIndex, rawSku] of inputSkus.entries()) {
|
|
256
482
|
const skuPath = `${path}.skus[${skuIndex}]`;
|
|
257
|
-
const sku =
|
|
483
|
+
const sku = record2(rawSku);
|
|
258
484
|
if (!sku) {
|
|
259
485
|
errors.push(`${skuPath} must be an object`);
|
|
260
486
|
continue;
|
|
261
487
|
}
|
|
262
|
-
const
|
|
263
|
-
if (!SLUG.test(
|
|
264
|
-
if (skuIds.has(
|
|
265
|
-
if (
|
|
488
|
+
const id2 = typeof sku.id === "string" ? sku.id : "";
|
|
489
|
+
if (!SLUG.test(id2)) errors.push(`${skuPath}.id must be a stable lowercase slug`);
|
|
490
|
+
if (skuIds.has(id2)) errors.push(`${skuPath}.id duplicates ${JSON.stringify(id2)}`);
|
|
491
|
+
if (id2) skuIds.add(id2);
|
|
266
492
|
if (sku.kind !== "membership" && sku.kind !== "add_on") errors.push(`${skuPath}.kind must be membership or add_on`);
|
|
267
493
|
if (typeof sku.active !== "boolean") errors.push(`${skuPath}.active must be boolean`);
|
|
268
494
|
const priceCents = typeof sku.priceCents === "number" ? sku.priceCents : Number.NaN;
|
|
@@ -279,7 +505,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
279
505
|
}
|
|
280
506
|
if (priceCents === 0 && sku.stripePriceId) errors.push(`${skuPath}.stripePriceId must be absent for a free SKU`);
|
|
281
507
|
if (typeof sku.stripePriceId === "string" && sku.stripePriceId) {
|
|
282
|
-
const owner = `${siteId}/${
|
|
508
|
+
const owner = `${siteId}/${id2}`;
|
|
283
509
|
const previous = priceOwners.get(sku.stripePriceId);
|
|
284
510
|
if (previous && previous !== owner) errors.push(`${skuPath}.stripePriceId is already owned by ${previous}`);
|
|
285
511
|
else priceOwners.set(sku.stripePriceId, owner);
|
|
@@ -297,12 +523,13 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
297
523
|
skus.push(sku);
|
|
298
524
|
}
|
|
299
525
|
if (!skus.some((sku) => sku.active && sku.kind === "membership")) errors.push(`${path}.skus must contain an active membership SKU`);
|
|
300
|
-
|
|
526
|
+
errors.push(...validateCommercialCases(site.commercialCases, skus, `${path}.commercialCases`));
|
|
527
|
+
const options = record2(site.options);
|
|
301
528
|
if (!options) errors.push(`${path}.options must be an object`);
|
|
302
529
|
if (options?.ordinaryInterview !== true) errors.push(`${path}.options.ordinaryInterview must be true`);
|
|
303
530
|
if (typeof options?.invitationInterviewWaiver !== "boolean") errors.push(`${path}.options.invitationInterviewWaiver must be boolean`);
|
|
304
531
|
if (options?.autoRenew !== true) errors.push(`${path}.options.autoRenew must be true`);
|
|
305
|
-
const seat =
|
|
532
|
+
const seat = record2(options?.additionalSeat);
|
|
306
533
|
if (options?.additionalSeat !== void 0 && !seat) errors.push(`${path}.options.additionalSeat must be an object`);
|
|
307
534
|
if (seat) {
|
|
308
535
|
const seatSku = skus.find((sku) => sku.id === seat.skuId);
|
|
@@ -325,17 +552,23 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
325
552
|
function scenario(input) {
|
|
326
553
|
return {
|
|
327
554
|
...input,
|
|
328
|
-
id: [input.suite, input.targetId, input.skuId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
|
|
555
|
+
id: [input.suite, input.targetId, input.skuId, input.parentSkuId, input.commercialCaseId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
|
|
329
556
|
};
|
|
330
557
|
}
|
|
331
558
|
function planChapterAcceptanceMatrix(manifest) {
|
|
332
559
|
const validation = validateChapterAcceptanceManifest(manifest);
|
|
333
560
|
if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
|
|
334
|
-
if (manifest.targets.some((site) => site.stripeMode !== "test")) {
|
|
335
|
-
throw new TypeError("provider acceptance runs are development-only and require Stripe test mode on every target");
|
|
561
|
+
if (manifest.targets.some((site) => site.stripeMode !== "test" || site.target.environment !== "dev" || site.target.dataEnvironment !== "dev")) {
|
|
562
|
+
throw new TypeError("provider acceptance runs are development-only and require Stripe test mode and dev runtime/data environments on every target");
|
|
336
563
|
}
|
|
337
564
|
const scenarios = [];
|
|
338
565
|
for (const site of manifest.targets) {
|
|
566
|
+
for (const vector of site.commercialCases ?? []) {
|
|
567
|
+
const base = { targetId: site.targetId, siteId: site.siteId, skuId: vector.skuId, commercialCaseId: vector.id };
|
|
568
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "commercial_policy_replay" }));
|
|
569
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "commercial_quote" }));
|
|
570
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "commercial_renewal" }));
|
|
571
|
+
}
|
|
339
572
|
for (const sku of site.skus.filter((candidate) => candidate.active)) {
|
|
340
573
|
scenarios.push(scenario({ suite: "hermetic", kind: "catalog_contract", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
|
|
341
574
|
if (sku.priceCents > 0) {
|
|
@@ -364,40 +597,27 @@ function planChapterAcceptanceMatrix(manifest) {
|
|
|
364
597
|
skuId: sku.id
|
|
365
598
|
}));
|
|
366
599
|
}
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "ordinary_interview"
|
|
370
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "renewal", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
|
|
371
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "refund_cancel", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
|
|
600
|
+
for (const membership of activeMemberships2(site)) {
|
|
601
|
+
const base = { targetId: site.targetId, siteId: site.siteId, skuId: membership.id };
|
|
602
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "ordinary_interview" }));
|
|
372
603
|
if (site.options.invitationInterviewWaiver) {
|
|
373
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "invitation_interview_waiver"
|
|
374
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "expired_interview_waiver"
|
|
375
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "replayed_interview_waiver"
|
|
604
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "invitation_interview_waiver" }));
|
|
605
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "expired_interview_waiver" }));
|
|
606
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "replayed_interview_waiver" }));
|
|
376
607
|
}
|
|
608
|
+
if (membership.priceCents === 0) continue;
|
|
609
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "renewal" }));
|
|
610
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "refund_cancel" }));
|
|
377
611
|
if (site.options.additionalSeat) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
scenarios.push(scenario({
|
|
386
|
-
suite: "hermetic",
|
|
387
|
-
kind: "duplicate_seat_email",
|
|
388
|
-
targetId: site.targetId,
|
|
389
|
-
siteId: site.siteId,
|
|
390
|
-
skuId: site.options.additionalSeat.skuId
|
|
391
|
-
}));
|
|
392
|
-
scenarios.push(scenario({
|
|
393
|
-
suite: "hermetic",
|
|
394
|
-
kind: "expired_seat_window",
|
|
395
|
-
targetId: site.targetId,
|
|
396
|
-
siteId: site.siteId,
|
|
397
|
-
skuId: site.options.additionalSeat.skuId
|
|
398
|
-
}));
|
|
612
|
+
const seat = { ...base, skuId: site.options.additionalSeat.skuId, parentSkuId: membership.id };
|
|
613
|
+
for (const kind of ["named_seat", "seat_declined_refund", "unaccepted_seat_refund", "seat_membership_alignment"]) {
|
|
614
|
+
scenarios.push(scenario({ ...seat, suite: "provider_dev", kind }));
|
|
615
|
+
}
|
|
616
|
+
for (const kind of ["duplicate_seat_email", "expired_seat_window"]) {
|
|
617
|
+
scenarios.push(scenario({ ...seat, suite: "hermetic", kind }));
|
|
618
|
+
}
|
|
399
619
|
}
|
|
400
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "canceled_membership"
|
|
620
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "canceled_membership" }));
|
|
401
621
|
}
|
|
402
622
|
}
|
|
403
623
|
return scenarios;
|