@pikku/core 0.12.63 → 0.12.66
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 +368 -0
- package/dist/dev/hot-reload.js +13 -55
- package/dist/dev/module-runner.d.ts +14 -0
- package/dist/dev/module-runner.js +59 -0
- package/dist/dev/reload-meta.d.ts +13 -0
- package/dist/dev/reload-meta.js +22 -0
- package/dist/errors/errors.d.ts +12 -0
- package/dist/errors/errors.js +19 -0
- package/dist/function/function-runner.d.ts +5 -5
- package/dist/function/function-runner.js +39 -111
- package/dist/function/functions.types.d.ts +17 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/middleware/auth-apikey.d.ts +1 -0
- package/dist/middleware/auth-bearer.d.ts +1 -0
- package/dist/middleware/auth-cookie.d.ts +1 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.d.ts +1 -0
- package/dist/middleware/telemetry.d.ts +2 -0
- package/dist/middleware/timeout.d.ts +1 -0
- package/dist/permissions.d.ts +42 -27
- package/dist/permissions.js +101 -203
- package/dist/pikku-state.js +0 -2
- package/dist/scopes.d.ts +17 -0
- package/dist/scopes.js +59 -0
- package/dist/services/ai-embedding-service.d.ts +31 -0
- package/dist/services/ai-embedding-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +3 -1
- package/dist/services/credential-wire-service.js +10 -4
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meta-service.d.ts +15 -2
- package/dist/services/meta-service.js +51 -1
- package/dist/services/queue-webhook-service.d.ts +55 -0
- package/dist/services/queue-webhook-service.js +136 -0
- package/dist/services/scope-service.d.ts +67 -0
- package/dist/services/scope-service.js +1 -0
- package/dist/services/typed-secret-service.d.ts +8 -0
- package/dist/services/typed-secret-service.js +39 -4
- package/dist/services/webhook-service.d.ts +140 -0
- package/dist/services/webhook-service.js +44 -0
- package/dist/services/workflow-service.d.ts +7 -0
- package/dist/testing/service-tests.js +37 -0
- package/dist/types/core.types.d.ts +34 -6
- package/dist/types/state.types.d.ts +20 -2
- package/dist/utils/hmac.d.ts +16 -0
- package/dist/utils/hmac.js +26 -0
- package/dist/utils/safe-fetch.d.ts +51 -0
- package/dist/utils/safe-fetch.js +192 -0
- package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
- package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
- package/dist/wirings/ai-agent/index.d.ts +2 -1
- package/dist/wirings/ai-agent/index.js +2 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
- package/dist/wirings/ai-agent/voice-input.js +5 -41
- package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
- package/dist/wirings/channel/channel-common.js +0 -1
- package/dist/wirings/channel/channel-handler.js +1 -4
- package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
- package/dist/wirings/channel/channel-middleware-runner.js +34 -19
- package/dist/wirings/channel/channel.types.d.ts +2 -6
- package/dist/wirings/cli/cli-runner.js +3 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -2
- package/dist/wirings/cli/command-parser.d.ts +2 -0
- package/dist/wirings/cli/command-parser.js +59 -2
- package/dist/wirings/credential/credential.types.d.ts +14 -0
- package/dist/wirings/credential/validate-credential-definitions.js +1 -0
- package/dist/wirings/gateway/gateway-runner.js +100 -50
- package/dist/wirings/gateway/gateway.types.d.ts +8 -5
- package/dist/wirings/http/http-routes.js +0 -3
- package/dist/wirings/http/http-runner.d.ts +9 -29
- package/dist/wirings/http/http-runner.js +9 -34
- package/dist/wirings/http/http.types.d.ts +4 -12
- package/dist/wirings/mcp/mcp-runner.js +0 -2
- package/dist/wirings/mcp/mcp.types.d.ts +5 -11
- package/dist/wirings/oauth2/index.d.ts +0 -3
- package/dist/wirings/oauth2/index.js +1 -2
- package/dist/wirings/rpc/addon-runner.d.ts +28 -0
- package/dist/wirings/rpc/addon-runner.js +173 -0
- package/dist/wirings/rpc/index.d.ts +5 -1
- package/dist/wirings/rpc/index.js +3 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
- package/dist/wirings/rpc/remote-addon-auth.js +43 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
- package/dist/wirings/rpc/rpc-runner.js +109 -3
- package/dist/wirings/rpc/rpc-types.d.ts +15 -1
- package/dist/wirings/rpc/wire-addon.js +9 -0
- package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
- package/dist/wirings/rpc/wire-remote-addon.js +19 -0
- package/dist/wirings/scope/index.d.ts +3 -0
- package/dist/wirings/scope/index.js +2 -0
- package/dist/wirings/scope/scope.types.d.ts +39 -0
- package/dist/wirings/scope/scope.types.js +1 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
- package/dist/wirings/scope/validate-scope-definitions.js +76 -0
- package/dist/wirings/scope/wire-scope.d.ts +33 -0
- package/dist/wirings/scope/wire-scope.js +32 -0
- package/dist/wirings/secret/secret.types.d.ts +14 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/variable/validate-variable-definitions.js +2 -0
- package/dist/wirings/variable/variable.types.d.ts +14 -0
- package/dist/wirings/workflow/dsl/index.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
- package/dist/wirings/workflow/index.d.ts +2 -2
- package/dist/wirings/workflow/index.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
- package/dist/wirings/workflow/workflow.types.d.ts +1 -3
- package/package.json +6 -1
- package/run-tests.sh +1 -0
- package/src/dev/hot-reload.ts +13 -68
- package/src/dev/module-runner.test.ts +169 -0
- package/src/dev/module-runner.ts +103 -0
- package/src/dev/reload-meta.test.ts +31 -2
- package/src/dev/reload-meta.ts +26 -0
- package/src/errors/errors.ts +24 -0
- package/src/function/function-runner.test.ts +276 -142
- package/src/function/function-runner.ts +52 -145
- package/src/function/functions.types.ts +28 -38
- package/src/index.ts +8 -6
- package/src/middleware/index.ts +1 -5
- package/src/permissions.test.ts +160 -373
- package/src/permissions.ts +137 -279
- package/src/pikku-state.ts +0 -2
- package/src/scopes.test.ts +167 -0
- package/src/scopes.ts +69 -0
- package/src/services/ai-embedding-service.ts +31 -0
- package/src/services/credential-wire-service.ts +10 -4
- package/src/services/index.ts +20 -0
- package/src/services/meta-service.ts +68 -3
- package/src/services/queue-webhook-service.test.ts +408 -0
- package/src/services/queue-webhook-service.ts +182 -0
- package/src/services/scope-service.ts +75 -0
- package/src/services/typed-secret-service.test.ts +35 -0
- package/src/services/typed-secret-service.ts +39 -4
- package/src/services/webhook-service.ts +180 -0
- package/src/services/workflow-service.ts +7 -0
- package/src/testing/service-tests.ts +49 -0
- package/src/types/core.types.ts +42 -14
- package/src/types/state.types.ts +23 -3
- package/src/utils/hmac.ts +27 -0
- package/src/utils/safe-fetch.test.ts +373 -0
- package/src/utils/safe-fetch.ts +213 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
- package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
- package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
- package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
- package/src/wirings/ai-agent/index.ts +5 -0
- package/src/wirings/ai-agent/voice-input.test.ts +90 -0
- package/src/wirings/ai-agent/voice-input.ts +9 -40
- package/src/wirings/channel/channel-common.ts +0 -1
- package/src/wirings/channel/channel-handler.ts +1 -9
- package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
- package/src/wirings/channel/channel-middleware-runner.ts +39 -27
- package/src/wirings/channel/channel.types.ts +0 -6
- package/src/wirings/cli/cli-runner.ts +4 -2
- package/src/wirings/cli/cli.types.ts +0 -2
- package/src/wirings/cli/command-parser.test.ts +130 -0
- package/src/wirings/cli/command-parser.ts +80 -2
- package/src/wirings/credential/credential.types.ts +14 -0
- package/src/wirings/credential/validate-credential-definitions.ts +1 -0
- package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
- package/src/wirings/gateway/gateway-runner.ts +114 -68
- package/src/wirings/gateway/gateway.types.ts +7 -9
- package/src/wirings/http/http-routes.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +1 -11
- package/src/wirings/http/http-runner.ts +16 -39
- package/src/wirings/http/http.types.ts +7 -14
- package/src/wirings/mcp/mcp-runner.ts +0 -2
- package/src/wirings/mcp/mcp.types.ts +3 -11
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/rpc/addon-runner.ts +233 -0
- package/src/wirings/rpc/index.ts +17 -1
- package/src/wirings/rpc/remote-addon-auth.ts +69 -0
- package/src/wirings/rpc/rpc-runner.test.ts +319 -16
- package/src/wirings/rpc/rpc-runner.ts +155 -3
- package/src/wirings/rpc/rpc-types.ts +16 -5
- package/src/wirings/rpc/wire-addon.test.ts +6 -3
- package/src/wirings/rpc/wire-addon.ts +9 -0
- package/src/wirings/rpc/wire-remote-addon.ts +57 -0
- package/src/wirings/scope/index.ts +14 -0
- package/src/wirings/scope/scope.test.ts +135 -0
- package/src/wirings/scope/scope.types.ts +44 -0
- package/src/wirings/scope/validate-scope-definitions.ts +110 -0
- package/src/wirings/scope/wire-scope.ts +34 -0
- package/src/wirings/secret/secret.types.ts +14 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/variable/validate-variable-definitions.ts +2 -0
- package/src/wirings/variable/variable.types.ts +14 -0
- package/src/wirings/workflow/dsl/index.ts +4 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
- package/src/wirings/workflow/graph/graph-node.ts +2 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/wirings/workflow/index.ts +5 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
- package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
- package/src/wirings/workflow/workflow.types.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
- package/src/wirings/oauth2/oauth2-client.ts +0 -331
- package/src/wirings/oauth2/oauth2-routes.ts +0 -234
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
/**
|
|
4
6
|
* Node.js filesystem implementation of MetaService.
|
|
@@ -54,6 +56,54 @@ export class LocalMetaService {
|
|
|
54
56
|
return null;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
59
|
+
// Resolve an installed addon package's root dir from the app's node_modules.
|
|
60
|
+
// The addon packages have no main `exports` entry, so `require.resolve(pkg)`
|
|
61
|
+
// throws; instead we walk the node_modules search paths (anchored at the app
|
|
62
|
+
// root, one level above `.pikku`) and pick the first that contains the package.
|
|
63
|
+
packageRootCache = new Map();
|
|
64
|
+
resolvePackageRoot(packageName) {
|
|
65
|
+
const cached = this.packageRootCache.get(packageName);
|
|
66
|
+
if (cached !== undefined)
|
|
67
|
+
return cached;
|
|
68
|
+
let root = null;
|
|
69
|
+
try {
|
|
70
|
+
const require = createRequire(join(this.basePath, '..', 'index.js'));
|
|
71
|
+
for (const dir of require.resolve.paths(packageName) ?? []) {
|
|
72
|
+
const candidate = join(dir, packageName);
|
|
73
|
+
if (existsSync(join(candidate, 'package.json'))) {
|
|
74
|
+
root = candidate;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
root = null;
|
|
81
|
+
}
|
|
82
|
+
this.packageRootCache.set(packageName, root);
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
async readPackageFile(packageName, relativePath) {
|
|
86
|
+
const root = this.resolvePackageRoot(packageName);
|
|
87
|
+
if (!root)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
return await readFile(join(root, '.pikku', relativePath), 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async readPackageDir(packageName, relativePath) {
|
|
97
|
+
const root = this.resolvePackageRoot(packageName);
|
|
98
|
+
if (!root)
|
|
99
|
+
return [];
|
|
100
|
+
try {
|
|
101
|
+
return await readdir(join(root, '.pikku', relativePath));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
57
107
|
clearCache() {
|
|
58
108
|
this.httpMetaCache = null;
|
|
59
109
|
this.channelsMetaCache = null;
|
|
@@ -246,7 +296,7 @@ export class LocalMetaService {
|
|
|
246
296
|
const content = await this.readMetaJson('permissions', 'pikku-permissions-groups-meta');
|
|
247
297
|
this.permissionsGroupsMetaCache = content
|
|
248
298
|
? JSON.parse(content)
|
|
249
|
-
: { definitions: {}
|
|
299
|
+
: { definitions: {} };
|
|
250
300
|
return this.permissionsGroupsMetaCache;
|
|
251
301
|
}
|
|
252
302
|
async getAgentsMeta() {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JobOptions, QueueService } from '../wirings/queue/queue.types.js';
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
import { type SendWebhookInput, type SendWebhookResult, type WebhookJobData, WebhookService } from './webhook-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default {@link WebhookService} implementation: serializes and signs the
|
|
6
|
+
* payload, then enqueues a delivery job onto the `pikku-outgoing-webhooks`
|
|
7
|
+
* queue. The signature is computed at enqueue time so the signing key never
|
|
8
|
+
* enters the queue payload; the actual HTTP POST happens in
|
|
9
|
+
* {@link pikkuWebhookWorkerFunc}, and the queue's `attempts`/`backoff`
|
|
10
|
+
* options drive retries.
|
|
11
|
+
*/
|
|
12
|
+
export declare class QueueWebhookService extends WebhookService {
|
|
13
|
+
protected queueService: QueueService;
|
|
14
|
+
/**
|
|
15
|
+
* The queue is a constructor dependency rather than a `getSingletonServices()`
|
|
16
|
+
* lookup so that a project wiring up outgoing webhooks without a queue fails
|
|
17
|
+
* to compile, instead of throwing on the first `send()`.
|
|
18
|
+
*/
|
|
19
|
+
constructor(queueService: QueueService);
|
|
20
|
+
send(input: SendWebhookInput): Promise<SendWebhookResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Build the signed job payload and its retry options — the shared work a
|
|
23
|
+
* `send()` does before enqueueing. Factored out (and `protected`) so a
|
|
24
|
+
* store-backed subclass can persist a delivery row and attach its
|
|
25
|
+
* `deliveryId` without re-implementing signing or the retry policy.
|
|
26
|
+
*/
|
|
27
|
+
protected prepareDelivery(input: SendWebhookInput): Promise<{
|
|
28
|
+
jobData: WebhookJobData;
|
|
29
|
+
options: JobOptions;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a delivery's retry policy into queue job options, mirroring the
|
|
33
|
+
* workflow service: per-call values win over `config.webhook` defaults, an
|
|
34
|
+
* explicitly-set `retries` (including 0) is always honored, and `attempts`
|
|
35
|
+
* is ALWAYS passed so the queue can never fall back to its own default.
|
|
36
|
+
* Backoff is exponential unless a concrete `retryDelay` selects a fixed one.
|
|
37
|
+
*/
|
|
38
|
+
private resolveJobOptions;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Queue worker for the `pikku-outgoing-webhooks` queue: POSTs the delivery to its
|
|
42
|
+
* target URL. Any non-2xx response (or network error) throws so the queue
|
|
43
|
+
* retries the job according to the `attempts`/`backoff` set at enqueue time;
|
|
44
|
+
* the queue runner logs each failed attempt.
|
|
45
|
+
*
|
|
46
|
+
* A `deliveryId` is only present when a store-backed `webhookService` (e.g.
|
|
47
|
+
* `KyselyWebhookService`) enqueued the job, so each attempt (success or failure)
|
|
48
|
+
* is persisted via `webhookService.recordAttempt` before the throw — the console
|
|
49
|
+
* delivery history reflects every try, not just the outcome. The queue-only
|
|
50
|
+
* default never sets a `deliveryId`, so its base `recordAttempt` is never hit.
|
|
51
|
+
*/
|
|
52
|
+
export declare function pikkuWebhookWorkerFunc(services: {
|
|
53
|
+
logger: Logger;
|
|
54
|
+
webhookService?: WebhookService;
|
|
55
|
+
}, { url, body, headers, deliveryId }: WebhookJobData): Promise<void>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getSingletonServices } from '../pikku-state.js';
|
|
2
|
+
import { getDurationInMilliseconds } from '../time-utils.js';
|
|
3
|
+
import { safeFetch } from '../utils/safe-fetch.js';
|
|
4
|
+
import { DEFAULT_WEBHOOK_RETRIES, DEFAULT_WEBHOOK_SIGNATURE_HEADER, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, } from './webhook-service.js';
|
|
5
|
+
/** Cap on the response body captured on a failed attempt. */
|
|
6
|
+
const MAX_CAPTURED_RESPONSE_BODY = 2_000;
|
|
7
|
+
/**
|
|
8
|
+
* Default {@link WebhookService} implementation: serializes and signs the
|
|
9
|
+
* payload, then enqueues a delivery job onto the `pikku-outgoing-webhooks`
|
|
10
|
+
* queue. The signature is computed at enqueue time so the signing key never
|
|
11
|
+
* enters the queue payload; the actual HTTP POST happens in
|
|
12
|
+
* {@link pikkuWebhookWorkerFunc}, and the queue's `attempts`/`backoff`
|
|
13
|
+
* options drive retries.
|
|
14
|
+
*/
|
|
15
|
+
export class QueueWebhookService extends WebhookService {
|
|
16
|
+
queueService;
|
|
17
|
+
/**
|
|
18
|
+
* The queue is a constructor dependency rather than a `getSingletonServices()`
|
|
19
|
+
* lookup so that a project wiring up outgoing webhooks without a queue fails
|
|
20
|
+
* to compile, instead of throwing on the first `send()`.
|
|
21
|
+
*/
|
|
22
|
+
constructor(queueService) {
|
|
23
|
+
super();
|
|
24
|
+
this.queueService = queueService;
|
|
25
|
+
}
|
|
26
|
+
async send(input) {
|
|
27
|
+
const { jobData, options } = await this.prepareDelivery(input);
|
|
28
|
+
const jobId = await this.queueService.add(PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, jobData, options);
|
|
29
|
+
return { jobId };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the signed job payload and its retry options — the shared work a
|
|
33
|
+
* `send()` does before enqueueing. Factored out (and `protected`) so a
|
|
34
|
+
* store-backed subclass can persist a delivery row and attach its
|
|
35
|
+
* `deliveryId` without re-implementing signing or the retry policy.
|
|
36
|
+
*/
|
|
37
|
+
async prepareDelivery(input) {
|
|
38
|
+
const services = getSingletonServices();
|
|
39
|
+
const webhookConfig = services.config?.webhook;
|
|
40
|
+
const body = JSON.stringify(input.data);
|
|
41
|
+
const headers = {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
...(input.event ? { 'X-Pikku-Event': input.event } : {}),
|
|
44
|
+
...input.headers,
|
|
45
|
+
};
|
|
46
|
+
let secret = input.secret;
|
|
47
|
+
if (secret === undefined && webhookConfig?.secret) {
|
|
48
|
+
// Naive read — any caching is the secret service's concern, not ours.
|
|
49
|
+
secret = await services.secrets.getSecret(webhookConfig.secret);
|
|
50
|
+
if (!secret) {
|
|
51
|
+
services.logger.error(`Webhook signing secret '${webhookConfig.secret}' (config.webhook.secret) resolved to nothing — outgoing webhooks will be sent UNSIGNED.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (secret) {
|
|
55
|
+
const signatureHeader = webhookConfig?.signatureHeader ?? DEFAULT_WEBHOOK_SIGNATURE_HEADER;
|
|
56
|
+
headers[signatureHeader] = this.sign(secret, body);
|
|
57
|
+
}
|
|
58
|
+
const jobData = {
|
|
59
|
+
url: input.url,
|
|
60
|
+
...(input.event ? { event: input.event } : {}),
|
|
61
|
+
body,
|
|
62
|
+
headers,
|
|
63
|
+
};
|
|
64
|
+
return { jobData, options: this.resolveJobOptions(input) };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a delivery's retry policy into queue job options, mirroring the
|
|
68
|
+
* workflow service: per-call values win over `config.webhook` defaults, an
|
|
69
|
+
* explicitly-set `retries` (including 0) is always honored, and `attempts`
|
|
70
|
+
* is ALWAYS passed so the queue can never fall back to its own default.
|
|
71
|
+
* Backoff is exponential unless a concrete `retryDelay` selects a fixed one.
|
|
72
|
+
*/
|
|
73
|
+
resolveJobOptions(input) {
|
|
74
|
+
const webhookConfig = getSingletonServices().config?.webhook;
|
|
75
|
+
const retries = input.retries ?? webhookConfig?.retries ?? DEFAULT_WEBHOOK_RETRIES;
|
|
76
|
+
const retryDelay = input.retryDelay ?? webhookConfig?.retryDelay;
|
|
77
|
+
const backoff = retryDelay !== undefined && retryDelay !== 'exponential'
|
|
78
|
+
? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
|
|
79
|
+
: retries > 0 || retryDelay === 'exponential'
|
|
80
|
+
? 'exponential'
|
|
81
|
+
: undefined;
|
|
82
|
+
return { attempts: retries + 1, ...(backoff ? { backoff } : {}) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Queue worker for the `pikku-outgoing-webhooks` queue: POSTs the delivery to its
|
|
87
|
+
* target URL. Any non-2xx response (or network error) throws so the queue
|
|
88
|
+
* retries the job according to the `attempts`/`backoff` set at enqueue time;
|
|
89
|
+
* the queue runner logs each failed attempt.
|
|
90
|
+
*
|
|
91
|
+
* A `deliveryId` is only present when a store-backed `webhookService` (e.g.
|
|
92
|
+
* `KyselyWebhookService`) enqueued the job, so each attempt (success or failure)
|
|
93
|
+
* is persisted via `webhookService.recordAttempt` before the throw — the console
|
|
94
|
+
* delivery history reflects every try, not just the outcome. The queue-only
|
|
95
|
+
* default never sets a `deliveryId`, so its base `recordAttempt` is never hit.
|
|
96
|
+
*/
|
|
97
|
+
export async function pikkuWebhookWorkerFunc(services, { url, body, headers, deliveryId }) {
|
|
98
|
+
let statusCode;
|
|
99
|
+
let responseBody;
|
|
100
|
+
let error;
|
|
101
|
+
let delivered = false;
|
|
102
|
+
let allowedHosts;
|
|
103
|
+
try {
|
|
104
|
+
allowedHosts = getSingletonServices().config?.webhook?.allowedHosts;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Singleton services not initialised (e.g. a bare worker invocation) — fall
|
|
108
|
+
// back to the default private-host block with no allowlist.
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const response = await safeFetch(url, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers,
|
|
114
|
+
body,
|
|
115
|
+
signal: AbortSignal.timeout(30_000),
|
|
116
|
+
}, { allowedHosts });
|
|
117
|
+
statusCode = response.status;
|
|
118
|
+
delivered = response.status >= 200 && response.status < 300;
|
|
119
|
+
if (!delivered) {
|
|
120
|
+
responseBody = (await response.text().catch(() => ''))?.slice(0, MAX_CAPTURED_RESPONSE_BODY);
|
|
121
|
+
error = `Webhook delivery to ${url} failed with status ${response.status}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
error = e instanceof Error ? e.message : String(e);
|
|
126
|
+
}
|
|
127
|
+
if (deliveryId && services.webhookService) {
|
|
128
|
+
// Best-effort history: a store failure must not mask the delivery result.
|
|
129
|
+
await services.webhookService
|
|
130
|
+
.recordAttempt(deliveryId, { statusCode, responseBody, error, delivered })
|
|
131
|
+
.catch((storeError) => services.logger.error(`Failed to record webhook delivery attempt for ${deliveryId}`, storeError));
|
|
132
|
+
}
|
|
133
|
+
if (!delivered) {
|
|
134
|
+
throw new Error(error ?? `Webhook delivery to ${url} failed`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FlatScope } from '../wirings/scope/scope.types.js';
|
|
2
|
+
/** A role: a named, admin-composed bag of scopes. */
|
|
3
|
+
export interface Role {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
scopes: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolves and administers the scopes granted to a user.
|
|
10
|
+
*
|
|
11
|
+
* Implementations are called at the **session boundary** — e.g. better-auth's
|
|
12
|
+
* `mapSession` — never by the function runner. The runner reads
|
|
13
|
+
* `session.scopes` and never performs I/O, which keeps it viable on Workers
|
|
14
|
+
* and Lambda. Because the session is rebuilt per request, a scope change takes
|
|
15
|
+
* effect on the next call with nothing cached and nothing to invalidate.
|
|
16
|
+
*
|
|
17
|
+
* `syncScopes` is deliberately additive: scopes are declared in code, so a
|
|
18
|
+
* removed declaration leaves an inert row rather than silently revoking a
|
|
19
|
+
* grant mid-deploy. Removal is an explicit operation (`pikku scopes prune`).
|
|
20
|
+
*/
|
|
21
|
+
export interface ScopeService {
|
|
22
|
+
/**
|
|
23
|
+
* Registers the declared scope set. Additive — never deletes.
|
|
24
|
+
* Called once during startup with the generated scope list.
|
|
25
|
+
*/
|
|
26
|
+
syncScopes(scopes: FlatScope[]): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Every scope a user holds: the union of their role-derived scopes and any
|
|
29
|
+
* scopes granted to them directly.
|
|
30
|
+
*/
|
|
31
|
+
resolveScopes(userId: string): Promise<string[]>;
|
|
32
|
+
/**
|
|
33
|
+
* The scope vocabulary in the store: everything a role can be composed from.
|
|
34
|
+
*
|
|
35
|
+
* `declared: false` marks a scope that is still present but no longer
|
|
36
|
+
* declared in code — inert (no function can require it) and awaiting
|
|
37
|
+
* `pikku scopes prune`.
|
|
38
|
+
*/
|
|
39
|
+
listScopes(): Promise<Array<FlatScope & {
|
|
40
|
+
declared: boolean;
|
|
41
|
+
}>>;
|
|
42
|
+
createRole(role: Role): Promise<void>;
|
|
43
|
+
deleteRole(name: string): Promise<void>;
|
|
44
|
+
setRoleScopes(name: string, scopes: string[]): Promise<void>;
|
|
45
|
+
listRoles(): Promise<Role[]>;
|
|
46
|
+
addUserToRole(userId: string, role: string, grantedBy?: string): Promise<void>;
|
|
47
|
+
removeUserFromRole(userId: string, role: string): Promise<void>;
|
|
48
|
+
listUserRoles(userId: string): Promise<string[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Grants a scope directly to a user, outside of any role. Additive: the
|
|
51
|
+
* resolved set is the union of role-derived and directly-granted scopes.
|
|
52
|
+
*/
|
|
53
|
+
addScopeToUser(userId: string, scope: string, grantedBy?: string): Promise<void>;
|
|
54
|
+
removeScopeFromUser(userId: string, scope: string): Promise<void>;
|
|
55
|
+
/** Only the scopes granted directly, not those inherited from roles. */
|
|
56
|
+
listUserScopes(userId: string): Promise<string[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Scopes present in the store that are no longer declared in code, with the
|
|
59
|
+
* roles that would lose them. Powers `pikku scopes audit`.
|
|
60
|
+
*/
|
|
61
|
+
findStaleScopes(): Promise<Array<{
|
|
62
|
+
scope: string;
|
|
63
|
+
roles: string[];
|
|
64
|
+
}>>;
|
|
65
|
+
/** Removes undeclared scopes, cascading them out of roles. */
|
|
66
|
+
pruneScopes(): Promise<string[]>;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,6 +18,14 @@ export type CredentialMeta = {
|
|
|
18
18
|
export declare class TypedSecretService<TMap = Record<string, unknown>> implements SecretService {
|
|
19
19
|
private secrets;
|
|
20
20
|
private credentialsMeta;
|
|
21
|
+
/**
|
|
22
|
+
* In-process cache of resolved secrets, so callers can read naively without
|
|
23
|
+
* re-hitting the underlying service on every call. Only successful reads are
|
|
24
|
+
* cached (a miss throws and isn't stored); `setSecret`/`deleteSecret`
|
|
25
|
+
* invalidate the key. No TTL — a secret rotated out-of-band won't refresh
|
|
26
|
+
* until restart (see pikkujs/pikku#964).
|
|
27
|
+
*/
|
|
28
|
+
private cache;
|
|
21
29
|
constructor(secrets: SecretService, credentialsMeta: Record<string, CredentialMeta>);
|
|
22
30
|
getSecret<K extends keyof TMap & string>(key: K): Promise<TMap[K]>;
|
|
23
31
|
getSecret<T = string>(key: string): Promise<T>;
|
|
@@ -1,24 +1,59 @@
|
|
|
1
1
|
export class TypedSecretService {
|
|
2
2
|
secrets;
|
|
3
3
|
credentialsMeta;
|
|
4
|
+
/**
|
|
5
|
+
* In-process cache of resolved secrets, so callers can read naively without
|
|
6
|
+
* re-hitting the underlying service on every call. Only successful reads are
|
|
7
|
+
* cached (a miss throws and isn't stored); `setSecret`/`deleteSecret`
|
|
8
|
+
* invalidate the key. No TTL — a secret rotated out-of-band won't refresh
|
|
9
|
+
* until restart (see pikkujs/pikku#964).
|
|
10
|
+
*/
|
|
11
|
+
cache = new Map();
|
|
4
12
|
constructor(secrets, credentialsMeta) {
|
|
5
13
|
this.secrets = secrets;
|
|
6
14
|
this.credentialsMeta = credentialsMeta;
|
|
7
15
|
}
|
|
8
16
|
async getSecret(key) {
|
|
9
|
-
|
|
17
|
+
if (this.cache.has(key)) {
|
|
18
|
+
return this.cache.get(key);
|
|
19
|
+
}
|
|
20
|
+
const value = await this.secrets.getSecret(key);
|
|
21
|
+
this.cache.set(key, value);
|
|
22
|
+
return value;
|
|
10
23
|
}
|
|
11
24
|
async hasSecret(key) {
|
|
25
|
+
if (this.cache.has(key)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
12
28
|
return this.secrets.hasSecret(key);
|
|
13
29
|
}
|
|
14
30
|
async setSecret(key, value) {
|
|
15
|
-
|
|
31
|
+
await this.secrets.setSecret(key, value);
|
|
32
|
+
this.cache.delete(key);
|
|
16
33
|
}
|
|
17
34
|
async deleteSecret(key) {
|
|
18
|
-
|
|
35
|
+
await this.secrets.deleteSecret(key);
|
|
36
|
+
this.cache.delete(key);
|
|
19
37
|
}
|
|
20
38
|
async getSecrets(keys) {
|
|
21
|
-
|
|
39
|
+
const result = {};
|
|
40
|
+
const missing = [];
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
if (this.cache.has(key)) {
|
|
43
|
+
result[key] = this.cache.get(key);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
missing.push(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (missing.length > 0) {
|
|
50
|
+
const fetched = await this.secrets.getSecrets(missing);
|
|
51
|
+
for (const [key, value] of Object.entries(fetched)) {
|
|
52
|
+
this.cache.set(key, value);
|
|
53
|
+
result[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
22
57
|
}
|
|
23
58
|
async getAllStatus() {
|
|
24
59
|
const results = [];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export interface SendWebhookInput {
|
|
2
|
+
/** The endpoint the webhook is delivered to via HTTP POST. */
|
|
3
|
+
url: string;
|
|
4
|
+
/** Optional event name, sent as the `X-Pikku-Event` header. */
|
|
5
|
+
event?: string;
|
|
6
|
+
/** The payload, JSON-serialized into the request body. */
|
|
7
|
+
data: unknown;
|
|
8
|
+
/** Extra headers merged over the defaults. */
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Raw HMAC signing key for this delivery. Overrides the config-level
|
|
12
|
+
* secret. When present, the body is signed into the signature header.
|
|
13
|
+
*/
|
|
14
|
+
secret?: string;
|
|
15
|
+
/** Retry count for failed deliveries. Overrides `config.webhook.retries`. */
|
|
16
|
+
retries?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Delay between retries (`30000` or `'30s'`). A concrete value selects a
|
|
19
|
+
* fixed backoff; when omitted, backoff is exponential.
|
|
20
|
+
*/
|
|
21
|
+
retryDelay?: string | number;
|
|
22
|
+
/**
|
|
23
|
+
* Organization this delivery belongs to. Persisted by a store-backed
|
|
24
|
+
* implementation (e.g. `KyselyWebhookService`) so deliveries can be
|
|
25
|
+
* queried per org; ignored by the queue-only default.
|
|
26
|
+
*/
|
|
27
|
+
organizationId?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface SendWebhookResult {
|
|
30
|
+
jobId: string;
|
|
31
|
+
}
|
|
32
|
+
export interface WebhookServiceConfig {
|
|
33
|
+
/** Default retry count for failed deliveries. */
|
|
34
|
+
retries?: number;
|
|
35
|
+
/** Default delay between retries; omitted means exponential backoff. */
|
|
36
|
+
retryDelay?: string | number;
|
|
37
|
+
/**
|
|
38
|
+
* Name of a secret (resolved through the secret service) used as the
|
|
39
|
+
* default HMAC signing key for all outgoing webhooks.
|
|
40
|
+
*/
|
|
41
|
+
secret?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Header the body signature is sent in. Defaults to
|
|
44
|
+
* {@link DEFAULT_WEBHOOK_SIGNATURE_HEADER}.
|
|
45
|
+
*/
|
|
46
|
+
signatureHeader?: string;
|
|
47
|
+
/**
|
|
48
|
+
* SSRF allowlist of hostnames outgoing webhooks may be delivered to. When set,
|
|
49
|
+
* a delivery target must be on this list. When omitted, obvious private and
|
|
50
|
+
* internal hosts (loopback, private ranges, cloud metadata) are blocked and
|
|
51
|
+
* all other public hosts are permitted.
|
|
52
|
+
*/
|
|
53
|
+
allowedHosts?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface WebhookJobData {
|
|
56
|
+
url: string;
|
|
57
|
+
event?: string;
|
|
58
|
+
body: string;
|
|
59
|
+
headers: Record<string, string>;
|
|
60
|
+
/**
|
|
61
|
+
* Delivery-record id, present when a store-backed implementation enqueued
|
|
62
|
+
* the job. The worker keys its per-attempt records off this (it doubles as
|
|
63
|
+
* the queue `jobId`), so it's stable across retries.
|
|
64
|
+
*/
|
|
65
|
+
deliveryId?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Outcome of one delivery attempt, recorded against a delivery. */
|
|
68
|
+
export interface WebhookAttemptResult {
|
|
69
|
+
/** HTTP status of the response, absent if the request never completed. */
|
|
70
|
+
statusCode?: number;
|
|
71
|
+
/** Response body (truncated) — captured on failure for debugging. */
|
|
72
|
+
responseBody?: string;
|
|
73
|
+
/** Network/timeout error message, when the request threw. */
|
|
74
|
+
error?: string;
|
|
75
|
+
/** Whether this attempt was a 2xx success. */
|
|
76
|
+
delivered: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** A persisted webhook delivery, as surfaced to the console. */
|
|
79
|
+
export interface WebhookDeliveryRecord {
|
|
80
|
+
deliveryId: string;
|
|
81
|
+
organizationId: string | null;
|
|
82
|
+
url: string;
|
|
83
|
+
event: string | null;
|
|
84
|
+
status: 'pending' | 'delivered' | 'failed';
|
|
85
|
+
attempts: number;
|
|
86
|
+
createdAt: Date;
|
|
87
|
+
updatedAt: Date;
|
|
88
|
+
deliveredAt: Date | null;
|
|
89
|
+
}
|
|
90
|
+
/** A single persisted delivery attempt. */
|
|
91
|
+
export interface WebhookAttemptRecord {
|
|
92
|
+
attemptId: string;
|
|
93
|
+
deliveryId: string;
|
|
94
|
+
attemptNumber: number;
|
|
95
|
+
statusCode: number | null;
|
|
96
|
+
responseBody: string | null;
|
|
97
|
+
error: string | null;
|
|
98
|
+
createdAt: Date;
|
|
99
|
+
}
|
|
100
|
+
/** A delivery with its full attempt history. */
|
|
101
|
+
export interface WebhookDeliveryWithAttempts {
|
|
102
|
+
delivery: WebhookDeliveryRecord;
|
|
103
|
+
attempts: WebhookAttemptRecord[];
|
|
104
|
+
}
|
|
105
|
+
export declare const PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME = "pikku-outgoing-webhooks";
|
|
106
|
+
export declare const DEFAULT_WEBHOOK_SIGNATURE_HEADER = "X-Pikku-Signature";
|
|
107
|
+
/** Fallback when neither the per-call nor `config.webhook.retries` is set. */
|
|
108
|
+
export declare const DEFAULT_WEBHOOK_RETRIES = 3;
|
|
109
|
+
/**
|
|
110
|
+
* Outgoing webhook delivery. {@link QueueWebhookService} is the default
|
|
111
|
+
* implementation; apps can extend this to deliver directly, or through a
|
|
112
|
+
* provider such as Svix.
|
|
113
|
+
*/
|
|
114
|
+
export declare abstract class WebhookService {
|
|
115
|
+
abstract send(input: SendWebhookInput): Promise<SendWebhookResult>;
|
|
116
|
+
/**
|
|
117
|
+
* Sign a body with HMAC-SHA256, producing the signature header value
|
|
118
|
+
* (e.g. `sha256=abc123...`).
|
|
119
|
+
*/
|
|
120
|
+
protected sign(secret: string, body: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Verify a signature produced by {@link WebhookService.sign} — for
|
|
123
|
+
* receivers, which share the signing scheme.
|
|
124
|
+
*/
|
|
125
|
+
verify(secret: string, signature: string, body: string): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Record a delivery attempt and roll the delivery's status/attempt-count
|
|
128
|
+
* forward, keyed by the delivery id carried in {@link WebhookJobData}. The
|
|
129
|
+
* default queue-only service keeps no history, so this throws — a store-backed
|
|
130
|
+
* implementation (e.g. `KyselyWebhookService` in `@pikku/kysely`) overrides it.
|
|
131
|
+
*/
|
|
132
|
+
recordAttempt(_deliveryId: string, _result: WebhookAttemptResult): Promise<void>;
|
|
133
|
+
/** List deliveries, most recent first, optionally scoped to an org. */
|
|
134
|
+
listDeliveries(_opts?: {
|
|
135
|
+
organizationId?: string;
|
|
136
|
+
limit?: number;
|
|
137
|
+
}): Promise<WebhookDeliveryRecord[]>;
|
|
138
|
+
/** A single delivery with its attempt history, or null if unknown. */
|
|
139
|
+
getDelivery(_deliveryId: string): Promise<WebhookDeliveryWithAttempts | null>;
|
|
140
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NotImplementedError } from '../errors/errors.js';
|
|
2
|
+
import { hmacSha256Hex, timingSafeStringEqual } from '../utils/hmac.js';
|
|
3
|
+
export const PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME = 'pikku-outgoing-webhooks';
|
|
4
|
+
export const DEFAULT_WEBHOOK_SIGNATURE_HEADER = 'X-Pikku-Signature';
|
|
5
|
+
/** Fallback when neither the per-call nor `config.webhook.retries` is set. */
|
|
6
|
+
export const DEFAULT_WEBHOOK_RETRIES = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Outgoing webhook delivery. {@link QueueWebhookService} is the default
|
|
9
|
+
* implementation; apps can extend this to deliver directly, or through a
|
|
10
|
+
* provider such as Svix.
|
|
11
|
+
*/
|
|
12
|
+
export class WebhookService {
|
|
13
|
+
/**
|
|
14
|
+
* Sign a body with HMAC-SHA256, producing the signature header value
|
|
15
|
+
* (e.g. `sha256=abc123...`).
|
|
16
|
+
*/
|
|
17
|
+
sign(secret, body) {
|
|
18
|
+
return `sha256=${hmacSha256Hex(secret, body)}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Verify a signature produced by {@link WebhookService.sign} — for
|
|
22
|
+
* receivers, which share the signing scheme.
|
|
23
|
+
*/
|
|
24
|
+
verify(secret, signature, body) {
|
|
25
|
+
return timingSafeStringEqual(this.sign(secret, body), signature);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Record a delivery attempt and roll the delivery's status/attempt-count
|
|
29
|
+
* forward, keyed by the delivery id carried in {@link WebhookJobData}. The
|
|
30
|
+
* default queue-only service keeps no history, so this throws — a store-backed
|
|
31
|
+
* implementation (e.g. `KyselyWebhookService` in `@pikku/kysely`) overrides it.
|
|
32
|
+
*/
|
|
33
|
+
recordAttempt(_deliveryId, _result) {
|
|
34
|
+
throw new NotImplementedError('webhook delivery persistence is not configured');
|
|
35
|
+
}
|
|
36
|
+
/** List deliveries, most recent first, optionally scoped to an org. */
|
|
37
|
+
listDeliveries(_opts) {
|
|
38
|
+
throw new NotImplementedError('webhook delivery persistence is not configured');
|
|
39
|
+
}
|
|
40
|
+
/** A single delivery with its attempt history, or null if unknown. */
|
|
41
|
+
getDelivery(_deliveryId) {
|
|
42
|
+
throw new NotImplementedError('webhook delivery persistence is not configured');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -22,6 +22,13 @@ export interface WorkflowService {
|
|
|
22
22
|
withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
23
23
|
close(): Promise<void>;
|
|
24
24
|
resumeWorkflow(runId: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Record a human decision against a `workflow.approval()` gate and wake the
|
|
27
|
+
* run. The payload is stored as given and validated on replay, inside the
|
|
28
|
+
* workflow body — so an invalid one re-closes the gate rather than failing the
|
|
29
|
+
* run. `reason` addresses the gate's first reach.
|
|
30
|
+
*/
|
|
31
|
+
approveStep(runId: string, reason: string, decision: unknown): Promise<void>;
|
|
25
32
|
startWorkflow<I>(name: string, input: I, wire: WorkflowRunWire, rpcService: any, options?: {
|
|
26
33
|
inline?: boolean;
|
|
27
34
|
startNode?: string;
|
|
@@ -742,6 +742,43 @@ export function defineServiceTests(config) {
|
|
|
742
742
|
const threads = await agentService.listThreads();
|
|
743
743
|
assert.ok(Array.isArray(threads));
|
|
744
744
|
});
|
|
745
|
+
if (services.aiStorageService) {
|
|
746
|
+
const storageFactory = services.aiStorageService;
|
|
747
|
+
// The `owners` constraint is what keeps the generated thread-management
|
|
748
|
+
// functions from leaking across tenants: a caller may only list threads
|
|
749
|
+
// owned by one of their session principals, matching the
|
|
750
|
+
// `principal:sub-partition` composition resolveOwnerResourceId writes.
|
|
751
|
+
test('listThreads scopes to the given owners, including sub-partitions', async () => {
|
|
752
|
+
const storage = await storageFactory();
|
|
753
|
+
await storage.createThread('owner-alice');
|
|
754
|
+
await storage.createThread('owner-alice:project-1');
|
|
755
|
+
await storage.createThread('owner-bob:secret');
|
|
756
|
+
const threads = await agentService.listThreads({
|
|
757
|
+
owners: ['owner-alice'],
|
|
758
|
+
});
|
|
759
|
+
const ids = threads.map((t) => t.resourceId);
|
|
760
|
+
assert.ok(ids.includes('owner-alice'));
|
|
761
|
+
assert.ok(ids.includes('owner-alice:project-1'));
|
|
762
|
+
assert.ok(!ids.some((id) => id.startsWith('owner-bob')), "another owner's threads must not be listed");
|
|
763
|
+
});
|
|
764
|
+
test('listThreads with an owner does not match a lookalike prefix', async () => {
|
|
765
|
+
const storage = await storageFactory();
|
|
766
|
+
await storage.createThread('owner-al');
|
|
767
|
+
await storage.createThread('owner-alice-evil:p');
|
|
768
|
+
const threads = await agentService.listThreads({
|
|
769
|
+
owners: ['owner-al'],
|
|
770
|
+
});
|
|
771
|
+
const ids = threads.map((t) => t.resourceId);
|
|
772
|
+
assert.ok(ids.includes('owner-al'));
|
|
773
|
+
assert.ok(!ids.includes('owner-alice-evil:p'));
|
|
774
|
+
});
|
|
775
|
+
test('listThreads with an empty owners list returns nothing', async () => {
|
|
776
|
+
const storage = await storageFactory();
|
|
777
|
+
await storage.createThread('owner-empty-check');
|
|
778
|
+
const threads = await agentService.listThreads({ owners: [] });
|
|
779
|
+
assert.deepEqual(threads, []);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
745
782
|
test('getThread returns null for missing', async () => {
|
|
746
783
|
const thread = await agentService.getThread('missing-thread');
|
|
747
784
|
assert.equal(thread, null);
|