@aletheia-dev/plugin-sdk 0.3.0 → 0.4.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.
@@ -0,0 +1,167 @@
1
+ import { DocumentHandle, PluginDocuments, Logger, PluginContext, PluginManifest, PluginConfig, WebhookRequest, Plugin } from './index.js';
2
+ import 'zod';
3
+
4
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
5
+ /** One call to the recording logger, with the bindings of the child logger that made it. */
6
+ interface LogEntry {
7
+ level: LogLevel;
8
+ message: string;
9
+ /** The object passed first (pino style), `{}` when the call started with the message. */
10
+ data: Record<string, unknown>;
11
+ bindings: Record<string, unknown>;
12
+ }
13
+ /** A `Logger` that keeps every call in `entries`; children share the array and merge bindings. */
14
+ interface RecordingLogger extends Logger {
15
+ entries: LogEntry[];
16
+ bindings: Record<string, unknown>;
17
+ child(bindings: Record<string, unknown>): RecordingLogger;
18
+ }
19
+ declare function createRecordingLogger(bindings?: Record<string, unknown>, entries?: LogEntry[]): RecordingLogger;
20
+ /** One call a plugin made through `ctx.fetch`, normalised for assertions. */
21
+ interface FetchCall {
22
+ url: string;
23
+ method: string;
24
+ /** Header names lower-cased, as the vendor would see them. */
25
+ headers: Record<string, string>;
26
+ /** The request body as bytes; `null` when there was none or it was not a string or bytes. */
27
+ body: Uint8Array | null;
28
+ /** The `init` the plugin passed, untouched, for anything the normalised view drops. */
29
+ init: RequestInit | undefined;
30
+ }
31
+ /** What answers the plugin's requests; the global `fetch` type fits. */
32
+ type FetchHandler = (input: string | URL | Request, init?: RequestInit) => Response | Promise<Response>;
33
+ /** A `fetch` that records its calls. The default handler answers every request with 404. */
34
+ type FetchStub = typeof fetch & {
35
+ calls: FetchCall[];
36
+ };
37
+ declare function createFetchStub(handler?: FetchHandler): FetchStub;
38
+ /** Builds a `DocumentHandle` from bytes or text, filling in the size. */
39
+ declare function documentHandle(doc: {
40
+ id: string;
41
+ bytes?: Uint8Array;
42
+ text?: string;
43
+ fileName?: string;
44
+ contentType?: string;
45
+ }): DocumentHandle;
46
+ /**
47
+ * In-memory `PluginDocuments`: holds the tenant's clean documents by id and records which ids a
48
+ * plugin read. Like the platform, an unknown id throws rather than returning `undefined`.
49
+ */
50
+ declare class MemoryDocuments implements PluginDocuments {
51
+ readonly reads: string[];
52
+ private readonly docs;
53
+ constructor(documents?: DocumentHandle[]);
54
+ add(doc: DocumentHandle): this;
55
+ read(documentId: string): Promise<DocumentHandle>;
56
+ }
57
+ interface TestContextOptions {
58
+ /** Raw tenant configuration; parsed through `configSchema`, so defaults apply. Default `{}`. */
59
+ config?: unknown;
60
+ /** Secret values by name. Every secret the manifest declares must be present, as at runtime. */
61
+ secrets?: Record<string, string>;
62
+ /**
63
+ * Documents the plugin may read. Default: an empty in-memory source. Pass `null` for a
64
+ * deployment without object storage (`ctx.documents` is then absent).
65
+ */
66
+ documents?: DocumentHandle[] | MemoryDocuments | null;
67
+ /** Answers `ctx.fetch`; calls are recorded either way. Default: 404 for everything. */
68
+ fetch?: FetchHandler;
69
+ tenantId?: string;
70
+ /** Default: `http://localhost:4000/webhooks/plugins/<encoded plugin name>`. */
71
+ callbackUrl?: string | null;
72
+ idempotencyKey?: string;
73
+ /** Default: a signal that never aborts. */
74
+ signal?: AbortSignal;
75
+ }
76
+ /** A `PluginContext` whose logger, fetch and documents record what the plugin did. */
77
+ interface TestContext<C = unknown> extends PluginContext<C> {
78
+ logger: RecordingLogger;
79
+ fetch: FetchStub;
80
+ documents?: MemoryDocuments;
81
+ }
82
+ declare const DEFAULT_TENANT_ID = "tenant-test";
83
+ /** The callback URL the platform hands a plugin for a given API base URL. */
84
+ declare function callbackUrlFor(pluginName: string, base?: string): string;
85
+ /**
86
+ * Builds the context the runtime would hand the plugin for one tenant: the config parsed through
87
+ * the manifest's `configSchema`, the declared secrets resolved from `options.secrets`, a logger
88
+ * bound to the plugin and tenant that records every entry, a `fetch` stub that records every
89
+ * call and an in-memory document source. Throws when the config is invalid or a secret is
90
+ * missing, which is what the runtime does at worker start.
91
+ */
92
+ declare function createTestContext<M extends PluginManifest>(manifest: M, options?: TestContextOptions): TestContext<PluginConfig<M>>;
93
+
94
+ /** One input an author knows the plugin handles, used to exercise the runtime's contract. */
95
+ interface ConformanceSample {
96
+ action: string;
97
+ input: unknown;
98
+ /**
99
+ * For an asynchronous action: builds the vendor's webhook for the external id `invoke`
100
+ * returned (use `webhookRequest` to sign it). `handleWebhook` must then resolve the same id.
101
+ */
102
+ webhook?: (externalId: string, ctx: PluginContext) => WebhookRequest | Promise<WebhookRequest>;
103
+ }
104
+ interface ConformanceOptions {
105
+ samples: ConformanceSample[];
106
+ /** The context the samples run with: options for `createTestContext`, or a ready context. */
107
+ context?: TestContextOptions | PluginContext;
108
+ }
109
+ interface ConformanceCheck {
110
+ name: string;
111
+ ok: boolean;
112
+ /** Why the check failed, or a note on what passed. */
113
+ message?: string;
114
+ }
115
+ interface ConformanceResult {
116
+ ok: boolean;
117
+ checks: ConformanceCheck[];
118
+ failures: ConformanceCheck[];
119
+ }
120
+ /**
121
+ * Runs the contract checks the platform applies to a plugin, without the platform: the manifest
122
+ * parses with `PluginManifestSchema`; every action's `input` and `output` are zod schemas;
123
+ * `invoke` rejects an unknown action; for each sample, the input validates and the output
124
+ * validates against the action's output schema; an asynchronous action returns `pending()` with
125
+ * a non-empty id and, when the sample supplies a webhook, `handleWebhook` resolves that id (and
126
+ * a completed event's output validates). Never throws for a failing plugin: read `result.ok` or
127
+ * use `assertConformance` with any test runner.
128
+ */
129
+ declare function conformance(plugin: Plugin, options: ConformanceOptions): Promise<ConformanceResult>;
130
+ /** `conformance`, throwing one error that lists every failed check. Works with any test runner. */
131
+ declare function assertConformance(plugin: Plugin, options: ConformanceOptions): Promise<ConformanceResult>;
132
+
133
+ /** The header `webhookRequest` signs into when the plugin does not name its own. */
134
+ declare const DEFAULT_SIGNATURE_HEADER = "x-signature";
135
+ interface WebhookRequestOptions {
136
+ /** An object is JSON-encoded; a string or bytes are sent as they are. */
137
+ body: unknown;
138
+ /** When set, the raw body is signed with HMAC-SHA256 into `headerName`. */
139
+ secret?: string;
140
+ /** Header that carries the signature. Default `x-signature`. */
141
+ headerName?: string;
142
+ /** Signature encoding passed to `signHmacSha256`. Default `hex`. */
143
+ encoding?: 'hex' | 'base64';
144
+ /** Extra headers; names are lower-cased like the API does. */
145
+ headers?: Record<string, string>;
146
+ /** Extra query parameters, as the API passes them through to the plugin. */
147
+ query?: Record<string, string>;
148
+ /**
149
+ * The vendor session the webhook is about. Put on the URL as `?externalId=` unless the plugin
150
+ * implements `webhookExternalId` (its vendor cannot carry a query string). When omitted, a
151
+ * string `externalId` field of a JSON body is used.
152
+ */
153
+ externalId?: string;
154
+ /** Default `POST`. */
155
+ method?: string;
156
+ /** Default `application/json` for an object body, `application/octet-stream` otherwise. */
157
+ contentType?: string;
158
+ }
159
+ /**
160
+ * Builds the `WebhookRequest` the API would hand `handleWebhook` for a vendor callback: raw
161
+ * body bytes, lower-cased headers, `POST`, and the query string with the external id when the
162
+ * plugin relies on the `?externalId=` convention. With `secret`, the raw body is signed with
163
+ * the SDK's `signHmacSha256`, so `verifyHmacSha256` in the plugin accepts it.
164
+ */
165
+ declare function webhookRequest(plugin: Plugin, options: WebhookRequestOptions): WebhookRequest;
166
+
167
+ export { type ConformanceCheck, type ConformanceOptions, type ConformanceResult, type ConformanceSample, DEFAULT_SIGNATURE_HEADER, DEFAULT_TENANT_ID, type FetchCall, type FetchHandler, type FetchStub, type LogEntry, type LogLevel, MemoryDocuments, type RecordingLogger, type TestContext, type TestContextOptions, type WebhookRequestOptions, assertConformance, callbackUrlFor, conformance, createFetchStub, createRecordingLogger, createTestContext, documentHandle, webhookRequest };
@@ -0,0 +1,312 @@
1
+ import {
2
+ PluginManifestSchema,
3
+ isPending,
4
+ signHmacSha256
5
+ } from "./chunk-FEZWGVUS.js";
6
+
7
+ // src/testing/context.ts
8
+ function createRecordingLogger(bindings = {}, entries = []) {
9
+ const record = (level) => (objOrMsg, msg) => {
10
+ const isObject = typeof objOrMsg === "object" && objOrMsg !== null;
11
+ entries.push({
12
+ level,
13
+ message: isObject ? msg ?? "" : String(objOrMsg),
14
+ data: isObject ? { ...objOrMsg } : {},
15
+ bindings
16
+ });
17
+ };
18
+ return {
19
+ entries,
20
+ bindings,
21
+ debug: record("debug"),
22
+ info: record("info"),
23
+ warn: record("warn"),
24
+ error: record("error"),
25
+ child: (more) => createRecordingLogger({ ...bindings, ...more }, entries)
26
+ };
27
+ }
28
+ var notFound = () => new Response("not found", { status: 404 });
29
+ function headersOf(input, init) {
30
+ const out = {};
31
+ const add = (name, value) => {
32
+ out[name.toLowerCase()] = value;
33
+ };
34
+ if (input instanceof Request) input.headers.forEach((value, name) => add(name, value));
35
+ const headers = init?.headers;
36
+ if (headers instanceof Headers) headers.forEach((value, name) => add(name, value));
37
+ else if (Array.isArray(headers)) {
38
+ for (const pair of headers) {
39
+ if (pair[0] !== void 0 && pair[1] !== void 0) add(pair[0], pair[1]);
40
+ }
41
+ } else if (headers) {
42
+ for (const [name, value] of Object.entries(headers)) {
43
+ add(name, Array.isArray(value) ? value.join(", ") : String(value));
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ function bodyOf(body) {
49
+ if (body === null || body === void 0) return null;
50
+ if (typeof body === "string") return new TextEncoder().encode(body);
51
+ if (body instanceof Uint8Array) return body;
52
+ if (body instanceof ArrayBuffer) return new Uint8Array(body);
53
+ if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString());
54
+ return null;
55
+ }
56
+ function createFetchStub(handler = notFound) {
57
+ const calls = [];
58
+ const stub = async (input, init) => {
59
+ calls.push({
60
+ url: input instanceof Request ? input.url : String(input),
61
+ method: (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(),
62
+ headers: headersOf(input, init),
63
+ body: bodyOf(init?.body),
64
+ init
65
+ });
66
+ return handler(input, init);
67
+ };
68
+ return Object.assign(stub, { calls });
69
+ }
70
+ function documentHandle(doc) {
71
+ const bytes = doc.bytes ?? new TextEncoder().encode(doc.text ?? "");
72
+ return {
73
+ id: doc.id,
74
+ fileName: doc.fileName ?? `${doc.id}.bin`,
75
+ contentType: doc.contentType ?? "application/octet-stream",
76
+ sizeBytes: bytes.byteLength,
77
+ bytes
78
+ };
79
+ }
80
+ var MemoryDocuments = class {
81
+ reads = [];
82
+ docs = /* @__PURE__ */ new Map();
83
+ constructor(documents = []) {
84
+ for (const doc of documents) this.add(doc);
85
+ }
86
+ add(doc) {
87
+ this.docs.set(doc.id, doc);
88
+ return this;
89
+ }
90
+ async read(documentId) {
91
+ this.reads.push(documentId);
92
+ const doc = this.docs.get(documentId);
93
+ if (!doc) throw new Error(`document ${documentId} not found or not clean`);
94
+ return doc;
95
+ }
96
+ };
97
+ var DEFAULT_TENANT_ID = "tenant-test";
98
+ var DEFAULT_CALLBACK_BASE = "http://localhost:4000";
99
+ function callbackUrlFor(pluginName, base = DEFAULT_CALLBACK_BASE) {
100
+ return `${base.replace(/\/$/, "")}/webhooks/plugins/${encodeURIComponent(pluginName)}`;
101
+ }
102
+ function createTestContext(manifest, options = {}) {
103
+ const config = manifest.configSchema.parse(options.config ?? {});
104
+ const tenantId = options.tenantId ?? DEFAULT_TENANT_ID;
105
+ const secrets = {};
106
+ const missing = [];
107
+ for (const name of manifest.secrets) {
108
+ const value = options.secrets?.[name];
109
+ if (value === void 0) missing.push(name);
110
+ else secrets[name] = value;
111
+ }
112
+ if (missing.length > 0) {
113
+ throw new Error(
114
+ `createTestContext: ${manifest.name} declares secrets that were not supplied: ${missing.join(", ")}`
115
+ );
116
+ }
117
+ const documents = options.documents === null ? void 0 : options.documents instanceof MemoryDocuments ? options.documents : new MemoryDocuments(options.documents);
118
+ const callbackUrl = options.callbackUrl === null ? void 0 : options.callbackUrl ?? callbackUrlFor(manifest.name);
119
+ return {
120
+ tenantId,
121
+ config,
122
+ secrets,
123
+ logger: createRecordingLogger({ plugin: manifest.name, tenantId }),
124
+ fetch: createFetchStub(options.fetch),
125
+ signal: options.signal ?? new AbortController().signal,
126
+ ...options.idempotencyKey !== void 0 && { idempotencyKey: options.idempotencyKey },
127
+ ...callbackUrl !== void 0 && { callbackUrl },
128
+ ...documents !== void 0 && { documents }
129
+ };
130
+ }
131
+
132
+ // src/testing/conformance.ts
133
+ var UNKNOWN_ACTION = "__conformance_unknown_action__";
134
+ var isContext = (value) => "logger" in value && "signal" in value;
135
+ var describeError = (error) => error instanceof Error ? error.message : String(error);
136
+ var issues = (error) => error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
137
+ var isZodSchema = (value) => typeof value === "object" && value !== null && "_zod" in value;
138
+ var Checks = class {
139
+ list = [];
140
+ pass(name, message) {
141
+ this.list.push({ name, ok: true, ...message !== void 0 && { message } });
142
+ }
143
+ fail(name, message) {
144
+ this.list.push({ name, ok: false, message });
145
+ }
146
+ /** Runs `fn`; a thrown error fails the check with its message. */
147
+ async run(name, fn) {
148
+ try {
149
+ const note = await fn();
150
+ this.pass(name, note ?? void 0);
151
+ return true;
152
+ } catch (error) {
153
+ this.fail(name, describeError(error));
154
+ return false;
155
+ }
156
+ }
157
+ };
158
+ async function conformance(plugin, options) {
159
+ const checks = new Checks();
160
+ const { manifest } = plugin;
161
+ const manifestOk = await checks.run("manifest parses with PluginManifestSchema", () => {
162
+ const parsed = PluginManifestSchema.safeParse(manifest);
163
+ if (!parsed.success) throw new Error(issues(parsed.error));
164
+ });
165
+ const actions = Object.entries(manifest.actions ?? {});
166
+ for (const [name, action] of actions) {
167
+ await checks.run(`action ${name}: input and output are zod schemas`, () => {
168
+ const bad = [
169
+ ...isZodSchema(action?.input) ? [] : ["input"],
170
+ ...isZodSchema(action?.output) ? [] : ["output"]
171
+ ];
172
+ if (bad.length > 0) throw new Error(`${bad.join(" and ")} is not a zod schema`);
173
+ });
174
+ }
175
+ if (actions.some(([, action]) => action?.async)) {
176
+ await checks.run("asynchronous actions: handleWebhook is implemented", () => {
177
+ if (typeof plugin.handleWebhook !== "function") {
178
+ throw new Error(
179
+ "the manifest declares an async action but the plugin has no handleWebhook"
180
+ );
181
+ }
182
+ });
183
+ }
184
+ let ctx;
185
+ const contextOk = await checks.run("context: config parses and secrets resolve", () => {
186
+ ctx = options.context && isContext(options.context) ? options.context : createTestContext(manifest, options.context);
187
+ });
188
+ if (manifestOk && contextOk && ctx) {
189
+ await checks.run("invoke rejects an unknown action", async () => {
190
+ let outcome;
191
+ try {
192
+ outcome = await plugin.invoke(UNKNOWN_ACTION, {}, ctx);
193
+ } catch {
194
+ return;
195
+ }
196
+ throw new Error(
197
+ `invoke(${JSON.stringify(UNKNOWN_ACTION)}) resolved with ${JSON.stringify(outcome)}`
198
+ );
199
+ });
200
+ for (const [index, sample] of options.samples.entries()) {
201
+ await runSample(plugin, ctx, sample, `sample ${index + 1} (${sample.action})`, checks);
202
+ }
203
+ }
204
+ const failures = checks.list.filter((check) => !check.ok);
205
+ return { ok: failures.length === 0, checks: checks.list, failures };
206
+ }
207
+ async function runSample(plugin, ctx, sample, label, checks) {
208
+ const action = plugin.manifest.actions[sample.action];
209
+ if (!action) {
210
+ checks.fail(`${label}: action exists`, `the manifest declares no action ${sample.action}`);
211
+ return;
212
+ }
213
+ const inputOk = await checks.run(`${label}: input validates against the input schema`, () => {
214
+ const parsed = action.input.safeParse(sample.input);
215
+ if (!parsed.success) throw new Error(issues(parsed.error));
216
+ });
217
+ if (!inputOk) return;
218
+ const input = action.input.parse(sample.input);
219
+ let result;
220
+ const invoked = await checks.run(`${label}: invoke resolves`, async () => {
221
+ result = await plugin.invoke(sample.action, input, ctx);
222
+ });
223
+ if (!invoked) return;
224
+ if (!action.async) {
225
+ await checks.run(`${label}: output validates against the output schema`, () => {
226
+ if (isPending(result)) throw new Error("a synchronous action returned pending()");
227
+ const parsed = action.output.safeParse(result);
228
+ if (!parsed.success) throw new Error(issues(parsed.error));
229
+ });
230
+ return;
231
+ }
232
+ const pendingOk = await checks.run(`${label}: returns pending() with a non-empty id`, () => {
233
+ if (!isPending(result))
234
+ throw new Error(`expected pending(externalId), got ${JSON.stringify(result)}`);
235
+ if (result.externalId.length === 0) throw new Error("externalId is empty");
236
+ });
237
+ if (!pendingOk || !sample.webhook || !isPending(result)) return;
238
+ const { externalId } = result;
239
+ await checks.run(`${label}: handleWebhook resolves the same external id`, async () => {
240
+ if (!plugin.handleWebhook) throw new Error("the plugin has no handleWebhook");
241
+ const request = await sample.webhook(externalId, ctx);
242
+ const event = await plugin.handleWebhook(request, ctx);
243
+ if (!event) throw new Error("handleWebhook ignored the sample webhook (returned null)");
244
+ if (event.externalId !== externalId) {
245
+ throw new Error(
246
+ `event.externalId is ${JSON.stringify(event.externalId)}, expected ${JSON.stringify(externalId)}`
247
+ );
248
+ }
249
+ if (event.status === "completed") {
250
+ const parsed = action.output.safeParse(event.output);
251
+ if (!parsed.success) throw new Error(`completed output is invalid: ${issues(parsed.error)}`);
252
+ }
253
+ return `status ${event.status}`;
254
+ });
255
+ }
256
+ async function assertConformance(plugin, options) {
257
+ const result = await conformance(plugin, options);
258
+ if (!result.ok) {
259
+ const lines = result.failures.map((check) => ` - ${check.name}: ${check.message ?? "failed"}`);
260
+ throw new Error(
261
+ `${plugin.manifest?.name ?? "plugin"} fails ${result.failures.length} conformance check(s):
262
+ ${lines.join("\n")}`
263
+ );
264
+ }
265
+ return result;
266
+ }
267
+
268
+ // src/testing/webhook.ts
269
+ var DEFAULT_SIGNATURE_HEADER = "x-signature";
270
+ function encodeBody(body) {
271
+ if (body instanceof Uint8Array) return { rawBody: body, isJson: false };
272
+ if (typeof body === "string") return { rawBody: new TextEncoder().encode(body), isJson: false };
273
+ return { rawBody: new TextEncoder().encode(JSON.stringify(body)), isJson: true };
274
+ }
275
+ function webhookRequest(plugin, options) {
276
+ const { rawBody, isJson } = encodeBody(options.body);
277
+ const headers = {
278
+ "content-type": options.contentType ?? (isJson ? "application/json" : "application/octet-stream")
279
+ };
280
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
281
+ headers[name.toLowerCase()] = value;
282
+ }
283
+ if (options.secret !== void 0) {
284
+ const headerName = (options.headerName ?? DEFAULT_SIGNATURE_HEADER).toLowerCase();
285
+ headers[headerName] = signHmacSha256(options.secret, rawBody, options.encoding ?? "hex");
286
+ }
287
+ const query = { ...options.query };
288
+ const externalId = options.externalId ?? externalIdOf(options.body, isJson);
289
+ if (externalId !== void 0 && !plugin.webhookExternalId && query.externalId === void 0) {
290
+ query.externalId = externalId;
291
+ }
292
+ return { method: options.method ?? "POST", headers, rawBody, query };
293
+ }
294
+ function externalIdOf(body, isJson) {
295
+ if (!isJson || typeof body !== "object" || body === null) return void 0;
296
+ const value = body.externalId;
297
+ return typeof value === "string" && value.length > 0 ? value : void 0;
298
+ }
299
+ export {
300
+ DEFAULT_SIGNATURE_HEADER,
301
+ DEFAULT_TENANT_ID,
302
+ MemoryDocuments,
303
+ assertConformance,
304
+ callbackUrlFor,
305
+ conformance,
306
+ createFetchStub,
307
+ createRecordingLogger,
308
+ createTestContext,
309
+ documentHandle,
310
+ webhookRequest
311
+ };
312
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/context.ts","../src/testing/conformance.ts","../src/testing/webhook.ts"],"sourcesContent":["import type { DocumentHandle, PluginDocuments } from '../documents.js';\nimport type { PluginConfig, PluginContext, PluginManifest } from '../index.js';\nimport type { Logger } from '../logger.js';\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/** One call to the recording logger, with the bindings of the child logger that made it. */\nexport interface LogEntry {\n level: LogLevel;\n message: string;\n /** The object passed first (pino style), `{}` when the call started with the message. */\n data: Record<string, unknown>;\n bindings: Record<string, unknown>;\n}\n\n/** A `Logger` that keeps every call in `entries`; children share the array and merge bindings. */\nexport interface RecordingLogger extends Logger {\n entries: LogEntry[];\n bindings: Record<string, unknown>;\n child(bindings: Record<string, unknown>): RecordingLogger;\n}\n\nexport function createRecordingLogger(\n bindings: Record<string, unknown> = {},\n entries: LogEntry[] = [],\n): RecordingLogger {\n const record =\n (level: LogLevel) =>\n (objOrMsg: object | string, msg?: string): void => {\n const isObject = typeof objOrMsg === 'object' && objOrMsg !== null;\n entries.push({\n level,\n message: isObject ? (msg ?? '') : String(objOrMsg),\n data: isObject ? { ...(objOrMsg as Record<string, unknown>) } : {},\n bindings,\n });\n };\n return {\n entries,\n bindings,\n debug: record('debug'),\n info: record('info'),\n warn: record('warn'),\n error: record('error'),\n child: (more) => createRecordingLogger({ ...bindings, ...more }, entries),\n };\n}\n\n/** One call a plugin made through `ctx.fetch`, normalised for assertions. */\nexport interface FetchCall {\n url: string;\n method: string;\n /** Header names lower-cased, as the vendor would see them. */\n headers: Record<string, string>;\n /** The request body as bytes; `null` when there was none or it was not a string or bytes. */\n body: Uint8Array | null;\n /** The `init` the plugin passed, untouched, for anything the normalised view drops. */\n init: RequestInit | undefined;\n}\n\n/** What answers the plugin's requests; the global `fetch` type fits. */\nexport type FetchHandler = (\n input: string | URL | Request,\n init?: RequestInit,\n) => Response | Promise<Response>;\n\n/** A `fetch` that records its calls. The default handler answers every request with 404. */\nexport type FetchStub = typeof fetch & { calls: FetchCall[] };\n\nconst notFound: FetchHandler = () => new Response('not found', { status: 404 });\n\nfunction headersOf(input: string | URL | Request, init: RequestInit | undefined) {\n const out: Record<string, string> = {};\n const add = (name: string, value: string) => {\n out[name.toLowerCase()] = value;\n };\n if (input instanceof Request) input.headers.forEach((value, name) => add(name, value));\n const headers = init?.headers;\n if (headers instanceof Headers) headers.forEach((value, name) => add(name, value));\n else if (Array.isArray(headers)) {\n for (const pair of headers) {\n if (pair[0] !== undefined && pair[1] !== undefined) add(pair[0], pair[1]);\n }\n } else if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n add(name, Array.isArray(value) ? value.join(', ') : String(value));\n }\n }\n return out;\n}\n\nfunction bodyOf(body: RequestInit['body']): Uint8Array | null {\n if (body === null || body === undefined) return null;\n if (typeof body === 'string') return new TextEncoder().encode(body);\n if (body instanceof Uint8Array) return body;\n if (body instanceof ArrayBuffer) return new Uint8Array(body);\n if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString());\n return null;\n}\n\nexport function createFetchStub(handler: FetchHandler = notFound): FetchStub {\n const calls: FetchCall[] = [];\n const stub = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n calls.push({\n url: input instanceof Request ? input.url : String(input),\n method: (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase(),\n headers: headersOf(input, init),\n body: bodyOf(init?.body),\n init,\n });\n return handler(input, init);\n };\n return Object.assign(stub as typeof fetch, { calls });\n}\n\n/** Builds a `DocumentHandle` from bytes or text, filling in the size. */\nexport function documentHandle(doc: {\n id: string;\n bytes?: Uint8Array;\n text?: string;\n fileName?: string;\n contentType?: string;\n}): DocumentHandle {\n const bytes = doc.bytes ?? new TextEncoder().encode(doc.text ?? '');\n return {\n id: doc.id,\n fileName: doc.fileName ?? `${doc.id}.bin`,\n contentType: doc.contentType ?? 'application/octet-stream',\n sizeBytes: bytes.byteLength,\n bytes,\n };\n}\n\n/**\n * In-memory `PluginDocuments`: holds the tenant's clean documents by id and records which ids a\n * plugin read. Like the platform, an unknown id throws rather than returning `undefined`.\n */\nexport class MemoryDocuments implements PluginDocuments {\n readonly reads: string[] = [];\n private readonly docs = new Map<string, DocumentHandle>();\n\n constructor(documents: DocumentHandle[] = []) {\n for (const doc of documents) this.add(doc);\n }\n\n add(doc: DocumentHandle): this {\n this.docs.set(doc.id, doc);\n return this;\n }\n\n async read(documentId: string): Promise<DocumentHandle> {\n this.reads.push(documentId);\n const doc = this.docs.get(documentId);\n if (!doc) throw new Error(`document ${documentId} not found or not clean`);\n return doc;\n }\n}\n\nexport interface TestContextOptions {\n /** Raw tenant configuration; parsed through `configSchema`, so defaults apply. Default `{}`. */\n config?: unknown;\n /** Secret values by name. Every secret the manifest declares must be present, as at runtime. */\n secrets?: Record<string, string>;\n /**\n * Documents the plugin may read. Default: an empty in-memory source. Pass `null` for a\n * deployment without object storage (`ctx.documents` is then absent).\n */\n documents?: DocumentHandle[] | MemoryDocuments | null;\n /** Answers `ctx.fetch`; calls are recorded either way. Default: 404 for everything. */\n fetch?: FetchHandler;\n tenantId?: string;\n /** Default: `http://localhost:4000/webhooks/plugins/<encoded plugin name>`. */\n callbackUrl?: string | null;\n idempotencyKey?: string;\n /** Default: a signal that never aborts. */\n signal?: AbortSignal;\n}\n\n/** A `PluginContext` whose logger, fetch and documents record what the plugin did. */\nexport interface TestContext<C = unknown> extends PluginContext<C> {\n logger: RecordingLogger;\n fetch: FetchStub;\n documents?: MemoryDocuments;\n}\n\nexport const DEFAULT_TENANT_ID = 'tenant-test';\nconst DEFAULT_CALLBACK_BASE = 'http://localhost:4000';\n\n/** The callback URL the platform hands a plugin for a given API base URL. */\nexport function callbackUrlFor(pluginName: string, base = DEFAULT_CALLBACK_BASE): string {\n return `${base.replace(/\\/$/, '')}/webhooks/plugins/${encodeURIComponent(pluginName)}`;\n}\n\n/**\n * Builds the context the runtime would hand the plugin for one tenant: the config parsed through\n * the manifest's `configSchema`, the declared secrets resolved from `options.secrets`, a logger\n * bound to the plugin and tenant that records every entry, a `fetch` stub that records every\n * call and an in-memory document source. Throws when the config is invalid or a secret is\n * missing, which is what the runtime does at worker start.\n */\nexport function createTestContext<M extends PluginManifest>(\n manifest: M,\n options: TestContextOptions = {},\n): TestContext<PluginConfig<M>> {\n const config = manifest.configSchema.parse(options.config ?? {}) as PluginConfig<M>;\n const tenantId = options.tenantId ?? DEFAULT_TENANT_ID;\n const secrets: Record<string, string> = {};\n const missing: string[] = [];\n for (const name of manifest.secrets) {\n const value = options.secrets?.[name];\n if (value === undefined) missing.push(name);\n else secrets[name] = value;\n }\n if (missing.length > 0) {\n throw new Error(\n `createTestContext: ${manifest.name} declares secrets that were not supplied: ${missing.join(', ')}`,\n );\n }\n const documents =\n options.documents === null\n ? undefined\n : options.documents instanceof MemoryDocuments\n ? options.documents\n : new MemoryDocuments(options.documents);\n const callbackUrl =\n options.callbackUrl === null\n ? undefined\n : (options.callbackUrl ?? callbackUrlFor(manifest.name));\n return {\n tenantId,\n config,\n secrets,\n logger: createRecordingLogger({ plugin: manifest.name, tenantId }),\n fetch: createFetchStub(options.fetch),\n signal: options.signal ?? new AbortController().signal,\n ...(options.idempotencyKey !== undefined && { idempotencyKey: options.idempotencyKey }),\n ...(callbackUrl !== undefined && { callbackUrl }),\n ...(documents !== undefined && { documents }),\n };\n}\n","import type { z } from 'zod';\nimport { PluginManifestSchema, isPending, type Plugin, type PluginContext } from '../index.js';\nimport type { WebhookRequest } from '../webhooks.js';\nimport { createTestContext, type TestContextOptions } from './context.js';\n\n/** One input an author knows the plugin handles, used to exercise the runtime's contract. */\nexport interface ConformanceSample {\n action: string;\n input: unknown;\n /**\n * For an asynchronous action: builds the vendor's webhook for the external id `invoke`\n * returned (use `webhookRequest` to sign it). `handleWebhook` must then resolve the same id.\n */\n webhook?: (externalId: string, ctx: PluginContext) => WebhookRequest | Promise<WebhookRequest>;\n}\n\nexport interface ConformanceOptions {\n samples: ConformanceSample[];\n /** The context the samples run with: options for `createTestContext`, or a ready context. */\n context?: TestContextOptions | PluginContext;\n}\n\nexport interface ConformanceCheck {\n name: string;\n ok: boolean;\n /** Why the check failed, or a note on what passed. */\n message?: string;\n}\n\nexport interface ConformanceResult {\n ok: boolean;\n checks: ConformanceCheck[];\n failures: ConformanceCheck[];\n}\n\nconst UNKNOWN_ACTION = '__conformance_unknown_action__';\n\nconst isContext = (value: TestContextOptions | PluginContext): value is PluginContext =>\n 'logger' in value && 'signal' in value;\n\nconst describeError = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nconst issues = (error: z.ZodError): string =>\n error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`).join('; ');\n\nconst isZodSchema = (value: unknown): value is z.ZodType =>\n typeof value === 'object' && value !== null && '_zod' in value;\n\nclass Checks {\n readonly list: ConformanceCheck[] = [];\n\n pass(name: string, message?: string): void {\n this.list.push({ name, ok: true, ...(message !== undefined && { message }) });\n }\n\n fail(name: string, message: string): void {\n this.list.push({ name, ok: false, message });\n }\n\n /** Runs `fn`; a thrown error fails the check with its message. */\n async run(name: string, fn: () => Promise<string | void> | string | void): Promise<boolean> {\n try {\n const note = await fn();\n this.pass(name, note ?? undefined);\n return true;\n } catch (error) {\n this.fail(name, describeError(error));\n return false;\n }\n }\n}\n\n/**\n * Runs the contract checks the platform applies to a plugin, without the platform: the manifest\n * parses with `PluginManifestSchema`; every action's `input` and `output` are zod schemas;\n * `invoke` rejects an unknown action; for each sample, the input validates and the output\n * validates against the action's output schema; an asynchronous action returns `pending()` with\n * a non-empty id and, when the sample supplies a webhook, `handleWebhook` resolves that id (and\n * a completed event's output validates). Never throws for a failing plugin: read `result.ok` or\n * use `assertConformance` with any test runner.\n */\nexport async function conformance(\n plugin: Plugin,\n options: ConformanceOptions,\n): Promise<ConformanceResult> {\n const checks = new Checks();\n const { manifest } = plugin;\n\n const manifestOk = await checks.run('manifest parses with PluginManifestSchema', () => {\n const parsed = PluginManifestSchema.safeParse(manifest);\n if (!parsed.success) throw new Error(issues(parsed.error));\n });\n\n const actions = Object.entries(manifest.actions ?? {});\n for (const [name, action] of actions) {\n await checks.run(`action ${name}: input and output are zod schemas`, () => {\n const bad = [\n ...(isZodSchema(action?.input) ? [] : ['input']),\n ...(isZodSchema(action?.output) ? [] : ['output']),\n ];\n if (bad.length > 0) throw new Error(`${bad.join(' and ')} is not a zod schema`);\n });\n }\n if (actions.some(([, action]) => action?.async)) {\n await checks.run('asynchronous actions: handleWebhook is implemented', () => {\n if (typeof plugin.handleWebhook !== 'function') {\n throw new Error(\n 'the manifest declares an async action but the plugin has no handleWebhook',\n );\n }\n });\n }\n\n let ctx: PluginContext | undefined;\n const contextOk = await checks.run('context: config parses and secrets resolve', () => {\n ctx =\n options.context && isContext(options.context)\n ? options.context\n : createTestContext(manifest, options.context);\n });\n\n if (manifestOk && contextOk && ctx) {\n await checks.run('invoke rejects an unknown action', async () => {\n let outcome: unknown;\n try {\n outcome = await plugin.invoke(UNKNOWN_ACTION, {}, ctx!);\n } catch {\n return;\n }\n throw new Error(\n `invoke(${JSON.stringify(UNKNOWN_ACTION)}) resolved with ${JSON.stringify(outcome)}`,\n );\n });\n for (const [index, sample] of options.samples.entries()) {\n await runSample(plugin, ctx, sample, `sample ${index + 1} (${sample.action})`, checks);\n }\n }\n\n const failures = checks.list.filter((check) => !check.ok);\n return { ok: failures.length === 0, checks: checks.list, failures };\n}\n\nasync function runSample(\n plugin: Plugin,\n ctx: PluginContext,\n sample: ConformanceSample,\n label: string,\n checks: Checks,\n): Promise<void> {\n const action = plugin.manifest.actions[sample.action];\n if (!action) {\n checks.fail(`${label}: action exists`, `the manifest declares no action ${sample.action}`);\n return;\n }\n const inputOk = await checks.run(`${label}: input validates against the input schema`, () => {\n const parsed = action.input.safeParse(sample.input);\n if (!parsed.success) throw new Error(issues(parsed.error));\n });\n if (!inputOk) return;\n const input = action.input.parse(sample.input);\n\n let result: unknown;\n const invoked = await checks.run(`${label}: invoke resolves`, async () => {\n result = await plugin.invoke(sample.action, input, ctx);\n });\n if (!invoked) return;\n\n if (!action.async) {\n await checks.run(`${label}: output validates against the output schema`, () => {\n if (isPending(result)) throw new Error('a synchronous action returned pending()');\n const parsed = action.output.safeParse(result);\n if (!parsed.success) throw new Error(issues(parsed.error));\n });\n return;\n }\n\n const pendingOk = await checks.run(`${label}: returns pending() with a non-empty id`, () => {\n if (!isPending(result))\n throw new Error(`expected pending(externalId), got ${JSON.stringify(result)}`);\n if (result.externalId.length === 0) throw new Error('externalId is empty');\n });\n if (!pendingOk || !sample.webhook || !isPending(result)) return;\n const { externalId } = result;\n\n await checks.run(`${label}: handleWebhook resolves the same external id`, async () => {\n if (!plugin.handleWebhook) throw new Error('the plugin has no handleWebhook');\n const request = await sample.webhook!(externalId, ctx);\n const event = await plugin.handleWebhook(request, ctx);\n if (!event) throw new Error('handleWebhook ignored the sample webhook (returned null)');\n if (event.externalId !== externalId) {\n throw new Error(\n `event.externalId is ${JSON.stringify(event.externalId)}, expected ${JSON.stringify(externalId)}`,\n );\n }\n if (event.status === 'completed') {\n const parsed = action.output.safeParse(event.output);\n if (!parsed.success) throw new Error(`completed output is invalid: ${issues(parsed.error)}`);\n }\n return `status ${event.status}`;\n });\n}\n\n/** `conformance`, throwing one error that lists every failed check. Works with any test runner. */\nexport async function assertConformance(\n plugin: Plugin,\n options: ConformanceOptions,\n): Promise<ConformanceResult> {\n const result = await conformance(plugin, options);\n if (!result.ok) {\n const lines = result.failures.map((check) => ` - ${check.name}: ${check.message ?? 'failed'}`);\n throw new Error(\n `${plugin.manifest?.name ?? 'plugin'} fails ${result.failures.length} conformance check(s):\\n${lines.join('\\n')}`,\n );\n }\n return result;\n}\n","import type { Plugin } from '../index.js';\nimport { signHmacSha256, type WebhookRequest } from '../webhooks.js';\n\n/** The header `webhookRequest` signs into when the plugin does not name its own. */\nexport const DEFAULT_SIGNATURE_HEADER = 'x-signature';\n\nexport interface WebhookRequestOptions {\n /** An object is JSON-encoded; a string or bytes are sent as they are. */\n body: unknown;\n /** When set, the raw body is signed with HMAC-SHA256 into `headerName`. */\n secret?: string;\n /** Header that carries the signature. Default `x-signature`. */\n headerName?: string;\n /** Signature encoding passed to `signHmacSha256`. Default `hex`. */\n encoding?: 'hex' | 'base64';\n /** Extra headers; names are lower-cased like the API does. */\n headers?: Record<string, string>;\n /** Extra query parameters, as the API passes them through to the plugin. */\n query?: Record<string, string>;\n /**\n * The vendor session the webhook is about. Put on the URL as `?externalId=` unless the plugin\n * implements `webhookExternalId` (its vendor cannot carry a query string). When omitted, a\n * string `externalId` field of a JSON body is used.\n */\n externalId?: string;\n /** Default `POST`. */\n method?: string;\n /** Default `application/json` for an object body, `application/octet-stream` otherwise. */\n contentType?: string;\n}\n\nfunction encodeBody(body: unknown): { rawBody: Uint8Array; isJson: boolean } {\n if (body instanceof Uint8Array) return { rawBody: body, isJson: false };\n if (typeof body === 'string') return { rawBody: new TextEncoder().encode(body), isJson: false };\n return { rawBody: new TextEncoder().encode(JSON.stringify(body)), isJson: true };\n}\n\n/**\n * Builds the `WebhookRequest` the API would hand `handleWebhook` for a vendor callback: raw\n * body bytes, lower-cased headers, `POST`, and the query string with the external id when the\n * plugin relies on the `?externalId=` convention. With `secret`, the raw body is signed with\n * the SDK's `signHmacSha256`, so `verifyHmacSha256` in the plugin accepts it.\n */\nexport function webhookRequest(plugin: Plugin, options: WebhookRequestOptions): WebhookRequest {\n const { rawBody, isJson } = encodeBody(options.body);\n const headers: Record<string, string> = {\n 'content-type':\n options.contentType ?? (isJson ? 'application/json' : 'application/octet-stream'),\n };\n for (const [name, value] of Object.entries(options.headers ?? {})) {\n headers[name.toLowerCase()] = value;\n }\n if (options.secret !== undefined) {\n const headerName = (options.headerName ?? DEFAULT_SIGNATURE_HEADER).toLowerCase();\n headers[headerName] = signHmacSha256(options.secret, rawBody, options.encoding ?? 'hex');\n }\n const query: Record<string, string> = { ...options.query };\n const externalId = options.externalId ?? externalIdOf(options.body, isJson);\n if (externalId !== undefined && !plugin.webhookExternalId && query.externalId === undefined) {\n query.externalId = externalId;\n }\n return { method: options.method ?? 'POST', headers, rawBody, query };\n}\n\nfunction externalIdOf(body: unknown, isJson: boolean): string | undefined {\n if (!isJson || typeof body !== 'object' || body === null) return undefined;\n const value = (body as { externalId?: unknown }).externalId;\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aletheia-dev/plugin-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SDK for writing Aletheia plugins: manifest, actions, webhooks, document access and the per-tenant context.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,6 +20,11 @@
20
20
  "types": "./dist/index.d.ts",
21
21
  "import": "./dist/index.js",
22
22
  "require": "./dist/index.cjs"
23
+ },
24
+ "./testing": {
25
+ "types": "./dist/testing.d.ts",
26
+ "import": "./dist/testing.js",
27
+ "require": "./dist/testing.cjs"
23
28
  }
24
29
  },
25
30
  "files": [
@@ -40,7 +45,7 @@
40
45
  "typescript": "5.9.3",
41
46
  "vitest": "^5.0.3",
42
47
  "zod": "^4.6.5",
43
- "@aletheia-dev/core": "0.1.0"
48
+ "@aletheia-dev/core": "0.2.0"
44
49
  },
45
50
  "scripts": {
46
51
  "build": "tsup",