@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 +40 -0
- package/LICENSE +202 -0
- package/README.md +247 -0
- package/dist/index.cjs +118 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +194 -0
- package/dist/index.d.ts +194 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|