@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.
- package/README.md +386 -0
- package/dist/actions.d.ts +233 -0
- package/dist/actions.js +116 -0
- package/dist/assert.d.ts +182 -0
- package/dist/assert.js +83 -0
- package/dist/builder-DTWMrbuv.d.ts +133 -0
- package/dist/chunk-2VUWFRR5.js +20 -0
- package/dist/chunk-DCJBLS2U.js +26 -0
- package/dist/chunk-KWUKVAGI.js +227 -0
- package/dist/chunk-MGATMMCZ.js +16 -0
- package/dist/chunk-X2FROZPN.js +149 -0
- package/dist/compiler.d.ts +22 -0
- package/dist/compiler.js +7 -0
- package/dist/control.d.ts +24 -0
- package/dist/control.js +27 -0
- package/dist/express.d.ts +12 -0
- package/dist/express.js +102 -0
- package/dist/fastify.d.ts +12 -0
- package/dist/fastify.js +72 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +464 -0
- package/dist/locators.d.ts +30 -0
- package/dist/locators.js +10 -0
- package/dist/nextjs.d.ts +12 -0
- package/dist/nextjs.js +105 -0
- package/dist/step-DLfkKI3V.d.ts +19 -0
- package/package.json +94 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var dslConfigSchema = z.object({
|
|
4
|
+
appUrl: z.string(),
|
|
5
|
+
preconditionsUrl: z.string(),
|
|
6
|
+
projectId: z.string(),
|
|
7
|
+
webhookSecret: z.string()
|
|
8
|
+
});
|
|
9
|
+
function readTestValue(value) {
|
|
10
|
+
return value.value;
|
|
11
|
+
}
|
|
12
|
+
function createTestValue(value) {
|
|
13
|
+
return { value };
|
|
14
|
+
}
|
|
15
|
+
function readPreconditionName(p) {
|
|
16
|
+
return p.name;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/engine.ts
|
|
20
|
+
function createEngine(ripplo) {
|
|
21
|
+
return {
|
|
22
|
+
executeBatch: (names, options) => executeBatch(ripplo, names, options),
|
|
23
|
+
teardown: (names, data) => teardown(ripplo, names, data)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function executeBatch(ripplo, names, options) {
|
|
27
|
+
const runId = crypto.randomUUID().slice(0, 12);
|
|
28
|
+
const cookies = [];
|
|
29
|
+
const defaultDomain = deriveDefaultDomain(options?.appUrl);
|
|
30
|
+
const state = {
|
|
31
|
+
cookies,
|
|
32
|
+
ctx: createSetupContext({ cookies, defaultDomain, runId }),
|
|
33
|
+
data: {},
|
|
34
|
+
defsByName: buildDefMap(ripplo.getPreconditions()),
|
|
35
|
+
executed: [],
|
|
36
|
+
runId
|
|
37
|
+
};
|
|
38
|
+
return runBatchSequence(state, names);
|
|
39
|
+
}
|
|
40
|
+
async function runBatchSequence(state, names) {
|
|
41
|
+
let index = 0;
|
|
42
|
+
while (index < names.length) {
|
|
43
|
+
const name = names[index];
|
|
44
|
+
if (name == null) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const error = validatePrecondition(state.defsByName, name);
|
|
48
|
+
if (error != null) {
|
|
49
|
+
return fail(state, error);
|
|
50
|
+
}
|
|
51
|
+
const stepError = await executeOnePrecondition(state, name);
|
|
52
|
+
if (stepError != null) {
|
|
53
|
+
return fail(state, stepError);
|
|
54
|
+
}
|
|
55
|
+
index += 1;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
cookies: state.cookies,
|
|
59
|
+
data: state.data,
|
|
60
|
+
error: void 0,
|
|
61
|
+
executed: state.executed,
|
|
62
|
+
runId: state.runId,
|
|
63
|
+
success: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function validatePrecondition(defsByName, name) {
|
|
67
|
+
const def = defsByName.get(name);
|
|
68
|
+
if (def == null) {
|
|
69
|
+
return `Unknown precondition: "${name}"`;
|
|
70
|
+
}
|
|
71
|
+
if (!def.implemented) {
|
|
72
|
+
return `Precondition "${name}" is not implemented`;
|
|
73
|
+
}
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
async function executeOnePrecondition(state, name) {
|
|
77
|
+
const def = state.defsByName.get(name);
|
|
78
|
+
if (def == null) {
|
|
79
|
+
return `Unknown precondition: "${name}"`;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const result = await def.setup(state.ctx, state.data);
|
|
83
|
+
const resolved = {};
|
|
84
|
+
Object.entries(result).forEach(([key, value]) => {
|
|
85
|
+
resolved[key] = readTestValue(value);
|
|
86
|
+
});
|
|
87
|
+
state.data[name] = resolved;
|
|
88
|
+
state.executed.push(name);
|
|
89
|
+
return void 0;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return error instanceof Error ? error.message : String(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function fail(state, error) {
|
|
95
|
+
return {
|
|
96
|
+
cookies: state.cookies,
|
|
97
|
+
data: state.data,
|
|
98
|
+
error,
|
|
99
|
+
executed: state.executed,
|
|
100
|
+
runId: state.runId,
|
|
101
|
+
success: false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function teardown(ripplo, names, data) {
|
|
105
|
+
const defsByName = buildDefMap(ripplo.getPreconditions());
|
|
106
|
+
const reversed = [...names].toReversed();
|
|
107
|
+
let index = 0;
|
|
108
|
+
while (index < reversed.length) {
|
|
109
|
+
const name = reversed[index];
|
|
110
|
+
if (name != null) {
|
|
111
|
+
await teardownOne(defsByName, name, data);
|
|
112
|
+
}
|
|
113
|
+
index += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function teardownOne(defsByName, name, data) {
|
|
117
|
+
const def = defsByName.get(name);
|
|
118
|
+
if (def?.teardown == null) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await def.teardown({ data: data[name] ?? {} });
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function buildDefMap(defs) {
|
|
127
|
+
return new Map(defs.map((d) => [d.name, d]));
|
|
128
|
+
}
|
|
129
|
+
function createSetupContext({
|
|
130
|
+
cookies,
|
|
131
|
+
defaultDomain,
|
|
132
|
+
runId
|
|
133
|
+
}) {
|
|
134
|
+
return {
|
|
135
|
+
runId,
|
|
136
|
+
fixed: (value) => createTestValue(value),
|
|
137
|
+
setCookie: (name, value, options) => {
|
|
138
|
+
const resolvedOptions = options != null && options.domain == null && defaultDomain != null ? { ...options, domain: defaultDomain } : options ?? void 0;
|
|
139
|
+
cookies.push({ name, options: resolvedOptions, value });
|
|
140
|
+
},
|
|
141
|
+
uniqueEmail: () => createTestValue(`ripplo-test-${runId}@test.ripplo.ai`),
|
|
142
|
+
uniqueId: (prefix) => createTestValue(`ripplo-test-${prefix}-${runId}`)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function deriveDefaultDomain(baseUrl) {
|
|
146
|
+
if (baseUrl == null) {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
return new URL(baseUrl).hostname;
|
|
151
|
+
} catch {
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/adapters/shared.ts
|
|
157
|
+
import { Webhook, WebhookVerificationError } from "standardwebhooks";
|
|
158
|
+
import { z as z2 } from "zod";
|
|
159
|
+
var batchRequestSchema = z2.object({
|
|
160
|
+
preconditions: z2.array(z2.string().min(1))
|
|
161
|
+
});
|
|
162
|
+
var teardownRequestSchema = z2.object({
|
|
163
|
+
data: z2.record(z2.string(), z2.record(z2.string(), z2.string())),
|
|
164
|
+
preconditions: z2.array(z2.string().min(1))
|
|
165
|
+
});
|
|
166
|
+
function verifyWebhookSignature(payload, headers, secret) {
|
|
167
|
+
try {
|
|
168
|
+
const wh = new Webhook(secret);
|
|
169
|
+
wh.verify(payload, {
|
|
170
|
+
"webhook-id": headers["webhook-id"] ?? "",
|
|
171
|
+
"webhook-signature": headers["webhook-signature"] ?? "",
|
|
172
|
+
"webhook-timestamp": headers["webhook-timestamp"] ?? ""
|
|
173
|
+
});
|
|
174
|
+
return true;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error instanceof WebhookVerificationError) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function serializeCookie(cookie) {
|
|
183
|
+
return {
|
|
184
|
+
domain: cookie.options?.domain,
|
|
185
|
+
expires: cookie.options?.expires == null ? void 0 : new Date(cookie.options.expires * 1e3),
|
|
186
|
+
httpOnly: cookie.options?.httpOnly,
|
|
187
|
+
name: cookie.name,
|
|
188
|
+
path: cookie.options?.path,
|
|
189
|
+
sameSite: cookie.options?.sameSite,
|
|
190
|
+
secure: cookie.options?.secure,
|
|
191
|
+
value: cookie.value
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function buildSetCookieHeader(cookie) {
|
|
195
|
+
const parts = [`${cookie.name}=${cookie.value}`];
|
|
196
|
+
if (cookie.domain != null) {
|
|
197
|
+
parts.push(`Domain=${cookie.domain}`);
|
|
198
|
+
}
|
|
199
|
+
if (cookie.path != null) {
|
|
200
|
+
parts.push(`Path=${cookie.path}`);
|
|
201
|
+
}
|
|
202
|
+
if (cookie.expires != null) {
|
|
203
|
+
parts.push(`Expires=${cookie.expires.toUTCString()}`);
|
|
204
|
+
}
|
|
205
|
+
if (cookie.httpOnly === true) {
|
|
206
|
+
parts.push("HttpOnly");
|
|
207
|
+
}
|
|
208
|
+
if (cookie.secure === true) {
|
|
209
|
+
parts.push("Secure");
|
|
210
|
+
}
|
|
211
|
+
if (cookie.sameSite != null) {
|
|
212
|
+
const capitalized = cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1);
|
|
213
|
+
parts.push(`SameSite=${capitalized}`);
|
|
214
|
+
}
|
|
215
|
+
return parts.join("; ");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export {
|
|
219
|
+
dslConfigSchema,
|
|
220
|
+
readPreconditionName,
|
|
221
|
+
createEngine,
|
|
222
|
+
batchRequestSchema,
|
|
223
|
+
teardownRequestSchema,
|
|
224
|
+
verifyWebhookSignature,
|
|
225
|
+
serializeCookie,
|
|
226
|
+
buildSetCookieHeader
|
|
227
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStep,
|
|
3
|
+
readStep
|
|
4
|
+
} from "./chunk-MGATMMCZ.js";
|
|
5
|
+
|
|
6
|
+
// src/compiler.ts
|
|
7
|
+
function compile(ripplo) {
|
|
8
|
+
const preconditionDefs = ripplo.getPreconditions();
|
|
9
|
+
const testDefs = ripplo.getTests();
|
|
10
|
+
validateUniqueIds(testDefs);
|
|
11
|
+
const tests = testDefs.map((def) => compileTest(def));
|
|
12
|
+
const graph = compileGraph(preconditionDefs, testDefs);
|
|
13
|
+
return { config: ripplo.getConfig(), graph, tests };
|
|
14
|
+
}
|
|
15
|
+
function validateUniqueIds(defs) {
|
|
16
|
+
const seen = /* @__PURE__ */ new Map();
|
|
17
|
+
defs.forEach((def) => {
|
|
18
|
+
const existing = seen.get(def.id);
|
|
19
|
+
if (existing != null) {
|
|
20
|
+
throw new Error(`Duplicate test id "${def.id}" used by "${existing}" and "${def.name}"`);
|
|
21
|
+
}
|
|
22
|
+
seen.set(def.id, def.name);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function compileTest(def) {
|
|
26
|
+
const slug = def.id;
|
|
27
|
+
const { accessedKeys, vars } = buildPlaceholderVars(def.requiresKeys);
|
|
28
|
+
const startsAtUrl = def.startsAtFn == null ? void 0 : def.startsAtFn(vars);
|
|
29
|
+
const userSteps = def.stepsFn == null ? [] : def.stepsFn(vars);
|
|
30
|
+
const allSteps = startsAtUrl == null ? userSteps : [createGotoStep(startsAtUrl), ...userSteps];
|
|
31
|
+
const spec = compileSteps(allSteps, accessedKeys, def.requiresKeys);
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const hasRequires = Object.keys(def.requiresKeys).length > 0;
|
|
34
|
+
if (hasRequires && accessedKeys.size === 0 && def.implemented) {
|
|
35
|
+
warnings.push(
|
|
36
|
+
"Test requires preconditions but never references their data \u2014 destructure and use precondition data in steps()"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
additionalChecks: [],
|
|
41
|
+
description: def.description,
|
|
42
|
+
expectedOutcome: def.expectedOutcome,
|
|
43
|
+
name: def.name,
|
|
44
|
+
slug,
|
|
45
|
+
spec,
|
|
46
|
+
warnings
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createGotoStep(url) {
|
|
50
|
+
return createStep({ type: "goto", url: { type: "static", value: url } }).as(
|
|
51
|
+
`navigate to ${url}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function buildPlaceholderVars(requiresKeys) {
|
|
55
|
+
const accessedKeys = /* @__PURE__ */ new Set();
|
|
56
|
+
const vars = {};
|
|
57
|
+
Object.keys(requiresKeys).forEach((ns) => {
|
|
58
|
+
vars[ns] = new Proxy(
|
|
59
|
+
{},
|
|
60
|
+
{
|
|
61
|
+
get(_target, prop) {
|
|
62
|
+
if (typeof prop === "string") {
|
|
63
|
+
const qualifiedKey = `${ns}.${prop}`;
|
|
64
|
+
accessedKeys.add(qualifiedKey);
|
|
65
|
+
return `{{${qualifiedKey}}}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
return { accessedKeys, vars };
|
|
72
|
+
}
|
|
73
|
+
function compileSteps(steps, accessedKeys, requiresKeys) {
|
|
74
|
+
const nodes = {};
|
|
75
|
+
steps.forEach((step, index) => {
|
|
76
|
+
const id = `step-${String(index)}`;
|
|
77
|
+
const next = index < steps.length - 1 ? `step-${String(index + 1)}` : void 0;
|
|
78
|
+
nodes[id] = compileNode(step, id, next);
|
|
79
|
+
});
|
|
80
|
+
const variables = {};
|
|
81
|
+
accessedKeys.forEach((key) => {
|
|
82
|
+
variables[key] = { default: `test-${key}`, type: "string" };
|
|
83
|
+
});
|
|
84
|
+
const variableNamespaces = { ...requiresKeys };
|
|
85
|
+
return { entryNode: "step-0", nodes, variableNamespaces, variables, version: 2 };
|
|
86
|
+
}
|
|
87
|
+
function compileNode(step, id, next) {
|
|
88
|
+
const { label, node: raw } = readStep(step);
|
|
89
|
+
return { ...raw, id, label, next };
|
|
90
|
+
}
|
|
91
|
+
function compileGraph(preconditionDefs, testDefs) {
|
|
92
|
+
const preconditions = {};
|
|
93
|
+
preconditionDefs.forEach((def) => {
|
|
94
|
+
preconditions[def.name] = {
|
|
95
|
+
depends: [...def.dependsOn],
|
|
96
|
+
description: def.description,
|
|
97
|
+
returns: [...def.returns]
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
const states = {};
|
|
101
|
+
const edges = [];
|
|
102
|
+
testDefs.forEach((def) => {
|
|
103
|
+
if (!def.implemented || def.startsAtFn == null) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const resolved = resolveDependencyChain(def.requires, preconditionDefs);
|
|
107
|
+
const { vars } = buildPlaceholderVars(def.requiresKeys);
|
|
108
|
+
const route = def.startsAtFn(vars);
|
|
109
|
+
const stateId = deriveStateId(resolved, route);
|
|
110
|
+
states[stateId] = { preconditions: [...resolved], route };
|
|
111
|
+
edges.push({
|
|
112
|
+
from: stateId,
|
|
113
|
+
requiresKeys: def.requiresKeys,
|
|
114
|
+
to: stateId,
|
|
115
|
+
workflow: def.id
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return { edges, preconditions, states, version: 3 };
|
|
119
|
+
}
|
|
120
|
+
function resolveDependencyChain(requires, preconditionDefs) {
|
|
121
|
+
const defsByName = new Map(preconditionDefs.map((d) => [d.name, d]));
|
|
122
|
+
const resolved = [];
|
|
123
|
+
const visited = /* @__PURE__ */ new Set();
|
|
124
|
+
function visit(name) {
|
|
125
|
+
if (visited.has(name)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
visited.add(name);
|
|
129
|
+
defsByName.get(name)?.dependsOn.forEach((dep) => {
|
|
130
|
+
visit(dep);
|
|
131
|
+
});
|
|
132
|
+
resolved.push(name);
|
|
133
|
+
}
|
|
134
|
+
requires.forEach((name) => {
|
|
135
|
+
visit(name);
|
|
136
|
+
});
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
function deriveStateId(preconditions, route) {
|
|
140
|
+
const sorted = preconditions.toSorted((a, b) => a.localeCompare(b));
|
|
141
|
+
return slugify(`${sorted.join("-")}-${route}`);
|
|
142
|
+
}
|
|
143
|
+
function slugify(input) {
|
|
144
|
+
return input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
compile
|
|
149
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StateGraph, WorkflowSpec } from '@ripplo/spec';
|
|
2
|
+
import { D as DslConfig, R as RipploBuilder } from './builder-DTWMrbuv.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import './step-DLfkKI3V.js';
|
|
5
|
+
|
|
6
|
+
interface CompileResult {
|
|
7
|
+
readonly config: DslConfig;
|
|
8
|
+
readonly graph: StateGraph;
|
|
9
|
+
readonly tests: ReadonlyArray<CompiledTest>;
|
|
10
|
+
}
|
|
11
|
+
interface CompiledTest {
|
|
12
|
+
readonly additionalChecks: ReadonlyArray<string>;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly expectedOutcome: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly slug: string;
|
|
17
|
+
readonly spec: WorkflowSpec;
|
|
18
|
+
readonly warnings: ReadonlyArray<string>;
|
|
19
|
+
}
|
|
20
|
+
declare function compile(ripplo: RipploBuilder): CompileResult;
|
|
21
|
+
|
|
22
|
+
export { type CompileResult, type CompiledTest, compile };
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { U as UnlabeledStep } from './step-DLfkKI3V.js';
|
|
2
|
+
import { AnyLocator } from './locators.js';
|
|
3
|
+
import '@ripplo/spec';
|
|
4
|
+
|
|
5
|
+
declare const VARIABLE_INTERNAL: unique symbol;
|
|
6
|
+
interface Variable<_TName extends string> {
|
|
7
|
+
readonly [VARIABLE_INTERNAL]: _TName;
|
|
8
|
+
}
|
|
9
|
+
declare function variable<TName extends string>(name: TName): Variable<TName>;
|
|
10
|
+
declare function readVariable(v: Variable<string>): string;
|
|
11
|
+
declare function extract(locator: AnyLocator, target: Variable<string>): UnlabeledStep<{
|
|
12
|
+
locator: {
|
|
13
|
+
by: "testId";
|
|
14
|
+
value: string;
|
|
15
|
+
} | {
|
|
16
|
+
by: "role";
|
|
17
|
+
role: string;
|
|
18
|
+
name?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
type: "extractText";
|
|
21
|
+
variable: string;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export { type Variable, extract, readVariable, variable };
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toSpecLocator
|
|
3
|
+
} from "./chunk-2VUWFRR5.js";
|
|
4
|
+
import {
|
|
5
|
+
createStep
|
|
6
|
+
} from "./chunk-MGATMMCZ.js";
|
|
7
|
+
import "./chunk-DCJBLS2U.js";
|
|
8
|
+
|
|
9
|
+
// src/steps/control.ts
|
|
10
|
+
function variable(name) {
|
|
11
|
+
return { name };
|
|
12
|
+
}
|
|
13
|
+
function readVariable(v) {
|
|
14
|
+
return v.name;
|
|
15
|
+
}
|
|
16
|
+
function extract(locator, target) {
|
|
17
|
+
return createStep({
|
|
18
|
+
locator: toSpecLocator(locator),
|
|
19
|
+
type: "extractText",
|
|
20
|
+
variable: readVariable(target)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
extract,
|
|
25
|
+
readVariable,
|
|
26
|
+
variable
|
|
27
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { R as RipploBuilder } from './builder-DTWMrbuv.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import './step-DLfkKI3V.js';
|
|
5
|
+
import '@ripplo/spec';
|
|
6
|
+
|
|
7
|
+
interface CreateExpressHandlerParams {
|
|
8
|
+
readonly ripplo: RipploBuilder;
|
|
9
|
+
}
|
|
10
|
+
declare function createExpressHandler({ ripplo }: CreateExpressHandlerParams): Router;
|
|
11
|
+
|
|
12
|
+
export { type CreateExpressHandlerParams, createExpressHandler };
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batchRequestSchema,
|
|
3
|
+
createEngine,
|
|
4
|
+
serializeCookie,
|
|
5
|
+
teardownRequestSchema,
|
|
6
|
+
verifyWebhookSignature
|
|
7
|
+
} from "./chunk-KWUKVAGI.js";
|
|
8
|
+
|
|
9
|
+
// src/adapters/express.ts
|
|
10
|
+
import { Router, json } from "express";
|
|
11
|
+
function createExpressHandler({ ripplo }) {
|
|
12
|
+
const engine = createEngine(ripplo);
|
|
13
|
+
const webhookSecret = ripplo.getConfig().webhookSecret;
|
|
14
|
+
const router = Router();
|
|
15
|
+
router.use(json());
|
|
16
|
+
router.use((req, res, next) => {
|
|
17
|
+
if (webhookSecret.length === 0) {
|
|
18
|
+
res.status(403).json({ error: "Webhook secret not configured" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const payload = JSON.stringify(req.body);
|
|
22
|
+
const headers = extractWebhookHeaders(req);
|
|
23
|
+
if (!verifyWebhookSignature(payload, headers, webhookSecret)) {
|
|
24
|
+
res.status(401).json({ error: "Invalid webhook signature" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
next();
|
|
28
|
+
});
|
|
29
|
+
router.put("/execute-batch", (req, res) => {
|
|
30
|
+
const parsed = batchRequestSchema.safeParse(req.body);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
res.status(400).json({ error: "Invalid request body", success: false });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const appUrl = `${req.protocol}://${req.get("host") ?? ""}`;
|
|
36
|
+
void engine.executeBatch(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
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
router.put("/teardown", (req, res) => {
|
|
51
|
+
const parsed = teardownRequestSchema.safeParse(req.body);
|
|
52
|
+
if (!parsed.success) {
|
|
53
|
+
res.status(400).json({ error: "Invalid request body", success: false });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
void engine.teardown(parsed.data.preconditions, parsed.data.data).then(() => {
|
|
57
|
+
res.json({ success: true });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return router;
|
|
61
|
+
}
|
|
62
|
+
function extractWebhookHeaders(req) {
|
|
63
|
+
return {
|
|
64
|
+
"webhook-id": asString(req.headers["webhook-id"]),
|
|
65
|
+
"webhook-signature": asString(req.headers["webhook-signature"]),
|
|
66
|
+
"webhook-timestamp": asString(req.headers["webhook-timestamp"])
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function asString(value) {
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return value[0] ?? void 0;
|
|
75
|
+
}
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
function buildExpressCookieOptions(s) {
|
|
79
|
+
const opts = {};
|
|
80
|
+
if (s.domain != null) {
|
|
81
|
+
opts.domain = s.domain;
|
|
82
|
+
}
|
|
83
|
+
if (s.expires != null) {
|
|
84
|
+
opts.expires = s.expires;
|
|
85
|
+
}
|
|
86
|
+
if (s.httpOnly != null) {
|
|
87
|
+
opts.httpOnly = s.httpOnly;
|
|
88
|
+
}
|
|
89
|
+
if (s.path != null) {
|
|
90
|
+
opts.path = s.path;
|
|
91
|
+
}
|
|
92
|
+
if (s.sameSite != null) {
|
|
93
|
+
opts.sameSite = s.sameSite;
|
|
94
|
+
}
|
|
95
|
+
if (s.secure != null) {
|
|
96
|
+
opts.secure = s.secure;
|
|
97
|
+
}
|
|
98
|
+
return opts;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
createExpressHandler
|
|
102
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FastifyInstance } from 'fastify';
|
|
2
|
+
import { R as RipploBuilder } from './builder-DTWMrbuv.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import './step-DLfkKI3V.js';
|
|
5
|
+
import '@ripplo/spec';
|
|
6
|
+
|
|
7
|
+
interface RegisterFastifyHandlerParams {
|
|
8
|
+
readonly ripplo: RipploBuilder;
|
|
9
|
+
}
|
|
10
|
+
declare function registerFastifyHandler({ ripplo, }: RegisterFastifyHandlerParams): (fastify: FastifyInstance) => Promise<void>;
|
|
11
|
+
|
|
12
|
+
export { type RegisterFastifyHandlerParams, registerFastifyHandler };
|
package/dist/fastify.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batchRequestSchema,
|
|
3
|
+
buildSetCookieHeader,
|
|
4
|
+
createEngine,
|
|
5
|
+
serializeCookie,
|
|
6
|
+
teardownRequestSchema,
|
|
7
|
+
verifyWebhookSignature
|
|
8
|
+
} from "./chunk-KWUKVAGI.js";
|
|
9
|
+
|
|
10
|
+
// src/adapters/fastify.ts
|
|
11
|
+
function registerFastifyHandler({
|
|
12
|
+
ripplo
|
|
13
|
+
}) {
|
|
14
|
+
const engine = createEngine(ripplo);
|
|
15
|
+
const webhookSecret = ripplo.getConfig().webhookSecret;
|
|
16
|
+
return async (fastify) => {
|
|
17
|
+
fastify.addHook("preHandler", async (req, reply) => {
|
|
18
|
+
if (webhookSecret.length === 0) {
|
|
19
|
+
return reply.code(403).send({ error: "Webhook secret not configured" });
|
|
20
|
+
}
|
|
21
|
+
const payload = JSON.stringify(req.body);
|
|
22
|
+
const headers = {
|
|
23
|
+
"webhook-id": extractHeader(req, "webhook-id"),
|
|
24
|
+
"webhook-signature": extractHeader(req, "webhook-signature"),
|
|
25
|
+
"webhook-timestamp": extractHeader(req, "webhook-timestamp")
|
|
26
|
+
};
|
|
27
|
+
if (!verifyWebhookSignature(payload, headers, webhookSecret)) {
|
|
28
|
+
return reply.code(401).send({ error: "Invalid webhook signature" });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
fastify.put("/execute-batch", async (req, reply) => {
|
|
32
|
+
const parsed = batchRequestSchema.safeParse(req.body);
|
|
33
|
+
if (!parsed.success) {
|
|
34
|
+
return reply.code(400).send({ error: "Invalid request body", success: false });
|
|
35
|
+
}
|
|
36
|
+
const appUrl = `${req.protocol}://${req.hostname}`;
|
|
37
|
+
const result = await engine.executeBatch(parsed.data.preconditions, { appUrl });
|
|
38
|
+
result.cookies.forEach((cookie) => {
|
|
39
|
+
const s = serializeCookie(cookie);
|
|
40
|
+
reply.header("Set-Cookie", buildSetCookieHeader(s));
|
|
41
|
+
});
|
|
42
|
+
return reply.send({
|
|
43
|
+
data: result.data,
|
|
44
|
+
error: result.error,
|
|
45
|
+
executed: result.executed,
|
|
46
|
+
runId: result.runId,
|
|
47
|
+
success: result.success
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
fastify.put("/teardown", async (req, reply) => {
|
|
51
|
+
const parsed = teardownRequestSchema.safeParse(req.body);
|
|
52
|
+
if (!parsed.success) {
|
|
53
|
+
return reply.code(400).send({ error: "Invalid request body", success: false });
|
|
54
|
+
}
|
|
55
|
+
await engine.teardown(parsed.data.preconditions, parsed.data.data);
|
|
56
|
+
return reply.send({ success: true });
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function extractHeader(req, name) {
|
|
61
|
+
const value = req.headers[name];
|
|
62
|
+
if (typeof value === "string") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value[0] ?? void 0;
|
|
67
|
+
}
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
registerFastifyHandler
|
|
72
|
+
};
|