@pikku/core 0.12.123 → 0.12.126
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 +26 -0
- package/dist/function/function-runner.js +7 -0
- package/dist/pikku-state.js +2 -0
- package/dist/services/file-scenario-run-store.js +13 -1
- package/dist/services/incoming-webhook-service.d.ts +48 -0
- package/dist/services/incoming-webhook-service.js +46 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/meta-service.d.ts +8 -0
- package/dist/services/meta-service.js +18 -0
- package/dist/types/core.types.d.ts +3 -0
- package/dist/types/state.types.d.ts +3 -0
- package/dist/wirings/addon/addon-runner.js +30 -0
- package/dist/wirings/addon/wire-addon.d.ts +2 -0
- package/dist/wirings/addon/wire-addon.js +6 -0
- package/dist/wirings/trigger/index.d.ts +4 -0
- package/dist/wirings/trigger/index.js +2 -0
- package/dist/wirings/trigger/webhook-source-runner.d.ts +45 -0
- package/dist/wirings/trigger/webhook-source-runner.js +221 -0
- package/dist/wirings/trigger/webhook-source.types.d.ts +106 -0
- package/dist/wirings/trigger/webhook-source.types.js +1 -0
- package/dist/wirings/webhook/define-outgoing-webhook.d.ts +28 -0
- package/dist/wirings/webhook/define-outgoing-webhook.js +1 -0
- package/dist/wirings/webhook/index.d.ts +2 -0
- package/dist/wirings/webhook/index.js +1 -0
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +1 -1
- package/dist/wirings/workflow/scenario-run.types.d.ts +45 -2
- package/dist/wirings/workflow/scenario-step.types.d.ts +3 -0
- package/package.json +2 -1
- package/src/public-surface.json +21 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
## 0.12.126
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 44da33f: A scenario run records which version of the suite it ran against — the commit, and which attempt against that commit it is
|
|
6
|
+
- 44da33f: The scenarios screen and the runs screen are one surface: the declared suite is the document and a run is a lens over it. A scenario is filed as `running` the moment it starts, so a console watching a run in progress can tell what is on screen now from what is still waiting, and the run snapshots each scenario's title, description and cast so the record reads as prose.
|
|
7
|
+
|
|
8
|
+
## 0.12.125
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 5e93f30: Add `defineOutgoingWebhook({ event, title, description?, payload })` in `@pikku/core/webhook`. The CLI collects every exported declaration into `.pikku/webhooks/pikku-outgoing-webhooks-meta.gen.json` and `pikku-outgoing-webhooks.gen.ts`, which exports `OutgoingWebhooksMap`, `TypedWebhookService` and `typedWebhookService(service)`: `send` checks `data` against the declared payload for a declared event and accepts any other event unchanged. `MetaService.getOutgoingWebhooksMeta()` and the console addon's `outgoingWebhooksMeta` serve the declarations.
|
|
13
|
+
- 5e93f30: Add `wireTriggerWebhookSource({ name, method?, route?, secret?, events, receive?, check?, setup?, teardown? })` in `#pikku/trigger`. Each source becomes a `POST /webhooks/<name>` route whose events are validated against their schemas and queued on `pikku-incoming-webhooks` through `IncomingWebhookService`; a generated worker runs the matching `wireTrigger({ name: '<source>:<event>' })` and the queue retries it on failure. `pikku webhooks status | setup | teardown --url --labelPrefix [--previous]` registers the routes with the provider and prints one JSON line per source.
|
|
14
|
+
|
|
15
|
+
`KyselyIncomingWebhookService` (with the `incoming-webhook` schema) records a receipt per event, drops a provider's redelivery of an event it already accepted, and keeps each dispatch's attempts and last error. `pikku dev` and `pikku serve` use it when a Kysely database is configured.
|
|
16
|
+
|
|
17
|
+
## 0.12.124
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 1394385: Carry the wired instance through a `ref('ns:fn')` an app writes. The wiring is
|
|
22
|
+
the app's, so the reference arrives with no instance and the function used to
|
|
23
|
+
run against the addon's declared secrets alone — the consuming app's
|
|
24
|
+
`secretOverrides` and grants silently did not apply, and the app's own global
|
|
25
|
+
middleware was denied secrets it owns.
|
|
26
|
+
|
|
1
27
|
## 0.12.123
|
|
2
28
|
|
|
3
29
|
### Patch Changes
|
|
@@ -98,6 +98,13 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
98
98
|
funcMeta = pikkuState(addonTarget.packageName, 'function', 'meta')[addonTarget.localName];
|
|
99
99
|
if (funcMeta) {
|
|
100
100
|
funcPackageName = addonTarget.packageName;
|
|
101
|
+
// A `ref('ns:fn')` the app wired arrives with no instance, because the
|
|
102
|
+
// wiring is the app's. Without adopting the one the namespace resolves
|
|
103
|
+
// to, the function runs with the addon's declared secrets alone — the
|
|
104
|
+
// consuming app's `secretOverrides` and grants silently do not apply,
|
|
105
|
+
// and the app's own global middleware, running inside that scope, is
|
|
106
|
+
// denied secrets it owns.
|
|
107
|
+
addonInstance = addonInstance ?? addonTarget.instance;
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
}
|
package/dist/pikku-state.js
CHANGED
|
@@ -15,6 +15,7 @@ export const scenarioRunSummary = (record) => ({
|
|
|
15
15
|
runId: record.runId,
|
|
16
16
|
environment: record.environment,
|
|
17
17
|
surface: record.surface,
|
|
18
|
+
...(record.version ? { version: record.version } : {}),
|
|
18
19
|
status: record.status,
|
|
19
20
|
startedAt: record.startedAt,
|
|
20
21
|
finishedAt: record.finishedAt,
|
|
@@ -82,7 +83,18 @@ export class FileScenarioRunStore {
|
|
|
82
83
|
if (!record) {
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
record.results.
|
|
86
|
+
const existing = record.results.findIndex((candidate) => candidate.name === result.name);
|
|
87
|
+
if (existing === -1) {
|
|
88
|
+
record.results.push(result);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
record.results[existing] = {
|
|
92
|
+
...(record.results[existing].artifacts
|
|
93
|
+
? { artifacts: record.results[existing].artifacts }
|
|
94
|
+
: {}),
|
|
95
|
+
...result,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
86
98
|
await this.write(record);
|
|
87
99
|
});
|
|
88
100
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { QueueService } from '../wirings/queue/queue.types.js';
|
|
2
|
+
import { type TriggerEvent, type WebhookRequest, type WebhookSourceJob } from '../wirings/trigger/webhook-source.types.js';
|
|
3
|
+
export type IncomingWebhookAttempt = {
|
|
4
|
+
trigger: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
};
|
|
7
|
+
export type IncomingWebhookReceiptRecord = {
|
|
8
|
+
receiptId: string;
|
|
9
|
+
source: string;
|
|
10
|
+
providerEventId: string | null;
|
|
11
|
+
event: string;
|
|
12
|
+
status: 'pending' | 'delivered' | 'failed';
|
|
13
|
+
attempts: number;
|
|
14
|
+
lastError: string | null;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
deliveredAt: Date | null;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Queues each event a webhook source received for the
|
|
20
|
+
* `pikku-incoming-webhooks` worker, so the provider is answered once the event
|
|
21
|
+
* is safe and our own processing is retried by the queue, not by the provider.
|
|
22
|
+
*
|
|
23
|
+
* The queue is a constructor argument, as in `QueueWebhookService`: a webhook
|
|
24
|
+
* source wired without one fails to compile rather than on its first delivery.
|
|
25
|
+
*/
|
|
26
|
+
export declare class IncomingWebhookService {
|
|
27
|
+
protected queueService: QueueService;
|
|
28
|
+
/** How often a failing trigger is retried before the event is given up on. */
|
|
29
|
+
protected retries: number;
|
|
30
|
+
constructor(queueService: QueueService,
|
|
31
|
+
/** How often a failing trigger is retried before the event is given up on. */
|
|
32
|
+
retries?: number);
|
|
33
|
+
/** Returns how many events were queued. A store-backed service drops ones it has already seen. */
|
|
34
|
+
accept({ source, events, }: {
|
|
35
|
+
source: string;
|
|
36
|
+
request: WebhookRequest;
|
|
37
|
+
events: TriggerEvent[];
|
|
38
|
+
}): Promise<number>;
|
|
39
|
+
/** The job id is the provider's event id under its event name, which de-duplicates on queues that honour job ids. */
|
|
40
|
+
protected enqueue(job: WebhookSourceJob): Promise<string>;
|
|
41
|
+
/** Keeps nothing by default; a store-backed service records each dispatch. */
|
|
42
|
+
recordAttempt(_receiptId: string, _attempt: IncomingWebhookAttempt): Promise<void>;
|
|
43
|
+
/** Most recent first. Empty when nothing is stored. */
|
|
44
|
+
listReceipts(_opts?: {
|
|
45
|
+
source?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
}): Promise<IncomingWebhookReceiptRecord[]>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DEFAULT_WEBHOOK_RETRIES } from './webhook-service.js';
|
|
2
|
+
import { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME, } from '../wirings/trigger/webhook-source.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Queues each event a webhook source received for the
|
|
5
|
+
* `pikku-incoming-webhooks` worker, so the provider is answered once the event
|
|
6
|
+
* is safe and our own processing is retried by the queue, not by the provider.
|
|
7
|
+
*
|
|
8
|
+
* The queue is a constructor argument, as in `QueueWebhookService`: a webhook
|
|
9
|
+
* source wired without one fails to compile rather than on its first delivery.
|
|
10
|
+
*/
|
|
11
|
+
export class IncomingWebhookService {
|
|
12
|
+
queueService;
|
|
13
|
+
retries;
|
|
14
|
+
constructor(queueService,
|
|
15
|
+
/** How often a failing trigger is retried before the event is given up on. */
|
|
16
|
+
retries = DEFAULT_WEBHOOK_RETRIES) {
|
|
17
|
+
this.queueService = queueService;
|
|
18
|
+
this.retries = retries;
|
|
19
|
+
}
|
|
20
|
+
/** Returns how many events were queued. A store-backed service drops ones it has already seen. */
|
|
21
|
+
async accept({ source, events, }) {
|
|
22
|
+
for (const event of events) {
|
|
23
|
+
await this.enqueue({ source, event });
|
|
24
|
+
}
|
|
25
|
+
return events.length;
|
|
26
|
+
}
|
|
27
|
+
/** The job id is the provider's event id under its event name, which de-duplicates on queues that honour job ids. */
|
|
28
|
+
async enqueue(job) {
|
|
29
|
+
const jobId = job.receiptId ??
|
|
30
|
+
(job.event.id
|
|
31
|
+
? `${job.source}:${job.event.name}:${job.event.id}`
|
|
32
|
+
: undefined);
|
|
33
|
+
// knowledge: decisions/internals/queue-jobs-always-carry-an-explicit-attempts-count.md
|
|
34
|
+
return this.queueService.add(PIKKU_INCOMING_WEBHOOK_QUEUE_NAME, job, {
|
|
35
|
+
attempts: this.retries + 1,
|
|
36
|
+
...(this.retries > 0 ? { backoff: 'exponential' } : {}),
|
|
37
|
+
...(jobId ? { jobId } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Keeps nothing by default; a store-backed service records each dispatch. */
|
|
41
|
+
async recordAttempt(_receiptId, _attempt) { }
|
|
42
|
+
/** Most recent first. Empty when nothing is stored. */
|
|
43
|
+
async listReceipts(_opts) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { LocalVariablesService } from './local-variables.js';
|
|
|
12
12
|
export { ConsoleLogger, JsonConsoleLogger } from './logger-console.js';
|
|
13
13
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
|
|
14
14
|
export { QueueWebhookService, pikkuWebhookWorkerFunc, } from './queue-webhook-service.js';
|
|
15
|
+
export { IncomingWebhookService, type IncomingWebhookAttempt, type IncomingWebhookReceiptRecord, } from './incoming-webhook-service.js';
|
|
15
16
|
export { REMOTE_JOBS_SECRET_HEADER, REMOTE_JOBS_SECRET_VARIABLE, REMOTE_QUEUE_JOB_PATH, REMOTE_SCHEDULER_JOB_PATH, pikkuRemoteQueueJobFunc, pikkuRemoteScheduledJobFunc, type RemoteQueueJobData, type RemoteScheduledJobData, } from './remote-jobs.js';
|
|
16
17
|
export { InMemoryQueueService } from './in-memory-queue-service.js';
|
|
17
18
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
package/dist/services/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { LocalVariablesService } from './local-variables.js';
|
|
|
12
12
|
export { ConsoleLogger, JsonConsoleLogger } from './logger-console.js';
|
|
13
13
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
|
|
14
14
|
export { QueueWebhookService, pikkuWebhookWorkerFunc, } from './queue-webhook-service.js';
|
|
15
|
+
export { IncomingWebhookService, } from './incoming-webhook-service.js';
|
|
15
16
|
export { REMOTE_JOBS_SECRET_HEADER, REMOTE_JOBS_SECRET_VARIABLE, REMOTE_QUEUE_JOB_PATH, REMOTE_SCHEDULER_JOB_PATH, pikkuRemoteQueueJobFunc, pikkuRemoteScheduledJobFunc, } from './remote-jobs.js';
|
|
16
17
|
export { InMemoryQueueService } from './in-memory-queue-service.js';
|
|
17
18
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
@@ -15,6 +15,8 @@ import type { TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.
|
|
|
15
15
|
import type { SecretDefinitionsMeta } from '../wirings/secret/secret.types.js';
|
|
16
16
|
import type { CredentialDefinitionsMeta } from '../wirings/credential/credential.types.js';
|
|
17
17
|
import type { VariableDefinitionsMeta } from '../wirings/variable/variable.types.js';
|
|
18
|
+
import type { OutgoingWebhooksMeta } from '../wirings/webhook/define-outgoing-webhook.js';
|
|
19
|
+
import type { WebhookSourcesMeta } from '../wirings/trigger/webhook-source.types.js';
|
|
18
20
|
import type { FunctionMeta, FunctionsMeta, PermissionMetadata } from '../function/function-meta.types.js';
|
|
19
21
|
import type { MiddlewareMetadata } from '../middleware/middleware.types.js';
|
|
20
22
|
import type { AgentsMeta } from '../wirings/agent/agent.types.js';
|
|
@@ -174,6 +176,8 @@ export interface MetaService {
|
|
|
174
176
|
getSecretsMeta(): Promise<SecretDefinitionsMeta>;
|
|
175
177
|
getCredentialsMeta(): Promise<CredentialDefinitionsMeta>;
|
|
176
178
|
getVariablesMeta(): Promise<VariableDefinitionsMeta>;
|
|
179
|
+
getOutgoingWebhooksMeta(): Promise<OutgoingWebhooksMeta>;
|
|
180
|
+
getWebhookSourcesMeta(): Promise<WebhookSourcesMeta>;
|
|
177
181
|
getEmailMeta(): Promise<EmailsMeta>;
|
|
178
182
|
getEmailTemplateAssets(templateName: string, locale: string): Promise<EmailTemplateAssets>;
|
|
179
183
|
getServicesMeta(): Promise<ServicesMetaRecord>;
|
|
@@ -184,6 +188,7 @@ export interface MetaService {
|
|
|
184
188
|
export declare class LocalMetaService implements MetaService {
|
|
185
189
|
readonly basePath: string;
|
|
186
190
|
private httpMetaCache;
|
|
191
|
+
private webhookSourcesMetaCache;
|
|
187
192
|
private channelsMetaCache;
|
|
188
193
|
private schedulerMetaCache;
|
|
189
194
|
private queueMetaCache;
|
|
@@ -204,6 +209,7 @@ export declare class LocalMetaService implements MetaService {
|
|
|
204
209
|
private secretsMetaCache;
|
|
205
210
|
private credentialsMetaCache;
|
|
206
211
|
private variablesMetaCache;
|
|
212
|
+
private outgoingWebhooksMetaCache;
|
|
207
213
|
private middlewareGroupsMetaCache;
|
|
208
214
|
private permissionsGroupsMetaCache;
|
|
209
215
|
private agentsMetaCache;
|
|
@@ -242,6 +248,8 @@ export declare class LocalMetaService implements MetaService {
|
|
|
242
248
|
getSecretsMeta(): Promise<SecretDefinitionsMeta>;
|
|
243
249
|
getCredentialsMeta(): Promise<CredentialDefinitionsMeta>;
|
|
244
250
|
getVariablesMeta(): Promise<VariableDefinitionsMeta>;
|
|
251
|
+
getOutgoingWebhooksMeta(): Promise<OutgoingWebhooksMeta>;
|
|
252
|
+
getWebhookSourcesMeta(): Promise<WebhookSourcesMeta>;
|
|
245
253
|
getEmailMeta(): Promise<EmailsMeta>;
|
|
246
254
|
getEmailTemplateAssets(templateName: string, locale: string): Promise<EmailTemplateAssets>;
|
|
247
255
|
getServicesMeta(): Promise<ServicesMetaRecord>;
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
export class LocalMetaService {
|
|
6
6
|
basePath;
|
|
7
7
|
httpMetaCache = null;
|
|
8
|
+
webhookSourcesMetaCache = null;
|
|
8
9
|
channelsMetaCache = null;
|
|
9
10
|
schedulerMetaCache = null;
|
|
10
11
|
queueMetaCache = null;
|
|
@@ -25,6 +26,7 @@ export class LocalMetaService {
|
|
|
25
26
|
secretsMetaCache = null;
|
|
26
27
|
credentialsMetaCache = null;
|
|
27
28
|
variablesMetaCache = null;
|
|
29
|
+
outgoingWebhooksMetaCache = null;
|
|
28
30
|
middlewareGroupsMetaCache = null;
|
|
29
31
|
permissionsGroupsMetaCache = null;
|
|
30
32
|
agentsMetaCache = null;
|
|
@@ -123,6 +125,8 @@ export class LocalMetaService {
|
|
|
123
125
|
this.secretsMetaCache = null;
|
|
124
126
|
this.credentialsMetaCache = null;
|
|
125
127
|
this.variablesMetaCache = null;
|
|
128
|
+
this.outgoingWebhooksMetaCache = null;
|
|
129
|
+
this.webhookSourcesMetaCache = null;
|
|
126
130
|
this.middlewareGroupsMetaCache = null;
|
|
127
131
|
this.permissionsGroupsMetaCache = null;
|
|
128
132
|
this.agentsMetaCache = null;
|
|
@@ -375,6 +379,20 @@ export class LocalMetaService {
|
|
|
375
379
|
this.variablesMetaCache = content ? JSON.parse(content) : {};
|
|
376
380
|
return this.variablesMetaCache;
|
|
377
381
|
}
|
|
382
|
+
async getOutgoingWebhooksMeta() {
|
|
383
|
+
if (this.outgoingWebhooksMetaCache)
|
|
384
|
+
return this.outgoingWebhooksMetaCache;
|
|
385
|
+
const content = await this.readFile('webhooks/pikku-outgoing-webhooks-meta.gen.json');
|
|
386
|
+
this.outgoingWebhooksMetaCache = content ? JSON.parse(content) : {};
|
|
387
|
+
return this.outgoingWebhooksMetaCache;
|
|
388
|
+
}
|
|
389
|
+
async getWebhookSourcesMeta() {
|
|
390
|
+
if (this.webhookSourcesMetaCache)
|
|
391
|
+
return this.webhookSourcesMetaCache;
|
|
392
|
+
const content = await this.readFile('webhooks/pikku-webhook-sources-meta.gen.json');
|
|
393
|
+
this.webhookSourcesMetaCache = content ? JSON.parse(content) : {};
|
|
394
|
+
return this.webhookSourcesMetaCache;
|
|
395
|
+
}
|
|
378
396
|
async getEmailMeta() {
|
|
379
397
|
// knowledge: decisions/internals/email-meta-is-read-uncached-because-codegen-rewrites-it-mid-session.md
|
|
380
398
|
const content = await this.readFile('email/pikku-emails-meta.gen.json');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IncomingWebhookService } from '../services/incoming-webhook-service.js';
|
|
1
2
|
import type { Logger, LogLevel } from '../services/logger.js';
|
|
2
3
|
import type { VariablesService } from '../services/variables-service.js';
|
|
3
4
|
import type { SecretService } from '../services/secret-service.js';
|
|
@@ -133,6 +134,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
133
134
|
* the delivery-read methods; a store-backed implementation records history.
|
|
134
135
|
*/
|
|
135
136
|
webhookService?: WebhookService;
|
|
137
|
+
/** Queues what webhook trigger sources receive. Required once any `wireTriggerWebhookSource` is wired. */
|
|
138
|
+
incomingWebhookService?: IncomingWebhookService;
|
|
136
139
|
metaService?: MetaService;
|
|
137
140
|
/**
|
|
138
141
|
* Where virtual-user runs are recorded. A run is dispatched and answered for
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CoreTriggerWebhookSource, WebhookSourcesMeta } from '../wirings/trigger/webhook-source.types.js';
|
|
1
2
|
import type { CredentialOverrides } from '../wirings/credential/credential-overrides.js';
|
|
2
3
|
import type { PikkuErrorConstructor, ErrorDetails } from '../errors/error-handler.js';
|
|
3
4
|
import type { CorePikkuFunctionConfig, CorePermissionGroup, CorePikkuPermission } from '../function/functions.types.js';
|
|
@@ -101,6 +102,8 @@ export interface PikkuPackageState {
|
|
|
101
102
|
triggerSources: Map<string, CoreTriggerSource>;
|
|
102
103
|
meta: TriggerMeta;
|
|
103
104
|
sourceMeta: TriggerSourceMeta;
|
|
105
|
+
webhookSources: Map<string, CoreTriggerWebhookSource>;
|
|
106
|
+
webhookSourceMeta: WebhookSourcesMeta;
|
|
104
107
|
};
|
|
105
108
|
mcp: {
|
|
106
109
|
resources: Map<string, CoreMCPResource>;
|
|
@@ -84,6 +84,26 @@ const wrapWorkflowServiceForPackage = (service, packageName, namespace) => {
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
};
|
|
87
|
+
const scopedWebhookServices = new WeakSet();
|
|
88
|
+
/** An addon names its outgoing events locally; they leave as `<instance>:<event>`. */
|
|
89
|
+
const scopeWebhookService = (service, namespace) => {
|
|
90
|
+
if (scopedWebhookServices.has(service))
|
|
91
|
+
return service;
|
|
92
|
+
const scoped = new Proxy(service, {
|
|
93
|
+
get(target, prop, receiver) {
|
|
94
|
+
const value = Reflect.get(target, prop, receiver);
|
|
95
|
+
if (prop !== 'send' || typeof value !== 'function')
|
|
96
|
+
return value;
|
|
97
|
+
return function (input, ...rest) {
|
|
98
|
+
return value.call(this, input?.event && !input.event.startsWith(`${namespace}:`)
|
|
99
|
+
? { ...input, event: `${namespace}:${input.event}` }
|
|
100
|
+
: input, ...rest);
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
scopedWebhookServices.add(scoped);
|
|
105
|
+
return scoped;
|
|
106
|
+
};
|
|
87
107
|
export const getOrCreatePackageSingletonServices = async (packageName, parentServices, addonInstance) => {
|
|
88
108
|
const cacheKey = addonInstance?.namespace ?? packageName;
|
|
89
109
|
const cachedServices = pikkuState(cacheKey, 'package', 'singletonServices');
|
|
@@ -116,6 +136,13 @@ export const getOrCreatePackageSingletonServices = async (packageName, parentSer
|
|
|
116
136
|
credentialService: new ScopedCredentialService(existingServices.credentialService, allowedNames(Object.keys(pikkuState(packageName, 'package', 'credentialsMeta') ?? {}), addonInstance?.credentialGrants, addonInstance?.credentialOverrides)),
|
|
117
137
|
};
|
|
118
138
|
}
|
|
139
|
+
const namespace = addonInstance?.namespace ?? findAddonNamespaceForPackage(packageName);
|
|
140
|
+
if (namespace && existingServices.webhookService) {
|
|
141
|
+
existingServices = {
|
|
142
|
+
...existingServices,
|
|
143
|
+
webhookService: scopeWebhookService(existingServices.webhookService, namespace),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
119
146
|
if (!factories || !factories.createSingletonServices) {
|
|
120
147
|
return existingServices;
|
|
121
148
|
}
|
|
@@ -128,6 +155,9 @@ export const getOrCreatePackageSingletonServices = async (packageName, parentSer
|
|
|
128
155
|
typeof packageServices.workflowService === 'object') {
|
|
129
156
|
packageServices.workflowService = wrapWorkflowServiceForPackage(packageServices.workflowService, packageName, addonInstance?.namespace ?? null);
|
|
130
157
|
}
|
|
158
|
+
if (namespace && packageServices.webhookService) {
|
|
159
|
+
packageServices.webhookService = scopeWebhookService(packageServices.webhookService, namespace);
|
|
160
|
+
}
|
|
131
161
|
pikkuState(cacheKey, 'package', 'singletonServices', packageServices);
|
|
132
162
|
return packageServices;
|
|
133
163
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CredentialOverrides } from '../credential/credential-overrides.js';
|
|
2
2
|
import type { CorePikkuMiddleware } from '../../middleware/middleware.types.js';
|
|
3
|
+
import type { AddonInstance } from './addon-runner.js';
|
|
3
4
|
export type WireAddonConfig = {
|
|
4
5
|
/** How this instance is addressed. One package may be wired more than once, and the name is what tells the instances apart. */
|
|
5
6
|
name: string;
|
|
@@ -112,6 +113,7 @@ export declare const resolveAddonAuth: (packageName: string | null, namespace?:
|
|
|
112
113
|
export declare const resolveAddonFunctionTarget: (funcName: string, packageName: string | null) => {
|
|
113
114
|
packageName: string;
|
|
114
115
|
localName: string;
|
|
116
|
+
instance: AddonInstance;
|
|
115
117
|
} | null;
|
|
116
118
|
/**
|
|
117
119
|
* Addon tags name middleware the *consuming app* registered, so they resolve
|
|
@@ -125,6 +125,12 @@ export const resolveAddonFunctionTarget = (funcName, packageName) => {
|
|
|
125
125
|
return {
|
|
126
126
|
packageName: config.package,
|
|
127
127
|
localName: funcName.slice(separator + 1),
|
|
128
|
+
// The instance, not just its package: everything the consuming app lent
|
|
129
|
+
// this addon — `secretOverrides`, grants, the scoping opt-outs — hangs off
|
|
130
|
+
// here, and a caller that only learns the package runs the function with
|
|
131
|
+
// none of it. The namespace is the map key rather than a field on the
|
|
132
|
+
// stored config, so it is put back on the way out.
|
|
133
|
+
instance: { namespace, ...config },
|
|
128
134
|
};
|
|
129
135
|
};
|
|
130
136
|
/**
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { wireTrigger, wireTriggerSource } from './trigger-runner.js';
|
|
2
|
+
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
|
+
export type { WebhookSourceOutcome } from './webhook-source-runner.js';
|
|
4
|
+
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME } from './webhook-source.types.js';
|
|
5
|
+
export type { CoreTriggerWebhookSource, TriggerEvent, WebhookCheckResult, WebhookLifecycleInput, WebhookReceiveResult, WebhookRequest, WebhookSetupResult, WebhookSourceJob, WebhookSourceMeta, WebhookSourcesMeta, WebhookSourceState, WebhookTeardownInput, WebhookTeardownResult, } from './webhook-source.types.js';
|
|
2
6
|
export { PikkuTriggerService } from './pikku-trigger-service.js';
|
|
3
7
|
export type { TriggerMeta, TriggerSourceMeta, CorePikkuTriggerFunction, CorePikkuTriggerFunctionConfig, CoreTrigger, } from './trigger.types.js';
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { wireTrigger, wireTriggerSource } from './trigger-runner.js';
|
|
2
|
+
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
|
+
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME } from './webhook-source.types.js';
|
|
2
4
|
export { PikkuTriggerService } from './pikku-trigger-service.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { CoreSingletonServices } from '../../types/core.types.js';
|
|
3
|
+
import type { PikkuHTTP } from '../http/http.types.js';
|
|
4
|
+
import type { CoreTriggerWebhookSource, WebhookCheckResult, WebhookSetupResult, WebhookSourceJob, WebhookSourceState, WebhookTeardownResult } from './webhook-source.types.js';
|
|
5
|
+
export declare const wireTriggerWebhookSource: <Events extends Record<string, StandardSchemaV1>>(source: CoreTriggerWebhookSource<Events>) => void;
|
|
6
|
+
/** The declared events some trigger is wired to: what `setup` registers with the provider. */
|
|
7
|
+
export declare const subscribedWebhookEvents: (source: string) => string[];
|
|
8
|
+
/**
|
|
9
|
+
* The body of a webhook source's route: `receive`, validate, and queue every
|
|
10
|
+
* event some trigger listens for. Events nobody listens for are answered and
|
|
11
|
+
* dropped, so the provider does not retry them forever.
|
|
12
|
+
*/
|
|
13
|
+
export declare const receiveWebhookSourceRequest: (sourceName: string, wire: {
|
|
14
|
+
http?: PikkuHTTP;
|
|
15
|
+
}) => Promise<Response | {
|
|
16
|
+
received: number;
|
|
17
|
+
}>;
|
|
18
|
+
/** The `pikku-incoming-webhooks` worker: runs the trigger an event was queued for. Throws so the queue retries. */
|
|
19
|
+
export declare const dispatchWebhookSourceJob: (job: WebhookSourceJob) => Promise<void>;
|
|
20
|
+
export type WebhookSourceOutcome = {
|
|
21
|
+
source: string;
|
|
22
|
+
url: string;
|
|
23
|
+
status: WebhookCheckResult['status'] | WebhookSetupResult['status'] | WebhookTeardownResult['status'] | 'skipped' | 'failed';
|
|
24
|
+
reason?: string;
|
|
25
|
+
state?: WebhookSourceState;
|
|
26
|
+
/** The name the produced signing secret must be stored under. */
|
|
27
|
+
secretName?: string;
|
|
28
|
+
secret?: string;
|
|
29
|
+
instructions?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Runs one lifecycle step for every webhook source, as `pikku webhooks
|
|
34
|
+
* status | setup | teardown` does at deploy. `setup` runs only where `check`
|
|
35
|
+
* does not report `ok`. One failing source does not stop the rest.
|
|
36
|
+
*/
|
|
37
|
+
export declare const runWebhookSourceLifecycle: ({ action, baseUrl, labelPrefix, previous, singletonServices, }: {
|
|
38
|
+
action: 'check' | 'setup' | 'teardown';
|
|
39
|
+
/** Where the app's routes are served, e.g. `https://shop.example.com/api`. */
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
labelPrefix: string;
|
|
42
|
+
/** What the last `setup` returned as `state`, by source name. */
|
|
43
|
+
previous?: Record<string, WebhookSourceState>;
|
|
44
|
+
singletonServices?: CoreSingletonServices;
|
|
45
|
+
}) => Promise<WebhookSourceOutcome[]>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { getSingletonServices, pikkuState } from '../../pikku-state.js';
|
|
2
|
+
import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
|
|
3
|
+
import { PikkuMissingMetaError } from '../../errors/errors.js';
|
|
4
|
+
const LIFECYCLE = ['receive', 'check', 'setup', 'teardown'];
|
|
5
|
+
export const wireTriggerWebhookSource = (source) => {
|
|
6
|
+
const meta = pikkuState(null, 'trigger', 'webhookSourceMeta')[source.name];
|
|
7
|
+
if (!meta) {
|
|
8
|
+
console.warn(`[pikku] Skipping webhook source '${source.name}' — metadata not found. Consider moving this wiring to its own file.`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const sources = pikkuState(null, 'trigger', 'webhookSources');
|
|
12
|
+
if (sources.has(source.name)) {
|
|
13
|
+
throw new Error(`Webhook source already exists: ${source.name}`);
|
|
14
|
+
}
|
|
15
|
+
sources.set(source.name, source);
|
|
16
|
+
for (const step of LIFECYCLE) {
|
|
17
|
+
const funcId = meta[step];
|
|
18
|
+
const config = source[step];
|
|
19
|
+
if (funcId && typeof config?.func === 'function') {
|
|
20
|
+
addFunction(funcId, config);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const triggerName = (source, event) => event ? `${source}:${event}` : source;
|
|
25
|
+
/** The declared events some trigger is wired to: what `setup` registers with the provider. */
|
|
26
|
+
export const subscribedWebhookEvents = (source) => {
|
|
27
|
+
const triggers = pikkuState(null, 'trigger', 'meta');
|
|
28
|
+
const meta = pikkuState(null, 'trigger', 'webhookSourceMeta')[source];
|
|
29
|
+
return (meta?.events ?? []).filter((event) => triggers[triggerName(source, event)]);
|
|
30
|
+
};
|
|
31
|
+
const getSourceMeta = (source) => {
|
|
32
|
+
const meta = pikkuState(null, 'trigger', 'webhookSourceMeta')[source];
|
|
33
|
+
if (!meta) {
|
|
34
|
+
throw new PikkuMissingMetaError(`Missing generated metadata for webhook source '${source}'`);
|
|
35
|
+
}
|
|
36
|
+
return meta;
|
|
37
|
+
};
|
|
38
|
+
const runSourceStep = (singletonServices, source, funcId, data) => runPikkuFunc('trigger', source, funcId, {
|
|
39
|
+
singletonServices,
|
|
40
|
+
auth: false,
|
|
41
|
+
data: () => data,
|
|
42
|
+
wire: {},
|
|
43
|
+
});
|
|
44
|
+
const readRequest = async (http) => {
|
|
45
|
+
const request = http?.request;
|
|
46
|
+
if (!request) {
|
|
47
|
+
throw new Error('A webhook source can only be reached over HTTP');
|
|
48
|
+
}
|
|
49
|
+
const query = {};
|
|
50
|
+
for (const [key, value] of Object.entries(request.query() ?? {})) {
|
|
51
|
+
if (value !== undefined)
|
|
52
|
+
query[key] = String(value);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
body: new Uint8Array(await request.arrayBuffer()),
|
|
56
|
+
headers: request.headers(),
|
|
57
|
+
method: request.method(),
|
|
58
|
+
url: request.path(),
|
|
59
|
+
query,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const validateEvents = async (source, events, logger) => {
|
|
63
|
+
const valid = [];
|
|
64
|
+
for (const event of events) {
|
|
65
|
+
const schema = source?.events?.[event.name];
|
|
66
|
+
if (!schema) {
|
|
67
|
+
valid.push(event);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const result = await schema['~standard'].validate(event.data);
|
|
71
|
+
if (result.issues) {
|
|
72
|
+
logger.warn(`Webhook source '${source.name}' dropped '${event.name}'${event.id ? ` (${event.id})` : ''}: ${result.issues.map((issue) => issue.message).join('; ')}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
valid.push({ ...event, data: result.value });
|
|
76
|
+
}
|
|
77
|
+
return valid;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The body of a webhook source's route: `receive`, validate, and queue every
|
|
81
|
+
* event some trigger listens for. Events nobody listens for are answered and
|
|
82
|
+
* dropped, so the provider does not retry them forever.
|
|
83
|
+
*/
|
|
84
|
+
export const receiveWebhookSourceRequest = async (sourceName, wire) => {
|
|
85
|
+
const singletonServices = getSingletonServices();
|
|
86
|
+
const meta = getSourceMeta(sourceName);
|
|
87
|
+
const source = pikkuState(null, 'trigger', 'webhookSources').get(sourceName);
|
|
88
|
+
const request = await readRequest(wire.http);
|
|
89
|
+
const result = meta.receive
|
|
90
|
+
? await runSourceStep(singletonServices, sourceName, meta.receive, request)
|
|
91
|
+
: {
|
|
92
|
+
events: [
|
|
93
|
+
{
|
|
94
|
+
name: '',
|
|
95
|
+
data: request.body.length
|
|
96
|
+
? JSON.parse(new TextDecoder().decode(request.body))
|
|
97
|
+
: undefined,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
if ('respond' in result) {
|
|
102
|
+
const { status, body, headers } = result.respond;
|
|
103
|
+
const text = body === undefined
|
|
104
|
+
? null
|
|
105
|
+
: typeof body === 'string'
|
|
106
|
+
? body
|
|
107
|
+
: JSON.stringify(body);
|
|
108
|
+
return new Response(text, { status, headers });
|
|
109
|
+
}
|
|
110
|
+
const triggers = pikkuState(null, 'trigger', 'meta');
|
|
111
|
+
const listened = result.events.filter((event) => triggers[triggerName(sourceName, event.name)]);
|
|
112
|
+
const events = await validateEvents(source, listened, singletonServices.logger);
|
|
113
|
+
if (events.length === 0)
|
|
114
|
+
return { received: 0 };
|
|
115
|
+
const service = singletonServices.incomingWebhookService;
|
|
116
|
+
if (!service) {
|
|
117
|
+
throw new Error(`Webhook source '${sourceName}' received events but no incomingWebhookService is configured to queue them.`);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
received: await service.accept({ source: sourceName, request, events }),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/** The `pikku-incoming-webhooks` worker: runs the trigger an event was queued for. Throws so the queue retries. */
|
|
124
|
+
export const dispatchWebhookSourceJob = async (job) => {
|
|
125
|
+
const singletonServices = getSingletonServices();
|
|
126
|
+
const name = triggerName(job.source, job.event.name);
|
|
127
|
+
const trigger = pikkuState(null, 'trigger', 'meta')[name];
|
|
128
|
+
if (!trigger) {
|
|
129
|
+
singletonServices.logger.warn(`No trigger named '${name}' is wired; dropping ${job.event.id ?? 'an event'} from '${job.source}'.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let error;
|
|
133
|
+
try {
|
|
134
|
+
await runPikkuFunc('trigger', name, trigger.pikkuFuncId, {
|
|
135
|
+
singletonServices,
|
|
136
|
+
auth: false,
|
|
137
|
+
data: () => job.event.data,
|
|
138
|
+
wire: {},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
error = e instanceof Error ? e.message : String(e);
|
|
143
|
+
if (job.receiptId) {
|
|
144
|
+
await recordAttempt(singletonServices, job.receiptId, name, error);
|
|
145
|
+
}
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
if (job.receiptId) {
|
|
149
|
+
await recordAttempt(singletonServices, job.receiptId, name);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const recordAttempt = (singletonServices, receiptId, trigger, error) => singletonServices.incomingWebhookService
|
|
153
|
+
?.recordAttempt(receiptId, { trigger, ...(error ? { error } : {}) })
|
|
154
|
+
.catch((storeError) => singletonServices.logger.error(`Failed to record the attempt for webhook receipt ${receiptId}`, storeError));
|
|
155
|
+
/**
|
|
156
|
+
* Runs one lifecycle step for every webhook source, as `pikku webhooks
|
|
157
|
+
* status | setup | teardown` does at deploy. `setup` runs only where `check`
|
|
158
|
+
* does not report `ok`. One failing source does not stop the rest.
|
|
159
|
+
*/
|
|
160
|
+
export const runWebhookSourceLifecycle = async ({ action, baseUrl, labelPrefix, previous = {}, singletonServices = getSingletonServices(), }) => {
|
|
161
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
162
|
+
const outcomes = [];
|
|
163
|
+
for (const meta of Object.values(pikkuState(null, 'trigger', 'webhookSourceMeta'))) {
|
|
164
|
+
const url = `${base}${meta.route}`;
|
|
165
|
+
const label = `${labelPrefix}:${meta.name}`;
|
|
166
|
+
const input = {
|
|
167
|
+
url,
|
|
168
|
+
label,
|
|
169
|
+
events: subscribedWebhookEvents(meta.name),
|
|
170
|
+
...(previous[meta.name] ? { previous: previous[meta.name] } : {}),
|
|
171
|
+
};
|
|
172
|
+
const outcome = (status, extra = {}) => outcomes.push({ source: meta.name, url, status, ...extra });
|
|
173
|
+
try {
|
|
174
|
+
if (action === 'teardown') {
|
|
175
|
+
if (!meta.teardown) {
|
|
176
|
+
outcome('skipped');
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const result = await runSourceStep(singletonServices, meta.name, meta.teardown, { label, ...(input.previous ? { previous: input.previous } : {}) });
|
|
180
|
+
outcome(result.status);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (meta.check) {
|
|
184
|
+
const checked = await runSourceStep(singletonServices, meta.name, meta.check, input);
|
|
185
|
+
if (action === 'check' || checked.status === 'ok') {
|
|
186
|
+
outcome(checked.status === 'ok' && action === 'setup'
|
|
187
|
+
? 'unchanged'
|
|
188
|
+
: checked.status, checked.status === 'drifted' ? { reason: checked.reason } : {});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (action === 'check') {
|
|
193
|
+
outcome('skipped');
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!meta.setup) {
|
|
197
|
+
outcome('manual', {
|
|
198
|
+
instructions: `Register ${url} with the provider for: ${input.events.join(', ') || 'every event'}.`,
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const result = await runSourceStep(singletonServices, meta.name, meta.setup, input);
|
|
203
|
+
if (result.status === 'manual') {
|
|
204
|
+
outcome('manual', { instructions: result.instructions });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
outcome(result.status, {
|
|
208
|
+
...(result.state ? { state: result.state } : {}),
|
|
209
|
+
...(result.secret && meta.secret
|
|
210
|
+
? { secretName: meta.secret, secret: result.secret }
|
|
211
|
+
: {}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
outcome('failed', {
|
|
216
|
+
error: error instanceof Error ? error.message : String(error),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return outcomes;
|
|
221
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
|
+
export declare const PIKKU_INCOMING_WEBHOOK_QUEUE_NAME = "pikku-incoming-webhooks";
|
|
4
|
+
/** What every trigger source produces, whichever way it arrives. */
|
|
5
|
+
export type TriggerEvent<Name extends string = string, Data = unknown> = {
|
|
6
|
+
/** The provider's event type; dispatched to the trigger named `<source>:<name>`. Empty dispatches to `<source>`. */
|
|
7
|
+
name: Name;
|
|
8
|
+
/** The provider's event id, which de-duplicates redeliveries. */
|
|
9
|
+
id?: string;
|
|
10
|
+
data: Data;
|
|
11
|
+
};
|
|
12
|
+
/** The request exactly as it arrived: providers sign the raw bytes, not re-serialised JSON. */
|
|
13
|
+
export type WebhookRequest = {
|
|
14
|
+
body: Uint8Array;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
method: string;
|
|
17
|
+
url: string;
|
|
18
|
+
query: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
export type WebhookReceiveResult = {
|
|
21
|
+
events: TriggerEvent[];
|
|
22
|
+
}
|
|
23
|
+
/** A handshake, such as Slack's `url_verification`: answered directly, nothing is dispatched. */
|
|
24
|
+
| {
|
|
25
|
+
respond: {
|
|
26
|
+
status: number;
|
|
27
|
+
body?: unknown;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/** Whatever `setup` wants back on the next deploy, for providers whose endpoints cannot be found by label. */
|
|
32
|
+
export type WebhookSourceState = Record<string, unknown>;
|
|
33
|
+
export type WebhookLifecycleInput = {
|
|
34
|
+
/** Always this deployment's own route: nothing can point it elsewhere. */
|
|
35
|
+
url: string;
|
|
36
|
+
/** Stable across deploys of one app and stage, for providers that let endpoints be tagged and listed. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** The declared events some trigger is wired to. */
|
|
39
|
+
events: string[];
|
|
40
|
+
previous?: WebhookSourceState;
|
|
41
|
+
};
|
|
42
|
+
export type WebhookCheckResult = {
|
|
43
|
+
status: 'ok';
|
|
44
|
+
} | {
|
|
45
|
+
status: 'missing';
|
|
46
|
+
} | {
|
|
47
|
+
status: 'drifted';
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
export type WebhookSetupResult = {
|
|
51
|
+
status: 'created' | 'updated' | 'unchanged';
|
|
52
|
+
state?: WebhookSourceState;
|
|
53
|
+
/** Only when the provider issued a new signing secret. */
|
|
54
|
+
secret?: string;
|
|
55
|
+
}
|
|
56
|
+
/** The provider has no API for this: `instructions` tell a person what to set by hand. */
|
|
57
|
+
| {
|
|
58
|
+
status: 'manual';
|
|
59
|
+
instructions: string;
|
|
60
|
+
};
|
|
61
|
+
export type WebhookTeardownInput = {
|
|
62
|
+
label: string;
|
|
63
|
+
previous?: WebhookSourceState;
|
|
64
|
+
};
|
|
65
|
+
export type WebhookTeardownResult = {
|
|
66
|
+
status: 'deleted' | 'absent';
|
|
67
|
+
};
|
|
68
|
+
type SourceFunction<In, Out> = CorePikkuFunctionConfig<any, any> & {
|
|
69
|
+
func: (services: any, data: In, wire: any) => Promise<Out>;
|
|
70
|
+
};
|
|
71
|
+
export type CoreTriggerWebhookSource<Events extends Record<string, StandardSchemaV1> = Record<string, StandardSchemaV1>> = {
|
|
72
|
+
/** Triggers subscribe to `<name>:<event>`. Unique across every kind of trigger source. */
|
|
73
|
+
name: string;
|
|
74
|
+
method?: 'post' | 'put' | 'get';
|
|
75
|
+
/** Defaults to `/webhooks/<name>`. */
|
|
76
|
+
route?: string;
|
|
77
|
+
/** The secret `receive` verifies with, which `setup` produces. */
|
|
78
|
+
secret?: string;
|
|
79
|
+
/** What the source can produce. Each event's data is validated against its schema before it is queued. */
|
|
80
|
+
events?: Events;
|
|
81
|
+
/** Omitted: the JSON body is one event dispatched to the trigger named `<name>`. */
|
|
82
|
+
receive?: SourceFunction<WebhookRequest, WebhookReceiveResult>;
|
|
83
|
+
check?: SourceFunction<WebhookLifecycleInput, WebhookCheckResult>;
|
|
84
|
+
setup?: SourceFunction<WebhookLifecycleInput, WebhookSetupResult>;
|
|
85
|
+
teardown?: SourceFunction<WebhookTeardownInput, WebhookTeardownResult>;
|
|
86
|
+
};
|
|
87
|
+
export type WebhookSourceMeta = {
|
|
88
|
+
name: string;
|
|
89
|
+
method: 'post' | 'put' | 'get';
|
|
90
|
+
route: string;
|
|
91
|
+
secret?: string;
|
|
92
|
+
events: string[];
|
|
93
|
+
receive?: string;
|
|
94
|
+
check?: string;
|
|
95
|
+
setup?: string;
|
|
96
|
+
teardown?: string;
|
|
97
|
+
};
|
|
98
|
+
export type WebhookSourcesMeta = Record<string, WebhookSourceMeta>;
|
|
99
|
+
/** One queued event on its way to its trigger. */
|
|
100
|
+
export type WebhookSourceJob = {
|
|
101
|
+
source: string;
|
|
102
|
+
event: TriggerEvent;
|
|
103
|
+
/** Present when a store-backed service recorded the receipt. */
|
|
104
|
+
receiptId?: string;
|
|
105
|
+
};
|
|
106
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PIKKU_INCOMING_WEBHOOK_QUEUE_NAME = 'pikku-incoming-webhooks';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { Safe } from '../../classification/secret-value.js';
|
|
3
|
+
import type { SendWebhookInput, SendWebhookResult, WebhookService } from '../../services/webhook-service.js';
|
|
4
|
+
export type CoreOutgoingWebhook<Event extends string = string, Payload extends StandardSchemaV1 = StandardSchemaV1> = {
|
|
5
|
+
event: Event;
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
payload: Payload;
|
|
9
|
+
};
|
|
10
|
+
export type OutgoingWebhookMeta = {
|
|
11
|
+
event: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
payload?: Record<string, string>;
|
|
15
|
+
exportedName?: string;
|
|
16
|
+
sourceFile?: string;
|
|
17
|
+
};
|
|
18
|
+
export type OutgoingWebhooksMeta = Record<string, OutgoingWebhookMeta>;
|
|
19
|
+
export type OutgoingWebhookPayloadOf<W> = W extends CoreOutgoingWebhook<string, infer S> ? StandardSchemaV1.InferInput<S> : never;
|
|
20
|
+
export type OutgoingWebhookDataFor<TMap, Input> = Input extends {
|
|
21
|
+
event: infer Event;
|
|
22
|
+
} ? Event extends keyof TMap ? {
|
|
23
|
+
data: Safe<TMap[Event]>;
|
|
24
|
+
} : unknown : unknown;
|
|
25
|
+
export interface TypedWebhookService<TMap = Record<string, unknown>> extends Omit<WebhookService, 'send'> {
|
|
26
|
+
send<const T extends SendWebhookInput>(input: Safe<T> & OutgoingWebhookDataFor<TMap, T>): Promise<SendWebhookResult>;
|
|
27
|
+
}
|
|
28
|
+
export declare const defineOutgoingWebhook: <const Event extends string, Payload extends StandardSchemaV1>(webhook: CoreOutgoingWebhook<Event, Payload>) => CoreOutgoingWebhook<Event, Payload>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const defineOutgoingWebhook = (webhook) => webhook;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineOutgoingWebhook } from './define-outgoing-webhook.js';
|
|
@@ -8,7 +8,7 @@ import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } fr
|
|
|
8
8
|
import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
|
|
9
9
|
export { addFeature, resolveFeatureScenarios } from './feature.js';
|
|
10
10
|
export type { CoreFeature, CoreFeatureScenario, FeatureMeta, FeaturesMeta, PikkuBrowserWire, PikkuScenarioWire, ScenarioBrowserFailure, ScenarioBrowserProvider, ScenarioEnvironment, ScenarioStepKind, ScenarioStepMeta, ScenarioStepOptions, ScenarioScreenshotOptions, ScenarioStepPhase, ScenarioSurface, TestIdSelector, } from './scenario.types.js';
|
|
11
|
-
export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunSelection, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioStepRow, ScenarioStepVideoOffset, } from './scenario-run.types.js';
|
|
11
|
+
export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunSelection, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioRunVersion, ScenarioStepRow, ScenarioStepVideoOffset, } from './scenario-run.types.js';
|
|
12
12
|
export { SCENARIO_SURFACES } from './scenario-step.types.js';
|
|
13
13
|
export { resolveScenarioSurfaces } from './scenario-surface.js';
|
|
14
14
|
export { pollUntil, type PollOptions } from './scenario-poll.js';
|
|
@@ -95,7 +95,12 @@ export interface ScenarioFailureDetail {
|
|
|
95
95
|
}
|
|
96
96
|
export interface ScenarioResult {
|
|
97
97
|
name: string;
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* `running` is filed the moment the scenario starts and replaced when it
|
|
100
|
+
* ends, so a console watching a run in progress can tell the scenario on
|
|
101
|
+
* screen right now from the ones still waiting their turn.
|
|
102
|
+
*/
|
|
103
|
+
status: 'passed' | 'failed' | 'running';
|
|
99
104
|
durationMs: number;
|
|
100
105
|
output?: unknown;
|
|
101
106
|
error?: string;
|
|
@@ -117,6 +122,11 @@ export interface ScenarioResult {
|
|
|
117
122
|
* feature under, and it is what survives the title being rewritten.
|
|
118
123
|
*/
|
|
119
124
|
featureId?: string;
|
|
125
|
+
/** The scenario's declared title, snapshotted so the record reads as prose. */
|
|
126
|
+
title?: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
/** Who the scenario cast, in declaration order. */
|
|
129
|
+
actors?: string[];
|
|
120
130
|
tags?: string[];
|
|
121
131
|
/** Images and footage this scenario produced, filed under the run. */
|
|
122
132
|
artifacts?: ScenarioArtifact[];
|
|
@@ -155,6 +165,32 @@ export interface ScenarioRunSelection {
|
|
|
155
165
|
tags?: string[];
|
|
156
166
|
excludeTags?: string[];
|
|
157
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Which version of the suite a run ran against.
|
|
170
|
+
*
|
|
171
|
+
* A run is only comparable to another run of the same thing, and "the same
|
|
172
|
+
* thing" is the commit — two runs a day apart are not a flake and a fix if the
|
|
173
|
+
* suite moved between them. `attempt` counts the runs already filed against
|
|
174
|
+
* that commit, so a suite re-run until it passes reads as one version with
|
|
175
|
+
* several attempts rather than several unrelated runs.
|
|
176
|
+
*
|
|
177
|
+
* Absent when the project is not in a git repository, or has no commits yet:
|
|
178
|
+
* an unversioned run is still a run, and saying nothing is better than
|
|
179
|
+
* inventing a version for it.
|
|
180
|
+
*/
|
|
181
|
+
export interface ScenarioRunVersion {
|
|
182
|
+
/** The commit the working tree was at, in full. */
|
|
183
|
+
commit: string;
|
|
184
|
+
/**
|
|
185
|
+
* The tree had uncommitted changes, so the commit does not describe what
|
|
186
|
+
* actually ran. Recorded rather than refused — running against a dirty tree
|
|
187
|
+
* is the normal way to work — but a reader comparing two attempts of the
|
|
188
|
+
* same commit needs to know one of them was not really that commit.
|
|
189
|
+
*/
|
|
190
|
+
dirty?: boolean;
|
|
191
|
+
/** Which run this is against that commit, counting from one. */
|
|
192
|
+
attempt: number;
|
|
193
|
+
}
|
|
158
194
|
/**
|
|
159
195
|
* A whole run, as it is stored and read back.
|
|
160
196
|
*
|
|
@@ -170,6 +206,8 @@ export interface ScenarioRunRecord extends ScenarioRunReport {
|
|
|
170
206
|
surface: string;
|
|
171
207
|
/** Absent on a run of the whole suite; see {@link ScenarioRunSelection}. */
|
|
172
208
|
selection?: ScenarioRunSelection;
|
|
209
|
+
/** The suite version this ran against, when the project has one. */
|
|
210
|
+
version?: ScenarioRunVersion;
|
|
173
211
|
startedAt: string;
|
|
174
212
|
finishedAt?: string;
|
|
175
213
|
}
|
|
@@ -178,6 +216,7 @@ export interface ScenarioRunSummary {
|
|
|
178
216
|
runId: string;
|
|
179
217
|
environment: string;
|
|
180
218
|
surface: string;
|
|
219
|
+
version?: ScenarioRunVersion;
|
|
181
220
|
status: ScenarioRunStatus;
|
|
182
221
|
startedAt: string;
|
|
183
222
|
finishedAt?: string;
|
|
@@ -202,7 +241,11 @@ export interface ScenarioRunSummary {
|
|
|
202
241
|
export interface ScenarioRunStore {
|
|
203
242
|
/** Open a run. Called before the first scenario, with `status: 'running'`. */
|
|
204
243
|
start(record: ScenarioRunRecord): Promise<void>;
|
|
205
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* File one scenario's state against an open run, replacing whatever was
|
|
246
|
+
* filed for that name before — a scenario is recorded twice, once as
|
|
247
|
+
* `running` and once with its outcome.
|
|
248
|
+
*/
|
|
206
249
|
recordScenario(runId: string, result: ScenarioResult): Promise<void>;
|
|
207
250
|
/**
|
|
208
251
|
* File the run's artifacts against the scenarios that produced them.
|
|
@@ -180,6 +180,9 @@ export interface ScenarioScreenshotOptions {
|
|
|
180
180
|
/**
|
|
181
181
|
* One actor's browser session, handed to a step as `wire.browser`.
|
|
182
182
|
*
|
|
183
|
+
* Every actor gets their own, so a two-actor scenario drives two sessions and
|
|
184
|
+
* `screenshot` files each shot under the actor it belongs to.
|
|
185
|
+
*
|
|
183
186
|
* Only what every driver can honour is declared here. A driver package adds
|
|
184
187
|
* the rest by declaration-merging onto this interface — `@pikku/playwright`
|
|
185
188
|
* contributes `page`, `context` and `locate` — so a step written against the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.126",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"./persona": "./dist/wirings/persona/index.js",
|
|
70
70
|
"./secret": "./dist/wirings/secret/index.js",
|
|
71
71
|
"./variable": "./dist/wirings/variable/index.js",
|
|
72
|
+
"./webhook": "./dist/wirings/webhook/index.js",
|
|
72
73
|
"./oauth2": "./dist/wirings/oauth2/index.js",
|
|
73
74
|
"./errors": "./dist/errors/index.js",
|
|
74
75
|
"./services": "./dist/services/index.js",
|
package/src/public-surface.json
CHANGED
|
@@ -100,9 +100,7 @@
|
|
|
100
100
|
"reconstructStateAt"
|
|
101
101
|
],
|
|
102
102
|
"./workflow/types": [],
|
|
103
|
-
"./actor-flow": [
|
|
104
|
-
"runConversation"
|
|
105
|
-
],
|
|
103
|
+
"./actor-flow": ["runConversation"],
|
|
106
104
|
"./virtual-user": [
|
|
107
105
|
"DEFAULT_MAX_INTERVAL_MS",
|
|
108
106
|
"DEFAULT_MIN_INTERVAL_MS",
|
|
@@ -165,9 +163,7 @@
|
|
|
165
163
|
"wireHTTP",
|
|
166
164
|
"wireHTTPRoutes"
|
|
167
165
|
],
|
|
168
|
-
"./remote": [
|
|
169
|
-
"buildRemoteHeaders"
|
|
170
|
-
],
|
|
166
|
+
"./remote": ["buildRemoteHeaders"],
|
|
171
167
|
"./queue": [
|
|
172
168
|
"QueueJobDiscardedError",
|
|
173
169
|
"QueueJobFailedError",
|
|
@@ -184,9 +180,15 @@
|
|
|
184
180
|
"wireScheduler"
|
|
185
181
|
],
|
|
186
182
|
"./trigger": [
|
|
183
|
+
"PIKKU_INCOMING_WEBHOOK_QUEUE_NAME",
|
|
187
184
|
"PikkuTriggerService",
|
|
185
|
+
"dispatchWebhookSourceJob",
|
|
186
|
+
"receiveWebhookSourceRequest",
|
|
187
|
+
"runWebhookSourceLifecycle",
|
|
188
|
+
"subscribedWebhookEvents",
|
|
188
189
|
"wireTrigger",
|
|
189
|
-
"wireTriggerSource"
|
|
190
|
+
"wireTriggerSource",
|
|
191
|
+
"wireTriggerWebhookSource"
|
|
190
192
|
],
|
|
191
193
|
"./rpc": [
|
|
192
194
|
"PikkuRPCService",
|
|
@@ -207,10 +209,7 @@
|
|
|
207
209
|
"safeFetch",
|
|
208
210
|
"setDefaultHostResolver"
|
|
209
211
|
],
|
|
210
|
-
"./node-host-resolver": [
|
|
211
|
-
"installNodeHostResolver",
|
|
212
|
-
"nodeHostResolver"
|
|
213
|
-
],
|
|
212
|
+
"./node-host-resolver": ["installNodeHostResolver", "nodeHostResolver"],
|
|
214
213
|
"./mcp": [
|
|
215
214
|
"MCPEndpointRegistry",
|
|
216
215
|
"MCPError",
|
|
@@ -297,10 +296,7 @@
|
|
|
297
296
|
"wantsStackTrace",
|
|
298
297
|
"wireCLI"
|
|
299
298
|
],
|
|
300
|
-
"./cli/command-parser": [
|
|
301
|
-
"generateCommandHelp",
|
|
302
|
-
"parseCLIArguments"
|
|
303
|
-
],
|
|
299
|
+
"./cli/command-parser": ["generateCommandHelp", "parseCLIArguments"],
|
|
304
300
|
"./cli/channel": [
|
|
305
301
|
"executeCLIViaChannel",
|
|
306
302
|
"executeRawCLIViaChannel",
|
|
@@ -360,10 +356,8 @@
|
|
|
360
356
|
"deriveOAuth2AppSecrets",
|
|
361
357
|
"validateAndBuildSecretDefinitionsMeta"
|
|
362
358
|
],
|
|
363
|
-
"./variable": [
|
|
364
|
-
|
|
365
|
-
"validateAndBuildVariableDefinitionsMeta"
|
|
366
|
-
],
|
|
359
|
+
"./variable": ["defineVariable", "validateAndBuildVariableDefinitionsMeta"],
|
|
360
|
+
"./webhook": ["defineOutgoingWebhook"],
|
|
367
361
|
"./oauth2": [],
|
|
368
362
|
"./errors": [
|
|
369
363
|
"AIProviderAuthError",
|
|
@@ -428,6 +422,7 @@
|
|
|
428
422
|
"InMemorySessionStore",
|
|
429
423
|
"InMemoryTriggerService",
|
|
430
424
|
"InMemoryWorkflowService",
|
|
425
|
+
"IncomingWebhookService",
|
|
431
426
|
"JsonConsoleLogger",
|
|
432
427
|
"LocalCredentialService",
|
|
433
428
|
"LocalEmailService",
|
|
@@ -486,19 +481,10 @@
|
|
|
486
481
|
"subjectIdOf",
|
|
487
482
|
"validateAndBuildFeatureFlagDefinitionsMeta"
|
|
488
483
|
],
|
|
489
|
-
"./services/local-meta": [
|
|
490
|
-
|
|
491
|
-
],
|
|
492
|
-
"./services/
|
|
493
|
-
"V8CoverageService"
|
|
494
|
-
],
|
|
495
|
-
"./services/istanbul-coverage": [
|
|
496
|
-
"IstanbulCoverageService"
|
|
497
|
-
],
|
|
498
|
-
"./services/local-content": [
|
|
499
|
-
"LocalContent",
|
|
500
|
-
"signedContentPath"
|
|
501
|
-
],
|
|
484
|
+
"./services/local-meta": ["LocalMetaService"],
|
|
485
|
+
"./services/v8-coverage": ["V8CoverageService"],
|
|
486
|
+
"./services/istanbul-coverage": ["IstanbulCoverageService"],
|
|
487
|
+
"./services/local-content": ["LocalContent", "signedContentPath"],
|
|
502
488
|
"./services/local-content-request-handler": [
|
|
503
489
|
"createLocalContentRequestHandler",
|
|
504
490
|
"verifySignedContentRequest"
|
|
@@ -528,10 +514,7 @@
|
|
|
528
514
|
"verifyWithKeyMaterial",
|
|
529
515
|
"wrapDEK"
|
|
530
516
|
],
|
|
531
|
-
"./hmac": [
|
|
532
|
-
"hmacSha256Hex",
|
|
533
|
-
"timingSafeStringEqual"
|
|
534
|
-
],
|
|
517
|
+
"./hmac": ["hmacSha256Hex", "timingSafeStringEqual"],
|
|
535
518
|
"./state": [
|
|
536
519
|
"addPackageServiceFactories",
|
|
537
520
|
"getAllPackageStates",
|
|
@@ -578,13 +561,6 @@
|
|
|
578
561
|
"getSchema",
|
|
579
562
|
"validateSchema"
|
|
580
563
|
],
|
|
581
|
-
"./testing": [
|
|
582
|
-
|
|
583
|
-
"defineServiceTests"
|
|
584
|
-
],
|
|
585
|
-
"./dev": [
|
|
586
|
-
"pikkuDevReloader",
|
|
587
|
-
"reconcileAddonRegistry",
|
|
588
|
-
"reloadGeneratedMeta"
|
|
589
|
-
]
|
|
564
|
+
"./testing": ["clearPikkuRuntimeState", "defineServiceTests"],
|
|
565
|
+
"./dev": ["pikkuDevReloader", "reconcileAddonRegistry", "reloadGeneratedMeta"]
|
|
590
566
|
}
|