@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.js
CHANGED
|
@@ -1,4 +1,143 @@
|
|
|
1
|
+
// src/testing-commercial.ts
|
|
2
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3
|
+
var id = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9_.:-]{1,159}$/.test(value);
|
|
4
|
+
var time = (value) => Number.isSafeInteger(value) && Number(value) > 0;
|
|
5
|
+
function validateCommercialCases(value, skus, path) {
|
|
6
|
+
if (value === void 0) return [];
|
|
7
|
+
if (!Array.isArray(value) || !value.length || value.length > 32) return [`${path} must be a non-empty array of at most 32 cases`];
|
|
8
|
+
const errors = [];
|
|
9
|
+
const ids = /* @__PURE__ */ new Set();
|
|
10
|
+
for (const [index, raw] of value.entries()) {
|
|
11
|
+
const item = object(raw);
|
|
12
|
+
const at = `${path}[${index}]`;
|
|
13
|
+
if (!item) {
|
|
14
|
+
errors.push(`${at} must be an object`);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!id(item.id) || ids.has(String(item.id))) errors.push(`${at}.id must be a unique stable id`);
|
|
18
|
+
ids.add(String(item.id));
|
|
19
|
+
const sku = skus.find((candidate) => candidate.id === item.skuId && candidate.active && candidate.kind === "membership" && candidate.priceCents > 0);
|
|
20
|
+
if (!sku) errors.push(`${at}.skuId must name an active paid base membership SKU`);
|
|
21
|
+
if (sku?.compareAtPriceCents !== void 0 || sku?.discountDuration !== void 0) {
|
|
22
|
+
errors.push(`${at} must not combine a discounted SKU with a separate commercial policy`);
|
|
23
|
+
}
|
|
24
|
+
if (!id(item.offeringVersionId)) errors.push(`${at}.offeringVersionId is required`);
|
|
25
|
+
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`);
|
|
26
|
+
if (!time(item.purchasedAt) || !time(item.initialTermEndsAt) || item.initialTermEndsAt <= item.purchasedAt) {
|
|
27
|
+
errors.push(`${at} must specify a purchase instant and a later initial term end`);
|
|
28
|
+
}
|
|
29
|
+
const discount = object(item.discount);
|
|
30
|
+
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))) {
|
|
31
|
+
errors.push(`${at}.discount must specify a version, percentage, and explicit expiry policy`);
|
|
32
|
+
}
|
|
33
|
+
if (sku) {
|
|
34
|
+
const due = Math.round(sku.priceCents * (1e4 - Number(discount?.percentOffBps ?? 0)) / 1e4);
|
|
35
|
+
const ongoing = discount?.duration === "while_active" || discount?.duration === "fixed_end" && Number(discount.endsAt) > Number(item.initialTermEndsAt);
|
|
36
|
+
if (item.dueTodayCents !== due || item.renewalCents !== (ongoing ? due : sku.priceCents)) {
|
|
37
|
+
errors.push(`${at} expected initial and renewal amounts must match the separate policy on the base price`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return errors;
|
|
42
|
+
}
|
|
43
|
+
function assertCommercialQuote(expected, sku, actual) {
|
|
44
|
+
const quote = object(actual), lines = object(quote?.lineItems), terms = object(quote?.commercial);
|
|
45
|
+
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);
|
|
46
|
+
}
|
|
47
|
+
|
|
1
48
|
// src/testing-probe.ts
|
|
49
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
50
|
+
var TIMEOUT_MS = 1e4;
|
|
51
|
+
function record(value) {
|
|
52
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
53
|
+
}
|
|
54
|
+
async function readBoundedJson(response, signal) {
|
|
55
|
+
const reader = response.body?.getReader();
|
|
56
|
+
if (!reader) throw new Error("invalid_json");
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let size = 0;
|
|
59
|
+
const abort = () => {
|
|
60
|
+
void reader.cancel().catch(() => {
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
64
|
+
try {
|
|
65
|
+
if (signal.aborted) throw new Error("response_failed");
|
|
66
|
+
for (; ; ) {
|
|
67
|
+
const { done, value } = await reader.read();
|
|
68
|
+
if (done) break;
|
|
69
|
+
size += value.byteLength;
|
|
70
|
+
if (size > MAX_BODY_BYTES) throw new Error("response_too_large");
|
|
71
|
+
chunks.push(value);
|
|
72
|
+
}
|
|
73
|
+
const bytes = new Uint8Array(size);
|
|
74
|
+
let offset = 0;
|
|
75
|
+
for (const chunk of chunks) {
|
|
76
|
+
bytes.set(chunk, offset);
|
|
77
|
+
offset += chunk.byteLength;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error("invalid_json");
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
signal.removeEventListener("abort", abort);
|
|
86
|
+
await reader.cancel().catch(() => {
|
|
87
|
+
});
|
|
88
|
+
reader.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function probe(url, fetcher, json) {
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
let timer;
|
|
94
|
+
const timeout = new Promise((resolve) => {
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
controller.abort();
|
|
97
|
+
resolve({ status: 0, error: "request_timeout" });
|
|
98
|
+
}, TIMEOUT_MS);
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([timeout, (async () => {
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await fetcher(url, {
|
|
105
|
+
headers: { "cache-control": "no-cache" },
|
|
106
|
+
redirect: "manual",
|
|
107
|
+
signal: controller.signal
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
return { status: 0, error: "request_failed" };
|
|
111
|
+
}
|
|
112
|
+
if (!json || !response.ok) {
|
|
113
|
+
await response.body?.cancel().catch(() => {
|
|
114
|
+
});
|
|
115
|
+
return { status: response.status };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return {
|
|
119
|
+
status: response.status,
|
|
120
|
+
json: await readBoundedJson(response, controller.signal),
|
|
121
|
+
identity: Object.fromEntries(["app-id", "chapter-id", "data-environment", "runtime"].map((name) => [name, slug(response.headers.get(`x-odla-${name}`) ?? void 0)]))
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const code = error instanceof Error && ["invalid_json", "response_too_large"].includes(error.message) ? error.message : "response_failed";
|
|
125
|
+
return { status: response.status, error: code };
|
|
126
|
+
}
|
|
127
|
+
})()]);
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function slug(value) {
|
|
133
|
+
return value === void 0 ? void 0 : typeof value === "string" && /^[a-z0-9][a-z0-9-]{1,62}$/.test(value) ? value : "invalid_value";
|
|
134
|
+
}
|
|
135
|
+
function amount(value) {
|
|
136
|
+
return value === void 0 ? void 0 : Number.isSafeInteger(value) && Number(value) >= 0 ? value : "invalid_value";
|
|
137
|
+
}
|
|
138
|
+
function boolean(value) {
|
|
139
|
+
return value === void 0 ? void 0 : typeof value === "boolean" ? value : "invalid_value";
|
|
140
|
+
}
|
|
2
141
|
function activeMemberships(site) {
|
|
3
142
|
return site.skus.filter((sku) => sku.active && sku.kind === "membership");
|
|
4
143
|
}
|
|
@@ -7,31 +146,35 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
|
|
|
7
146
|
if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
|
|
8
147
|
const findings = [];
|
|
9
148
|
for (const site of manifest.targets) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
}
|
|
149
|
+
const [health, configResponse] = await Promise.all([
|
|
150
|
+
probe(`${site.origin}/api/health`, fetcher, true),
|
|
151
|
+
probe(`${site.origin}/api/join-config`, fetcher, true)
|
|
152
|
+
]);
|
|
21
153
|
const targetNote = (path, expected, actual) => {
|
|
22
154
|
if (JSON.stringify(expected) !== JSON.stringify(actual)) findings.push({ targetId: site.targetId, siteId: site.siteId, path, expected, actual });
|
|
23
155
|
};
|
|
24
|
-
targetNote("/api/health status", 200, health.status);
|
|
25
|
-
targetNote("/api/
|
|
26
|
-
if (!
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
156
|
+
targetNote("/api/health status", 200, health.error ?? health.status);
|
|
157
|
+
if (health.status === 200 && !health.error) targetNote("/api/health ok", true, boolean(record(health.json)?.ok));
|
|
158
|
+
if (health.status === 200 && !health.error) {
|
|
159
|
+
for (const [name, expected] of Object.entries({
|
|
160
|
+
"app-id": site.target.appId,
|
|
161
|
+
"chapter-id": site.target.chapterId,
|
|
162
|
+
"data-environment": site.target.dataEnvironment,
|
|
163
|
+
"runtime": site.target.runtime
|
|
164
|
+
})) targetNote(`/api/health x-odla-${name}`, expected, health.identity?.[name]);
|
|
165
|
+
}
|
|
166
|
+
targetNote("/api/join-config status", 200, configResponse.status || configResponse.error);
|
|
167
|
+
if (configResponse.status !== 200) continue;
|
|
168
|
+
if (configResponse.error) {
|
|
169
|
+
targetNote("/api/join-config JSON", "object", configResponse.error === "invalid_json" ? "invalid JSON" : configResponse.error);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const config = record(configResponse.json);
|
|
173
|
+
if (!config) {
|
|
174
|
+
targetNote("/api/join-config JSON", "object", "invalid shape");
|
|
32
175
|
continue;
|
|
33
176
|
}
|
|
34
|
-
targetNote("id", site.target.chapterId, config.id);
|
|
177
|
+
targetNote("id", site.target.chapterId, slug(config.id));
|
|
35
178
|
const actualTiers = Array.isArray(config.tiers) ? config.tiers : [];
|
|
36
179
|
const expectedTiers = activeMemberships(site).map((sku) => ({
|
|
37
180
|
id: sku.id,
|
|
@@ -41,17 +184,18 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
|
|
|
41
184
|
free: sku.priceCents === 0
|
|
42
185
|
}));
|
|
43
186
|
const projected = actualTiers.map((tier) => {
|
|
44
|
-
const value = tier;
|
|
187
|
+
const value = record(tier);
|
|
188
|
+
if (!value) return "invalid_tier";
|
|
45
189
|
return {
|
|
46
|
-
id: value.id,
|
|
47
|
-
priceCents: value.priceCents,
|
|
48
|
-
currency: value.currency,
|
|
49
|
-
interval: value.interval,
|
|
50
|
-
free: value.free
|
|
190
|
+
id: slug(value.id),
|
|
191
|
+
priceCents: amount(value.priceCents),
|
|
192
|
+
currency: value.currency === void 0 ? void 0 : typeof value.currency === "string" && /^[a-z]{3}$/.test(value.currency) ? value.currency : "invalid_value",
|
|
193
|
+
interval: value.interval === void 0 ? void 0 : ["month", "year", "once"].includes(String(value.interval)) ? value.interval : "invalid_value",
|
|
194
|
+
free: boolean(value.free)
|
|
51
195
|
};
|
|
52
196
|
});
|
|
53
197
|
targetNote("tiers", expectedTiers, projected);
|
|
54
|
-
targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), config.paymentsReady);
|
|
198
|
+
targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), boolean(config.paymentsReady));
|
|
55
199
|
}
|
|
56
200
|
return { ok: findings.length === 0, findings };
|
|
57
201
|
}
|
|
@@ -59,16 +203,21 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
|
|
|
59
203
|
// src/testing-runner.ts
|
|
60
204
|
var SAFE_LABEL = /^[a-z0-9][a-z0-9._:-]{0,159}$/;
|
|
61
205
|
var SAFE_REFERENCE = /^[A-Za-z0-9_:.=-]{3,240}$/;
|
|
206
|
+
var CREDENTIAL = /(?:^|[_.:-])(?:sk|rk|pk)_(?:test|live)_|whsec_|_secret_|eyJ[A-Za-z0-9_-]+\./i;
|
|
207
|
+
var ERROR_CODES = /* @__PURE__ */ new Set(["invalid_evidence", "assertion_failed", "preflight_failed", "unsupported_scenario", "execution_failed"]);
|
|
208
|
+
function safeValue(value, pattern) {
|
|
209
|
+
return pattern.test(value) && !CREDENTIAL.test(value);
|
|
210
|
+
}
|
|
62
211
|
function safeErrorCode(error) {
|
|
63
212
|
if (error && typeof error === "object") {
|
|
64
213
|
const code = error.code;
|
|
65
|
-
if (typeof code === "string" &&
|
|
214
|
+
if (typeof code === "string" && ERROR_CODES.has(code)) return code;
|
|
66
215
|
}
|
|
67
216
|
return "execution_failed";
|
|
68
217
|
}
|
|
69
218
|
function validateLabels(values, path) {
|
|
70
219
|
if (!Array.isArray(values) || values.length === 0) throw Object.assign(new TypeError(`${path} must be non-empty`), { code: "invalid_evidence" });
|
|
71
|
-
if (values.length > 64 || values.some((value) => typeof value !== "string" || !
|
|
220
|
+
if (values.length > 64 || values.some((value) => typeof value !== "string" || !safeValue(value, SAFE_LABEL))) {
|
|
72
221
|
throw Object.assign(new TypeError(`${path} contains an unsafe or unbounded label`), { code: "invalid_evidence" });
|
|
73
222
|
}
|
|
74
223
|
}
|
|
@@ -77,7 +226,7 @@ function validateReferences(values) {
|
|
|
77
226
|
if (!Array.isArray(values) || values.length > 64) throw Object.assign(new TypeError("references must be bounded"), { code: "invalid_evidence" });
|
|
78
227
|
const kinds = /* @__PURE__ */ new Set(["stripe_event", "stripe_object", "clerk_user", "calendar_event", "email_message", "event_digest"]);
|
|
79
228
|
for (const reference of values) {
|
|
80
|
-
if (!reference || !kinds.has(reference.kind) || typeof reference.value !== "string" || !
|
|
229
|
+
if (!reference || !kinds.has(reference.kind) || typeof reference.value !== "string" || !safeValue(reference.value, SAFE_REFERENCE) || reference.value.includes("@")) {
|
|
81
230
|
throw Object.assign(new TypeError("references may contain only opaque provider ids or digests"), { code: "invalid_evidence" });
|
|
82
231
|
}
|
|
83
232
|
}
|
|
@@ -86,52 +235,70 @@ function validateEvidence(evidence, provider) {
|
|
|
86
235
|
if (!evidence || typeof evidence !== "object") throw Object.assign(new TypeError("executor returned no evidence"), { code: "invalid_evidence" });
|
|
87
236
|
validateLabels(evidence.checks, "evidence.checks");
|
|
88
237
|
validateReferences(evidence.references);
|
|
89
|
-
if (provider && (typeof evidence.deploymentVersion !== "string" || !
|
|
238
|
+
if ((provider || evidence.deploymentVersion !== void 0) && (typeof evidence.deploymentVersion !== "string" || !safeValue(evidence.deploymentVersion, SAFE_REFERENCE))) {
|
|
90
239
|
throw Object.assign(new TypeError("provider evidence requires a safe deploymentVersion"), { code: "invalid_evidence" });
|
|
91
240
|
}
|
|
241
|
+
return {
|
|
242
|
+
checks: [...evidence.checks],
|
|
243
|
+
...evidence.deploymentVersion !== void 0 ? { deploymentVersion: evidence.deploymentVersion } : {},
|
|
244
|
+
...evidence.references !== void 0 ? {
|
|
245
|
+
references: evidence.references.map(({ kind, value }) => ({ kind, value }))
|
|
246
|
+
} : {}
|
|
247
|
+
};
|
|
92
248
|
}
|
|
93
249
|
function validateCleanup(cleanup) {
|
|
94
250
|
if (!cleanup || cleanup.status !== "complete" && cleanup.status !== "incomplete") {
|
|
95
251
|
throw Object.assign(new TypeError("executor returned invalid cleanup evidence"), { code: "cleanup_failed" });
|
|
96
252
|
}
|
|
97
253
|
validateLabels(cleanup.checks, "cleanup.checks");
|
|
254
|
+
return { status: cleanup.status, checks: [...cleanup.checks] };
|
|
98
255
|
}
|
|
99
|
-
function contextFor(manifest,
|
|
100
|
-
const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id ===
|
|
101
|
-
if (!scenario2) throw new TypeError(`unknown acceptance scenario ${
|
|
256
|
+
function contextFor(manifest, id2) {
|
|
257
|
+
const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id === id2);
|
|
258
|
+
if (!scenario2) throw new TypeError(`unknown acceptance scenario ${id2}`);
|
|
102
259
|
const target = manifest.targets.find((item) => item.targetId === scenario2.targetId);
|
|
103
260
|
return {
|
|
104
261
|
scenario: scenario2,
|
|
105
262
|
target,
|
|
106
263
|
...scenario2.skuId ? { sku: target.skus.find((item) => item.id === scenario2.skuId) } : {},
|
|
264
|
+
...scenario2.parentSkuId ? { parentSku: target.skus.find((item) => item.id === scenario2.parentSkuId) } : {},
|
|
265
|
+
...scenario2.commercialCaseId ? { commercialCase: target.commercialCases?.find((item) => item.id === scenario2.commercialCaseId) } : {},
|
|
107
266
|
...scenario2.siblingTargetId ? {
|
|
108
267
|
siblingTarget: manifest.targets.find((item) => item.targetId === scenario2.siblingTargetId)
|
|
109
268
|
} : {}
|
|
110
269
|
};
|
|
111
270
|
}
|
|
112
271
|
async function runChapterAcceptanceSuite(manifest, suite, executor) {
|
|
272
|
+
if (suite !== "hermetic" && suite !== "provider_dev") throw new TypeError("unknown acceptance suite");
|
|
113
273
|
const scenarios = planChapterAcceptanceMatrix(manifest).filter((item) => item.suite === suite);
|
|
114
274
|
if (suite === "provider_dev" && typeof executor.cleanup !== "function") {
|
|
115
275
|
throw new TypeError("provider_dev acceptance requires a cleanup executor before any case can run");
|
|
116
276
|
}
|
|
117
277
|
const cases = [];
|
|
278
|
+
let cleanupFailed = false;
|
|
118
279
|
for (const scenario2 of scenarios) {
|
|
280
|
+
if (cleanupFailed) {
|
|
281
|
+
cases.push({ id: scenario2.id, suite, outcome: "failed", errorCode: "not_run_cleanup_failed" });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
119
284
|
const context = contextFor(manifest, scenario2.id);
|
|
120
285
|
let evidence;
|
|
121
286
|
let errorCode;
|
|
122
287
|
let cleanup;
|
|
123
288
|
try {
|
|
124
|
-
evidence = await executor.execute(context);
|
|
125
|
-
validateEvidence(evidence, suite === "provider_dev");
|
|
289
|
+
evidence = validateEvidence(await executor.execute(context), suite === "provider_dev");
|
|
126
290
|
} catch (error) {
|
|
127
291
|
errorCode = safeErrorCode(error);
|
|
128
292
|
} finally {
|
|
129
293
|
if (suite === "provider_dev") {
|
|
130
294
|
try {
|
|
131
|
-
cleanup = await executor.cleanup(context);
|
|
132
|
-
|
|
133
|
-
|
|
295
|
+
cleanup = validateCleanup(await executor.cleanup(context));
|
|
296
|
+
if (cleanup.status !== "complete") {
|
|
297
|
+
cleanupFailed = true;
|
|
298
|
+
errorCode = "cleanup_incomplete";
|
|
299
|
+
}
|
|
134
300
|
} catch {
|
|
301
|
+
cleanupFailed = true;
|
|
135
302
|
errorCode = "cleanup_failed";
|
|
136
303
|
}
|
|
137
304
|
}
|
|
@@ -156,6 +323,66 @@ function formatChapterAcceptanceRun(result) {
|
|
|
156
323
|
return lines.join("\n");
|
|
157
324
|
}
|
|
158
325
|
|
|
326
|
+
// src/testing-release.ts
|
|
327
|
+
var ID = /^[a-z0-9][a-z0-9_.:-]{1,199}$/;
|
|
328
|
+
var validId = (value) => typeof value === "string" && ID.test(value);
|
|
329
|
+
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);
|
|
330
|
+
var keys = ["targetId", "appId", "chapterId", "runtime", "environment", "dataEnvironment", "stripeMode", "sourceCommit", "deploymentVersion"];
|
|
331
|
+
function assessChapterReleaseEvidence(plan, proofs) {
|
|
332
|
+
const blockers = [];
|
|
333
|
+
const block = (requirementId, code) => blockers.push({ requirementId, code });
|
|
334
|
+
const targets = /* @__PURE__ */ new Map();
|
|
335
|
+
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) {
|
|
336
|
+
return {
|
|
337
|
+
readyForReview: false,
|
|
338
|
+
productionAuthorized: false,
|
|
339
|
+
required: 0,
|
|
340
|
+
passed: 0,
|
|
341
|
+
blockers: [{ requirementId: "plan", code: "invalid_release_plan" }]
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
for (const target of plan.targets) {
|
|
345
|
+
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)) {
|
|
346
|
+
block("plan", "invalid_or_undeployed_target");
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
targets.set(target.targetId, target);
|
|
350
|
+
}
|
|
351
|
+
const requirementIds = /* @__PURE__ */ new Set();
|
|
352
|
+
let passed = 0;
|
|
353
|
+
for (const requirement of plan.requirements) {
|
|
354
|
+
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)) {
|
|
355
|
+
block("plan", "invalid_requirement");
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
requirementIds.add(requirement.id);
|
|
359
|
+
const target = targets.get(requirement.targetId);
|
|
360
|
+
if (!target) {
|
|
361
|
+
block(requirement.id, "missing_candidate_target");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const matching = proofs.filter((proof2) => proof2?.requirementId === requirement.id);
|
|
365
|
+
if (matching.length !== 1) {
|
|
366
|
+
block(requirement.id, matching.length ? "duplicate_proof" : "missing_proof");
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const proof = matching[0];
|
|
370
|
+
const before = blockers.length;
|
|
371
|
+
if (!proof.target || keys.some((key) => proof.target[key] !== target[key])) block(requirement.id, "candidate_mismatch");
|
|
372
|
+
if (proof.phase !== requirement.phase) block(requirement.id, "wrong_execution_phase");
|
|
373
|
+
if (proof.outcome !== "passed") block(requirement.id, "attempt_not_passed");
|
|
374
|
+
if (!proof.assertions || requirement.assertions.some((id2) => proof.assertions[id2] !== true)) block(requirement.id, "assertions_incomplete");
|
|
375
|
+
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");
|
|
376
|
+
if (!proof.cleanup || requirement.cleanup.some((id2) => proof.cleanup[id2] !== true)) block(requirement.id, "cleanup_incomplete");
|
|
377
|
+
if (blockers.length === before) passed++;
|
|
378
|
+
}
|
|
379
|
+
if (proofs.some((proof) => !proof || !requirementIds.has(proof.requirementId))) block("plan", "unexpected_proof");
|
|
380
|
+
for (const target of targets.values()) {
|
|
381
|
+
if (!plan.requirements.some((requirement) => requirement?.targetId === target.targetId)) block("plan", "uncovered_target");
|
|
382
|
+
}
|
|
383
|
+
return { readyForReview: blockers.length === 0, productionAuthorized: false, required: plan.requirements.length, passed, blockers };
|
|
384
|
+
}
|
|
385
|
+
|
|
159
386
|
// src/testing.ts
|
|
160
387
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
161
388
|
var CURRENCY = /^[a-z]{3}$/;
|
|
@@ -163,15 +390,12 @@ var STRIPE_PRICE = /^price_[A-Za-z0-9]{8,}$/;
|
|
|
163
390
|
function activeMemberships2(site) {
|
|
164
391
|
return site.skus.filter((sku) => sku.active && sku.kind === "membership");
|
|
165
392
|
}
|
|
166
|
-
function
|
|
167
|
-
return activeMemberships2(site).find((sku) => sku.priceCents > 0);
|
|
168
|
-
}
|
|
169
|
-
function record(value) {
|
|
393
|
+
function record2(value) {
|
|
170
394
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
171
395
|
}
|
|
172
396
|
function validateChapterAcceptanceManifest(value) {
|
|
173
397
|
const errors = [];
|
|
174
|
-
const input =
|
|
398
|
+
const input = record2(value);
|
|
175
399
|
if (!input) return { ok: false, errors: ["manifest must be an object"] };
|
|
176
400
|
if (input.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
177
401
|
const inputTargets = Array.isArray(input.targets) ? input.targets : [];
|
|
@@ -182,7 +406,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
182
406
|
const priceOwners = /* @__PURE__ */ new Map();
|
|
183
407
|
for (const [siteIndex, rawSite] of inputTargets.entries()) {
|
|
184
408
|
const path = `targets[${siteIndex}]`;
|
|
185
|
-
const site =
|
|
409
|
+
const site = record2(rawSite);
|
|
186
410
|
if (!site) {
|
|
187
411
|
errors.push(`${path} must be an object`);
|
|
188
412
|
continue;
|
|
@@ -197,13 +421,13 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
197
421
|
try {
|
|
198
422
|
if (typeof site.origin !== "string") throw new TypeError("origin");
|
|
199
423
|
const origin = new URL(site.origin);
|
|
200
|
-
if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash) {
|
|
424
|
+
if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash || origin.username || origin.password) {
|
|
201
425
|
errors.push(`${path}.origin must be an HTTPS origin without a path, query, or fragment`);
|
|
202
426
|
}
|
|
203
427
|
} catch {
|
|
204
428
|
errors.push(`${path}.origin must be a valid HTTPS origin`);
|
|
205
429
|
}
|
|
206
|
-
const target =
|
|
430
|
+
const target = record2(site.target);
|
|
207
431
|
const targetValues = {};
|
|
208
432
|
for (const key of ["appId", "environment", "dataEnvironment", "runtime", "chapterId"]) {
|
|
209
433
|
targetValues[key] = typeof target?.[key] === "string" ? String(target[key]) : "";
|
|
@@ -224,15 +448,15 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
224
448
|
const skus = [];
|
|
225
449
|
for (const [skuIndex, rawSku] of inputSkus.entries()) {
|
|
226
450
|
const skuPath = `${path}.skus[${skuIndex}]`;
|
|
227
|
-
const sku =
|
|
451
|
+
const sku = record2(rawSku);
|
|
228
452
|
if (!sku) {
|
|
229
453
|
errors.push(`${skuPath} must be an object`);
|
|
230
454
|
continue;
|
|
231
455
|
}
|
|
232
|
-
const
|
|
233
|
-
if (!SLUG.test(
|
|
234
|
-
if (skuIds.has(
|
|
235
|
-
if (
|
|
456
|
+
const id2 = typeof sku.id === "string" ? sku.id : "";
|
|
457
|
+
if (!SLUG.test(id2)) errors.push(`${skuPath}.id must be a stable lowercase slug`);
|
|
458
|
+
if (skuIds.has(id2)) errors.push(`${skuPath}.id duplicates ${JSON.stringify(id2)}`);
|
|
459
|
+
if (id2) skuIds.add(id2);
|
|
236
460
|
if (sku.kind !== "membership" && sku.kind !== "add_on") errors.push(`${skuPath}.kind must be membership or add_on`);
|
|
237
461
|
if (typeof sku.active !== "boolean") errors.push(`${skuPath}.active must be boolean`);
|
|
238
462
|
const priceCents = typeof sku.priceCents === "number" ? sku.priceCents : Number.NaN;
|
|
@@ -249,7 +473,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
249
473
|
}
|
|
250
474
|
if (priceCents === 0 && sku.stripePriceId) errors.push(`${skuPath}.stripePriceId must be absent for a free SKU`);
|
|
251
475
|
if (typeof sku.stripePriceId === "string" && sku.stripePriceId) {
|
|
252
|
-
const owner = `${siteId}/${
|
|
476
|
+
const owner = `${siteId}/${id2}`;
|
|
253
477
|
const previous = priceOwners.get(sku.stripePriceId);
|
|
254
478
|
if (previous && previous !== owner) errors.push(`${skuPath}.stripePriceId is already owned by ${previous}`);
|
|
255
479
|
else priceOwners.set(sku.stripePriceId, owner);
|
|
@@ -267,12 +491,13 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
267
491
|
skus.push(sku);
|
|
268
492
|
}
|
|
269
493
|
if (!skus.some((sku) => sku.active && sku.kind === "membership")) errors.push(`${path}.skus must contain an active membership SKU`);
|
|
270
|
-
|
|
494
|
+
errors.push(...validateCommercialCases(site.commercialCases, skus, `${path}.commercialCases`));
|
|
495
|
+
const options = record2(site.options);
|
|
271
496
|
if (!options) errors.push(`${path}.options must be an object`);
|
|
272
497
|
if (options?.ordinaryInterview !== true) errors.push(`${path}.options.ordinaryInterview must be true`);
|
|
273
498
|
if (typeof options?.invitationInterviewWaiver !== "boolean") errors.push(`${path}.options.invitationInterviewWaiver must be boolean`);
|
|
274
499
|
if (options?.autoRenew !== true) errors.push(`${path}.options.autoRenew must be true`);
|
|
275
|
-
const seat =
|
|
500
|
+
const seat = record2(options?.additionalSeat);
|
|
276
501
|
if (options?.additionalSeat !== void 0 && !seat) errors.push(`${path}.options.additionalSeat must be an object`);
|
|
277
502
|
if (seat) {
|
|
278
503
|
const seatSku = skus.find((sku) => sku.id === seat.skuId);
|
|
@@ -295,17 +520,23 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
295
520
|
function scenario(input) {
|
|
296
521
|
return {
|
|
297
522
|
...input,
|
|
298
|
-
id: [input.suite, input.targetId, input.skuId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
|
|
523
|
+
id: [input.suite, input.targetId, input.skuId, input.parentSkuId, input.commercialCaseId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
|
|
299
524
|
};
|
|
300
525
|
}
|
|
301
526
|
function planChapterAcceptanceMatrix(manifest) {
|
|
302
527
|
const validation = validateChapterAcceptanceManifest(manifest);
|
|
303
528
|
if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
|
|
304
|
-
if (manifest.targets.some((site) => site.stripeMode !== "test")) {
|
|
305
|
-
throw new TypeError("provider acceptance runs are development-only and require Stripe test mode on every target");
|
|
529
|
+
if (manifest.targets.some((site) => site.stripeMode !== "test" || site.target.environment !== "dev" || site.target.dataEnvironment !== "dev")) {
|
|
530
|
+
throw new TypeError("provider acceptance runs are development-only and require Stripe test mode and dev runtime/data environments on every target");
|
|
306
531
|
}
|
|
307
532
|
const scenarios = [];
|
|
308
533
|
for (const site of manifest.targets) {
|
|
534
|
+
for (const vector of site.commercialCases ?? []) {
|
|
535
|
+
const base = { targetId: site.targetId, siteId: site.siteId, skuId: vector.skuId, commercialCaseId: vector.id };
|
|
536
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "commercial_policy_replay" }));
|
|
537
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "commercial_quote" }));
|
|
538
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "commercial_renewal" }));
|
|
539
|
+
}
|
|
309
540
|
for (const sku of site.skus.filter((candidate) => candidate.active)) {
|
|
310
541
|
scenarios.push(scenario({ suite: "hermetic", kind: "catalog_contract", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
|
|
311
542
|
if (sku.priceCents > 0) {
|
|
@@ -334,45 +565,34 @@ function planChapterAcceptanceMatrix(manifest) {
|
|
|
334
565
|
skuId: sku.id
|
|
335
566
|
}));
|
|
336
567
|
}
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "ordinary_interview"
|
|
340
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "renewal", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
|
|
341
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "refund_cancel", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
|
|
568
|
+
for (const membership of activeMemberships2(site)) {
|
|
569
|
+
const base = { targetId: site.targetId, siteId: site.siteId, skuId: membership.id };
|
|
570
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "ordinary_interview" }));
|
|
342
571
|
if (site.options.invitationInterviewWaiver) {
|
|
343
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "invitation_interview_waiver"
|
|
344
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "expired_interview_waiver"
|
|
345
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "replayed_interview_waiver"
|
|
572
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "invitation_interview_waiver" }));
|
|
573
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "expired_interview_waiver" }));
|
|
574
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "replayed_interview_waiver" }));
|
|
346
575
|
}
|
|
576
|
+
if (membership.priceCents === 0) continue;
|
|
577
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "renewal" }));
|
|
578
|
+
scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "refund_cancel" }));
|
|
347
579
|
if (site.options.additionalSeat) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
scenarios.push(scenario({
|
|
356
|
-
suite: "hermetic",
|
|
357
|
-
kind: "duplicate_seat_email",
|
|
358
|
-
targetId: site.targetId,
|
|
359
|
-
siteId: site.siteId,
|
|
360
|
-
skuId: site.options.additionalSeat.skuId
|
|
361
|
-
}));
|
|
362
|
-
scenarios.push(scenario({
|
|
363
|
-
suite: "hermetic",
|
|
364
|
-
kind: "expired_seat_window",
|
|
365
|
-
targetId: site.targetId,
|
|
366
|
-
siteId: site.siteId,
|
|
367
|
-
skuId: site.options.additionalSeat.skuId
|
|
368
|
-
}));
|
|
580
|
+
const seat = { ...base, skuId: site.options.additionalSeat.skuId, parentSkuId: membership.id };
|
|
581
|
+
for (const kind of ["named_seat", "seat_declined_refund", "unaccepted_seat_refund", "seat_membership_alignment"]) {
|
|
582
|
+
scenarios.push(scenario({ ...seat, suite: "provider_dev", kind }));
|
|
583
|
+
}
|
|
584
|
+
for (const kind of ["duplicate_seat_email", "expired_seat_window"]) {
|
|
585
|
+
scenarios.push(scenario({ ...seat, suite: "hermetic", kind }));
|
|
586
|
+
}
|
|
369
587
|
}
|
|
370
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "canceled_membership"
|
|
588
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "canceled_membership" }));
|
|
371
589
|
}
|
|
372
590
|
}
|
|
373
591
|
return scenarios;
|
|
374
592
|
}
|
|
375
593
|
export {
|
|
594
|
+
assertCommercialQuote,
|
|
595
|
+
assessChapterReleaseEvidence,
|
|
376
596
|
formatChapterAcceptanceRun,
|
|
377
597
|
planChapterAcceptanceMatrix,
|
|
378
598
|
probeChapterJoinContracts,
|