@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/function/function-runner.js +7 -0
  3. package/dist/pikku-state.js +2 -0
  4. package/dist/services/file-scenario-run-store.js +13 -1
  5. package/dist/services/incoming-webhook-service.d.ts +48 -0
  6. package/dist/services/incoming-webhook-service.js +46 -0
  7. package/dist/services/index.d.ts +1 -0
  8. package/dist/services/index.js +1 -0
  9. package/dist/services/meta-service.d.ts +8 -0
  10. package/dist/services/meta-service.js +18 -0
  11. package/dist/types/core.types.d.ts +3 -0
  12. package/dist/types/state.types.d.ts +3 -0
  13. package/dist/wirings/addon/addon-runner.js +30 -0
  14. package/dist/wirings/addon/wire-addon.d.ts +2 -0
  15. package/dist/wirings/addon/wire-addon.js +6 -0
  16. package/dist/wirings/trigger/index.d.ts +4 -0
  17. package/dist/wirings/trigger/index.js +2 -0
  18. package/dist/wirings/trigger/webhook-source-runner.d.ts +45 -0
  19. package/dist/wirings/trigger/webhook-source-runner.js +221 -0
  20. package/dist/wirings/trigger/webhook-source.types.d.ts +106 -0
  21. package/dist/wirings/trigger/webhook-source.types.js +1 -0
  22. package/dist/wirings/webhook/define-outgoing-webhook.d.ts +28 -0
  23. package/dist/wirings/webhook/define-outgoing-webhook.js +1 -0
  24. package/dist/wirings/webhook/index.d.ts +2 -0
  25. package/dist/wirings/webhook/index.js +1 -0
  26. package/dist/wirings/workflow/pikku-scenario-service.d.ts +1 -1
  27. package/dist/wirings/workflow/scenario-run.types.d.ts +45 -2
  28. package/dist/wirings/workflow/scenario-step.types.d.ts +3 -0
  29. package/package.json +2 -1
  30. 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
  }
@@ -73,6 +73,8 @@ const createEmptyPackageState = () => ({
73
73
  triggerSources: new Map(),
74
74
  meta: {},
75
75
  sourceMeta: {},
76
+ webhookSources: new Map(),
77
+ webhookSourceMeta: {},
76
78
  },
77
79
  mcp: {
78
80
  resources: new Map(),
@@ -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.push(result);
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
+ }
@@ -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';
@@ -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,2 @@
1
+ export { defineOutgoingWebhook } from './define-outgoing-webhook.js';
2
+ export type { CoreOutgoingWebhook, OutgoingWebhookDataFor, TypedWebhookService, OutgoingWebhookMeta, OutgoingWebhooksMeta, OutgoingWebhookPayloadOf, } from './define-outgoing-webhook.js';
@@ -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
- status: 'passed' | 'failed';
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
- /** Append one finished scenario to an open run. */
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.123",
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",
@@ -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
- "defineVariable",
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
- "LocalMetaService"
491
- ],
492
- "./services/v8-coverage": [
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
- "clearPikkuRuntimeState",
583
- "defineServiceTests"
584
- ],
585
- "./dev": [
586
- "pikkuDevReloader",
587
- "reconcileAddonRegistry",
588
- "reloadGeneratedMeta"
589
- ]
564
+ "./testing": ["clearPikkuRuntimeState", "defineServiceTests"],
565
+ "./dev": ["pikkuDevReloader", "reconcileAddonRegistry", "reloadGeneratedMeta"]
590
566
  }