@ripplo/testing 0.0.1

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,56 @@
1
+ import { C as CookieEntry, R as RipploBuilder } from './builder-DTWMrbuv.js';
2
+ export { a as CookieOptions, D as DslConfig, P as Precondition, b as PreconditionDeps, c as PreconditionImpl, d as PreconditionRecord, e as ResolveDeps, S as SetupContext, T as TeardownContext, f as createRipplo } from './builder-DTWMrbuv.js';
3
+ import { CompileResult } from './compiler.js';
4
+ export { CompiledTest, compile } from './compiler.js';
5
+ export { D as DslNodeInput } from './step-DLfkKI3V.js';
6
+ import 'zod';
7
+ import '@ripplo/spec';
8
+
9
+ interface LintDiagnostic {
10
+ readonly message: string;
11
+ readonly rule: string;
12
+ readonly step: string | undefined;
13
+ readonly test: string;
14
+ }
15
+ interface LintResult {
16
+ readonly diagnostics: ReadonlyArray<LintDiagnostic>;
17
+ }
18
+ declare function lint(result: CompileResult): LintResult;
19
+
20
+ interface EngineResult {
21
+ readonly cookies: ReadonlyArray<CookieEntry>;
22
+ readonly data: Record<string, Record<string, string>>;
23
+ readonly error: string | undefined;
24
+ readonly executed: ReadonlyArray<string>;
25
+ readonly runId: string;
26
+ readonly success: boolean;
27
+ }
28
+ interface ExecuteBatchOptions {
29
+ readonly appUrl: string | undefined;
30
+ }
31
+ interface PreconditionEngine {
32
+ readonly executeBatch: (names: ReadonlyArray<string>, options?: ExecuteBatchOptions) => Promise<EngineResult>;
33
+ readonly teardown: (names: ReadonlyArray<string>, data: Record<string, Record<string, string>>) => Promise<void>;
34
+ }
35
+ declare function createEngine(ripplo: RipploBuilder): PreconditionEngine;
36
+
37
+ interface WebhookHeaders {
38
+ readonly "webhook-id": string | undefined;
39
+ readonly "webhook-signature": string | undefined;
40
+ readonly "webhook-timestamp": string | undefined;
41
+ }
42
+ declare function verifyWebhookSignature(payload: string, headers: WebhookHeaders, secret: string): boolean;
43
+ interface SerializedCookie {
44
+ readonly domain: string | undefined;
45
+ readonly expires: Date | undefined;
46
+ readonly httpOnly: boolean | undefined;
47
+ readonly name: string;
48
+ readonly path: string | undefined;
49
+ readonly sameSite: "lax" | "none" | "strict" | undefined;
50
+ readonly secure: boolean | undefined;
51
+ readonly value: string;
52
+ }
53
+ declare function serializeCookie(cookie: CookieEntry): SerializedCookie;
54
+ declare function buildSetCookieHeader(cookie: SerializedCookie): string;
55
+
56
+ export { CompileResult, CookieEntry, type EngineResult, type ExecuteBatchOptions, type LintDiagnostic, type LintResult, type PreconditionEngine, RipploBuilder, type SerializedCookie, buildSetCookieHeader, createEngine, lint, serializeCookie, verifyWebhookSignature };
package/dist/index.js ADDED
@@ -0,0 +1,464 @@
1
+ import {
2
+ compile
3
+ } from "./chunk-X2FROZPN.js";
4
+ import "./chunk-MGATMMCZ.js";
5
+ import {
6
+ buildSetCookieHeader,
7
+ createEngine,
8
+ dslConfigSchema,
9
+ readPreconditionName,
10
+ serializeCookie,
11
+ verifyWebhookSignature
12
+ } from "./chunk-KWUKVAGI.js";
13
+
14
+ // src/builder.ts
15
+ function createRipplo(rawConfig) {
16
+ const config = dslConfigSchema.parse(rawConfig);
17
+ const preconditions = [];
18
+ const tests = [];
19
+ const preconditionNames = /* @__PURE__ */ new Set();
20
+ const testNames = /* @__PURE__ */ new Set();
21
+ return {
22
+ getConfig: () => config,
23
+ getPreconditions: () => preconditions,
24
+ getTests: () => tests,
25
+ getUnimplemented() {
26
+ return {
27
+ preconditions: preconditions.filter((p) => !p.implemented).map((p) => p.name),
28
+ tests: tests.filter((t) => !t.implemented).map((t) => t.name)
29
+ };
30
+ },
31
+ implement(handle, impl) {
32
+ const preconditionName = readPreconditionName(handle);
33
+ const idx = preconditions.findIndex((p) => p.name === preconditionName);
34
+ if (idx === -1) {
35
+ throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
36
+ }
37
+ const existing = preconditions[idx];
38
+ if (existing == null) {
39
+ throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
40
+ }
41
+ if (existing.implemented) {
42
+ throw new Error(`Precondition "${preconditionName}" is already implemented`);
43
+ }
44
+ const mapping = existing.depMapping;
45
+ preconditions[idx] = {
46
+ ...existing,
47
+ implemented: true,
48
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TData narrows Record<string, string>, safe at engine boundary
49
+ teardown: impl.teardown,
50
+ setup: async (ctx, allDeps) => {
51
+ const resolved = {};
52
+ mapping.forEach(([key, depName]) => {
53
+ const data = allDeps[depName];
54
+ if (data != null) {
55
+ resolved[key] = data;
56
+ }
57
+ });
58
+ return impl.setup(ctx, resolved);
59
+ }
60
+ };
61
+ },
62
+ precondition(name) {
63
+ if (preconditionNames.has(name)) {
64
+ throw new Error(`Duplicate precondition name: "${name}"`);
65
+ }
66
+ preconditionNames.add(name);
67
+ return buildPrecondition(name, preconditions);
68
+ },
69
+ test(id) {
70
+ validateTestId(id);
71
+ if (testNames.has(id)) {
72
+ throw new Error(`Duplicate test id: "${id}"`);
73
+ }
74
+ testNames.add(id);
75
+ return buildTestName(id, tests);
76
+ }
77
+ };
78
+ }
79
+ function buildDepMapping(deps) {
80
+ return Object.entries(deps).map(([key, dep]) => [key, readPreconditionName(dep)]);
81
+ }
82
+ function remapDeps(mapping, allDeps) {
83
+ const resolved = {};
84
+ mapping.forEach(([key, depName]) => {
85
+ const data = allDeps[depName];
86
+ if (data != null) {
87
+ resolved[key] = data;
88
+ }
89
+ });
90
+ return resolved;
91
+ }
92
+ function makePrecondition(name) {
93
+ return { name };
94
+ }
95
+ function castBuilder(value) {
96
+ return value;
97
+ }
98
+ function pushStub(params) {
99
+ params.preconditions.push({
100
+ dependsOn: params.depMapping.map(([_, depName]) => depName),
101
+ depMapping: params.depMapping,
102
+ description: params.description,
103
+ implemented: false,
104
+ name: params.name,
105
+ returns: [],
106
+ teardown: void 0,
107
+ setup: () => Promise.resolve({})
108
+ });
109
+ return makePrecondition(params.name);
110
+ }
111
+ function buildPrecondition(name, preconditions) {
112
+ let description = "";
113
+ let currentDeps = {};
114
+ const self = {
115
+ contract: castBuilder(
116
+ () => pushStub({ depMapping: buildDepMapping(currentDeps), description, name, preconditions })
117
+ ),
118
+ requires: castBuilder(requiresImpl),
119
+ setup: castBuilder(setupNoDepsImpl),
120
+ description(text) {
121
+ description = text;
122
+ return self;
123
+ },
124
+ notImplemented() {
125
+ pushStub({ depMapping: [], description, name, preconditions });
126
+ return makePrecondition(name);
127
+ }
128
+ };
129
+ return self;
130
+ function requiresImpl(deps) {
131
+ currentDeps = deps;
132
+ const mapping = buildDepMapping(deps);
133
+ const withDeps = {
134
+ contract: castBuilder(
135
+ () => pushStub({ depMapping: mapping, description, name, preconditions })
136
+ ),
137
+ setup: castBuilder(setupWithDepsImpl),
138
+ description(text) {
139
+ description = text;
140
+ return withDeps;
141
+ },
142
+ notImplemented() {
143
+ pushStub({ depMapping: mapping, description, name, preconditions });
144
+ return makePrecondition(name);
145
+ }
146
+ };
147
+ return castBuilder(withDeps);
148
+ function setupWithDepsImpl(fn) {
149
+ return registerPrecondition(deps, fn);
150
+ }
151
+ }
152
+ function setupNoDepsImpl(fn) {
153
+ return registerPrecondition({}, async (ctx) => fn(ctx));
154
+ }
155
+ function registerPrecondition(deps, fn) {
156
+ const mapping = buildDepMapping(deps);
157
+ const def = {
158
+ dependsOn: mapping.map(([_, depName]) => depName),
159
+ depMapping: mapping,
160
+ description,
161
+ implemented: true,
162
+ name,
163
+ returns: [],
164
+ teardown: void 0,
165
+ setup: async (ctx, allDeps) => {
166
+ const resolved = remapDeps(mapping, allDeps);
167
+ return fn(ctx, resolved);
168
+ }
169
+ };
170
+ preconditions.push(def);
171
+ return {
172
+ teardown(tdFn) {
173
+ def.teardown = tdFn;
174
+ return makePrecondition(name);
175
+ }
176
+ };
177
+ }
178
+ }
179
+ var TEST_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
180
+ function validateTestId(id) {
181
+ if (!TEST_ID_PATTERN.test(id)) {
182
+ throw new Error(
183
+ `Invalid test id "${id}". Must be lowercase alphanumeric with hyphens (e.g. "my-test-id").`
184
+ );
185
+ }
186
+ }
187
+ function buildTestName(id, tests) {
188
+ return {
189
+ name(displayName) {
190
+ return buildTestRequires({ id, name: displayName, tests });
191
+ }
192
+ };
193
+ }
194
+ function buildTestRequires({ id, name, tests }) {
195
+ let description = "";
196
+ const self = {
197
+ requires: castBuilder(requiresImpl),
198
+ description(text) {
199
+ description = text;
200
+ return self;
201
+ }
202
+ };
203
+ return self;
204
+ function requiresImpl(reqs) {
205
+ const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
206
+ const requiresKeys = {};
207
+ Object.entries(reqs).forEach(([key, precondition]) => {
208
+ requiresKeys[key] = readPreconditionName(precondition);
209
+ });
210
+ return buildOutcome({ description, id, name, reqNames, requiresKeys, tests });
211
+ }
212
+ }
213
+ function buildOutcome({
214
+ description: initialDesc,
215
+ id,
216
+ name,
217
+ reqNames,
218
+ requiresKeys,
219
+ tests
220
+ }) {
221
+ let description = initialDesc;
222
+ return {
223
+ description(text) {
224
+ description = text;
225
+ return this;
226
+ },
227
+ expectedOutcome(text) {
228
+ return buildStartsAt({
229
+ description,
230
+ expectedOutcome: text,
231
+ id,
232
+ name,
233
+ reqNames,
234
+ requiresKeys,
235
+ tests
236
+ });
237
+ }
238
+ };
239
+ }
240
+ function buildStartsAt({
241
+ description,
242
+ expectedOutcome,
243
+ id,
244
+ name,
245
+ reqNames,
246
+ requiresKeys,
247
+ tests
248
+ }) {
249
+ return {
250
+ startsAt: castBuilder(startsAtImpl),
251
+ notImplemented() {
252
+ tests.push({
253
+ description,
254
+ expectedOutcome,
255
+ id,
256
+ implemented: false,
257
+ name,
258
+ requires: [...reqNames],
259
+ requiresKeys,
260
+ startsAtFn: void 0,
261
+ stepsFn: void 0
262
+ });
263
+ }
264
+ };
265
+ function startsAtImpl(fn) {
266
+ return {
267
+ steps: castBuilder(stepsImpl)
268
+ };
269
+ function stepsImpl(stepsFn) {
270
+ tests.push({
271
+ description,
272
+ expectedOutcome,
273
+ id,
274
+ implemented: true,
275
+ name,
276
+ requires: [...reqNames],
277
+ requiresKeys,
278
+ startsAtFn: fn,
279
+ stepsFn
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ // src/lint.ts
286
+ function lint(result) {
287
+ const diagnostics = [];
288
+ result.tests.forEach((test) => {
289
+ const nodes = getOrderedNodes(test);
290
+ const report = (diagnostic) => {
291
+ diagnostics.push({ ...diagnostic, test: test.slug });
292
+ };
293
+ RULES.forEach((rule) => {
294
+ rule(nodes, test, report);
295
+ });
296
+ });
297
+ return { diagnostics };
298
+ }
299
+ function getOrderedNodes(test) {
300
+ const result = [];
301
+ let currentId = test.spec.entryNode;
302
+ const visited = /* @__PURE__ */ new Set();
303
+ while (currentId != null && !visited.has(currentId)) {
304
+ visited.add(currentId);
305
+ const found = test.spec.nodes[currentId];
306
+ if (found == null) {
307
+ break;
308
+ }
309
+ result.push(found);
310
+ currentId = found.next;
311
+ }
312
+ return result;
313
+ }
314
+ function exactTextMatch(nodes, _test, report) {
315
+ nodes.forEach((node) => {
316
+ if (node.type === "assertText" && "operator" in node && node.operator !== "equals") {
317
+ report({
318
+ message: `${node.type} uses operator "${node.operator}" \u2014 only "equals" is allowed for determinism`,
319
+ rule: "exact-text-match",
320
+ step: node.label ?? node.id
321
+ });
322
+ }
323
+ });
324
+ }
325
+ function noHardcodedData(nodes, test, report) {
326
+ const hasVariables = Object.keys(test.spec.variables ?? {}).length > 0;
327
+ if (!hasVariables) {
328
+ return;
329
+ }
330
+ nodes.forEach((node) => {
331
+ if (node.type === "fill" && isStaticStringValue(node.value)) {
332
+ const val = node.value.value;
333
+ if (!isTemplateVar(val) && looksLikeDynamicData(val)) {
334
+ report({
335
+ message: `fill() uses hardcoded value "${val}" \u2014 consider using precondition data via {{namespace.key}}`,
336
+ rule: "no-hardcoded-data",
337
+ step: node.label ?? node.id
338
+ });
339
+ }
340
+ }
341
+ });
342
+ }
343
+ function preferPreconditionData(nodes, test, report) {
344
+ const variableKeys = Object.keys(test.spec.variables ?? {});
345
+ if (variableKeys.length === 0) {
346
+ return;
347
+ }
348
+ const hasAnyTemplateRef = nodes.some((node) => JSON.stringify(node).includes("{{"));
349
+ if (!hasAnyTemplateRef) {
350
+ report({
351
+ message: "Test requires preconditions but steps() never references precondition data \u2014 destructure and use it",
352
+ rule: "prefer-precondition-data",
353
+ step: void 0
354
+ });
355
+ }
356
+ }
357
+ function missingLabel(nodes, _test, report) {
358
+ nodes.forEach((node) => {
359
+ if (node.label == null || node.label.length === 0) {
360
+ report({
361
+ message: `Step "${node.id}" lacks .as("...") label \u2014 every step must be labeled`,
362
+ rule: "missing-label",
363
+ step: node.id
364
+ });
365
+ }
366
+ });
367
+ }
368
+ function noDuplicateLabels(nodes, _test, report) {
369
+ const seen = /* @__PURE__ */ new Map();
370
+ nodes.forEach((node) => {
371
+ if (node.label == null) {
372
+ return;
373
+ }
374
+ const existing = seen.get(node.label);
375
+ if (existing == null) {
376
+ seen.set(node.label, node.id);
377
+ } else {
378
+ report({
379
+ message: `Duplicate label "${node.label}" \u2014 also used by ${existing}`,
380
+ rule: "no-duplicate-labels",
381
+ step: node.label
382
+ });
383
+ }
384
+ });
385
+ }
386
+ function assertAfterAction(nodes, _test, report) {
387
+ let consecutiveActions = 0;
388
+ nodes.forEach((node) => {
389
+ if (isAssertionNode(node)) {
390
+ consecutiveActions = 0;
391
+ } else {
392
+ consecutiveActions++;
393
+ if (consecutiveActions === 3) {
394
+ report({
395
+ message: "3+ consecutive actions without an assertion \u2014 add verification between actions",
396
+ rule: "assert-after-action",
397
+ step: node.label ?? node.id
398
+ });
399
+ }
400
+ }
401
+ });
402
+ }
403
+ function assertMatchesOutcome(nodes, _test, report) {
404
+ if (nodes.length === 0) {
405
+ return;
406
+ }
407
+ const lastNode = nodes.at(-1);
408
+ if (lastNode != null && !isAssertionNode(lastNode)) {
409
+ report({
410
+ message: "Last step is an action, not an assertion \u2014 expectedOutcome should be verified by assertions at the end",
411
+ rule: "assert-matches-outcome",
412
+ step: lastNode.label ?? lastNode.id
413
+ });
414
+ }
415
+ }
416
+ function noEmptySteps(nodes, _test, report) {
417
+ if (nodes.length === 0) {
418
+ report({
419
+ message: "Test has zero steps",
420
+ rule: "no-empty-steps",
421
+ step: void 0
422
+ });
423
+ }
424
+ }
425
+ function isStaticStringValue(val) {
426
+ if (typeof val !== "object" || val == null) {
427
+ return false;
428
+ }
429
+ if (!("type" in val) || val.type !== "static") {
430
+ return false;
431
+ }
432
+ if (!("value" in val) || typeof val.value !== "string") {
433
+ return false;
434
+ }
435
+ return true;
436
+ }
437
+ function isTemplateVar(value) {
438
+ return value.includes("{{");
439
+ }
440
+ function looksLikeDynamicData(value) {
441
+ return value.includes("@") || /\b[a-f0-9]{8,}\b/.test(value) || /^(test|ripplo|example|sample|demo)/i.test(value);
442
+ }
443
+ function isAssertionNode(node) {
444
+ return node.type.startsWith("assert");
445
+ }
446
+ var RULES = [
447
+ exactTextMatch,
448
+ noHardcodedData,
449
+ preferPreconditionData,
450
+ missingLabel,
451
+ noDuplicateLabels,
452
+ assertAfterAction,
453
+ assertMatchesOutcome,
454
+ noEmptySteps
455
+ ];
456
+ export {
457
+ buildSetCookieHeader,
458
+ compile,
459
+ createEngine,
460
+ createRipplo,
461
+ lint,
462
+ serializeCookie,
463
+ verifyWebhookSignature
464
+ };
@@ -0,0 +1,30 @@
1
+ type AriaRole = "alert" | "alertdialog" | "button" | "checkbox" | "combobox" | "dialog" | "form" | "grid" | "heading" | "img" | "link" | "list" | "listbox" | "listitem" | "menu" | "menuitem" | "navigation" | "option" | "progressbar" | "radio" | "region" | "row" | "searchbox" | "separator" | "slider" | "spinbutton" | "status" | "switch" | "tab" | "tabpanel" | "textbox" | "toolbar" | "tooltip" | "tree" | "treeitem";
2
+ type LocatorSpec = RoleLocatorSpec | TestIdLocatorSpec;
3
+ interface RoleLocatorSpec {
4
+ readonly by: "role";
5
+ readonly name: string | undefined;
6
+ readonly role: AriaRole;
7
+ }
8
+ interface TestIdLocatorSpec {
9
+ readonly by: "testId";
10
+ readonly value: string;
11
+ }
12
+ declare const STRATEGY_BRAND: unique symbol;
13
+ declare const ROLE_BRAND: unique symbol;
14
+ declare const LOCATOR_INTERNAL: unique symbol;
15
+ interface Locator<TStrategy extends string> {
16
+ readonly [LOCATOR_INTERNAL]: LocatorSpec;
17
+ readonly [STRATEGY_BRAND]: TStrategy;
18
+ }
19
+ interface RoleLocator<TRole extends AriaRole> extends Locator<"role"> {
20
+ readonly [ROLE_BRAND]: TRole;
21
+ }
22
+ declare function readLocator(loc: Locator<string>): LocatorSpec;
23
+ type AnyLocator = Locator<"role" | "testId">;
24
+ type InputLocator = Locator<"testId"> | RoleLocator<"combobox" | "searchbox" | "spinbutton" | "textbox">;
25
+ type SelectLocator = Locator<"testId"> | RoleLocator<"combobox" | "listbox">;
26
+ type CheckLocator = Locator<"testId"> | RoleLocator<"checkbox" | "switch">;
27
+ declare function role<TRole extends AriaRole>(ariaRole: TRole, name?: string): RoleLocator<TRole>;
28
+ declare function testId(id: string): Locator<"testId">;
29
+
30
+ export { type AnyLocator, type AriaRole, type CheckLocator, type InputLocator, type Locator, type LocatorSpec, type RoleLocator, type SelectLocator, readLocator, role, testId };
@@ -0,0 +1,10 @@
1
+ import {
2
+ readLocator,
3
+ role,
4
+ testId
5
+ } from "./chunk-DCJBLS2U.js";
6
+ export {
7
+ readLocator,
8
+ role,
9
+ testId
10
+ };
@@ -0,0 +1,12 @@
1
+ import { R as RipploBuilder } from './builder-DTWMrbuv.js';
2
+ import 'zod';
3
+ import './step-DLfkKI3V.js';
4
+ import '@ripplo/spec';
5
+
6
+ interface CreateNextHandlerParams {
7
+ readonly ripplo: RipploBuilder;
8
+ }
9
+ type NextHandler = (req: Request) => Promise<Response>;
10
+ declare function createNextHandler({ ripplo }: CreateNextHandlerParams): NextHandler;
11
+
12
+ export { type CreateNextHandlerParams, createNextHandler };
package/dist/nextjs.js ADDED
@@ -0,0 +1,105 @@
1
+ import {
2
+ batchRequestSchema,
3
+ buildSetCookieHeader,
4
+ createEngine,
5
+ serializeCookie,
6
+ teardownRequestSchema,
7
+ verifyWebhookSignature
8
+ } from "./chunk-KWUKVAGI.js";
9
+
10
+ // src/adapters/nextjs.ts
11
+ function createNextHandler({ ripplo }) {
12
+ const engine = createEngine(ripplo);
13
+ const webhookSecret = ripplo.getConfig().webhookSecret;
14
+ return async function handler(req) {
15
+ const action = lastPathSegment(req.url);
16
+ if (action !== "execute-batch" && action !== "teardown") {
17
+ return jsonResponse({ error: "Not found" }, 404);
18
+ }
19
+ const verified = await verifyAndReadBody(req, webhookSecret);
20
+ if ("error" in verified) {
21
+ return verified.error;
22
+ }
23
+ if (action === "execute-batch") {
24
+ return handleExecuteBatch({ body: verified.body, engine, req });
25
+ }
26
+ return handleTeardown({ body: verified.body, engine });
27
+ };
28
+ }
29
+ async function handleExecuteBatch({
30
+ body,
31
+ engine,
32
+ req
33
+ }) {
34
+ const json = tryParseJson(body);
35
+ const parsed = json == null ? null : batchRequestSchema.safeParse(json);
36
+ if (parsed == null || !parsed.success) {
37
+ return jsonResponse({ error: "Invalid request body", success: false }, 400);
38
+ }
39
+ const host = req.headers.get("host");
40
+ if (host == null || host.length === 0) {
41
+ return jsonResponse({ error: "Missing host header", success: false }, 400);
42
+ }
43
+ const proto = req.headers.get("x-forwarded-proto") ?? "http";
44
+ const appUrl = `${proto}://${host}`;
45
+ const result = await engine.executeBatch(parsed.data.preconditions, { appUrl });
46
+ const headers = new Headers({ "content-type": "application/json" });
47
+ result.cookies.forEach((cookie) => {
48
+ headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
49
+ });
50
+ return new Response(
51
+ JSON.stringify({
52
+ data: result.data,
53
+ error: result.error,
54
+ executed: result.executed,
55
+ runId: result.runId,
56
+ success: result.success
57
+ }),
58
+ { headers, status: 200 }
59
+ );
60
+ }
61
+ async function handleTeardown({ body, engine }) {
62
+ const json = tryParseJson(body);
63
+ const parsed = json == null ? null : teardownRequestSchema.safeParse(json);
64
+ if (parsed == null || !parsed.success) {
65
+ return jsonResponse({ error: "Invalid request body", success: false }, 400);
66
+ }
67
+ await engine.teardown(parsed.data.preconditions, parsed.data.data);
68
+ return jsonResponse({ success: true }, 200);
69
+ }
70
+ async function verifyAndReadBody(req, webhookSecret) {
71
+ if (webhookSecret.length === 0) {
72
+ return { error: jsonResponse({ error: "Webhook secret not configured" }, 403) };
73
+ }
74
+ const body = await req.text();
75
+ const headers = {
76
+ "webhook-id": req.headers.get("webhook-id") ?? void 0,
77
+ "webhook-signature": req.headers.get("webhook-signature") ?? void 0,
78
+ "webhook-timestamp": req.headers.get("webhook-timestamp") ?? void 0
79
+ };
80
+ if (!verifyWebhookSignature(body, headers, webhookSecret)) {
81
+ return { error: jsonResponse({ error: "Invalid webhook signature" }, 401) };
82
+ }
83
+ return { body };
84
+ }
85
+ function lastPathSegment(url) {
86
+ const pathname = new URL(url).pathname;
87
+ const segments = pathname.split("/").filter((s) => s.length > 0);
88
+ return segments.at(-1) ?? "";
89
+ }
90
+ function tryParseJson(body) {
91
+ try {
92
+ return JSON.parse(body);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ function jsonResponse(payload, status) {
98
+ return new Response(JSON.stringify(payload), {
99
+ headers: { "content-type": "application/json" },
100
+ status
101
+ });
102
+ }
103
+ export {
104
+ createNextHandler
105
+ };
@@ -0,0 +1,19 @@
1
+ import { SpecNode } from '@ripplo/spec';
2
+
3
+ type DslNodeType = "assertAttribute" | "assertChecked" | "assertCount" | "assertDisabled" | "assertEnabled" | "assertFocused" | "assertNotChecked" | "assertNotFocused" | "assertNotVisible" | "assertText" | "assertUrl" | "assertValue" | "assertVisible" | "check" | "clear" | "click" | "clipboard" | "dblclick" | "drag" | "extractText" | "fill" | "focus" | "goto" | "handleDialog" | "hover" | "press" | "rightClick" | "scrollIntoView" | "select" | "setPermission" | "setViewport" | "type" | "uncheck" | "upload";
4
+ type StepBaseFields = "comment" | "id" | "label" | "next" | "timeout";
5
+ type DslNodeInput = SpecNode extends infer N ? N extends SpecNode ? N["type"] extends DslNodeType ? Omit<N, StepBaseFields> : never : never : never;
6
+
7
+ declare const STEP_INTERNAL: unique symbol;
8
+ interface StepData<TNode extends DslNodeInput> {
9
+ readonly label: string;
10
+ readonly node: TNode;
11
+ }
12
+ interface Step<TNode extends DslNodeInput = DslNodeInput> {
13
+ readonly [STEP_INTERNAL]: StepData<TNode>;
14
+ }
15
+ interface UnlabeledStep<TNode extends DslNodeInput = DslNodeInput> {
16
+ as(label: string): Step<TNode>;
17
+ }
18
+
19
+ export type { DslNodeInput as D, Step as S, UnlabeledStep as U };