@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 +15 -0
- package/README.md +74 -19
- package/dist/chunk-FEZWGVUS.js +86 -0
- package/dist/chunk-FEZWGVUS.js.map +1 -0
- package/dist/index.js +11 -73
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +387 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +167 -0
- package/dist/testing.d.ts +167 -0
- package/dist/testing.js +312 -0
- package/dist/testing.js.map +1 -0
- package/package.json +7 -2
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
|
-
|
|
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 {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
[
|
|
234
|
-
catalogue
|
|
235
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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":[]}
|