@ripplo/testing 0.4.8 → 0.5.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 CHANGED
@@ -77,6 +77,17 @@ export const preconditions = { authLoggedIn, dataProject };
77
77
 
78
78
  `precondition(name)` → `.description()` → `.requires()` (optional) → `.contract<T>()`. Each field in `T` must be a primitive — `string`, `number`, or `boolean` (run-scoped value).
79
79
 
80
+ #### Preconditions are create-only
81
+
82
+ Precondition setups must be **additive**: insert new rows, don't `update` or `delete` existing ones. Concurrent runs share a database, and a `WHERE` clause that looks scoped to the current run can match rows that belong to another run in flight. Even when writes are reliably scoped, mutating a row another precondition produced creates ordering dependencies that break under composition.
83
+
84
+ If a test needs a non-default state, the precondition that **creates** the underlying row should accept that state as input — don't seed a default and mutate it from a downstream precondition.
85
+
86
+ Allowed exceptions:
87
+
88
+ - `upsert` on a row whose primary key makes it a 1:1 settings record for the current run (e.g. a per-`(userId, resourceId)` view). Treat it as create-with-default.
89
+ - Teardown may delete — but only data the precondition itself created.
90
+
80
91
  ### Observers
81
92
 
82
93
  ```typescript
@@ -278,12 +289,21 @@ import { prisma } from "../lib/prisma.js";
278
289
  export const engine = createEngine(ripplo, {
279
290
  preconditions: {
280
291
  authLoggedIn: {
281
- setup: async (ctx) => {
282
- // create user, set cookies via ctx.setCookie()
283
- return { userId: ctx.uniqueId("user") };
292
+ // setup receives a *batch* of items — one per concurrent run that requires this precondition.
293
+ // Issue one bulk write (createMany) and return one result per item, in input order.
294
+ setup: async (items) => {
295
+ const seeds = items.map(({ ctx }) => ({
296
+ id: ctx.uniqueId("user"),
297
+ email: ctx.uniqueEmail(),
298
+ }));
299
+ await prisma.user.createMany({ data: seeds });
300
+ // each item.ctx.setCookie(...) flows back to that run's browser
301
+ return seeds.map(({ id }) => ({ userId: id }));
284
302
  },
285
- teardown: async (ctx) => {
286
- // clean up using ctx.data.userId
303
+ teardown: async (items) => {
304
+ await prisma.user.deleteMany({
305
+ where: { id: { in: items.map((it) => it.ctx.data.userId) } },
306
+ });
287
307
  },
288
308
  },
289
309
  dataProject: notImplemented("awaiting prisma seed helper"), // stub for planning
@@ -302,15 +322,17 @@ export const engine = createEngine(ripplo, {
302
322
  });
303
323
  ```
304
324
 
325
+ `setup` and `teardown` are **batched**: the runtime collects all concurrent runs that need a precondition within a 200ms window and calls the impl once with the full batch. Use `createMany` / `deleteMany` over per-item `create` / `delete` to keep DB load proportional to wall-clock time, not run count. The result array length must equal the input array length and order must be preserved (the engine zips by index back to runs).
326
+
305
327
  ### Setup context
306
328
 
307
- `ctx` passed to each precondition `setup`:
329
+ `item.ctx` available inside each batched setup item:
308
330
 
309
331
  - `ctx.runId` — unique 12-char id for this run
310
332
  - `ctx.fixed<T extends string | number | boolean>(value: T)` — static test value (any primitive)
311
333
  - `ctx.uniqueId(prefix)` — `ripplo-test-<prefix>-<runId>`
312
334
  - `ctx.uniqueEmail()` — `ripplo-test-<runId>@test.ripplo.ai`
313
- - `ctx.setCookie(name, value, options?)` — forwarded to the test browser as `Set-Cookie`
335
+ - `ctx.setCookie(name, value, options?)` — applied to the run's browser context before the test starts
314
336
 
315
337
  Helpers return plain primitives — interpolate, JSON.stringify, or pass through observer params directly. The return type is type-branded, so a hardcoded literal in a `setup` return fails at compile time.
316
338
 
@@ -447,10 +469,14 @@ const app = new Elysia().group("/ripplo", (g) => g.use(createElysiaHandler({ ena
447
469
  For unsupported frameworks. The adapters above are thin wrappers over this API.
448
470
 
449
471
  ```ts
450
- import { buildSetCookieHeader, serializeCookie, verifyWebhookSignature } from "@ripplo/testing";
472
+ import {
473
+ readAdapterWebhookSecret,
474
+ toBatchRunResults,
475
+ verifyWebhookSignature,
476
+ } from "@ripplo/testing";
451
477
  import { engine } from "./test/engine.js";
452
478
 
453
- const webhookSecret = engine.getConfig().webhookSecret;
479
+ const webhookSecret = readAdapterWebhookSecret();
454
480
 
455
481
  async function executePreconditions(req: Request): Promise<Response> {
456
482
  const body = await req.text();
@@ -467,27 +493,31 @@ async function executePreconditions(req: Request): Promise<Response> {
467
493
  return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 401 });
468
494
  }
469
495
 
470
- const { preconditions } = JSON.parse(body);
496
+ // Request body is a batch: { batch: [{ runId, preconditions: [...names] }, ...] }
497
+ const { batch } = JSON.parse(body);
471
498
  const appUrl = `${req.headers.get("x-forwarded-proto") ?? "http"}://${req.headers.get("host")}`;
472
- const result = await engine.executePreconditions(preconditions, { appUrl });
473
-
474
- const headers = new Headers({ "content-type": "application/json" });
475
- result.cookies.forEach((c) =>
476
- headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(c))),
499
+ const results = await engine.executePreconditions(
500
+ batch.map((b) => ({ runId: b.runId, names: b.preconditions })),
501
+ { appUrl },
477
502
  );
478
- return new Response(JSON.stringify(result), { headers });
503
+
504
+ // Response body: { results: [{ runId, ok, cookies, data, executed } | { runId, ok: false, error }] }
505
+ return new Response(JSON.stringify({ results: toBatchRunResults(results) }), {
506
+ headers: { "content-type": "application/json" },
507
+ });
479
508
  }
480
509
  // teardown-preconditions and execute-observer follow the same verify-then-dispatch pattern.
510
+ // teardown's request shape is { batch: [{ runId, preconditions, data }, ...] }, response { results: [{ runId, ok, error? }] }.
481
511
  ```
482
512
 
483
- You're responsible for: webhook verification (always before invoking the engine), routing the three endpoints, forwarding `result.cookies` to the test browser as `Set-Cookie` headers, and reading the raw body for signature verification before `JSON.parse`.
513
+ You're responsible for: webhook verification (always before invoking the engine), routing the three endpoints, calling `toBatchRunResults` / `toTeardownResults` to shape the response body, and reading the raw body for signature verification before `JSON.parse`. Cookies travel inside the JSON response body — never as `Set-Cookie` headers; the runtime parses them out and applies them to the test browser context.
484
514
 
485
515
  ## Security & parallelism
486
516
 
487
517
  - All requests signed via Standard Webhooks (HMAC-SHA256). Headers: `webhook-id`, `webhook-timestamp`, `webhook-signature`. **Always verify before executing.**
488
518
  - `ENABLE_RIPPLO_TESTING` gates every adapter. Never expose in production.
489
519
  - Use `ctx.uniqueId(prefix)` / `ctx.uniqueEmail()` so parallel runs don't collide.
490
- - Return created entity IDs in the data contract; teardown deletes only that run's data using the captured `runId`. Never bulk-delete.
520
+ - Return created entity IDs in the data contract; teardown deletes only data this run created. Bulk operations are fine — and encouraged — as long as the `WHERE` is scoped to the batch's own ids (`deleteMany({ where: { id: { in: items.map((it) => it.ctx.data.userId) } } })`). Never write a `WHERE` that could match another run's rows.
491
521
 
492
522
  ## Lockfile
493
523
 
package/dist/assert.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as ObserverHandle, a as ObserverInput, P as Primitive, b as ObserverBudgetTier } from './types-16SB7zjP.js';
1
+ import { O as ObserverHandle, a as ObserverInput, P as Primitive, b as ObserverBudgetTier } from './types-BzZrl65Z.js';
2
2
  import { Variable, StaticStringRef, VariableRef } from './control.js';
3
3
  import { U as UnlabeledStep } from './step-De52hTLd.js';
4
4
  import { CheckLocator, AnyLocator } from './locators.js';
@@ -1,4 +1,4 @@
1
- import { P as Primitive, O as ObserverHandle, h as Precondition, m as PreconditionData, j as TestDefinition, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, e as ObserverContext, g as ObserverOutcome, S as SetupContext, k as TestValue, T as TeardownContext } from './types-16SB7zjP.js';
1
+ import { P as Primitive, O as ObserverHandle, h as Precondition, m as PreconditionData, j as TestDefinition, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, e as ObserverContext, g as ObserverOutcome, S as SetupContext, k as TestValue, T as TeardownContext } from './types-BzZrl65Z.js';
2
2
  import { ObserverBudget } from '@ripplo/spec';
3
3
  import { S as Step } from './step-De52hTLd.js';
4
4
 
@@ -90,9 +90,16 @@ interface TestNeedsCoverage {
90
90
  type PreconditionSetupResult<TData extends Record<string, Primitive>> = {
91
91
  readonly [K in keyof TData]: TestValue<TData[K]>;
92
92
  };
93
+ interface PreconditionSetupItem<TDeps extends Record<string, Record<string, Primitive>>> {
94
+ readonly ctx: SetupContext;
95
+ readonly deps: TDeps;
96
+ }
97
+ interface PreconditionTeardownItem<TData extends Record<string, Primitive>> {
98
+ readonly ctx: TeardownContext<TData>;
99
+ }
93
100
  interface PreconditionImpl<TData extends Record<string, Primitive>, TDeps extends Record<string, Record<string, Primitive>>> {
94
- readonly setup: (ctx: SetupContext, deps: TDeps) => Promise<PreconditionSetupResult<TData>>;
95
- readonly teardown: (ctx: TeardownContext<TData>) => Promise<void>;
101
+ readonly setup: (items: ReadonlyArray<PreconditionSetupItem<TDeps>>) => Promise<ReadonlyArray<PreconditionSetupResult<TData>>>;
102
+ readonly teardown: (items: ReadonlyArray<PreconditionTeardownItem<TData>>) => Promise<void>;
96
103
  }
97
104
  type ObserverImplFn<TInput extends Record<string, Primitive>> = (ctx: ObserverContext, params: TInput) => Promise<ObserverOutcome>;
98
105
  interface TestOptions {
@@ -10,26 +10,28 @@ function readAdapterWebhookSecret() {
10
10
  }
11
11
  return value;
12
12
  }
13
+ var primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
14
+ var dataSchema = z.record(z.string(), z.record(z.string(), primitiveSchema));
13
15
  var batchRequestSchema = z.object({
14
- preconditions: z.array(z.string().min(1))
16
+ batch: z.array(
17
+ z.object({
18
+ preconditions: z.array(z.string().min(1)),
19
+ runId: z.string().min(1)
20
+ })
21
+ ).min(1)
15
22
  });
16
23
  var teardownRequestSchema = z.object({
17
- data: z.record(z.string(), z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))),
18
- preconditions: z.array(z.string().min(1))
24
+ batch: z.array(
25
+ z.object({
26
+ data: dataSchema,
27
+ preconditions: z.array(z.string().min(1)),
28
+ runId: z.string().min(1)
29
+ })
30
+ ).min(1)
19
31
  });
20
32
  var observerRequestSchema = z.object({
21
33
  observer: z.string().min(1).max(200),
22
- params: z.record(z.string().max(200), z.union([z.string(), z.number(), z.boolean()]))
23
- });
24
- var observerOutcomeSchema = z.discriminatedUnion("kind", [
25
- z.object({ kind: z.literal("pass") }),
26
- z.object({ kind: z.literal("retry"), reason: z.string() }),
27
- z.object({ kind: z.literal("fail"), reason: z.string() })
28
- ]);
29
- var observerResponseSchema = z.object({
30
- error: z.string().optional(),
31
- outcome: observerOutcomeSchema.optional(),
32
- success: z.boolean()
34
+ params: z.record(z.string().max(200), primitiveSchema)
33
35
  });
34
36
  function verifyWebhookSignature(payload, headers, secret) {
35
37
  try {
@@ -50,7 +52,7 @@ function verifyWebhookSignature(payload, headers, secret) {
50
52
  function serializeCookie(cookie) {
51
53
  return {
52
54
  domain: cookie.options?.domain,
53
- expires: cookie.options?.expires == null ? void 0 : new Date(cookie.options.expires * 1e3),
55
+ expires: cookie.options?.expires,
54
56
  httpOnly: cookie.options?.httpOnly,
55
57
  name: cookie.name,
56
58
  path: cookie.options?.path,
@@ -59,28 +61,19 @@ function serializeCookie(cookie) {
59
61
  value: cookie.value
60
62
  };
61
63
  }
62
- function buildSetCookieHeader(cookie) {
63
- const parts = [`${cookie.name}=${cookie.value}`];
64
- if (cookie.domain != null) {
65
- parts.push(`Domain=${cookie.domain}`);
66
- }
67
- if (cookie.path != null) {
68
- parts.push(`Path=${cookie.path}`);
69
- }
70
- if (cookie.expires != null) {
71
- parts.push(`Expires=${cookie.expires.toUTCString()}`);
72
- }
73
- if (cookie.httpOnly === true) {
74
- parts.push("HttpOnly");
75
- }
76
- if (cookie.secure === true) {
77
- parts.push("Secure");
78
- }
79
- if (cookie.sameSite != null) {
80
- const capitalized = cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1);
81
- parts.push(`SameSite=${capitalized}`);
82
- }
83
- return parts.join("; ");
64
+ function toBatchRunResults(results) {
65
+ return results.map(
66
+ (r) => r.success ? {
67
+ cookies: r.cookies.map((c) => serializeCookie(c)),
68
+ data: r.data,
69
+ executed: [...r.executed],
70
+ ok: true,
71
+ runId: r.runId
72
+ } : { error: r.error ?? "unknown error", ok: false, runId: r.runId }
73
+ );
74
+ }
75
+ function toTeardownResults(results) {
76
+ return results.map((r) => ({ error: r.error, ok: r.success, runId: r.runId }));
84
77
  }
85
78
 
86
79
  export {
@@ -90,5 +83,6 @@ export {
90
83
  observerRequestSchema,
91
84
  verifyWebhookSignature,
92
85
  serializeCookie,
93
- buildSetCookieHeader
86
+ toBatchRunResults,
87
+ toTeardownResults
94
88
  };
@@ -1,6 +1,6 @@
1
1
  import { Observer, Precondition, WorkflowSpec } from '@ripplo/spec';
2
- import { d as RipploBuilder } from './builder-BMjy83Iy.js';
3
- import './types-16SB7zjP.js';
2
+ import { d as RipploBuilder } from './builder-DiVz3t1D.js';
3
+ import './types-BzZrl65Z.js';
4
4
  import './step-De52hTLd.js';
5
5
 
6
6
  interface CompiledFixture {
package/dist/elysia.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/elysia.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  batchRequestSchema,
3
- buildSetCookieHeader,
4
3
  observerRequestSchema,
5
4
  readAdapterWebhookSecret,
6
- serializeCookie,
7
5
  teardownRequestSchema,
6
+ toBatchRunResults,
7
+ toTeardownResults,
8
8
  verifyWebhookSignature
9
- } from "./chunk-V6LMXKGL.js";
9
+ } from "./chunk-XO36IU66.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/elysia.ts
@@ -27,29 +27,17 @@ function build({ enabled, engine }) {
27
27
  }
28
28
  const parsed = parseWith(gate.body, batchRequestSchema);
29
29
  if (parsed == null) {
30
- return jsonResponse({ error: "Invalid request body", success: false }, 400);
30
+ return jsonResponse({ error: "Invalid request body" }, 400);
31
31
  }
32
32
  const host = request.headers.get("host");
33
33
  if (host == null || host.length === 0) {
34
- return jsonResponse({ error: "Missing host header", success: false }, 400);
34
+ return jsonResponse({ error: "Missing host header" }, 400);
35
35
  }
36
36
  const proto = request.headers.get("x-forwarded-proto") ?? "http";
37
37
  const appUrl = `${proto}://${host}`;
38
- const result = await engine.executePreconditions(parsed.preconditions, { appUrl });
39
- const headers = new Headers({ "content-type": "application/json" });
40
- result.cookies.forEach((cookie) => {
41
- headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
42
- });
43
- return new Response(
44
- JSON.stringify({
45
- data: result.data,
46
- error: result.error,
47
- executed: result.executed,
48
- runId: result.runId,
49
- success: result.success
50
- }),
51
- { headers, status: 200 }
52
- );
38
+ const items = parsed.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
39
+ const results = await engine.executePreconditions(items, { appUrl });
40
+ return jsonResponse({ results: toBatchRunResults(results) }, 200);
53
41
  }).put("/execute-observer", async ({ request }) => {
54
42
  const gate = await verifyAndReadBody(request, webhookSecret);
55
43
  if ("response" in gate) {
@@ -71,10 +59,15 @@ function build({ enabled, engine }) {
71
59
  }
72
60
  const parsed = parseWith(gate.body, teardownRequestSchema);
73
61
  if (parsed == null) {
74
- return jsonResponse({ error: "Invalid request body", success: false }, 400);
62
+ return jsonResponse({ error: "Invalid request body" }, 400);
75
63
  }
76
- await engine.teardown(parsed.preconditions, parsed.data);
77
- return jsonResponse({ success: true }, 200);
64
+ const items = parsed.batch.map((b) => ({
65
+ data: b.data,
66
+ names: b.preconditions,
67
+ runId: b.runId
68
+ }));
69
+ const results = await engine.teardown(items);
70
+ return jsonResponse({ results: toTeardownResults(results) }, 200);
78
71
  });
79
72
  }
80
73
  async function verifyAndReadBody(req, webhookSecret) {
@@ -1,7 +1,7 @@
1
- import { c as PreconditionRegistry, a as ObserverRegistry, O as ObserverImplFn, P as PreconditionImpl, e as RipploInstance } from './builder-BMjy83Iy.js';
2
- import { P as Primitive, g as ObserverOutcome, C as CookieEntry, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, O as ObserverHandle, h as Precondition } from './types-16SB7zjP.js';
1
+ import { c as PreconditionRegistry, a as ObserverRegistry, O as ObserverImplFn, P as PreconditionImpl, e as RipploInstance } from './builder-DiVz3t1D.js';
2
+ import { P as Primitive, g as ObserverOutcome, C as CookieEntry, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, O as ObserverHandle, h as Precondition } from './types-BzZrl65Z.js';
3
3
 
4
- interface EngineResult {
4
+ interface EngineRunResult {
5
5
  readonly cookies: ReadonlyArray<CookieEntry>;
6
6
  readonly data: Record<string, Record<string, Primitive>>;
7
7
  readonly error: string | undefined;
@@ -12,6 +12,20 @@ interface EngineResult {
12
12
  interface ExecuteBatchOptions {
13
13
  readonly appUrl: string | undefined;
14
14
  }
15
+ interface ExecuteBatchItem {
16
+ readonly names: ReadonlyArray<string>;
17
+ readonly runId: string;
18
+ }
19
+ interface TeardownBatchItem {
20
+ readonly data: Record<string, Record<string, Primitive>>;
21
+ readonly names: ReadonlyArray<string>;
22
+ readonly runId: string;
23
+ }
24
+ interface TeardownRunResult {
25
+ readonly error: string | undefined;
26
+ readonly runId: string;
27
+ readonly success: boolean;
28
+ }
15
29
  interface ObserverExecutionResult {
16
30
  readonly error: string | undefined;
17
31
  readonly outcome: ObserverOutcome | undefined;
@@ -19,11 +33,11 @@ interface ObserverExecutionResult {
19
33
  }
20
34
  interface RipploEngine {
21
35
  readonly executeObserver: (name: string, params: Record<string, Primitive>) => Promise<ObserverExecutionResult>;
22
- readonly executePreconditions: (names: ReadonlyArray<string>, options?: ExecuteBatchOptions) => Promise<EngineResult>;
36
+ readonly executePreconditions: (items: ReadonlyArray<ExecuteBatchItem>, options?: ExecuteBatchOptions) => Promise<ReadonlyArray<EngineRunResult>>;
23
37
  readonly getObservers: () => ReadonlyArray<ObserverDefinition>;
24
38
  readonly getPreconditions: () => ReadonlyArray<PreconditionDefinition>;
25
39
  readonly getUnimplemented: () => UnimplementedItems;
26
- readonly teardown: (names: ReadonlyArray<string>, data: Record<string, Record<string, Primitive>>) => Promise<void>;
40
+ readonly teardown: (items: ReadonlyArray<TeardownBatchItem>) => Promise<ReadonlyArray<TeardownRunResult>>;
27
41
  }
28
42
  declare const NOT_IMPLEMENTED_BRAND: unique symbol;
29
43
  interface NotImplemented {
@@ -43,4 +57,4 @@ interface EngineImpls<P extends PreconditionRegistry, O extends ObserverRegistry
43
57
  }
44
58
  declare function createEngine<P extends PreconditionRegistry, O extends ObserverRegistry>(ripplo: RipploInstance<P, O>, impls: EngineImpls<P, O>): RipploEngine;
45
59
 
46
- export { type EngineImpls as E, type NotImplemented as N, type ObserverImplFnFor as O, type PreconditionImplFor as P, type RipploEngine as R, type EngineResult as a, type ExecuteBatchOptions as b, createEngine as c, notImplemented as n };
60
+ export { type EngineRunResult as E, type NotImplemented as N, type ObserverImplFnFor as O, type PreconditionImplFor as P, type RipploEngine as R, type TeardownRunResult as T, type EngineImpls as a, type ExecuteBatchItem as b, type ExecuteBatchOptions as c, type TeardownBatchItem as d, createEngine as e, notImplemented as n };
package/dist/express.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Router } from 'express';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/express.js CHANGED
@@ -2,10 +2,11 @@ import {
2
2
  batchRequestSchema,
3
3
  observerRequestSchema,
4
4
  readAdapterWebhookSecret,
5
- serializeCookie,
6
5
  teardownRequestSchema,
6
+ toBatchRunResults,
7
+ toTeardownResults,
7
8
  verifyWebhookSignature
8
- } from "./chunk-V6LMXKGL.js";
9
+ } from "./chunk-XO36IU66.js";
9
10
  import "./chunk-4MGIQFAJ.js";
10
11
 
11
12
  // src/adapters/express.ts
@@ -29,22 +30,13 @@ function createExpressHandler({ enabled, engine }) {
29
30
  router.put("/execute-preconditions", (req, res) => {
30
31
  const parsed = batchRequestSchema.safeParse(req.body);
31
32
  if (!parsed.success) {
32
- res.status(400).json({ error: "Invalid request body", success: false });
33
+ res.status(400).json({ error: "Invalid request body" });
33
34
  return;
34
35
  }
35
36
  const appUrl = `${req.protocol}://${req.get("host") ?? ""}`;
36
- void engine.executePreconditions(parsed.data.preconditions, { appUrl }).then((result) => {
37
- result.cookies.forEach((cookie) => {
38
- const s = serializeCookie(cookie);
39
- res.cookie(s.name, s.value, buildExpressCookieOptions(s));
40
- });
41
- res.json({
42
- data: result.data,
43
- error: result.error,
44
- executed: result.executed,
45
- runId: result.runId,
46
- success: result.success
47
- });
37
+ const items = parsed.data.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
38
+ void engine.executePreconditions(items, { appUrl }).then((results) => {
39
+ res.json({ results: toBatchRunResults(results) });
48
40
  });
49
41
  });
50
42
  router.put("/execute-observer", (req, res) => {
@@ -54,21 +46,22 @@ function createExpressHandler({ enabled, engine }) {
54
46
  return;
55
47
  }
56
48
  void engine.executeObserver(parsed.data.observer, parsed.data.params).then((result) => {
57
- res.json({
58
- error: result.error,
59
- outcome: result.outcome,
60
- success: result.success
61
- });
49
+ res.json({ error: result.error, outcome: result.outcome, success: result.success });
62
50
  });
63
51
  });
64
52
  router.put("/teardown-preconditions", (req, res) => {
65
53
  const parsed = teardownRequestSchema.safeParse(req.body);
66
54
  if (!parsed.success) {
67
- res.status(400).json({ error: "Invalid request body", success: false });
55
+ res.status(400).json({ error: "Invalid request body" });
68
56
  return;
69
57
  }
70
- void engine.teardown(parsed.data.preconditions, parsed.data.data).then(() => {
71
- res.json({ success: true });
58
+ const items = parsed.data.batch.map((b) => ({
59
+ data: b.data,
60
+ names: b.preconditions,
61
+ runId: b.runId
62
+ }));
63
+ void engine.teardown(items).then((results) => {
64
+ res.json({ results: toTeardownResults(results) });
72
65
  });
73
66
  });
74
67
  return router;
@@ -89,28 +82,6 @@ function asString(value) {
89
82
  }
90
83
  return void 0;
91
84
  }
92
- function buildExpressCookieOptions(s) {
93
- const opts = {};
94
- if (s.domain != null) {
95
- opts.domain = s.domain;
96
- }
97
- if (s.expires != null) {
98
- opts.expires = s.expires;
99
- }
100
- if (s.httpOnly != null) {
101
- opts.httpOnly = s.httpOnly;
102
- }
103
- if (s.path != null) {
104
- opts.path = s.path;
105
- }
106
- if (s.sameSite != null) {
107
- opts.sameSite = s.sameSite;
108
- }
109
- if (s.secure != null) {
110
- opts.secure = s.secure;
111
- }
112
- return opts;
113
- }
114
85
  export {
115
86
  createExpressHandler
116
87
  };
package/dist/fastify.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FastifyInstance } from 'fastify';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/fastify.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  batchRequestSchema,
3
- buildSetCookieHeader,
4
3
  observerRequestSchema,
5
4
  readAdapterWebhookSecret,
6
- serializeCookie,
7
5
  teardownRequestSchema,
6
+ toBatchRunResults,
7
+ toTeardownResults,
8
8
  verifyWebhookSignature
9
- } from "./chunk-V6LMXKGL.js";
9
+ } from "./chunk-XO36IU66.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/fastify.ts
@@ -34,21 +34,12 @@ function registerFastifyHandler({
34
34
  fastify.put("/execute-preconditions", async (req, reply) => {
35
35
  const parsed = batchRequestSchema.safeParse(req.body);
36
36
  if (!parsed.success) {
37
- return reply.code(400).send({ error: "Invalid request body", success: false });
37
+ return reply.code(400).send({ error: "Invalid request body" });
38
38
  }
39
39
  const appUrl = `${req.protocol}://${req.hostname}`;
40
- const result = await engine.executePreconditions(parsed.data.preconditions, { appUrl });
41
- result.cookies.forEach((cookie) => {
42
- const s = serializeCookie(cookie);
43
- reply.header("Set-Cookie", buildSetCookieHeader(s));
44
- });
45
- return reply.send({
46
- data: result.data,
47
- error: result.error,
48
- executed: result.executed,
49
- runId: result.runId,
50
- success: result.success
51
- });
40
+ const items = parsed.data.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
41
+ const results = await engine.executePreconditions(items, { appUrl });
42
+ return reply.send({ results: toBatchRunResults(results) });
52
43
  });
53
44
  fastify.put("/execute-observer", async (req, reply) => {
54
45
  const parsed = observerRequestSchema.safeParse(req.body);
@@ -56,19 +47,20 @@ function registerFastifyHandler({
56
47
  return reply.code(400).send({ error: "Invalid request body", success: false });
57
48
  }
58
49
  const result = await engine.executeObserver(parsed.data.observer, parsed.data.params);
59
- return reply.send({
60
- error: result.error,
61
- outcome: result.outcome,
62
- success: result.success
63
- });
50
+ return reply.send({ error: result.error, outcome: result.outcome, success: result.success });
64
51
  });
65
52
  fastify.put("/teardown-preconditions", async (req, reply) => {
66
53
  const parsed = teardownRequestSchema.safeParse(req.body);
67
54
  if (!parsed.success) {
68
- return reply.code(400).send({ error: "Invalid request body", success: false });
55
+ return reply.code(400).send({ error: "Invalid request body" });
69
56
  }
70
- await engine.teardown(parsed.data.preconditions, parsed.data.data);
71
- return reply.send({ success: true });
57
+ const items = parsed.data.batch.map((b) => ({
58
+ data: b.data,
59
+ names: b.preconditions,
60
+ runId: b.runId
61
+ }));
62
+ const results = await engine.teardown(items);
63
+ return reply.send({ results: toTeardownResults(results) });
72
64
  });
73
65
  };
74
66
  }
package/dist/hono.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7