@ripplo/testing 0.3.4 → 0.3.6

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
@@ -518,9 +518,88 @@ export const PUT = createNextHandler({
518
518
 
519
519
  The handler dispatches on the last URL segment (`execute-preconditions`, `execute-observer`, `teardown-preconditions`) and returns 404 for anything else. It depends only on the Web `Request` / `Response` types, so it runs on both the Node and Edge runtimes — no `next` import required.
520
520
 
521
+ ### Hono
522
+
523
+ ```ts
524
+ import { Hono } from "hono";
525
+ import { createHonoHandler } from "@ripplo/testing/hono";
526
+ import { engine } from "./test/engine.js";
527
+
528
+ const app = new Hono();
529
+ app.route(
530
+ "/ripplo",
531
+ createHonoHandler({
532
+ enabled: process.env.ENABLE_RIPPLO_TESTING === "true",
533
+ engine,
534
+ }),
535
+ );
536
+ ```
537
+
538
+ The Hono adapter uses the Web `Request` / `Response` standard and runs on Node, Bun, Deno, and Cloudflare Workers.
539
+
540
+ ### Koa
541
+
542
+ ```ts
543
+ import Koa from "koa";
544
+ import mount from "koa-mount";
545
+ import { createKoaHandler } from "@ripplo/testing/koa";
546
+ import { engine } from "./test/engine.js";
547
+
548
+ const app = new Koa();
549
+ app.use(
550
+ mount(
551
+ "/ripplo",
552
+ createKoaHandler({
553
+ enabled: process.env.ENABLE_RIPPLO_TESTING === "true",
554
+ engine,
555
+ }),
556
+ ),
557
+ );
558
+ ```
559
+
560
+ The Koa adapter reads the raw request body itself — do not mount a body-parser in front of it. Mount it at any prefix with `koa-mount`.
561
+
562
+ ### NestJS
563
+
564
+ ```ts
565
+ import { Module } from "@nestjs/common";
566
+ import { RipploTestingModule } from "@ripplo/testing/nestjs";
567
+ import { engine } from "./test/engine.js";
568
+
569
+ @Module({
570
+ imports: [
571
+ RipploTestingModule.forRoot({
572
+ enabled: process.env.ENABLE_RIPPLO_TESTING === "true",
573
+ engine,
574
+ path: "ripplo",
575
+ }),
576
+ ],
577
+ })
578
+ export class AppModule {}
579
+ ```
580
+
581
+ Requires the Express platform adapter (`@nestjs/platform-express`) and `reflect-metadata` at the entry point. `path` defaults to `"ripplo"`.
582
+
583
+ ### Elysia
584
+
585
+ ```ts
586
+ import { Elysia } from "elysia";
587
+ import { createElysiaHandler } from "@ripplo/testing/elysia";
588
+ import { engine } from "./test/engine.js";
589
+
590
+ const app = new Elysia().group("/ripplo", (app) =>
591
+ app.use(
592
+ createElysiaHandler({
593
+ enabled: process.env.ENABLE_RIPPLO_TESTING === "true",
594
+ engine,
595
+ }),
596
+ ),
597
+ );
598
+ ```
599
+
521
600
  ### Custom integration (raw engine)
522
601
 
523
- If your framework isn't covered above (Hono, Koa, Bun, Deno, Cloudflare Workers, etc.), use the engine directly. The adapters are thin wrappers over the same API.
602
+ If your framework isn't covered above, use the engine directly. The adapters are thin wrappers over the same API.
524
603
 
525
604
  ```ts
526
605
  import { buildSetCookieHeader, serializeCookie, verifyWebhookSignature } from "@ripplo/testing";
package/dist/actions.js CHANGED
@@ -2,11 +2,12 @@ import {
2
2
  readVariable,
3
3
  toSpecLocator,
4
4
  toStringValueRef
5
- } from "./chunk-P67G7RA7.js";
5
+ } from "./chunk-YJ4KB56W.js";
6
+ import "./chunk-DCJBLS2U.js";
6
7
  import {
7
8
  createStep
8
9
  } from "./chunk-MGATMMCZ.js";
9
- import "./chunk-DCJBLS2U.js";
10
+ import "./chunk-4MGIQFAJ.js";
10
11
 
11
12
  // src/steps/actions.ts
12
13
  function navigate(url) {
package/dist/assert.js CHANGED
@@ -1,15 +1,16 @@
1
- import {
2
- toSpecLocator,
3
- toStringValueRef
4
- } from "./chunk-P67G7RA7.js";
5
1
  import {
6
2
  readObserverBudget,
7
3
  readObserverName
8
- } from "./chunk-55HIO5LW.js";
4
+ } from "./chunk-XBYG5NBY.js";
5
+ import {
6
+ toSpecLocator,
7
+ toStringValueRef
8
+ } from "./chunk-YJ4KB56W.js";
9
+ import "./chunk-DCJBLS2U.js";
9
10
  import {
10
11
  createStep
11
12
  } from "./chunk-MGATMMCZ.js";
12
- import "./chunk-DCJBLS2U.js";
13
+ import "./chunk-4MGIQFAJ.js";
13
14
 
14
15
  // src/steps/assert.ts
15
16
  var assert = {
@@ -0,0 +1,16 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
+
13
+ export {
14
+ __decorateClass,
15
+ __decorateParam
16
+ };
@@ -1,28 +1,28 @@
1
1
  // src/types.ts
2
2
  import { z } from "zod";
3
3
  var DEFAULT_WATCH_PATHS = [
4
- "src/**",
5
- "app/**",
6
- "apps/**",
7
- "pages/**",
8
- "routes/**",
9
- "components/**",
10
- "server/**",
11
- "api/**",
12
- "backend/**",
13
- "features/**",
14
- "modules/**",
15
- "views/**",
16
- "ui/**",
17
- "hooks/**",
18
- "contexts/**",
19
- "providers/**",
20
- "controllers/**",
21
- "handlers/**",
22
- "resolvers/**",
23
- "services/**",
24
- "middleware/**",
25
- "lib/**"
4
+ "**/src/**",
5
+ "**/app/**",
6
+ "**/apps/**",
7
+ "**/pages/**",
8
+ "**/routes/**",
9
+ "**/components/**",
10
+ "**/server/**",
11
+ "**/api/**",
12
+ "**/backend/**",
13
+ "**/features/**",
14
+ "**/modules/**",
15
+ "**/views/**",
16
+ "**/ui/**",
17
+ "**/hooks/**",
18
+ "**/contexts/**",
19
+ "**/providers/**",
20
+ "**/controllers/**",
21
+ "**/handlers/**",
22
+ "**/resolvers/**",
23
+ "**/services/**",
24
+ "**/middleware/**",
25
+ "**/lib/**"
26
26
  ];
27
27
  var DEFAULT_IGNORE_PATHS = [
28
28
  "**/*.gen.*",
package/dist/compiler.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  compile
3
3
  } from "./chunk-KNF4K4JH.js";
4
4
  import "./chunk-MGATMMCZ.js";
5
+ import "./chunk-4MGIQFAJ.js";
5
6
  export {
6
7
  compile
7
8
  };
package/dist/control.js CHANGED
@@ -4,9 +4,10 @@ import {
4
4
  readVariable,
5
5
  toStringValueRef,
6
6
  variable
7
- } from "./chunk-P67G7RA7.js";
8
- import "./chunk-MGATMMCZ.js";
7
+ } from "./chunk-YJ4KB56W.js";
9
8
  import "./chunk-DCJBLS2U.js";
9
+ import "./chunk-MGATMMCZ.js";
10
+ import "./chunk-4MGIQFAJ.js";
10
11
  export {
11
12
  extract,
12
13
  isVariable,
@@ -0,0 +1,79 @@
1
+ import { Elysia } from 'elysia';
2
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
3
+ import './builder-BGr5JvrY.js';
4
+ import './types-Degkxs1f.js';
5
+ import 'zod';
6
+ import './step-De52hTLd.js';
7
+ import '@ripplo/spec';
8
+
9
+ interface CreateElysiaHandlerParams {
10
+ readonly enabled: boolean;
11
+ readonly engine: RipploEngine;
12
+ }
13
+ declare function createElysiaHandler(params: CreateElysiaHandlerParams): ReturnType<typeof build>;
14
+ declare function build({ enabled, engine }: CreateElysiaHandlerParams): Elysia<"", {
15
+ decorator: {};
16
+ store: {};
17
+ derive: {};
18
+ resolve: {};
19
+ }, {
20
+ typebox: {};
21
+ error: {};
22
+ }, {
23
+ schema: {};
24
+ standaloneSchema: {};
25
+ macro: {};
26
+ macroFn: {};
27
+ parser: {};
28
+ response: {};
29
+ }, {
30
+ "execute-preconditions": {
31
+ put: {
32
+ body: unknown;
33
+ params: {};
34
+ query: unknown;
35
+ headers: unknown;
36
+ response: {
37
+ 200: Response;
38
+ };
39
+ };
40
+ };
41
+ } & {
42
+ "execute-observer": {
43
+ put: {
44
+ body: unknown;
45
+ params: {};
46
+ query: unknown;
47
+ headers: unknown;
48
+ response: {
49
+ 200: Response;
50
+ };
51
+ };
52
+ };
53
+ } & {
54
+ "teardown-preconditions": {
55
+ put: {
56
+ body: unknown;
57
+ params: {};
58
+ query: unknown;
59
+ headers: unknown;
60
+ response: {
61
+ 200: Response;
62
+ };
63
+ };
64
+ };
65
+ }, {
66
+ derive: {};
67
+ resolve: {};
68
+ schema: {};
69
+ standaloneSchema: {};
70
+ response: {};
71
+ }, {
72
+ derive: {};
73
+ resolve: {};
74
+ schema: {};
75
+ standaloneSchema: {};
76
+ response: {};
77
+ }>;
78
+
79
+ export { type CreateElysiaHandlerParams, createElysiaHandler };
package/dist/elysia.js ADDED
@@ -0,0 +1,120 @@
1
+ import {
2
+ batchRequestSchema,
3
+ buildSetCookieHeader,
4
+ observerRequestSchema,
5
+ serializeCookie,
6
+ teardownRequestSchema,
7
+ verifyWebhookSignature
8
+ } from "./chunk-TO3T2D2Y.js";
9
+ import "./chunk-4MGIQFAJ.js";
10
+
11
+ // src/adapters/elysia.ts
12
+ import { Elysia } from "elysia";
13
+ function createElysiaHandler(params) {
14
+ return build(params);
15
+ }
16
+ function build({ enabled, engine }) {
17
+ const app = new Elysia();
18
+ if (!enabled) {
19
+ return app.put("/execute-preconditions", () => notFoundResponse()).put("/execute-observer", () => notFoundResponse()).put("/teardown-preconditions", () => notFoundResponse());
20
+ }
21
+ const webhookSecret = engine.getConfig().webhookSecret;
22
+ return app.put("/execute-preconditions", async ({ request }) => {
23
+ const gate = await verifyAndReadBody(request, webhookSecret);
24
+ if ("response" in gate) {
25
+ return gate.response;
26
+ }
27
+ const parsed = parseWith(gate.body, batchRequestSchema);
28
+ if (parsed == null) {
29
+ return jsonResponse({ error: "Invalid request body", success: false }, 400);
30
+ }
31
+ const host = request.headers.get("host");
32
+ if (host == null || host.length === 0) {
33
+ return jsonResponse({ error: "Missing host header", success: false }, 400);
34
+ }
35
+ const proto = request.headers.get("x-forwarded-proto") ?? "http";
36
+ const appUrl = `${proto}://${host}`;
37
+ const result = await engine.executePreconditions(parsed.preconditions, { appUrl });
38
+ const headers = new Headers({ "content-type": "application/json" });
39
+ result.cookies.forEach((cookie) => {
40
+ headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
41
+ });
42
+ return new Response(
43
+ JSON.stringify({
44
+ data: result.data,
45
+ error: result.error,
46
+ executed: result.executed,
47
+ runId: result.runId,
48
+ success: result.success
49
+ }),
50
+ { headers, status: 200 }
51
+ );
52
+ }).put("/execute-observer", async ({ request }) => {
53
+ const gate = await verifyAndReadBody(request, webhookSecret);
54
+ if ("response" in gate) {
55
+ return gate.response;
56
+ }
57
+ const parsed = parseWith(gate.body, observerRequestSchema);
58
+ if (parsed == null) {
59
+ return jsonResponse({ error: "Invalid request body", success: false }, 400);
60
+ }
61
+ const result = await engine.executeObserver(parsed.observer, parsed.params);
62
+ return jsonResponse(
63
+ { error: result.error, outcome: result.outcome, success: result.success },
64
+ 200
65
+ );
66
+ }).put("/teardown-preconditions", async ({ request }) => {
67
+ const gate = await verifyAndReadBody(request, webhookSecret);
68
+ if ("response" in gate) {
69
+ return gate.response;
70
+ }
71
+ const parsed = parseWith(gate.body, teardownRequestSchema);
72
+ if (parsed == null) {
73
+ return jsonResponse({ error: "Invalid request body", success: false }, 400);
74
+ }
75
+ await engine.teardown(parsed.preconditions, parsed.data);
76
+ return jsonResponse({ success: true }, 200);
77
+ });
78
+ }
79
+ async function verifyAndReadBody(req, webhookSecret) {
80
+ if (webhookSecret.length === 0) {
81
+ return { response: jsonResponse({ error: "Webhook secret not configured" }, 403) };
82
+ }
83
+ const body = await req.text();
84
+ const headers = {
85
+ "webhook-id": req.headers.get("webhook-id") ?? void 0,
86
+ "webhook-signature": req.headers.get("webhook-signature") ?? void 0,
87
+ "webhook-timestamp": req.headers.get("webhook-timestamp") ?? void 0
88
+ };
89
+ if (!verifyWebhookSignature(body, headers, webhookSecret)) {
90
+ return { response: jsonResponse({ error: "Invalid webhook signature" }, 401) };
91
+ }
92
+ return { body };
93
+ }
94
+ function parseWith(body, schema) {
95
+ const json = tryParseJson(body);
96
+ if (json == null) {
97
+ return null;
98
+ }
99
+ const result = schema.safeParse(json);
100
+ return result.success ? result.data : null;
101
+ }
102
+ function tryParseJson(body) {
103
+ try {
104
+ return JSON.parse(body);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function jsonResponse(payload, status) {
110
+ return new Response(JSON.stringify(payload), {
111
+ headers: { "content-type": "application/json" },
112
+ status
113
+ });
114
+ }
115
+ function notFoundResponse() {
116
+ return jsonResponse({ error: "Not found" }, 404);
117
+ }
118
+ export {
119
+ createElysiaHandler
120
+ };
@@ -17,7 +17,7 @@ interface ObserverExecutionResult {
17
17
  readonly outcome: ObserverOutcome | undefined;
18
18
  readonly success: boolean;
19
19
  }
20
- interface PreconditionEngine {
20
+ interface RipploEngine {
21
21
  readonly executeObserver: (name: string, params: Record<string, string>) => Promise<ObserverExecutionResult>;
22
22
  readonly executePreconditions: (names: ReadonlyArray<string>, options?: ExecuteBatchOptions) => Promise<EngineResult>;
23
23
  readonly getConfig: () => DslConfig;
@@ -42,6 +42,6 @@ interface EngineImpls<P extends PreconditionRegistry, O extends ObserverRegistry
42
42
  readonly [K in keyof P]: NotImplemented | PreconditionImplFor<P[K]>;
43
43
  };
44
44
  }
45
- declare function createEngine<P extends PreconditionRegistry, O extends ObserverRegistry>(ripplo: RipploInstance<P, O>, impls: EngineImpls<P, O>): PreconditionEngine;
45
+ declare function createEngine<P extends PreconditionRegistry, O extends ObserverRegistry>(ripplo: RipploInstance<P, O>, impls: EngineImpls<P, O>): RipploEngine;
46
46
 
47
- export { type EngineImpls as E, type NotImplemented as N, type ObserverImplFnFor as O, type PreconditionEngine as P, type EngineResult as a, type ExecuteBatchOptions as b, type PreconditionImplFor as c, createEngine as d, notImplemented as n };
47
+ 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 };
package/dist/express.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Router } from 'express';
2
- import { P as PreconditionEngine } from './engine-Bo5nkg02.js';
2
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
3
3
  import './builder-BGr5JvrY.js';
4
4
  import './types-Degkxs1f.js';
5
5
  import 'zod';
@@ -8,7 +8,7 @@ import '@ripplo/spec';
8
8
 
9
9
  interface CreateExpressHandlerParams {
10
10
  readonly enabled: boolean;
11
- readonly engine: PreconditionEngine;
11
+ readonly engine: RipploEngine;
12
12
  }
13
13
  declare function createExpressHandler({ enabled, engine }: CreateExpressHandlerParams): Router;
14
14
 
package/dist/express.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  teardownRequestSchema,
6
6
  verifyWebhookSignature
7
7
  } from "./chunk-TO3T2D2Y.js";
8
+ import "./chunk-4MGIQFAJ.js";
8
9
 
9
10
  // src/adapters/express.ts
10
11
  import { Router, json } from "express";
package/dist/fastify.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FastifyInstance } from 'fastify';
2
- import { P as PreconditionEngine } from './engine-Bo5nkg02.js';
2
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
3
3
  import './builder-BGr5JvrY.js';
4
4
  import './types-Degkxs1f.js';
5
5
  import 'zod';
@@ -8,7 +8,7 @@ import '@ripplo/spec';
8
8
 
9
9
  interface RegisterFastifyHandlerParams {
10
10
  readonly enabled: boolean;
11
- readonly engine: PreconditionEngine;
11
+ readonly engine: RipploEngine;
12
12
  }
13
13
  declare function registerFastifyHandler({ enabled, engine, }: RegisterFastifyHandlerParams): (fastify: FastifyInstance) => Promise<void>;
14
14
 
package/dist/fastify.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  teardownRequestSchema,
7
7
  verifyWebhookSignature
8
8
  } from "./chunk-TO3T2D2Y.js";
9
+ import "./chunk-4MGIQFAJ.js";
9
10
 
10
11
  // src/adapters/fastify.ts
11
12
  function registerFastifyHandler({
package/dist/hono.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { Hono } from 'hono';
2
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
3
+ import './builder-BGr5JvrY.js';
4
+ import './types-Degkxs1f.js';
5
+ import 'zod';
6
+ import './step-De52hTLd.js';
7
+ import '@ripplo/spec';
8
+
9
+ interface CreateHonoHandlerParams {
10
+ readonly enabled: boolean;
11
+ readonly engine: RipploEngine;
12
+ }
13
+ interface HonoEnv {
14
+ Variables: {
15
+ rawBody: string;
16
+ };
17
+ }
18
+ declare function createHonoHandler({ enabled, engine }: CreateHonoHandlerParams): Hono<HonoEnv>;
19
+
20
+ export { type CreateHonoHandlerParams, createHonoHandler };
package/dist/hono.js ADDED
@@ -0,0 +1,95 @@
1
+ import {
2
+ batchRequestSchema,
3
+ buildSetCookieHeader,
4
+ observerRequestSchema,
5
+ serializeCookie,
6
+ teardownRequestSchema,
7
+ verifyWebhookSignature
8
+ } from "./chunk-TO3T2D2Y.js";
9
+ import "./chunk-4MGIQFAJ.js";
10
+
11
+ // src/adapters/hono.ts
12
+ import { Hono } from "hono";
13
+ function createHonoHandler({ enabled, engine }) {
14
+ const app = new Hono();
15
+ if (!enabled) {
16
+ return app;
17
+ }
18
+ const webhookSecret = engine.getConfig().webhookSecret;
19
+ app.use("*", createWebhookMiddleware(webhookSecret));
20
+ app.put("/execute-preconditions", async (c) => {
21
+ const body = tryParseJson(c.get("rawBody"));
22
+ const parsed = body == null ? null : batchRequestSchema.safeParse(body);
23
+ if (parsed == null || !parsed.success) {
24
+ return c.json({ error: "Invalid request body", success: false }, 400);
25
+ }
26
+ const host = c.req.header("host");
27
+ if (host == null || host.length === 0) {
28
+ return c.json({ error: "Missing host header", success: false }, 400);
29
+ }
30
+ const proto = c.req.header("x-forwarded-proto") ?? "http";
31
+ const appUrl = `${proto}://${host}`;
32
+ const result = await engine.executePreconditions(parsed.data.preconditions, { appUrl });
33
+ result.cookies.forEach((cookie) => {
34
+ c.header("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)), { append: true });
35
+ });
36
+ return c.json({
37
+ data: result.data,
38
+ error: result.error,
39
+ executed: result.executed,
40
+ runId: result.runId,
41
+ success: result.success
42
+ });
43
+ });
44
+ app.put("/execute-observer", async (c) => {
45
+ const body = tryParseJson(c.get("rawBody"));
46
+ const parsed = body == null ? null : observerRequestSchema.safeParse(body);
47
+ if (parsed == null || !parsed.success) {
48
+ return c.json({ error: "Invalid request body", success: false }, 400);
49
+ }
50
+ const result = await engine.executeObserver(parsed.data.observer, parsed.data.params);
51
+ return c.json({
52
+ error: result.error,
53
+ outcome: result.outcome,
54
+ success: result.success
55
+ });
56
+ });
57
+ app.put("/teardown-preconditions", async (c) => {
58
+ const body = tryParseJson(c.get("rawBody"));
59
+ const parsed = body == null ? null : teardownRequestSchema.safeParse(body);
60
+ if (parsed == null || !parsed.success) {
61
+ return c.json({ error: "Invalid request body", success: false }, 400);
62
+ }
63
+ await engine.teardown(parsed.data.preconditions, parsed.data.data);
64
+ return c.json({ success: true });
65
+ });
66
+ return app;
67
+ }
68
+ function createWebhookMiddleware(webhookSecret) {
69
+ return async (c, next) => {
70
+ if (webhookSecret.length === 0) {
71
+ return c.json({ error: "Webhook secret not configured" }, 403);
72
+ }
73
+ const body = await c.req.text();
74
+ const headers = {
75
+ "webhook-id": c.req.header("webhook-id"),
76
+ "webhook-signature": c.req.header("webhook-signature"),
77
+ "webhook-timestamp": c.req.header("webhook-timestamp")
78
+ };
79
+ if (!verifyWebhookSignature(body, headers, webhookSecret)) {
80
+ return c.json({ error: "Invalid webhook signature" }, 401);
81
+ }
82
+ c.set("rawBody", body);
83
+ await next();
84
+ };
85
+ }
86
+ function tryParseJson(body) {
87
+ try {
88
+ return JSON.parse(body);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ export {
94
+ createHonoHandler
95
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { O as ObserverImplFn, a as ObserverRegistry, P as PreconditionImpl, b as PreconditionRecord, c as PreconditionRegistry, R as ResolveDeps, d as RipploBuilder, e as RipploInstance, f as RipploRegistries, g as createRipplo, o as observer, p as precondition, t as test } from './builder-BGr5JvrY.js';
2
2
  import { CompileResult } from './compiler.js';
3
3
  export { CompiledTest, compile } from './compiler.js';
4
- export { E as EngineImpls, a as EngineResult, b as ExecuteBatchOptions, N as NotImplemented, O as ObserverImplFnFor, P as PreconditionEngine, c as PreconditionImplFor, d as createEngine, n as notImplemented } from './engine-Bo5nkg02.js';
4
+ export { E as EngineImpls, a as EngineResult, b as ExecuteBatchOptions, N as NotImplemented, O as ObserverImplFnFor, P as PreconditionImplFor, R as RipploEngine, c as createEngine, n as notImplemented } from './engine-CZ4F_Csu.js';
5
5
  import { C as CookieEntry } from './types-Degkxs1f.js';
6
6
  export { c as CookieOptions, D as DEFAULT_IGNORE_PATHS, d as DEFAULT_WATCH_PATHS, e as DslConfig, f as ObserverContext, g as ObserverDefinition, O as ObserverHandle, a as ObserverInput, h as ObserverOutcome, P as Precondition, i as PreconditionDeps, S as SetupContext, T as TeardownContext, j as TestDefinition } from './types-Degkxs1f.js';
7
7
  export { D as DslNodeInput } from './step-De52hTLd.js';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  readPreconditionName,
13
13
  readTestValue,
14
14
  userDslConfigSchema
15
- } from "./chunk-55HIO5LW.js";
15
+ } from "./chunk-XBYG5NBY.js";
16
16
  import {
17
17
  compile
18
18
  } from "./chunk-KNF4K4JH.js";
@@ -22,6 +22,7 @@ import {
22
22
  serializeCookie,
23
23
  verifyWebhookSignature
24
24
  } from "./chunk-TO3T2D2Y.js";
25
+ import "./chunk-4MGIQFAJ.js";
25
26
 
26
27
  // src/observer.ts
27
28
  function createPassOutcome() {
package/dist/koa.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Middleware } from 'koa';
2
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
3
+ import './builder-BGr5JvrY.js';
4
+ import './types-Degkxs1f.js';
5
+ import 'zod';
6
+ import './step-De52hTLd.js';
7
+ import '@ripplo/spec';
8
+
9
+ interface CreateKoaHandlerParams {
10
+ readonly enabled: boolean;
11
+ readonly engine: RipploEngine;
12
+ }
13
+ declare function createKoaHandler({ enabled, engine }: CreateKoaHandlerParams): Middleware;
14
+
15
+ export { type CreateKoaHandlerParams, createKoaHandler };
package/dist/koa.js ADDED
@@ -0,0 +1,142 @@
1
+ import {
2
+ batchRequestSchema,
3
+ buildSetCookieHeader,
4
+ observerRequestSchema,
5
+ serializeCookie,
6
+ teardownRequestSchema,
7
+ verifyWebhookSignature
8
+ } from "./chunk-TO3T2D2Y.js";
9
+ import "./chunk-4MGIQFAJ.js";
10
+
11
+ // src/adapters/koa.ts
12
+ function createKoaHandler({ enabled, engine }) {
13
+ if (!enabled) {
14
+ return async (ctx, next) => {
15
+ await next();
16
+ };
17
+ }
18
+ const webhookSecret = engine.getConfig().webhookSecret;
19
+ return async (ctx, next) => {
20
+ if (ctx.method !== "PUT") {
21
+ await next();
22
+ return;
23
+ }
24
+ const action = lastPathSegment(ctx.path);
25
+ if (action == null) {
26
+ await next();
27
+ return;
28
+ }
29
+ if (webhookSecret.length === 0) {
30
+ ctx.status = 403;
31
+ ctx.body = { error: "Webhook secret not configured" };
32
+ return;
33
+ }
34
+ const body = await readBody(ctx);
35
+ const headers = {
36
+ "webhook-id": headerString(ctx.get("webhook-id")),
37
+ "webhook-signature": headerString(ctx.get("webhook-signature")),
38
+ "webhook-timestamp": headerString(ctx.get("webhook-timestamp"))
39
+ };
40
+ if (!verifyWebhookSignature(body, headers, webhookSecret)) {
41
+ ctx.status = 401;
42
+ ctx.body = { error: "Invalid webhook signature" };
43
+ return;
44
+ }
45
+ await dispatchAction({ action, body, ctx, engine });
46
+ };
47
+ }
48
+ async function dispatchAction({ action, body, ctx, engine }) {
49
+ if (action === "execute-preconditions") {
50
+ await handleExecutePreconditions({ body, ctx, engine });
51
+ return;
52
+ }
53
+ if (action === "execute-observer") {
54
+ await handleExecuteObserver({ body, ctx, engine });
55
+ return;
56
+ }
57
+ await handleTeardown({ body, ctx, engine });
58
+ }
59
+ async function handleExecutePreconditions({ body, ctx, engine }) {
60
+ const json = tryParseJson(body);
61
+ const parsed = json == null ? null : batchRequestSchema.safeParse(json);
62
+ if (parsed == null || !parsed.success) {
63
+ ctx.status = 400;
64
+ ctx.body = { error: "Invalid request body", success: false };
65
+ return;
66
+ }
67
+ const host = ctx.get("host");
68
+ if (host.length === 0) {
69
+ ctx.status = 400;
70
+ ctx.body = { error: "Missing host header", success: false };
71
+ return;
72
+ }
73
+ const proto = ctx.get("x-forwarded-proto").length > 0 ? ctx.get("x-forwarded-proto") : "http";
74
+ const appUrl = `${proto}://${host}`;
75
+ const result = await engine.executePreconditions(parsed.data.preconditions, { appUrl });
76
+ result.cookies.forEach((cookie) => {
77
+ ctx.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
78
+ });
79
+ ctx.status = 200;
80
+ ctx.body = {
81
+ data: result.data,
82
+ error: result.error,
83
+ executed: result.executed,
84
+ runId: result.runId,
85
+ success: result.success
86
+ };
87
+ }
88
+ async function handleExecuteObserver({ body, ctx, engine }) {
89
+ const json = tryParseJson(body);
90
+ const parsed = json == null ? null : observerRequestSchema.safeParse(json);
91
+ if (parsed == null || !parsed.success) {
92
+ ctx.status = 400;
93
+ ctx.body = { error: "Invalid request body", success: false };
94
+ return;
95
+ }
96
+ const result = await engine.executeObserver(parsed.data.observer, parsed.data.params);
97
+ ctx.status = 200;
98
+ ctx.body = { error: result.error, outcome: result.outcome, success: result.success };
99
+ }
100
+ async function handleTeardown({ body, ctx, engine }) {
101
+ const json = tryParseJson(body);
102
+ const parsed = json == null ? null : teardownRequestSchema.safeParse(json);
103
+ if (parsed == null || !parsed.success) {
104
+ ctx.status = 400;
105
+ ctx.body = { error: "Invalid request body", success: false };
106
+ return;
107
+ }
108
+ await engine.teardown(parsed.data.preconditions, parsed.data.data);
109
+ ctx.status = 200;
110
+ ctx.body = { success: true };
111
+ }
112
+ function lastPathSegment(path) {
113
+ const segments = path.split("/").filter((s) => s.length > 0);
114
+ const last = segments.at(-1);
115
+ if (last === "execute-preconditions" || last === "execute-observer" || last === "teardown-preconditions") {
116
+ return last;
117
+ }
118
+ return null;
119
+ }
120
+ async function readBody(ctx) {
121
+ return new Promise((resolve, reject) => {
122
+ const chunks = [];
123
+ ctx.req.on("data", (chunk) => chunks.push(chunk));
124
+ ctx.req.on("end", () => {
125
+ resolve(Buffer.concat(chunks).toString("utf8"));
126
+ });
127
+ ctx.req.on("error", reject);
128
+ });
129
+ }
130
+ function headerString(value) {
131
+ return value.length > 0 ? value : void 0;
132
+ }
133
+ function tryParseJson(body) {
134
+ try {
135
+ return JSON.parse(body);
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+ export {
141
+ createKoaHandler
142
+ };
package/dist/locators.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  role,
4
4
  testId
5
5
  } from "./chunk-DCJBLS2U.js";
6
+ import "./chunk-4MGIQFAJ.js";
6
7
  export {
7
8
  readLocator,
8
9
  role,
package/dist/lockfile.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-4MGIQFAJ.js";
2
+
1
3
  // src/lockfile.ts
2
4
  import fs from "fs/promises";
3
5
  import path from "path";
@@ -0,0 +1,18 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
3
+ import './builder-BGr5JvrY.js';
4
+ import './types-Degkxs1f.js';
5
+ import 'zod';
6
+ import './step-De52hTLd.js';
7
+ import '@ripplo/spec';
8
+
9
+ interface RipploTestingModuleOptions {
10
+ readonly enabled: boolean;
11
+ readonly engine: RipploEngine;
12
+ readonly path: string | undefined;
13
+ }
14
+ declare class RipploTestingModule {
15
+ static forRoot(options: RipploTestingModuleOptions): DynamicModule;
16
+ }
17
+
18
+ export { RipploTestingModule, type RipploTestingModuleOptions };
package/dist/nestjs.js ADDED
@@ -0,0 +1,143 @@
1
+ import {
2
+ batchRequestSchema,
3
+ buildSetCookieHeader,
4
+ observerRequestSchema,
5
+ serializeCookie,
6
+ teardownRequestSchema,
7
+ verifyWebhookSignature
8
+ } from "./chunk-TO3T2D2Y.js";
9
+ import {
10
+ __decorateClass,
11
+ __decorateParam
12
+ } from "./chunk-4MGIQFAJ.js";
13
+
14
+ // src/adapters/nestjs.ts
15
+ import { Controller, Inject, Module, Put, Req, Res } from "@nestjs/common";
16
+ var RIPPLO_OPTIONS = "RIPPLO_TESTING_OPTIONS";
17
+ var RipploTestingModule = class {
18
+ static forRoot(options) {
19
+ const path = options.path ?? "ripplo";
20
+ const controller = createController(path);
21
+ return {
22
+ controllers: [controller],
23
+ module: RipploTestingModule,
24
+ providers: [
25
+ {
26
+ provide: RIPPLO_OPTIONS,
27
+ useValue: { enabled: options.enabled, engine: options.engine }
28
+ }
29
+ ]
30
+ };
31
+ }
32
+ };
33
+ RipploTestingModule = __decorateClass([
34
+ Module({})
35
+ ], RipploTestingModule);
36
+ function createController(path) {
37
+ let RipploController = class {
38
+ constructor(opts) {
39
+ this.opts = opts;
40
+ }
41
+ async executePreconditions(req, res) {
42
+ if (!guard(req, res, this.opts)) {
43
+ return;
44
+ }
45
+ const parsed = batchRequestSchema.safeParse(req.body);
46
+ if (!parsed.success) {
47
+ res.status(400).json({ error: "Invalid request body", success: false });
48
+ return;
49
+ }
50
+ const host = req.get("host") ?? "";
51
+ const proto = req.get("x-forwarded-proto") ?? req.protocol;
52
+ const appUrl = `${proto}://${host}`;
53
+ const result = await this.opts.engine.executePreconditions(parsed.data.preconditions, {
54
+ appUrl
55
+ });
56
+ result.cookies.forEach((cookie) => {
57
+ res.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
58
+ });
59
+ res.status(200).json({
60
+ data: result.data,
61
+ error: result.error,
62
+ executed: result.executed,
63
+ runId: result.runId,
64
+ success: result.success
65
+ });
66
+ }
67
+ async executeObserver(req, res) {
68
+ if (!guard(req, res, this.opts)) {
69
+ return;
70
+ }
71
+ const parsed = observerRequestSchema.safeParse(req.body);
72
+ if (!parsed.success) {
73
+ res.status(400).json({ error: "Invalid request body", success: false });
74
+ return;
75
+ }
76
+ const result = await this.opts.engine.executeObserver(
77
+ parsed.data.observer,
78
+ parsed.data.params
79
+ );
80
+ res.status(200).json({ error: result.error, outcome: result.outcome, success: result.success });
81
+ }
82
+ async teardown(req, res) {
83
+ if (!guard(req, res, this.opts)) {
84
+ return;
85
+ }
86
+ const parsed = teardownRequestSchema.safeParse(req.body);
87
+ if (!parsed.success) {
88
+ res.status(400).json({ error: "Invalid request body", success: false });
89
+ return;
90
+ }
91
+ await this.opts.engine.teardown(parsed.data.preconditions, parsed.data.data);
92
+ res.status(200).json({ success: true });
93
+ }
94
+ };
95
+ __decorateClass([
96
+ Put("execute-preconditions"),
97
+ __decorateParam(0, Req()),
98
+ __decorateParam(1, Res())
99
+ ], RipploController.prototype, "executePreconditions", 1);
100
+ __decorateClass([
101
+ Put("execute-observer"),
102
+ __decorateParam(0, Req()),
103
+ __decorateParam(1, Res())
104
+ ], RipploController.prototype, "executeObserver", 1);
105
+ __decorateClass([
106
+ Put("teardown-preconditions"),
107
+ __decorateParam(0, Req()),
108
+ __decorateParam(1, Res())
109
+ ], RipploController.prototype, "teardown", 1);
110
+ RipploController = __decorateClass([
111
+ Controller(path),
112
+ __decorateParam(0, Inject(RIPPLO_OPTIONS))
113
+ ], RipploController);
114
+ return RipploController;
115
+ }
116
+ function guard(req, res, opts) {
117
+ if (!opts.enabled) {
118
+ res.status(404).json({ error: "Not found" });
119
+ return false;
120
+ }
121
+ const webhookSecret = opts.engine.getConfig().webhookSecret;
122
+ if (webhookSecret.length === 0) {
123
+ res.status(403).json({ error: "Webhook secret not configured" });
124
+ return false;
125
+ }
126
+ const payload = JSON.stringify(req.body);
127
+ const headers = {
128
+ "webhook-id": headerString(req.get("webhook-id")),
129
+ "webhook-signature": headerString(req.get("webhook-signature")),
130
+ "webhook-timestamp": headerString(req.get("webhook-timestamp"))
131
+ };
132
+ if (!verifyWebhookSignature(payload, headers, webhookSecret)) {
133
+ res.status(401).json({ error: "Invalid webhook signature" });
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+ function headerString(value) {
139
+ return value != null && value.length > 0 ? value : void 0;
140
+ }
141
+ export {
142
+ RipploTestingModule
143
+ };
package/dist/nextjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as PreconditionEngine } from './engine-Bo5nkg02.js';
1
+ import { R as RipploEngine } from './engine-CZ4F_Csu.js';
2
2
  import './builder-BGr5JvrY.js';
3
3
  import './types-Degkxs1f.js';
4
4
  import 'zod';
@@ -7,7 +7,7 @@ import '@ripplo/spec';
7
7
 
8
8
  interface CreateNextHandlerParams {
9
9
  readonly enabled: boolean;
10
- readonly engine: PreconditionEngine;
10
+ readonly engine: RipploEngine;
11
11
  }
12
12
  type NextHandler = (req: Request) => Promise<Response>;
13
13
  declare function createNextHandler({ enabled, engine }: CreateNextHandlerParams): NextHandler;
package/dist/nextjs.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  teardownRequestSchema,
7
7
  verifyWebhookSignature
8
8
  } from "./chunk-TO3T2D2Y.js";
9
+ import "./chunk-4MGIQFAJ.js";
9
10
 
10
11
  // src/adapters/nextjs.ts
11
12
  function createNextHandler({ enabled, engine }) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ripplo/testing",
3
3
  "description": "TypeScript DSL for defining and running Ripplo e2e workflow tests",
4
- "version": "0.3.4",
4
+ "version": "0.3.6",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -42,6 +42,11 @@
42
42
  "import": "./dist/lockfile.js",
43
43
  "default": "./dist/lockfile.js"
44
44
  },
45
+ "./elysia": {
46
+ "types": "./dist/elysia.d.ts",
47
+ "import": "./dist/elysia.js",
48
+ "default": "./dist/elysia.js"
49
+ },
45
50
  "./express": {
46
51
  "types": "./dist/express.d.ts",
47
52
  "import": "./dist/express.js",
@@ -52,6 +57,21 @@
52
57
  "import": "./dist/fastify.js",
53
58
  "default": "./dist/fastify.js"
54
59
  },
60
+ "./hono": {
61
+ "types": "./dist/hono.d.ts",
62
+ "import": "./dist/hono.js",
63
+ "default": "./dist/hono.js"
64
+ },
65
+ "./koa": {
66
+ "types": "./dist/koa.d.ts",
67
+ "import": "./dist/koa.js",
68
+ "default": "./dist/koa.js"
69
+ },
70
+ "./nestjs": {
71
+ "types": "./dist/nestjs.d.ts",
72
+ "import": "./dist/nestjs.js",
73
+ "default": "./dist/nestjs.js"
74
+ },
55
75
  "./nextjs": {
56
76
  "types": "./dist/nextjs.d.ts",
57
77
  "import": "./dist/nextjs.js",
@@ -63,31 +83,59 @@
63
83
  "zod": "^4.3.6"
64
84
  },
65
85
  "devDependencies": {
86
+ "@nestjs/common": "^10.0.0",
87
+ "@nestjs/core": "^10.0.0",
66
88
  "@types/express": "^5.0.2",
89
+ "@types/koa": "^2.15.0",
67
90
  "@types/node": "catalog:",
91
+ "elysia": "^1.0.0",
68
92
  "eslint": "catalog:",
69
93
  "express": "^5.1.0",
70
94
  "fastify": "^5.3.3",
95
+ "hono": "^4.0.0",
96
+ "koa": "^2.15.0",
97
+ "reflect-metadata": "^0.2.0",
98
+ "rxjs": "^7.8.0",
71
99
  "tsup": "^8.5.1",
72
100
  "typescript": "catalog:",
73
101
  "vitest": "^4.1.4",
74
- "@ripplo/spec": "^0.0.0",
75
- "@ripplo/eslint-config": "0.0.0"
102
+ "@ripplo/eslint-config": "0.0.0",
103
+ "@ripplo/spec": "^0.0.0"
76
104
  },
77
105
  "peerDependencies": {
106
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
107
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
78
108
  "dotenv": "^16.0.0 || ^17.0.0",
109
+ "elysia": "^1.0.0",
79
110
  "express": "^4.0.0 || ^5.0.0",
80
- "fastify": "^5.0.0"
111
+ "fastify": "^5.0.0",
112
+ "hono": "^4.0.0",
113
+ "koa": "^2.0.0 || ^3.0.0"
81
114
  },
82
115
  "peerDependenciesMeta": {
116
+ "@nestjs/common": {
117
+ "optional": true
118
+ },
119
+ "@nestjs/core": {
120
+ "optional": true
121
+ },
83
122
  "dotenv": {
84
123
  "optional": true
85
124
  },
125
+ "elysia": {
126
+ "optional": true
127
+ },
86
128
  "express": {
87
129
  "optional": true
88
130
  },
89
131
  "fastify": {
90
132
  "optional": true
133
+ },
134
+ "hono": {
135
+ "optional": true
136
+ },
137
+ "koa": {
138
+ "optional": true
91
139
  }
92
140
  },
93
141
  "scripts": {
@@ -1,9 +1,9 @@
1
- import {
2
- createStep
3
- } from "./chunk-MGATMMCZ.js";
4
1
  import {
5
2
  readLocator
6
3
  } from "./chunk-DCJBLS2U.js";
4
+ import {
5
+ createStep
6
+ } from "./chunk-MGATMMCZ.js";
7
7
 
8
8
  // src/steps/to-spec-locator.ts
9
9
  function toSpecLocator(loc) {