@aletheia-dev/plugin-sdk 0.3.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 ADDED
@@ -0,0 +1,40 @@
1
+ # @aletheia-dev/plugin-sdk
2
+
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`a783a44`](https://github.com/akhiljames/aletheia/commit/a783a44a1c27d08d09dbafe937e6aff61af592c4) Thanks [@akhiljames](https://github.com/akhiljames)! - `zod` becomes a peer dependency (`^4.0.0`) so plugins validate with their own zod instance, and the
8
+ SDK no longer depends on `@aletheia-dev/core`: `Logger`, `LogFn` and `noopLogger` are now the SDK's own
9
+ structurally identical declarations (a type-level test keeps them assignable both ways).
10
+
11
+ ## 0.2.0
12
+
13
+ ### Minor Changes
14
+
15
+ - `PluginContext.documents` (optional): `documents.read(documentId)` returns a `DocumentHandle`
16
+ (`id`, `fileName`, `contentType`, `sizeBytes`, `bytes`) for one of the tenant's documents. Only
17
+ documents with status `clean` can be read, and the bytes are the platform's post-processing
18
+ copy (sniffed, size-checked, images re-encoded), never the raw upload. The property is absent
19
+ when the deployment has no object storage.
20
+ - `Plugin.webhookExternalId(request)` (optional): a pure, secret-free extractor for vendors whose
21
+ webhook URL cannot carry `?externalId=`. The platform calls it to find the external id, resolves
22
+ the tenant from it and only then calls `handleWebhook`, which still verifies the signature.
23
+ - Webhook types and HMAC helpers now live in `webhooks.ts` and the document types in
24
+ `documents.ts`; both are re-exported from the package root, so imports are unchanged.
25
+
26
+ ## 0.1.0
27
+
28
+ ### Minor Changes
29
+
30
+ - First versioned release. The SDK is the only package a plugin imports; changes to it are
31
+ tracked here for external plugin authors.
32
+ - Actions can be asynchronous: declare `async: { callbackTimeoutSeconds }` on the action and
33
+ return `pending(externalId)` from `invoke`. The platform records a callback and waits.
34
+ - `Plugin.handleWebhook(request, ctx)` receives the raw vendor request (`WebhookRequest`) and
35
+ returns a `WebhookEvent` (`completed`, `failed` or `pending`) or `null` to ignore it. Throw
36
+ `WebhookRejectedError` when the signature does not verify.
37
+ - `verifyHmacSha256` and `signHmacSha256` helpers for raw-body signatures (constant-time compare).
38
+ - `PluginContext.callbackUrl`: the public URL a plugin registers with its vendor. Append
39
+ `?externalId=<id>` so the platform can route the callback to the right tenant and run.
40
+ - `PluginContext.idempotencyKey`, action `timeoutMs`, `retry` and `idempotent` flags (from phase 1).
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @aletheia-dev/plugin-sdk
2
+
3
+ The contract for writing [Aletheia](https://github.com/akhiljames/aletheia) plugins. Aletheia is
4
+ open-source risk management infrastructure (KYC/KYB onboarding, underwriting, transaction
5
+ monitoring) with a workflow engine, a rule engine and a plugin system. A plugin wraps one vendor
6
+ or capability — sanctions screening, document verification, fraud scoring — behind a typed,
7
+ tenant-configured interface. Workflows call plugins through `call_plugin` steps and rules through
8
+ the `plugin` rule type; the platform validates configuration, resolves secrets per tenant,
9
+ invokes actions with timeouts and retries, audits every call and receives vendor webhooks.
10
+
11
+ This package is the only thing a plugin imports. It carries no runtime dependency on the
12
+ platform.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm i @aletheia-dev/plugin-sdk zod
18
+ ```
19
+
20
+ `zod` (v4) is a peer dependency: the schemas in your manifest are validated with your own zod
21
+ instance. Node 22 or later; the package ships ESM and CommonJS builds with type declarations.
22
+
23
+ ## A plugin
24
+
25
+ A plugin is a module exporting a `Plugin` built with `definePlugin`, whose `manifest` (built with
26
+ `defineManifest`) declares what the plugin needs and what it offers:
27
+
28
+ ```ts
29
+ import { z } from 'zod';
30
+ import { defineManifest, definePlugin } from '@aletheia-dev/plugin-sdk';
31
+
32
+ export const manifest = defineManifest({
33
+ name: '@acme/plugin-vendor', // the npm package name; also the key in the catalogue and the API
34
+ version: '0.1.0',
35
+ description: 'Screens names against Vendor.',
36
+ capabilities: ['sanctions.screen'], // tags rules and workflows look plugins up by
37
+ configSchema: z.object({ threshold: z.number().min(0).max(1).default(0.8) }),
38
+ secrets: ['apiKey'], // resolved per tenant by the platform, never stored in config
39
+ actions: {
40
+ screen: {
41
+ description: 'Screens one name.',
42
+ input: z.object({ name: z.string().min(1) }),
43
+ output: z.object({ hit: z.boolean(), score: z.number() }),
44
+ timeoutMs: 5_000,
45
+ idempotent: true,
46
+ retry: { maxAttempts: 3, backoffMs: 200 },
47
+ },
48
+ },
49
+ });
50
+
51
+ export default definePlugin({
52
+ manifest,
53
+ async onInit(ctx) {
54
+ // Optional: runs once per tenant context; throwing surfaces at worker start.
55
+ },
56
+ async invoke(action, input, ctx) {
57
+ // `input` has already been validated against the action's input schema.
58
+ const res = await ctx.fetch('https://vendor.example/screen', {
59
+ method: 'POST',
60
+ headers: { authorization: `Bearer ${ctx.secrets.apiKey}` },
61
+ body: JSON.stringify(input),
62
+ signal: ctx.signal, // aborted at the action deadline
63
+ });
64
+ return res.json(); // validated against the output schema by the platform
65
+ },
66
+ });
67
+ ```
68
+
69
+ ### The manifest
70
+
71
+ | Field | Purpose |
72
+ | -------------- | ----------------------------------------------------------------------------------- |
73
+ | `name` | npm package name; the catalogue, the API and tenant configuration key off it. |
74
+ | `capabilities` | Free-form tags such as `sanctions.screen`; the platform finds plugins by them. |
75
+ | `configSchema` | zod schema for the tenant's non-secret configuration; defaults apply on every call. |
76
+ | `secrets` | Names of secrets; each tenant maps them to secret references. |
77
+ | `actions` | Named, typed operations. Input and output are validated on every call. |
78
+
79
+ Per action: `timeoutMs` bounds one attempt (default 30 s; `ctx.signal` aborts on expiry),
80
+ `retry` is applied only when it is safe (the action is `idempotent` or the caller supplied an
81
+ idempotency key) and `async` marks an action that completes through a webhook.
82
+ `PluginManifestSchema` validates the data parts of a manifest at runtime.
83
+
84
+ Use `ActionInput<M, K>`, `ActionOutput<M, K>` and `PluginConfig<M>` to type the body of `invoke`
85
+ from the manifest rather than repeating the shapes.
86
+
87
+ ### The context
88
+
89
+ Every call receives a `PluginContext`:
90
+
91
+ - `tenantId`, `config` (parsed through `configSchema`) and `secrets` (resolved values keyed by
92
+ secret name);
93
+ - `logger`, a structured logger with the plugin and tenant already bound, and `fetch` — use it,
94
+ never the global;
95
+ - `signal`, aborted when the action's deadline passes; pass it to `fetch`;
96
+ - `idempotencyKey`, present when the caller identifies the logical call (workflow run + step);
97
+ - `callbackUrl`, where the vendor must send webhooks for this plugin (see below);
98
+ - `documents`, read access to the tenant's documents when the deployment has object storage.
99
+
100
+ ## Synchronous and asynchronous actions
101
+
102
+ A synchronous action returns its result from `invoke`. An asynchronous action starts work at the
103
+ vendor and completes later through a webhook. Declare it with `async` and return
104
+ `pending(externalId)`:
105
+
106
+ ```ts
107
+ import { pending } from '@aletheia-dev/plugin-sdk';
108
+
109
+ actions: {
110
+ verify: {
111
+ input: z.object({ documentId: z.string() }),
112
+ output: z.object({ verdict: z.enum(['pass', 'fail']) }),
113
+ async: { callbackTimeoutSeconds: 3_600 }, // at most 7 days
114
+ },
115
+ },
116
+
117
+ async invoke(action, input, ctx) {
118
+ const session = await startVendorSession(input, ctx);
119
+ return pending(session.id); // the vendor's id for the session; webhooks must carry it back
120
+ }
121
+ ```
122
+
123
+ The platform records the pending call, parks the workflow run and waits up to
124
+ `callbackTimeoutSeconds` for a webhook reporting that `externalId`; on timeout the step fails
125
+ with a rule-visible `CallbackTimeout`. `isPending(value)` recognises the marker.
126
+
127
+ ## Webhooks
128
+
129
+ A plugin with asynchronous actions implements `handleWebhook(request, ctx)`. It receives the raw
130
+ request — `method`, lower-cased `headers`, `rawBody` as bytes and `query` — plus the tenant's
131
+ context, verifies the signature and returns a `WebhookEvent`, or `null` to ignore the request:
132
+
133
+ ```ts
134
+ import { WebhookRejectedError, verifyHmacSha256 } from '@aletheia-dev/plugin-sdk';
135
+
136
+ async handleWebhook(request, ctx) {
137
+ const signature = request.headers['x-vendor-signature'] ?? '';
138
+ if (!verifyHmacSha256(ctx.secrets.webhookSecret, request.rawBody, signature)) {
139
+ throw new WebhookRejectedError('bad signature'); // HTTP 401
140
+ }
141
+ const body = JSON.parse(Buffer.from(request.rawBody).toString('utf8'));
142
+ if (body.type !== 'session.completed') return null; // ignored, HTTP 202
143
+ return {
144
+ externalId: body.sessionId,
145
+ eventId: body.id, // de-duplicated per tenant
146
+ status: body.ok ? 'completed' : 'failed',
147
+ output: body.ok ? { verdict: body.verdict } : undefined, // validated against the output schema
148
+ error: body.ok ? undefined : body.reason,
149
+ };
150
+ }
151
+ ```
152
+
153
+ ### The `?externalId=` convention
154
+
155
+ The platform has to know the tenant before it can build a context, and the tenant's secrets are
156
+ what the signature check needs. The external id breaks that cycle, so webhook URLs carry it:
157
+ when you register the callback with the vendor, append it to `ctx.callbackUrl`:
158
+
159
+ ```ts
160
+ const url = `${ctx.callbackUrl}?externalId=${encodeURIComponent(externalId)}`;
161
+ ```
162
+
163
+ The platform resolves the tenant from the id, builds the context and calls `handleWebhook`.
164
+
165
+ ### `webhookExternalId` for dashboard-level webhook URLs
166
+
167
+ Some vendors take one webhook URL per account and never echo a per-session query string. Such a
168
+ plugin implements `webhookExternalId(request)`, a pure, secret-free extractor that decodes the id
169
+ from the body or headers (or returns `null`). It only selects the tenant; `handleWebhook` still
170
+ verifies the signature with that tenant's secret, so a forged id buys nothing but a 401.
171
+
172
+ ### HMAC helpers
173
+
174
+ `verifyHmacSha256(secret, rawBody, signature, encoding?)` is a constant-time check of a hex (or
175
+ base64) HMAC-SHA256 over the raw body; `signHmacSha256(secret, rawBody, encoding?)` produces one.
176
+ Vendors that sign differently (timestamped payloads, asymmetric keys) implement their own check
177
+ and should still compare in constant time.
178
+
179
+ ## Reading documents
180
+
181
+ Document checks receive a `documentId` in their input and read the bytes through the context:
182
+
183
+ ```ts
184
+ async invoke(action, input, ctx) {
185
+ if (!ctx.documents) throw new Error('document storage is not configured');
186
+ const doc = await ctx.documents.read(input.documentId);
187
+ // doc: { id, fileName, contentType, sizeBytes, bytes: Uint8Array }
188
+ }
189
+ ```
190
+
191
+ `ctx.documents` is present only when the deployment has object storage, so check before relying
192
+ on it. Only the tenant's own documents with status `clean` can be read; the bytes are the
193
+ platform's post-processing copy (type sniffed, size checked, images re-encoded, PDFs checked),
194
+ never the raw upload.
195
+
196
+ ## Logging
197
+
198
+ `ctx.logger` has `debug`, `info`, `warn` and `error` (pino-style: an optional object first, then
199
+ the message) and `child(bindings)`. The plugin name and tenant are already bound. Log vendor
200
+ request ids and outcomes, never secrets or document bytes. `noopLogger` is a silent
201
+ implementation for tests, and the `Logger` type lets you accept any compatible logger.
202
+
203
+ ## Testing locally
204
+
205
+ A plugin is a plain object, so unit tests call `invoke` and `handleWebhook` directly with a
206
+ hand-built context:
207
+
208
+ ```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
+ };
220
+ ```
221
+
222
+ The repository's example plugins are the reference implementations:
223
+
224
+ - [`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;
226
+ - [`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;
228
+ - [`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
230
+ vendors, including `webhookExternalId` for a dashboard-level webhook URL.
231
+
232
+ 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.
236
+
237
+ ## Versioning
238
+
239
+ The SDK follows [semver](https://semver.org) on its own cadence, independent of the platform.
240
+ While the major version is `0`, a minor release may change the contract (additive changes are
241
+ the norm; anything that requires plugin changes is called out in the
242
+ [changelog](https://github.com/akhiljames/aletheia/blob/main/packages/plugin-sdk/CHANGELOG.md)).
243
+ From 1.0 onwards, breaking changes to the contract are major releases.
244
+
245
+ ## License
246
+
247
+ Apache-2.0
package/dist/index.cjs ADDED
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ PluginManifestSchema: () => PluginManifestSchema,
24
+ WebhookRejectedError: () => WebhookRejectedError,
25
+ defineManifest: () => defineManifest,
26
+ definePlugin: () => definePlugin,
27
+ isPending: () => isPending,
28
+ noopLogger: () => noopLogger,
29
+ pending: () => pending,
30
+ signHmacSha256: () => signHmacSha256,
31
+ verifyHmacSha256: () => verifyHmacSha256
32
+ });
33
+ module.exports = __toCommonJS(src_exports);
34
+ var import_zod = require("zod");
35
+
36
+ // src/logger.ts
37
+ var noopLogger = {
38
+ debug: () => void 0,
39
+ info: () => void 0,
40
+ warn: () => void 0,
41
+ error: () => void 0,
42
+ child: () => noopLogger
43
+ };
44
+
45
+ // src/webhooks.ts
46
+ var import_node_crypto = require("crypto");
47
+ var WebhookRejectedError = class extends Error {
48
+ constructor(message = "webhook rejected") {
49
+ super(message);
50
+ this.name = "WebhookRejectedError";
51
+ }
52
+ };
53
+ function verifyHmacSha256(secret, rawBody, signature, encoding = "hex") {
54
+ const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(rawBody).digest(encoding);
55
+ const a = Buffer.from(expected);
56
+ const b = Buffer.from(signature.trim());
57
+ return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
58
+ }
59
+ function signHmacSha256(secret, rawBody, encoding = "hex") {
60
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(rawBody).digest(encoding);
61
+ }
62
+
63
+ // src/index.ts
64
+ function pending(externalId) {
65
+ return { pending: true, externalId };
66
+ }
67
+ function isPending(value) {
68
+ return typeof value === "object" && value !== null && value.pending === true && typeof value.externalId === "string";
69
+ }
70
+ function definePlugin(plugin) {
71
+ return plugin;
72
+ }
73
+ function defineManifest(manifest) {
74
+ return manifest;
75
+ }
76
+ var isZodSchema = (value) => typeof value === "object" && value !== null && "_zod" in value;
77
+ var ZodSchemaValue = import_zod.z.custom(isZodSchema, { message: "expected a zod schema" });
78
+ var PluginManifestSchema = import_zod.z.object({
79
+ name: import_zod.z.string().min(1).max(214).regex(
80
+ /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,
81
+ "expected an npm package name"
82
+ ),
83
+ version: import_zod.z.string().min(1),
84
+ description: import_zod.z.string().optional(),
85
+ capabilities: import_zod.z.array(import_zod.z.string().min(1)),
86
+ configSchema: ZodSchemaValue,
87
+ secrets: import_zod.z.array(import_zod.z.string().min(1)),
88
+ actions: import_zod.z.record(
89
+ import_zod.z.string().min(1),
90
+ import_zod.z.object({
91
+ description: import_zod.z.string().optional(),
92
+ input: ZodSchemaValue,
93
+ output: ZodSchemaValue,
94
+ timeoutMs: import_zod.z.number().int().positive().optional(),
95
+ retry: import_zod.z.object({
96
+ maxAttempts: import_zod.z.number().int().min(1).max(5),
97
+ backoffMs: import_zod.z.number().int().nonnegative()
98
+ }).optional(),
99
+ idempotent: import_zod.z.boolean().optional(),
100
+ async: import_zod.z.object({
101
+ callbackTimeoutSeconds: import_zod.z.number().int().min(1).max(7 * 24 * 3600)
102
+ }).optional()
103
+ })
104
+ )
105
+ });
106
+ // Annotate the CommonJS export names for ESM import in node:
107
+ 0 && (module.exports = {
108
+ PluginManifestSchema,
109
+ WebhookRejectedError,
110
+ defineManifest,
111
+ definePlugin,
112
+ isPending,
113
+ noopLogger,
114
+ pending,
115
+ signHmacSha256,
116
+ verifyHmacSha256
117
+ });
118
+ //# sourceMappingURL=index.cjs.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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;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,yBAA4C;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,eAAW,+BAAW,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,cAAU,oCAAgB,GAAG,CAAC;AACtD;AAEO,SAAS,eACd,QACA,SACA,WAA6B,OACrB;AACR,aAAO,+BAAW,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,aAAE,OAAkB,aAAa,EAAE,SAAS,wBAAwB,CAAC;AAGrF,IAAM,uBAAuB,aAAE,OAAO;AAAA,EAC3C,MAAM,aACH,OAAO,EACP,IAAI,CAAC,EACL,IAAI,GAAG,EACP;AAAA,IACC;AAAA,IACA;AAAA,EACF;AAAA,EACF,SAAS,aAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACzB,aAAa,aAAE,OAAO,EAAE,SAAS;AAAA,EACjC,cAAc,aAAE,MAAM,aAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,EACvC,cAAc;AAAA,EACd,SAAS,aAAE,MAAM,aAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,EAClC,SAAS,aAAE;AAAA,IACT,aAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAChB,aAAE,OAAO;AAAA,MACP,aAAa,aAAE,OAAO,EAAE,SAAS;AAAA,MACjC,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW,aAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,MAChD,OAAO,aACJ,OAAO;AAAA,QACN,aAAa,aAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,QAC1C,WAAW,aAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,MAC1C,CAAC,EACA,SAAS;AAAA,MACZ,YAAY,aAAE,QAAQ,EAAE,SAAS;AAAA,MACjC,OAAO,aACJ,OAAO;AAAA,QACN,wBAAwB,aACrB,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,IAAI,KAAK,IAAI;AAAA,MACtB,CAAC,EACA,SAAS;AAAA,IACd,CAAC;AAAA,EACH;AACF,CAAC;","names":[]}
@@ -0,0 +1,194 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Minimal structural logger contract handed to plugins (pino satisfies it).
5
+ *
6
+ * This is a deliberate copy of `@aletheia-dev/core`'s `logger.ts`: the SDK is published on its own
7
+ * and must not pull the platform's core package in for three declarations. `logger.test.ts`
8
+ * asserts the two stay assignable both ways, so the runtime can pass its core logger straight
9
+ * into a plugin context.
10
+ */
11
+ type LogFn = (objOrMsg: object | string, msg?: string, ...args: unknown[]) => void;
12
+ interface Logger {
13
+ debug: LogFn;
14
+ info: LogFn;
15
+ warn: LogFn;
16
+ error: LogFn;
17
+ child(bindings: Record<string, unknown>): Logger;
18
+ }
19
+ declare const noopLogger: Logger;
20
+
21
+ /**
22
+ * A clean document of the tenant, read for a plugin. The bytes are what the platform stored
23
+ * after finalisation (type sniffing, size limits, image re-encoding and PDF checks), never the
24
+ * applicant's original upload.
25
+ */
26
+ interface DocumentHandle {
27
+ id: string;
28
+ fileName: string;
29
+ contentType: string;
30
+ sizeBytes: number;
31
+ bytes: Uint8Array;
32
+ }
33
+ /** Read-only access to the tenant's documents, present on the context when storage is configured. */
34
+ interface PluginDocuments {
35
+ /** Throws when the document does not belong to the tenant or its status is not `clean`. */
36
+ read(documentId: string): Promise<DocumentHandle>;
37
+ }
38
+
39
+ /** A vendor webhook as received by the API, with the raw body for signature verification. */
40
+ interface WebhookRequest {
41
+ method: string;
42
+ /** Header names lower-cased. */
43
+ headers: Record<string, string>;
44
+ rawBody: Uint8Array;
45
+ query: Record<string, string>;
46
+ }
47
+ /** What a plugin extracted from a webhook. `null` from `handleWebhook` means "ignore". */
48
+ interface WebhookEvent {
49
+ /** The vendor session id returned by the asynchronous action. */
50
+ externalId: string;
51
+ /** Vendor event id (or a stable hash) used for de-duplication. */
52
+ eventId: string;
53
+ status: 'completed' | 'failed' | 'pending';
54
+ /** Validated against the action's output schema when `status` is `completed`. */
55
+ output?: unknown;
56
+ error?: string;
57
+ }
58
+ /** Thrown by `handleWebhook` when the signature or payload is not acceptable (HTTP 401). */
59
+ declare class WebhookRejectedError extends Error {
60
+ constructor(message?: string);
61
+ }
62
+ /** Constant-time HMAC-SHA256 check of a raw body against a hex (or base64) signature. */
63
+ declare function verifyHmacSha256(secret: string, rawBody: Uint8Array, signature: string, encoding?: 'hex' | 'base64'): boolean;
64
+ declare function signHmacSha256(secret: string, rawBody: Uint8Array, encoding?: 'hex' | 'base64'): string;
65
+
66
+ /** Retry hints for an action; applied by the runtime only when the call is safe to repeat. */
67
+ interface PluginRetryPolicy {
68
+ /** Total attempts including the first (1 to 5). */
69
+ maxAttempts: number;
70
+ /** Base delay between attempts; multiplied by the attempt number. */
71
+ backoffMs: number;
72
+ }
73
+ /** One callable action a plugin exposes. Input and output are validated by the runtime. */
74
+ interface PluginAction<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
75
+ description?: string;
76
+ input: I;
77
+ output: O;
78
+ /** Deadline for one attempt; the runtime aborts `ctx.signal` and fails the call when exceeded. */
79
+ timeoutMs?: number;
80
+ /** Retried by the runtime when `idempotent` is true or the caller supplies an idempotency key. */
81
+ retry?: PluginRetryPolicy;
82
+ /** Declares that repeating the action with the same input has no additional effect. */
83
+ idempotent?: boolean;
84
+ /**
85
+ * The action starts work at the vendor and completes later through a webhook. `invoke` must
86
+ * return `pending(externalId)`; the runtime waits for `handleWebhook` to report that id.
87
+ */
88
+ async?: {
89
+ callbackTimeoutSeconds: number;
90
+ };
91
+ }
92
+ /** What an asynchronous action returns after starting work at the vendor. */
93
+ interface PendingResult {
94
+ pending: true;
95
+ /** The vendor's identifier for the session, job or check; webhooks must carry it back. */
96
+ externalId: string;
97
+ }
98
+ declare function pending(externalId: string): PendingResult;
99
+ declare function isPending(value: unknown): value is PendingResult;
100
+ type PluginActions = Record<string, PluginAction>;
101
+ /** Static description of a plugin package: what it needs (config, secrets) and what it offers. */
102
+ interface PluginManifest<C extends z.ZodType = z.ZodType, A extends PluginActions = PluginActions> {
103
+ /** npm package name, e.g. '@aletheia-dev/plugin-mock-sanctions'. */
104
+ name: string;
105
+ version: string;
106
+ description?: string;
107
+ /** Capability tags, e.g. ['sanctions.screen']. */
108
+ capabilities: string[];
109
+ /** Validates the tenant-provided, non-secret config. */
110
+ configSchema: C;
111
+ /** Names of secrets the plugin needs; resolved by the runtime per tenant. */
112
+ secrets: string[];
113
+ actions: A;
114
+ }
115
+ /** Per-tenant runtime context handed to every plugin call. */
116
+ interface PluginContext<C = unknown> {
117
+ tenantId: string;
118
+ config: C;
119
+ secrets: Record<string, string>;
120
+ logger: Logger;
121
+ fetch: typeof fetch;
122
+ /** Aborted when the action's deadline passes; pass it to `fetch` so vendor calls stop too. */
123
+ signal: AbortSignal;
124
+ /** Stable key for the logical call (same across retries), when the caller provided one. */
125
+ idempotencyKey?: string;
126
+ /** Where the vendor must send webhooks for this plugin, when the deployment exposes one. */
127
+ callbackUrl?: string;
128
+ /**
129
+ * Read-only access to the tenant's clean documents (by document id). Present when the
130
+ * deployment has object storage; absent otherwise, so plugins must check before relying on it.
131
+ */
132
+ documents?: PluginDocuments;
133
+ }
134
+ /** Parsed config type of a manifest. */
135
+ type PluginConfig<M extends PluginManifest> = z.output<M['configSchema']>;
136
+ /** Union of a manifest's action names. */
137
+ type ActionName<M extends PluginManifest> = keyof M['actions'] & string;
138
+ /** Parsed input type of one action. */
139
+ type ActionInput<M extends PluginManifest, K extends ActionName<M>> = z.output<M['actions'][K]['input']>;
140
+ /** Output type of one action. */
141
+ type ActionOutput<M extends PluginManifest, K extends ActionName<M>> = z.output<M['actions'][K]['output']>;
142
+ interface Plugin<M extends PluginManifest = PluginManifest> {
143
+ manifest: M;
144
+ onInit?(ctx: PluginContext<PluginConfig<M>>): Promise<void> | void;
145
+ onShutdown?(): Promise<void> | void;
146
+ /**
147
+ * Dispatch one action. The runtime validates `input` against the action's input schema
148
+ * before calling and the return value against its output schema afterwards, so the
149
+ * signature stays loose here; use `ActionInput`/`ActionOutput` to type the body.
150
+ */
151
+ invoke(action: ActionName<M>, input: unknown, ctx: PluginContext<PluginConfig<M>>): Promise<unknown>;
152
+ /**
153
+ * Verifies and decodes a vendor webhook for one of this plugin's asynchronous actions.
154
+ * Return `null` to ignore the request, throw `WebhookRejectedError` for bad signatures.
155
+ */
156
+ handleWebhook?(request: WebhookRequest, ctx: PluginContext<PluginConfig<M>>): Promise<WebhookEvent | null>;
157
+ /**
158
+ * Extracts the vendor's external id from a webhook whose URL carries no `?externalId=`
159
+ * (vendors with one dashboard-level webhook URL). Must be pure and need no secrets: it runs
160
+ * before the tenant is known, so it only decodes the body or headers. The platform resolves
161
+ * the tenant from the id and only then calls `handleWebhook` with the tenant context, which
162
+ * must still verify the signature. Return `null` when the request carries no id.
163
+ */
164
+ webhookExternalId?(request: WebhookRequest): string | null;
165
+ }
166
+ /** Identity helper so `manifest` drives inference for `onInit`/`invoke`. */
167
+ declare function definePlugin<M extends PluginManifest>(plugin: Plugin<M>): Plugin<M>;
168
+ /** Identity helper that preserves the concrete schema and action types. */
169
+ declare function defineManifest<C extends z.ZodType, A extends PluginActions>(manifest: PluginManifest<C, A>): PluginManifest<C, A>;
170
+ /** Runtime validation of a manifest's data parts (schemas are only checked to be zod schemas). */
171
+ declare const PluginManifestSchema: z.ZodObject<{
172
+ name: z.ZodString;
173
+ version: z.ZodString;
174
+ description: z.ZodOptional<z.ZodString>;
175
+ capabilities: z.ZodArray<z.ZodString>;
176
+ configSchema: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
177
+ secrets: z.ZodArray<z.ZodString>;
178
+ actions: z.ZodRecord<z.ZodString, z.ZodObject<{
179
+ description: z.ZodOptional<z.ZodString>;
180
+ input: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
181
+ output: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
182
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
183
+ retry: z.ZodOptional<z.ZodObject<{
184
+ maxAttempts: z.ZodNumber;
185
+ backoffMs: z.ZodNumber;
186
+ }, z.core.$strip>>;
187
+ idempotent: z.ZodOptional<z.ZodBoolean>;
188
+ async: z.ZodOptional<z.ZodObject<{
189
+ callbackTimeoutSeconds: z.ZodNumber;
190
+ }, z.core.$strip>>;
191
+ }, z.core.$strip>>;
192
+ }, z.core.$strip>;
193
+
194
+ export { type ActionInput, type ActionName, type ActionOutput, type DocumentHandle, type LogFn, type Logger, type PendingResult, type Plugin, type PluginAction, type PluginActions, type PluginConfig, type PluginContext, type PluginDocuments, type PluginManifest, PluginManifestSchema, type PluginRetryPolicy, type WebhookEvent, WebhookRejectedError, type WebhookRequest, defineManifest, definePlugin, isPending, noopLogger, pending, signHmacSha256, verifyHmacSha256 };
@@ -0,0 +1,194 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Minimal structural logger contract handed to plugins (pino satisfies it).
5
+ *
6
+ * This is a deliberate copy of `@aletheia-dev/core`'s `logger.ts`: the SDK is published on its own
7
+ * and must not pull the platform's core package in for three declarations. `logger.test.ts`
8
+ * asserts the two stay assignable both ways, so the runtime can pass its core logger straight
9
+ * into a plugin context.
10
+ */
11
+ type LogFn = (objOrMsg: object | string, msg?: string, ...args: unknown[]) => void;
12
+ interface Logger {
13
+ debug: LogFn;
14
+ info: LogFn;
15
+ warn: LogFn;
16
+ error: LogFn;
17
+ child(bindings: Record<string, unknown>): Logger;
18
+ }
19
+ declare const noopLogger: Logger;
20
+
21
+ /**
22
+ * A clean document of the tenant, read for a plugin. The bytes are what the platform stored
23
+ * after finalisation (type sniffing, size limits, image re-encoding and PDF checks), never the
24
+ * applicant's original upload.
25
+ */
26
+ interface DocumentHandle {
27
+ id: string;
28
+ fileName: string;
29
+ contentType: string;
30
+ sizeBytes: number;
31
+ bytes: Uint8Array;
32
+ }
33
+ /** Read-only access to the tenant's documents, present on the context when storage is configured. */
34
+ interface PluginDocuments {
35
+ /** Throws when the document does not belong to the tenant or its status is not `clean`. */
36
+ read(documentId: string): Promise<DocumentHandle>;
37
+ }
38
+
39
+ /** A vendor webhook as received by the API, with the raw body for signature verification. */
40
+ interface WebhookRequest {
41
+ method: string;
42
+ /** Header names lower-cased. */
43
+ headers: Record<string, string>;
44
+ rawBody: Uint8Array;
45
+ query: Record<string, string>;
46
+ }
47
+ /** What a plugin extracted from a webhook. `null` from `handleWebhook` means "ignore". */
48
+ interface WebhookEvent {
49
+ /** The vendor session id returned by the asynchronous action. */
50
+ externalId: string;
51
+ /** Vendor event id (or a stable hash) used for de-duplication. */
52
+ eventId: string;
53
+ status: 'completed' | 'failed' | 'pending';
54
+ /** Validated against the action's output schema when `status` is `completed`. */
55
+ output?: unknown;
56
+ error?: string;
57
+ }
58
+ /** Thrown by `handleWebhook` when the signature or payload is not acceptable (HTTP 401). */
59
+ declare class WebhookRejectedError extends Error {
60
+ constructor(message?: string);
61
+ }
62
+ /** Constant-time HMAC-SHA256 check of a raw body against a hex (or base64) signature. */
63
+ declare function verifyHmacSha256(secret: string, rawBody: Uint8Array, signature: string, encoding?: 'hex' | 'base64'): boolean;
64
+ declare function signHmacSha256(secret: string, rawBody: Uint8Array, encoding?: 'hex' | 'base64'): string;
65
+
66
+ /** Retry hints for an action; applied by the runtime only when the call is safe to repeat. */
67
+ interface PluginRetryPolicy {
68
+ /** Total attempts including the first (1 to 5). */
69
+ maxAttempts: number;
70
+ /** Base delay between attempts; multiplied by the attempt number. */
71
+ backoffMs: number;
72
+ }
73
+ /** One callable action a plugin exposes. Input and output are validated by the runtime. */
74
+ interface PluginAction<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
75
+ description?: string;
76
+ input: I;
77
+ output: O;
78
+ /** Deadline for one attempt; the runtime aborts `ctx.signal` and fails the call when exceeded. */
79
+ timeoutMs?: number;
80
+ /** Retried by the runtime when `idempotent` is true or the caller supplies an idempotency key. */
81
+ retry?: PluginRetryPolicy;
82
+ /** Declares that repeating the action with the same input has no additional effect. */
83
+ idempotent?: boolean;
84
+ /**
85
+ * The action starts work at the vendor and completes later through a webhook. `invoke` must
86
+ * return `pending(externalId)`; the runtime waits for `handleWebhook` to report that id.
87
+ */
88
+ async?: {
89
+ callbackTimeoutSeconds: number;
90
+ };
91
+ }
92
+ /** What an asynchronous action returns after starting work at the vendor. */
93
+ interface PendingResult {
94
+ pending: true;
95
+ /** The vendor's identifier for the session, job or check; webhooks must carry it back. */
96
+ externalId: string;
97
+ }
98
+ declare function pending(externalId: string): PendingResult;
99
+ declare function isPending(value: unknown): value is PendingResult;
100
+ type PluginActions = Record<string, PluginAction>;
101
+ /** Static description of a plugin package: what it needs (config, secrets) and what it offers. */
102
+ interface PluginManifest<C extends z.ZodType = z.ZodType, A extends PluginActions = PluginActions> {
103
+ /** npm package name, e.g. '@aletheia-dev/plugin-mock-sanctions'. */
104
+ name: string;
105
+ version: string;
106
+ description?: string;
107
+ /** Capability tags, e.g. ['sanctions.screen']. */
108
+ capabilities: string[];
109
+ /** Validates the tenant-provided, non-secret config. */
110
+ configSchema: C;
111
+ /** Names of secrets the plugin needs; resolved by the runtime per tenant. */
112
+ secrets: string[];
113
+ actions: A;
114
+ }
115
+ /** Per-tenant runtime context handed to every plugin call. */
116
+ interface PluginContext<C = unknown> {
117
+ tenantId: string;
118
+ config: C;
119
+ secrets: Record<string, string>;
120
+ logger: Logger;
121
+ fetch: typeof fetch;
122
+ /** Aborted when the action's deadline passes; pass it to `fetch` so vendor calls stop too. */
123
+ signal: AbortSignal;
124
+ /** Stable key for the logical call (same across retries), when the caller provided one. */
125
+ idempotencyKey?: string;
126
+ /** Where the vendor must send webhooks for this plugin, when the deployment exposes one. */
127
+ callbackUrl?: string;
128
+ /**
129
+ * Read-only access to the tenant's clean documents (by document id). Present when the
130
+ * deployment has object storage; absent otherwise, so plugins must check before relying on it.
131
+ */
132
+ documents?: PluginDocuments;
133
+ }
134
+ /** Parsed config type of a manifest. */
135
+ type PluginConfig<M extends PluginManifest> = z.output<M['configSchema']>;
136
+ /** Union of a manifest's action names. */
137
+ type ActionName<M extends PluginManifest> = keyof M['actions'] & string;
138
+ /** Parsed input type of one action. */
139
+ type ActionInput<M extends PluginManifest, K extends ActionName<M>> = z.output<M['actions'][K]['input']>;
140
+ /** Output type of one action. */
141
+ type ActionOutput<M extends PluginManifest, K extends ActionName<M>> = z.output<M['actions'][K]['output']>;
142
+ interface Plugin<M extends PluginManifest = PluginManifest> {
143
+ manifest: M;
144
+ onInit?(ctx: PluginContext<PluginConfig<M>>): Promise<void> | void;
145
+ onShutdown?(): Promise<void> | void;
146
+ /**
147
+ * Dispatch one action. The runtime validates `input` against the action's input schema
148
+ * before calling and the return value against its output schema afterwards, so the
149
+ * signature stays loose here; use `ActionInput`/`ActionOutput` to type the body.
150
+ */
151
+ invoke(action: ActionName<M>, input: unknown, ctx: PluginContext<PluginConfig<M>>): Promise<unknown>;
152
+ /**
153
+ * Verifies and decodes a vendor webhook for one of this plugin's asynchronous actions.
154
+ * Return `null` to ignore the request, throw `WebhookRejectedError` for bad signatures.
155
+ */
156
+ handleWebhook?(request: WebhookRequest, ctx: PluginContext<PluginConfig<M>>): Promise<WebhookEvent | null>;
157
+ /**
158
+ * Extracts the vendor's external id from a webhook whose URL carries no `?externalId=`
159
+ * (vendors with one dashboard-level webhook URL). Must be pure and need no secrets: it runs
160
+ * before the tenant is known, so it only decodes the body or headers. The platform resolves
161
+ * the tenant from the id and only then calls `handleWebhook` with the tenant context, which
162
+ * must still verify the signature. Return `null` when the request carries no id.
163
+ */
164
+ webhookExternalId?(request: WebhookRequest): string | null;
165
+ }
166
+ /** Identity helper so `manifest` drives inference for `onInit`/`invoke`. */
167
+ declare function definePlugin<M extends PluginManifest>(plugin: Plugin<M>): Plugin<M>;
168
+ /** Identity helper that preserves the concrete schema and action types. */
169
+ declare function defineManifest<C extends z.ZodType, A extends PluginActions>(manifest: PluginManifest<C, A>): PluginManifest<C, A>;
170
+ /** Runtime validation of a manifest's data parts (schemas are only checked to be zod schemas). */
171
+ declare const PluginManifestSchema: z.ZodObject<{
172
+ name: z.ZodString;
173
+ version: z.ZodString;
174
+ description: z.ZodOptional<z.ZodString>;
175
+ capabilities: z.ZodArray<z.ZodString>;
176
+ configSchema: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
177
+ secrets: z.ZodArray<z.ZodString>;
178
+ actions: z.ZodRecord<z.ZodString, z.ZodObject<{
179
+ description: z.ZodOptional<z.ZodString>;
180
+ input: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
181
+ output: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
182
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
183
+ retry: z.ZodOptional<z.ZodObject<{
184
+ maxAttempts: z.ZodNumber;
185
+ backoffMs: z.ZodNumber;
186
+ }, z.core.$strip>>;
187
+ idempotent: z.ZodOptional<z.ZodBoolean>;
188
+ async: z.ZodOptional<z.ZodObject<{
189
+ callbackTimeoutSeconds: z.ZodNumber;
190
+ }, z.core.$strip>>;
191
+ }, z.core.$strip>>;
192
+ }, z.core.$strip>;
193
+
194
+ export { type ActionInput, type ActionName, type ActionOutput, type DocumentHandle, type LogFn, type Logger, type PendingResult, type Plugin, type PluginAction, type PluginActions, type PluginConfig, type PluginContext, type PluginDocuments, type PluginManifest, PluginManifestSchema, type PluginRetryPolicy, type WebhookEvent, WebhookRejectedError, type WebhookRequest, defineManifest, definePlugin, isPending, noopLogger, pending, signHmacSha256, verifyHmacSha256 };
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
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
+ export {
75
+ PluginManifestSchema,
76
+ WebhookRejectedError,
77
+ defineManifest,
78
+ definePlugin,
79
+ isPending,
80
+ noopLogger,
81
+ pending,
82
+ signHmacSha256,
83
+ verifyHmacSha256
84
+ };
85
+ //# sourceMappingURL=index.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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@aletheia-dev/plugin-sdk",
3
+ "version": "0.3.0",
4
+ "description": "SDK for writing Aletheia plugins: manifest, actions, webhooks, document access and the per-tenant context.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/akhiljames/aletheia.git",
10
+ "directory": "packages/plugin-sdk"
11
+ },
12
+ "homepage": "https://github.com/akhiljames/aletheia/tree/main/packages/plugin-sdk#readme",
13
+ "bugs": "https://github.com/akhiljames/aletheia/issues",
14
+ "sideEffects": false,
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "peerDependencies": {
34
+ "zod": "^4.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.20.4",
38
+ "eslint": "^10.11.0",
39
+ "tsup": "^8.5.1",
40
+ "typescript": "5.9.3",
41
+ "vitest": "^5.0.3",
42
+ "zod": "^4.6.5",
43
+ "@aletheia-dev/core": "0.1.0"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "lint": "eslint .",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "clean": "rm -rf dist .turbo"
52
+ }
53
+ }