@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @aletheia-dev/plugin-sdk
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#10](https://github.com/akhiljames/aletheia/pull/10) [`82d5f0c`](https://github.com/akhiljames/aletheia/commit/82d5f0c54c499c633618008b5146ac3a1e7fd327) Thanks [@akhiljames](https://github.com/akhiljames)! - New subpath `@aletheia-dev/plugin-sdk/testing` with helpers for plugin authors' tests, with no
8
+ dependency on the platform: `createTestContext(manifest, options)` builds a `PluginContext` the
9
+ way the runtime does (config parsed through `configSchema`, declared secrets required) with a
10
+ recording logger (`entries`), a recording `fetch` stub (`calls`, 404 by default or a supplied
11
+ handler) and an in-memory document source (`MemoryDocuments`, `documentHandle`);
12
+ `conformance(plugin, { samples })` and `assertConformance` run the contract checks the runtime
13
+ applies (manifest, zod schemas, unknown action, outputs for sample inputs, `pending()` and the
14
+ `handleWebhook` round trip for asynchronous actions); `webhookRequest(plugin, { body, secret,
15
+ headerName? })` builds the signed `WebhookRequest` the API would deliver. Additive: the root
16
+ export and the secret-reference convention are unchanged.
17
+
3
18
  ## 0.3.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -202,37 +202,92 @@ implementation for tests, and the `Logger` type lets you accept any compatible l
202
202
 
203
203
  ## Testing locally
204
204
 
205
- A plugin is a plain object, so unit tests call `invoke` and `handleWebhook` directly with a
206
- hand-built context:
205
+ `@aletheia-dev/plugin-sdk/testing` reproduces what the platform does around a plugin, so unit
206
+ tests need no hand-built context or fakes. It has no dependency on the platform and works with
207
+ any test runner.
207
208
 
208
209
  ```ts
209
- import { noopLogger, type PluginContext } from '@aletheia-dev/plugin-sdk';
210
-
211
- const ctx: PluginContext<{ threshold: number }> = {
212
- tenantId: 'tenant-1',
213
- config: { threshold: 0.8 },
214
- secrets: { apiKey: 'test' },
215
- logger: noopLogger,
216
- fetch: async () => new Response(JSON.stringify({ hit: false, score: 0 })),
217
- signal: new AbortController().signal,
218
- callbackUrl: 'http://localhost:4000/webhooks/plugins/@acme/plugin-vendor',
219
- };
210
+ import { WebhookRejectedError } from '@aletheia-dev/plugin-sdk';
211
+ import {
212
+ assertConformance,
213
+ createTestContext,
214
+ documentHandle,
215
+ webhookRequest,
216
+ } from '@aletheia-dev/plugin-sdk/testing';
217
+ import plugin, { SIGNATURE_HEADER, manifest } from './index.js';
218
+
219
+ const secrets = { apiKey: 'test', webhookSecret: 'wh' };
220
+
221
+ // A context built like the runtime builds one: config parsed through configSchema (defaults
222
+ // apply, bad config throws), declared secrets required, logger and fetch recording, documents
223
+ // in memory. The default fetch answers 404; pass a handler to play the vendor.
224
+ const ctx = createTestContext(manifest, {
225
+ config: { threshold: 0.9 },
226
+ secrets,
227
+ fetch: () => new Response(JSON.stringify({ matches: [] })),
228
+ documents: [documentHandle({ id: 'doc-1', text: '%PDF-1.4', contentType: 'application/pdf' })],
229
+ });
230
+ await plugin.invoke('screen', { name: 'Someone' }, ctx);
231
+ ctx.fetch.calls[0]?.headers.authorization; // 'Bearer test'
232
+ ctx.logger.entries; // [{ level, message, data, bindings }]
233
+
234
+ // The request the API would hand handleWebhook, signed with signHmacSha256 into the header.
235
+ const request = webhookRequest(plugin, {
236
+ body: { id: 'evt-1', externalId: 'ext-1', ok: true },
237
+ secret: 'wh',
238
+ headerName: SIGNATURE_HEADER,
239
+ });
240
+ await plugin.handleWebhook!(request, ctx);
241
+ await expect(
242
+ plugin.handleWebhook!(
243
+ webhookRequest(plugin, { body: {}, secret: 'wrong', headerName: SIGNATURE_HEADER }),
244
+ ctx,
245
+ ),
246
+ ).rejects.toThrow(WebhookRejectedError);
247
+
248
+ // The contract checks the runtime applies: manifest, schemas, unknown action, outputs for your
249
+ // sample inputs, pending() and the webhook round trip for asynchronous actions.
250
+ await assertConformance(plugin, {
251
+ context: { secrets },
252
+ samples: [
253
+ { action: 'screen', input: { name: 'Someone' } },
254
+ {
255
+ action: 'screenAsync',
256
+ input: { name: 'Someone' },
257
+ webhook: (externalId) =>
258
+ webhookRequest(plugin, {
259
+ body: { id: 'evt-2', externalId, ok: true },
260
+ secret: 'wh',
261
+ headerName: SIGNATURE_HEADER,
262
+ }),
263
+ },
264
+ ],
265
+ });
220
266
  ```
221
267
 
268
+ `conformance` returns the same result (`{ ok, checks, failures }`) without throwing. The
269
+ [authoring guide](https://github.com/akhiljames/aletheia/blob/main/docs/integrate/plugins/authoring.md)
270
+ builds a plugin and its tests step by step, and the
271
+ [reference](https://github.com/akhiljames/aletheia/blob/main/docs/integrate/plugins/reference.md#testing-helpers-aletheia-devplugin-sdktesting)
272
+ lists every option of the helpers.
273
+
222
274
  The repository's example plugins are the reference implementations:
223
275
 
276
+ - [`examples/plugins/acme-screening`](https://github.com/akhiljames/aletheia/tree/main/examples/plugins/acme-screening)
277
+ is the authoring guide's plugin, built outside the monorepo against the published SDK;
224
278
  - [`plugins/mock-sanctions`](https://github.com/akhiljames/aletheia/tree/main/plugins/mock-sanctions)
225
- — a synchronous action, an asynchronous twin and a signed webhook, with tests;
279
+ has a synchronous action, an asynchronous twin and a signed webhook, with tests;
226
280
  - [`plugins/doc-verify-mock`](https://github.com/akhiljames/aletheia/tree/main/plugins/doc-verify-mock)
227
- — reads a document through `ctx.documents` and fires its own signed webhook;
281
+ reads a document through `ctx.documents` and fires its own signed webhook;
228
282
  - [`plugins/opensanctions`](https://github.com/akhiljames/aletheia/tree/main/plugins/opensanctions)
229
- and [`plugins/sumsub`](https://github.com/akhiljames/aletheia/tree/main/plugins/sumsub) — real
283
+ and [`plugins/sumsub`](https://github.com/akhiljames/aletheia/tree/main/plugins/sumsub) are real
230
284
  vendors, including `webhookExternalId` for a dashboard-level webhook URL.
231
285
 
232
286
  To run a plugin against the platform, add it to the catalogue and configure it for a tenant:
233
- [`docs/plugins.md`](https://github.com/akhiljames/aletheia/blob/main/docs/plugins.md) covers the
234
- catalogue, tenant configuration, input mapping from workflows and rules, and what the platform
235
- does with each webhook outcome.
287
+ [Vendor plugins](https://github.com/akhiljames/aletheia/blob/main/docs/integrate/plugins/vendors.md)
288
+ covers the catalogue and tenant configuration, and the
289
+ [reference](https://github.com/akhiljames/aletheia/blob/main/docs/integrate/plugins/reference.md)
290
+ input mapping from workflows and rules and what the platform does with each webhook outcome.
236
291
 
237
292
  ## Versioning
238
293
 
@@ -0,0 +1,86 @@
1
+ // src/index.ts
2
+ import { z } from "zod";
3
+
4
+ // src/logger.ts
5
+ var noopLogger = {
6
+ debug: () => void 0,
7
+ info: () => void 0,
8
+ warn: () => void 0,
9
+ error: () => void 0,
10
+ child: () => noopLogger
11
+ };
12
+
13
+ // src/webhooks.ts
14
+ import { createHmac, timingSafeEqual } from "crypto";
15
+ var WebhookRejectedError = class extends Error {
16
+ constructor(message = "webhook rejected") {
17
+ super(message);
18
+ this.name = "WebhookRejectedError";
19
+ }
20
+ };
21
+ function verifyHmacSha256(secret, rawBody, signature, encoding = "hex") {
22
+ const expected = createHmac("sha256", secret).update(rawBody).digest(encoding);
23
+ const a = Buffer.from(expected);
24
+ const b = Buffer.from(signature.trim());
25
+ return a.length === b.length && timingSafeEqual(a, b);
26
+ }
27
+ function signHmacSha256(secret, rawBody, encoding = "hex") {
28
+ return createHmac("sha256", secret).update(rawBody).digest(encoding);
29
+ }
30
+
31
+ // src/index.ts
32
+ function pending(externalId) {
33
+ return { pending: true, externalId };
34
+ }
35
+ function isPending(value) {
36
+ return typeof value === "object" && value !== null && value.pending === true && typeof value.externalId === "string";
37
+ }
38
+ function definePlugin(plugin) {
39
+ return plugin;
40
+ }
41
+ function defineManifest(manifest) {
42
+ return manifest;
43
+ }
44
+ var isZodSchema = (value) => typeof value === "object" && value !== null && "_zod" in value;
45
+ var ZodSchemaValue = z.custom(isZodSchema, { message: "expected a zod schema" });
46
+ var PluginManifestSchema = z.object({
47
+ name: z.string().min(1).max(214).regex(
48
+ /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,
49
+ "expected an npm package name"
50
+ ),
51
+ version: z.string().min(1),
52
+ description: z.string().optional(),
53
+ capabilities: z.array(z.string().min(1)),
54
+ configSchema: ZodSchemaValue,
55
+ secrets: z.array(z.string().min(1)),
56
+ actions: z.record(
57
+ z.string().min(1),
58
+ z.object({
59
+ description: z.string().optional(),
60
+ input: ZodSchemaValue,
61
+ output: ZodSchemaValue,
62
+ timeoutMs: z.number().int().positive().optional(),
63
+ retry: z.object({
64
+ maxAttempts: z.number().int().min(1).max(5),
65
+ backoffMs: z.number().int().nonnegative()
66
+ }).optional(),
67
+ idempotent: z.boolean().optional(),
68
+ async: z.object({
69
+ callbackTimeoutSeconds: z.number().int().min(1).max(7 * 24 * 3600)
70
+ }).optional()
71
+ })
72
+ )
73
+ });
74
+
75
+ export {
76
+ noopLogger,
77
+ WebhookRejectedError,
78
+ verifyHmacSha256,
79
+ signHmacSha256,
80
+ pending,
81
+ isPending,
82
+ definePlugin,
83
+ defineManifest,
84
+ PluginManifestSchema
85
+ };
86
+ //# sourceMappingURL=chunk-FEZWGVUS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/logger.ts","../src/webhooks.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { Logger } from './logger.js';\nimport type { PluginDocuments } from './documents.js';\nimport type { WebhookEvent, WebhookRequest } from './webhooks.js';\n\nexport { noopLogger, type LogFn, type Logger } from './logger.js';\nexport type { DocumentHandle, PluginDocuments } from './documents.js';\nexport {\n WebhookRejectedError,\n signHmacSha256,\n verifyHmacSha256,\n type WebhookEvent,\n type WebhookRequest,\n} from './webhooks.js';\n\n/** Retry hints for an action; applied by the runtime only when the call is safe to repeat. */\nexport interface PluginRetryPolicy {\n /** Total attempts including the first (1 to 5). */\n maxAttempts: number;\n /** Base delay between attempts; multiplied by the attempt number. */\n backoffMs: number;\n}\n\n/** One callable action a plugin exposes. Input and output are validated by the runtime. */\nexport interface PluginAction<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {\n description?: string;\n input: I;\n output: O;\n /** Deadline for one attempt; the runtime aborts `ctx.signal` and fails the call when exceeded. */\n timeoutMs?: number;\n /** Retried by the runtime when `idempotent` is true or the caller supplies an idempotency key. */\n retry?: PluginRetryPolicy;\n /** Declares that repeating the action with the same input has no additional effect. */\n idempotent?: boolean;\n /**\n * The action starts work at the vendor and completes later through a webhook. `invoke` must\n * return `pending(externalId)`; the runtime waits for `handleWebhook` to report that id.\n */\n async?: { callbackTimeoutSeconds: number };\n}\n\n/** What an asynchronous action returns after starting work at the vendor. */\nexport interface PendingResult {\n pending: true;\n /** The vendor's identifier for the session, job or check; webhooks must carry it back. */\n externalId: string;\n}\n\nexport function pending(externalId: string): PendingResult {\n return { pending: true, externalId };\n}\n\nexport function isPending(value: unknown): value is PendingResult {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { pending?: unknown }).pending === true &&\n typeof (value as { externalId?: unknown }).externalId === 'string'\n );\n}\n\nexport type PluginActions = Record<string, PluginAction>;\n\n/** Static description of a plugin package: what it needs (config, secrets) and what it offers. */\nexport interface PluginManifest<\n C extends z.ZodType = z.ZodType,\n A extends PluginActions = PluginActions,\n> {\n /** npm package name, e.g. '@aletheia-dev/plugin-mock-sanctions'. */\n name: string;\n version: string;\n description?: string;\n /** Capability tags, e.g. ['sanctions.screen']. */\n capabilities: string[];\n /** Validates the tenant-provided, non-secret config. */\n configSchema: C;\n /** Names of secrets the plugin needs; resolved by the runtime per tenant. */\n secrets: string[];\n actions: A;\n}\n\n/** Per-tenant runtime context handed to every plugin call. */\nexport interface PluginContext<C = unknown> {\n tenantId: string;\n config: C;\n secrets: Record<string, string>;\n logger: Logger;\n fetch: typeof fetch;\n /** Aborted when the action's deadline passes; pass it to `fetch` so vendor calls stop too. */\n signal: AbortSignal;\n /** Stable key for the logical call (same across retries), when the caller provided one. */\n idempotencyKey?: string;\n /** Where the vendor must send webhooks for this plugin, when the deployment exposes one. */\n callbackUrl?: string;\n /**\n * Read-only access to the tenant's clean documents (by document id). Present when the\n * deployment has object storage; absent otherwise, so plugins must check before relying on it.\n */\n documents?: PluginDocuments;\n}\n\n/** Parsed config type of a manifest. */\nexport type PluginConfig<M extends PluginManifest> = z.output<M['configSchema']>;\n/** Union of a manifest's action names. */\nexport type ActionName<M extends PluginManifest> = keyof M['actions'] & string;\n/** Parsed input type of one action. */\nexport type ActionInput<M extends PluginManifest, K extends ActionName<M>> = z.output<\n M['actions'][K]['input']\n>;\n/** Output type of one action. */\nexport type ActionOutput<M extends PluginManifest, K extends ActionName<M>> = z.output<\n M['actions'][K]['output']\n>;\n\nexport interface Plugin<M extends PluginManifest = PluginManifest> {\n manifest: M;\n onInit?(ctx: PluginContext<PluginConfig<M>>): Promise<void> | void;\n onShutdown?(): Promise<void> | void;\n /**\n * Dispatch one action. The runtime validates `input` against the action's input schema\n * before calling and the return value against its output schema afterwards, so the\n * signature stays loose here; use `ActionInput`/`ActionOutput` to type the body.\n */\n invoke(\n action: ActionName<M>,\n input: unknown,\n ctx: PluginContext<PluginConfig<M>>,\n ): Promise<unknown>;\n /**\n * Verifies and decodes a vendor webhook for one of this plugin's asynchronous actions.\n * Return `null` to ignore the request, throw `WebhookRejectedError` for bad signatures.\n */\n handleWebhook?(\n request: WebhookRequest,\n ctx: PluginContext<PluginConfig<M>>,\n ): Promise<WebhookEvent | null>;\n /**\n * Extracts the vendor's external id from a webhook whose URL carries no `?externalId=`\n * (vendors with one dashboard-level webhook URL). Must be pure and need no secrets: it runs\n * before the tenant is known, so it only decodes the body or headers. The platform resolves\n * the tenant from the id and only then calls `handleWebhook` with the tenant context, which\n * must still verify the signature. Return `null` when the request carries no id.\n */\n webhookExternalId?(request: WebhookRequest): string | null;\n}\n\n/** Identity helper so `manifest` drives inference for `onInit`/`invoke`. */\nexport function definePlugin<M extends PluginManifest>(plugin: Plugin<M>): Plugin<M> {\n return plugin;\n}\n\n/** Identity helper that preserves the concrete schema and action types. */\nexport function defineManifest<C extends z.ZodType, A extends PluginActions>(\n manifest: PluginManifest<C, A>,\n): PluginManifest<C, A> {\n return manifest;\n}\n\nconst isZodSchema = (value: unknown): value is z.ZodType =>\n typeof value === 'object' && value !== null && '_zod' in value;\n\nconst ZodSchemaValue = z.custom<z.ZodType>(isZodSchema, { message: 'expected a zod schema' });\n\n/** Runtime validation of a manifest's data parts (schemas are only checked to be zod schemas). */\nexport const PluginManifestSchema = z.object({\n name: z\n .string()\n .min(1)\n .max(214)\n .regex(\n /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/,\n 'expected an npm package name',\n ),\n version: z.string().min(1),\n description: z.string().optional(),\n capabilities: z.array(z.string().min(1)),\n configSchema: ZodSchemaValue,\n secrets: z.array(z.string().min(1)),\n actions: z.record(\n z.string().min(1),\n z.object({\n description: z.string().optional(),\n input: ZodSchemaValue,\n output: ZodSchemaValue,\n timeoutMs: z.number().int().positive().optional(),\n retry: z\n .object({\n maxAttempts: z.number().int().min(1).max(5),\n backoffMs: z.number().int().nonnegative(),\n })\n .optional(),\n idempotent: z.boolean().optional(),\n async: z\n .object({\n callbackTimeoutSeconds: z\n .number()\n .int()\n .min(1)\n .max(7 * 24 * 3600),\n })\n .optional(),\n }),\n ),\n});\n","/**\n * Minimal structural logger contract handed to plugins (pino satisfies it).\n *\n * This is a deliberate copy of `@aletheia-dev/core`'s `logger.ts`: the SDK is published on its own\n * and must not pull the platform's core package in for three declarations. `logger.test.ts`\n * asserts the two stay assignable both ways, so the runtime can pass its core logger straight\n * into a plugin context.\n */\nexport type LogFn = (objOrMsg: object | string, msg?: string, ...args: unknown[]) => void;\n\nexport interface Logger {\n debug: LogFn;\n info: LogFn;\n warn: LogFn;\n error: LogFn;\n child(bindings: Record<string, unknown>): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => undefined,\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n child: () => noopLogger,\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** A vendor webhook as received by the API, with the raw body for signature verification. */\nexport interface WebhookRequest {\n method: string;\n /** Header names lower-cased. */\n headers: Record<string, string>;\n rawBody: Uint8Array;\n query: Record<string, string>;\n}\n\n/** What a plugin extracted from a webhook. `null` from `handleWebhook` means \"ignore\". */\nexport interface WebhookEvent {\n /** The vendor session id returned by the asynchronous action. */\n externalId: string;\n /** Vendor event id (or a stable hash) used for de-duplication. */\n eventId: string;\n status: 'completed' | 'failed' | 'pending';\n /** Validated against the action's output schema when `status` is `completed`. */\n output?: unknown;\n error?: string;\n}\n\n/** Thrown by `handleWebhook` when the signature or payload is not acceptable (HTTP 401). */\nexport class WebhookRejectedError extends Error {\n constructor(message = 'webhook rejected') {\n super(message);\n this.name = 'WebhookRejectedError';\n }\n}\n\n/** Constant-time HMAC-SHA256 check of a raw body against a hex (or base64) signature. */\nexport function verifyHmacSha256(\n secret: string,\n rawBody: Uint8Array,\n signature: string,\n encoding: 'hex' | 'base64' = 'hex',\n): boolean {\n const expected = createHmac('sha256', secret).update(rawBody).digest(encoding);\n const a = Buffer.from(expected);\n const b = Buffer.from(signature.trim());\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nexport function signHmacSha256(\n secret: string,\n rawBody: Uint8Array,\n encoding: 'hex' | 'base64' = 'hex',\n): string {\n return createHmac('sha256', secret).update(rawBody).digest(encoding);\n}\n"],"mappings":";AAAA,SAAS,SAAS;;;ACkBX,IAAM,aAAqB;AAAA,EAChC,OAAO,MAAM;AAAA,EACb,MAAM,MAAM;AAAA,EACZ,MAAM,MAAM;AAAA,EACZ,OAAO,MAAM;AAAA,EACb,OAAO,MAAM;AACf;;;ACxBA,SAAS,YAAY,uBAAuB;AAwBrC,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,UAAU,oBAAoB;AACxC,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAGO,SAAS,iBACd,QACA,SACA,WACA,WAA6B,OACpB;AACT,QAAM,WAAW,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,QAAQ;AAC7E,QAAM,IAAI,OAAO,KAAK,QAAQ;AAC9B,QAAM,IAAI,OAAO,KAAK,UAAU,KAAK,CAAC;AACtC,SAAO,EAAE,WAAW,EAAE,UAAU,gBAAgB,GAAG,CAAC;AACtD;AAEO,SAAS,eACd,QACA,SACA,WAA6B,OACrB;AACR,SAAO,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,QAAQ;AACrE;;;AFFO,SAAS,QAAQ,YAAmC;AACzD,SAAO,EAAE,SAAS,MAAM,WAAW;AACrC;AAEO,SAAS,UAAU,OAAwC;AAChE,SACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAgC,YAAY,QAC7C,OAAQ,MAAmC,eAAe;AAE9D;AAwFO,SAAS,aAAuC,QAA8B;AACnF,SAAO;AACT;AAGO,SAAS,eACd,UACsB;AACtB,SAAO;AACT;AAEA,IAAM,cAAc,CAAC,UACnB,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU;AAE3D,IAAM,iBAAiB,EAAE,OAAkB,aAAa,EAAE,SAAS,wBAAwB,CAAC;AAGrF,IAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,MAAM,EACH,OAAO,EACP,IAAI,CAAC,EACL,IAAI,GAAG,EACP;AAAA,IACC;AAAA,IACA;AAAA,EACF;AAAA,EACF,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACzB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,EACvC,cAAc;AAAA,EACd,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,EAClC,SAAS,EAAE;AAAA,IACT,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAChB,EAAE,OAAO;AAAA,MACP,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,MACjC,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,MAChD,OAAO,EACJ,OAAO;AAAA,QACN,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,QAC1C,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,MAC1C,CAAC,EACA,SAAS;AAAA,MACZ,YAAY,EAAE,QAAQ,EAAE,SAAS;AAAA,MACjC,OAAO,EACJ,OAAO;AAAA,QACN,wBAAwB,EACrB,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,IAAI,KAAK,IAAI;AAAA,MACtB,CAAC,EACA,SAAS;AAAA,IACd,CAAC;AAAA,EACH;AACF,CAAC;","names":[]}
package/dist/index.js CHANGED
@@ -1,76 +1,14 @@
1
- // src/index.ts
2
- import { z } from "zod";
3
-
4
- // src/logger.ts
5
- var noopLogger = {
6
- debug: () => void 0,
7
- info: () => void 0,
8
- warn: () => void 0,
9
- error: () => void 0,
10
- child: () => noopLogger
11
- };
12
-
13
- // src/webhooks.ts
14
- import { createHmac, timingSafeEqual } from "crypto";
15
- var WebhookRejectedError = class extends Error {
16
- constructor(message = "webhook rejected") {
17
- super(message);
18
- this.name = "WebhookRejectedError";
19
- }
20
- };
21
- function verifyHmacSha256(secret, rawBody, signature, encoding = "hex") {
22
- const expected = createHmac("sha256", secret).update(rawBody).digest(encoding);
23
- const a = Buffer.from(expected);
24
- const b = Buffer.from(signature.trim());
25
- return a.length === b.length && timingSafeEqual(a, b);
26
- }
27
- function signHmacSha256(secret, rawBody, encoding = "hex") {
28
- return createHmac("sha256", secret).update(rawBody).digest(encoding);
29
- }
30
-
31
- // src/index.ts
32
- function pending(externalId) {
33
- return { pending: true, externalId };
34
- }
35
- function isPending(value) {
36
- return typeof value === "object" && value !== null && value.pending === true && typeof value.externalId === "string";
37
- }
38
- function definePlugin(plugin) {
39
- return plugin;
40
- }
41
- function defineManifest(manifest) {
42
- return manifest;
43
- }
44
- var isZodSchema = (value) => typeof value === "object" && value !== null && "_zod" in value;
45
- var ZodSchemaValue = z.custom(isZodSchema, { message: "expected a zod schema" });
46
- var PluginManifestSchema = z.object({
47
- name: z.string().min(1).max(214).regex(
48
- /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,
49
- "expected an npm package name"
50
- ),
51
- version: z.string().min(1),
52
- description: z.string().optional(),
53
- capabilities: z.array(z.string().min(1)),
54
- configSchema: ZodSchemaValue,
55
- secrets: z.array(z.string().min(1)),
56
- actions: z.record(
57
- z.string().min(1),
58
- z.object({
59
- description: z.string().optional(),
60
- input: ZodSchemaValue,
61
- output: ZodSchemaValue,
62
- timeoutMs: z.number().int().positive().optional(),
63
- retry: z.object({
64
- maxAttempts: z.number().int().min(1).max(5),
65
- backoffMs: z.number().int().nonnegative()
66
- }).optional(),
67
- idempotent: z.boolean().optional(),
68
- async: z.object({
69
- callbackTimeoutSeconds: z.number().int().min(1).max(7 * 24 * 3600)
70
- }).optional()
71
- })
72
- )
73
- });
1
+ import {
2
+ PluginManifestSchema,
3
+ WebhookRejectedError,
4
+ defineManifest,
5
+ definePlugin,
6
+ isPending,
7
+ noopLogger,
8
+ pending,
9
+ signHmacSha256,
10
+ verifyHmacSha256
11
+ } from "./chunk-FEZWGVUS.js";
74
12
  export {
75
13
  PluginManifestSchema,
76
14
  WebhookRejectedError,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/logger.ts","../src/webhooks.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { Logger } from './logger.js';\nimport type { PluginDocuments } from './documents.js';\nimport type { WebhookEvent, WebhookRequest } from './webhooks.js';\n\nexport { noopLogger, type LogFn, type Logger } from './logger.js';\nexport type { DocumentHandle, PluginDocuments } from './documents.js';\nexport {\n WebhookRejectedError,\n signHmacSha256,\n verifyHmacSha256,\n type WebhookEvent,\n type WebhookRequest,\n} from './webhooks.js';\n\n/** Retry hints for an action; applied by the runtime only when the call is safe to repeat. */\nexport interface PluginRetryPolicy {\n /** Total attempts including the first (1 to 5). */\n maxAttempts: number;\n /** Base delay between attempts; multiplied by the attempt number. */\n backoffMs: number;\n}\n\n/** One callable action a plugin exposes. Input and output are validated by the runtime. */\nexport interface PluginAction<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {\n description?: string;\n input: I;\n output: O;\n /** Deadline for one attempt; the runtime aborts `ctx.signal` and fails the call when exceeded. */\n timeoutMs?: number;\n /** Retried by the runtime when `idempotent` is true or the caller supplies an idempotency key. */\n retry?: PluginRetryPolicy;\n /** Declares that repeating the action with the same input has no additional effect. */\n idempotent?: boolean;\n /**\n * The action starts work at the vendor and completes later through a webhook. `invoke` must\n * return `pending(externalId)`; the runtime waits for `handleWebhook` to report that id.\n */\n async?: { callbackTimeoutSeconds: number };\n}\n\n/** What an asynchronous action returns after starting work at the vendor. */\nexport interface PendingResult {\n pending: true;\n /** The vendor's identifier for the session, job or check; webhooks must carry it back. */\n externalId: string;\n}\n\nexport function pending(externalId: string): PendingResult {\n return { pending: true, externalId };\n}\n\nexport function isPending(value: unknown): value is PendingResult {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { pending?: unknown }).pending === true &&\n typeof (value as { externalId?: unknown }).externalId === 'string'\n );\n}\n\nexport type PluginActions = Record<string, PluginAction>;\n\n/** Static description of a plugin package: what it needs (config, secrets) and what it offers. */\nexport interface PluginManifest<\n C extends z.ZodType = z.ZodType,\n A extends PluginActions = PluginActions,\n> {\n /** npm package name, e.g. '@aletheia-dev/plugin-mock-sanctions'. */\n name: string;\n version: string;\n description?: string;\n /** Capability tags, e.g. ['sanctions.screen']. */\n capabilities: string[];\n /** Validates the tenant-provided, non-secret config. */\n configSchema: C;\n /** Names of secrets the plugin needs; resolved by the runtime per tenant. */\n secrets: string[];\n actions: A;\n}\n\n/** Per-tenant runtime context handed to every plugin call. */\nexport interface PluginContext<C = unknown> {\n tenantId: string;\n config: C;\n secrets: Record<string, string>;\n logger: Logger;\n fetch: typeof fetch;\n /** Aborted when the action's deadline passes; pass it to `fetch` so vendor calls stop too. */\n signal: AbortSignal;\n /** Stable key for the logical call (same across retries), when the caller provided one. */\n idempotencyKey?: string;\n /** Where the vendor must send webhooks for this plugin, when the deployment exposes one. */\n callbackUrl?: string;\n /**\n * Read-only access to the tenant's clean documents (by document id). Present when the\n * deployment has object storage; absent otherwise, so plugins must check before relying on it.\n */\n documents?: PluginDocuments;\n}\n\n/** Parsed config type of a manifest. */\nexport type PluginConfig<M extends PluginManifest> = z.output<M['configSchema']>;\n/** Union of a manifest's action names. */\nexport type ActionName<M extends PluginManifest> = keyof M['actions'] & string;\n/** Parsed input type of one action. */\nexport type ActionInput<M extends PluginManifest, K extends ActionName<M>> = z.output<\n M['actions'][K]['input']\n>;\n/** Output type of one action. */\nexport type ActionOutput<M extends PluginManifest, K extends ActionName<M>> = z.output<\n M['actions'][K]['output']\n>;\n\nexport interface Plugin<M extends PluginManifest = PluginManifest> {\n manifest: M;\n onInit?(ctx: PluginContext<PluginConfig<M>>): Promise<void> | void;\n onShutdown?(): Promise<void> | void;\n /**\n * Dispatch one action. The runtime validates `input` against the action's input schema\n * before calling and the return value against its output schema afterwards, so the\n * signature stays loose here; use `ActionInput`/`ActionOutput` to type the body.\n */\n invoke(\n action: ActionName<M>,\n input: unknown,\n ctx: PluginContext<PluginConfig<M>>,\n ): Promise<unknown>;\n /**\n * Verifies and decodes a vendor webhook for one of this plugin's asynchronous actions.\n * Return `null` to ignore the request, throw `WebhookRejectedError` for bad signatures.\n */\n handleWebhook?(\n request: WebhookRequest,\n ctx: PluginContext<PluginConfig<M>>,\n ): Promise<WebhookEvent | null>;\n /**\n * Extracts the vendor's external id from a webhook whose URL carries no `?externalId=`\n * (vendors with one dashboard-level webhook URL). Must be pure and need no secrets: it runs\n * before the tenant is known, so it only decodes the body or headers. The platform resolves\n * the tenant from the id and only then calls `handleWebhook` with the tenant context, which\n * must still verify the signature. Return `null` when the request carries no id.\n */\n webhookExternalId?(request: WebhookRequest): string | null;\n}\n\n/** Identity helper so `manifest` drives inference for `onInit`/`invoke`. */\nexport function definePlugin<M extends PluginManifest>(plugin: Plugin<M>): Plugin<M> {\n return plugin;\n}\n\n/** Identity helper that preserves the concrete schema and action types. */\nexport function defineManifest<C extends z.ZodType, A extends PluginActions>(\n manifest: PluginManifest<C, A>,\n): PluginManifest<C, A> {\n return manifest;\n}\n\nconst isZodSchema = (value: unknown): value is z.ZodType =>\n typeof value === 'object' && value !== null && '_zod' in value;\n\nconst ZodSchemaValue = z.custom<z.ZodType>(isZodSchema, { message: 'expected a zod schema' });\n\n/** Runtime validation of a manifest's data parts (schemas are only checked to be zod schemas). */\nexport const PluginManifestSchema = z.object({\n name: z\n .string()\n .min(1)\n .max(214)\n .regex(\n /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/,\n 'expected an npm package name',\n ),\n version: z.string().min(1),\n description: z.string().optional(),\n capabilities: z.array(z.string().min(1)),\n configSchema: ZodSchemaValue,\n secrets: z.array(z.string().min(1)),\n actions: z.record(\n z.string().min(1),\n z.object({\n description: z.string().optional(),\n input: ZodSchemaValue,\n output: ZodSchemaValue,\n timeoutMs: z.number().int().positive().optional(),\n retry: z\n .object({\n maxAttempts: z.number().int().min(1).max(5),\n backoffMs: z.number().int().nonnegative(),\n })\n .optional(),\n idempotent: z.boolean().optional(),\n async: z\n .object({\n callbackTimeoutSeconds: z\n .number()\n .int()\n .min(1)\n .max(7 * 24 * 3600),\n })\n .optional(),\n }),\n ),\n});\n","/**\n * Minimal structural logger contract handed to plugins (pino satisfies it).\n *\n * This is a deliberate copy of `@aletheia-dev/core`'s `logger.ts`: the SDK is published on its own\n * and must not pull the platform's core package in for three declarations. `logger.test.ts`\n * asserts the two stay assignable both ways, so the runtime can pass its core logger straight\n * into a plugin context.\n */\nexport type LogFn = (objOrMsg: object | string, msg?: string, ...args: unknown[]) => void;\n\nexport interface Logger {\n debug: LogFn;\n info: LogFn;\n warn: LogFn;\n error: LogFn;\n child(bindings: Record<string, unknown>): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => undefined,\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n child: () => noopLogger,\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** A vendor webhook as received by the API, with the raw body for signature verification. */\nexport interface WebhookRequest {\n method: string;\n /** Header names lower-cased. */\n headers: Record<string, string>;\n rawBody: Uint8Array;\n query: Record<string, string>;\n}\n\n/** What a plugin extracted from a webhook. `null` from `handleWebhook` means \"ignore\". */\nexport interface WebhookEvent {\n /** The vendor session id returned by the asynchronous action. */\n externalId: string;\n /** Vendor event id (or a stable hash) used for de-duplication. */\n eventId: string;\n status: 'completed' | 'failed' | 'pending';\n /** Validated against the action's output schema when `status` is `completed`. */\n output?: unknown;\n error?: string;\n}\n\n/** Thrown by `handleWebhook` when the signature or payload is not acceptable (HTTP 401). */\nexport class WebhookRejectedError extends Error {\n constructor(message = 'webhook rejected') {\n super(message);\n this.name = 'WebhookRejectedError';\n }\n}\n\n/** Constant-time HMAC-SHA256 check of a raw body against a hex (or base64) signature. */\nexport function verifyHmacSha256(\n secret: string,\n rawBody: Uint8Array,\n signature: string,\n encoding: 'hex' | 'base64' = 'hex',\n): boolean {\n const expected = createHmac('sha256', secret).update(rawBody).digest(encoding);\n const a = Buffer.from(expected);\n const b = Buffer.from(signature.trim());\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nexport function signHmacSha256(\n secret: string,\n rawBody: Uint8Array,\n encoding: 'hex' | 'base64' = 'hex',\n): string {\n return createHmac('sha256', secret).update(rawBody).digest(encoding);\n}\n"],"mappings":";AAAA,SAAS,SAAS;;;ACkBX,IAAM,aAAqB;AAAA,EAChC,OAAO,MAAM;AAAA,EACb,MAAM,MAAM;AAAA,EACZ,MAAM,MAAM;AAAA,EACZ,OAAO,MAAM;AAAA,EACb,OAAO,MAAM;AACf;;;ACxBA,SAAS,YAAY,uBAAuB;AAwBrC,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,UAAU,oBAAoB;AACxC,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAGO,SAAS,iBACd,QACA,SACA,WACA,WAA6B,OACpB;AACT,QAAM,WAAW,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,QAAQ;AAC7E,QAAM,IAAI,OAAO,KAAK,QAAQ;AAC9B,QAAM,IAAI,OAAO,KAAK,UAAU,KAAK,CAAC;AACtC,SAAO,EAAE,WAAW,EAAE,UAAU,gBAAgB,GAAG,CAAC;AACtD;AAEO,SAAS,eACd,QACA,SACA,WAA6B,OACrB;AACR,SAAO,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,QAAQ;AACrE;;;AFFO,SAAS,QAAQ,YAAmC;AACzD,SAAO,EAAE,SAAS,MAAM,WAAW;AACrC;AAEO,SAAS,UAAU,OAAwC;AAChE,SACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAgC,YAAY,QAC7C,OAAQ,MAAmC,eAAe;AAE9D;AAwFO,SAAS,aAAuC,QAA8B;AACnF,SAAO;AACT;AAGO,SAAS,eACd,UACsB;AACtB,SAAO;AACT;AAEA,IAAM,cAAc,CAAC,UACnB,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU;AAE3D,IAAM,iBAAiB,EAAE,OAAkB,aAAa,EAAE,SAAS,wBAAwB,CAAC;AAGrF,IAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,MAAM,EACH,OAAO,EACP,IAAI,CAAC,EACL,IAAI,GAAG,EACP;AAAA,IACC;AAAA,IACA;AAAA,EACF;AAAA,EACF,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACzB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,EACvC,cAAc;AAAA,EACd,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,EAClC,SAAS,EAAE;AAAA,IACT,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAChB,EAAE,OAAO;AAAA,MACP,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,MACjC,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,MAChD,OAAO,EACJ,OAAO;AAAA,QACN,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,QAC1C,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,MAC1C,CAAC,EACA,SAAS;AAAA,MACZ,YAAY,EAAE,QAAQ,EAAE,SAAS;AAAA,MACjC,OAAO,EACJ,OAAO;AAAA,QACN,wBAAwB,EACrB,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,IAAI,KAAK,IAAI;AAAA,MACtB,CAAC,EACA,SAAS;AAAA,IACd,CAAC;AAAA,EACH;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}