@pikku/core 0.12.129 → 0.12.130
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 +16 -0
- package/dist/function/function-meta.types.d.ts +14 -0
- package/dist/services/trigger-source-store.d.ts +19 -2
- package/dist/services/trigger-source-store.js +14 -1
- package/dist/wirings/trigger/index.d.ts +2 -2
- package/dist/wirings/trigger/index.js +1 -1
- package/dist/wirings/trigger/webhook-source-runner.d.ts +51 -4
- package/dist/wirings/trigger/webhook-source-runner.js +156 -14
- package/dist/wirings/workflow/pikku-workflow-service.js +5 -5
- package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +4 -3
- package/package.json +1 -1
- package/src/public-surface.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.12.130
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- dfcd351: The deploy planner now binds a unit to the units of the functions its functions call with `rpc.invoke('name')` / `rpc.remote('name')`. Before, a caller and callee split into different units (e.g. a no-service function in `svc-base` calling a DB function in `svc-kysely`) got no service binding and failed on the deployed stage with "No service binding for function". The inspector records literal RPC names per function as `invokes` (single, double or substitution-free template quotes; `rpc!`, `wire.rpc`) and warns on a computed name, which the planner cannot see (#1883).
|
|
6
|
+
- cf40182: Deployed units that call `rpc.startWorkflow('x')` now get x's meta, so the call no longer fails with `WorkflowNotFoundError`. The inspector records `startsWorkflows` for literal `rpc.startWorkflow(...)` calls. It warns when a handler computes the workflow name or passes `rpc` to a helper, because the planner cannot see those calls.
|
|
7
|
+
|
|
8
|
+
With workflow queues, the deploy planner gives a starter unit the workflow meta and orchestrator queue meta only (new `--workflowMeta` filter), plus `workflow-state` and `queue` services. Without queues the start runs inline, so the whole workflow is bundled. Core's `startWorkflow` now requires the workflow registration only for inline runs; queued runs need only the meta.
|
|
9
|
+
|
|
10
|
+
- 5bce779: `pikku dev` registers webhook sources with their providers when `PIKKU_DEV_WEBHOOK_URL` is set. What it registered, signing secrets included, is kept in a git-ignored `.webhook-registrations.gen.json` next to `pikku.config.json`, so a database reset or `.pikku` wipe does not register again, and nothing reaches a provider while the url and events are unchanged. Registrations no source declares any more are warned about on every run, with the endpoint and label to delete by hand. The prefix defaults to `dev-<username>`; override it with `PIKKU_DEV_WEBHOOK_LABEL_PREFIX`. New in core: `reconcileWebhookRegistrations`.
|
|
11
|
+
- 5bce779: Webhook trigger sources are off until someone turns them on, and addons declare their own.
|
|
12
|
+
|
|
13
|
+
An addon calls `wireTriggerWebhookSource` in its own package, and an app that wires the addon gets the source (its route included) without declaring it: named and routed after the addon's namespace, so two instances get one each. A source the app declares under the same name wins.
|
|
14
|
+
|
|
15
|
+
Every source now has an `enabled` switch in the `triggerSourceStore`, off by default. `reconcileTriggerSources` registers only enabled sources and records each one's `baseUrl` and `labelPrefix`; a disabled source's route answers 404 without running `receive`. New in core: `enableTriggerSource` registers a source with its provider and `disableTriggerSource` stops it receiving, then tears it down, both at the recorded address unless one is given. The admin addon exposes them as `triggerSourceEnable` and `triggerSourceDisable`. The `pikkuTriggerSource` table gains `enabled`, `baseUrl` and `labelPrefix`: run `pikku db generate` for the migration.
|
|
16
|
+
|
|
1
17
|
## 0.12.129
|
|
2
18
|
|
|
3
19
|
### Patch Changes
|
|
@@ -138,12 +138,26 @@ export type FunctionMeta = FunctionRuntimeMeta & Partial<{
|
|
|
138
138
|
isDirectFunction: boolean;
|
|
139
139
|
sourceFile: string;
|
|
140
140
|
exportedName: string;
|
|
141
|
+
/**
|
|
142
|
+
* RPC names this function's own body calls by literal name through
|
|
143
|
+
* `rpc.invoke(...)` / `rpc.remote(...)`. The deploy planner binds the
|
|
144
|
+
* function's unit to each callee's unit from this; a call with a computed
|
|
145
|
+
* name is not listed, and is warned about instead.
|
|
146
|
+
*/
|
|
147
|
+
invokes: string[];
|
|
141
148
|
/** File containing the handler body when it differs from sourceFile (imported handlers) */
|
|
142
149
|
bodySourceFile?: string;
|
|
143
150
|
/** 1-indexed first line of the handler body (verbose meta; coverage mapping) */
|
|
144
151
|
bodyStart: number;
|
|
145
152
|
/** 1-indexed last line of the handler body (verbose meta; coverage mapping) */
|
|
146
153
|
bodyEnd: number;
|
|
154
|
+
/**
|
|
155
|
+
* Workflow names this function's own body starts by literal name through
|
|
156
|
+
* `rpc.startWorkflow(...)`. The deploy planner bundles each one's meta
|
|
157
|
+
* into the function's unit, which is all a queued start needs. A call
|
|
158
|
+
* made from a helper the handler passes `rpc` to is not listed.
|
|
159
|
+
*/
|
|
160
|
+
startsWorkflows: string[];
|
|
147
161
|
} & CommonWireMeta>;
|
|
148
162
|
export type FunctionsRuntimeMeta = Record<string, FunctionRuntimeMeta>;
|
|
149
163
|
export type FunctionsMeta = Record<string, FunctionMeta>;
|
|
@@ -3,15 +3,22 @@ export type TriggerSourceKind = 'webhook';
|
|
|
3
3
|
export type DeclaredTriggerSource = {
|
|
4
4
|
name: string;
|
|
5
5
|
kind: TriggerSourceKind;
|
|
6
|
+
/** Where the running app is served and how it labels provider endpoints: what enabling a source registers it with. */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
labelPrefix?: string;
|
|
6
9
|
};
|
|
7
10
|
/**
|
|
8
11
|
* What was registered with the provider for one declared trigger source.
|
|
9
12
|
* `state` is what the last `setup` returned — provider identifiers only; any
|
|
10
13
|
* secret it issued lives in the credential service.
|
|
11
14
|
*/
|
|
12
|
-
export type TriggerSourceRow = DeclaredTriggerSource & {
|
|
15
|
+
export type TriggerSourceRow = Required<Pick<DeclaredTriggerSource, 'name' | 'kind'>> & {
|
|
16
|
+
baseUrl: string | null;
|
|
17
|
+
labelPrefix: string | null;
|
|
13
18
|
/** False once the declaration has gone from code without a teardown: an orphan. */
|
|
14
19
|
declared: boolean;
|
|
20
|
+
/** Only an enabled source is registered with its provider or receives events. Off until someone turns it on. */
|
|
21
|
+
enabled: boolean;
|
|
15
22
|
status: string | null;
|
|
16
23
|
state: WebhookSourceState | null;
|
|
17
24
|
/** The last step's instructions or error. */
|
|
@@ -25,11 +32,12 @@ export type TriggerSourceResult = {
|
|
|
25
32
|
};
|
|
26
33
|
/** What each declared trigger source registered with its provider. */
|
|
27
34
|
export interface TriggerSourceStore {
|
|
28
|
-
/** Upserts the declared sources and marks the rest undeclared. */
|
|
35
|
+
/** Upserts the declared sources, keeping a known address when none is given, and marks the rest undeclared. */
|
|
29
36
|
syncTriggerSources(sources: DeclaredTriggerSource[]): Promise<void>;
|
|
30
37
|
listTriggerSources(): Promise<TriggerSourceRow[]>;
|
|
31
38
|
getTriggerSource(name: string): Promise<TriggerSourceRow | null>;
|
|
32
39
|
recordTriggerSource(name: string, result: TriggerSourceResult): Promise<void>;
|
|
40
|
+
setTriggerSourceEnabled(name: string, enabled: boolean): Promise<void>;
|
|
33
41
|
deleteTriggerSource(name: string): Promise<void>;
|
|
34
42
|
}
|
|
35
43
|
export declare class InMemoryTriggerSourceStore implements TriggerSourceStore {
|
|
@@ -38,8 +46,12 @@ export declare class InMemoryTriggerSourceStore implements TriggerSourceStore {
|
|
|
38
46
|
listTriggerSources(): Promise<{
|
|
39
47
|
name: string;
|
|
40
48
|
kind: TriggerSourceKind;
|
|
49
|
+
baseUrl: string | null;
|
|
50
|
+
labelPrefix: string | null;
|
|
41
51
|
/** False once the declaration has gone from code without a teardown: an orphan. */
|
|
42
52
|
declared: boolean;
|
|
53
|
+
/** Only an enabled source is registered with its provider or receives events. Off until someone turns it on. */
|
|
54
|
+
enabled: boolean;
|
|
43
55
|
status: string | null;
|
|
44
56
|
state: WebhookSourceState | null;
|
|
45
57
|
/** The last step's instructions or error. */
|
|
@@ -49,8 +61,12 @@ export declare class InMemoryTriggerSourceStore implements TriggerSourceStore {
|
|
|
49
61
|
getTriggerSource(name: string): Promise<{
|
|
50
62
|
name: string;
|
|
51
63
|
kind: TriggerSourceKind;
|
|
64
|
+
baseUrl: string | null;
|
|
65
|
+
labelPrefix: string | null;
|
|
52
66
|
/** False once the declaration has gone from code without a teardown: an orphan. */
|
|
53
67
|
declared: boolean;
|
|
68
|
+
/** Only an enabled source is registered with its provider or receives events. Off until someone turns it on. */
|
|
69
|
+
enabled: boolean;
|
|
54
70
|
status: string | null;
|
|
55
71
|
state: WebhookSourceState | null;
|
|
56
72
|
/** The last step's instructions or error. */
|
|
@@ -58,5 +74,6 @@ export declare class InMemoryTriggerSourceStore implements TriggerSourceStore {
|
|
|
58
74
|
updatedAt: string | null;
|
|
59
75
|
} | null>;
|
|
60
76
|
recordTriggerSource(name: string, result: TriggerSourceResult): Promise<void>;
|
|
77
|
+
setTriggerSourceEnabled(name: string, enabled: boolean): Promise<void>;
|
|
61
78
|
deleteTriggerSource(name: string): Promise<void>;
|
|
62
79
|
}
|
|
@@ -5,16 +5,23 @@ export class InMemoryTriggerSourceStore {
|
|
|
5
5
|
for (const row of this.rows.values()) {
|
|
6
6
|
row.declared = names.has(row.name);
|
|
7
7
|
}
|
|
8
|
-
for (const { name, kind } of sources) {
|
|
8
|
+
for (const { name, kind, baseUrl, labelPrefix } of sources) {
|
|
9
9
|
const row = this.rows.get(name);
|
|
10
10
|
if (row) {
|
|
11
11
|
row.kind = kind;
|
|
12
|
+
if (baseUrl !== undefined)
|
|
13
|
+
row.baseUrl = baseUrl;
|
|
14
|
+
if (labelPrefix !== undefined)
|
|
15
|
+
row.labelPrefix = labelPrefix;
|
|
12
16
|
continue;
|
|
13
17
|
}
|
|
14
18
|
this.rows.set(name, {
|
|
15
19
|
name,
|
|
16
20
|
kind,
|
|
21
|
+
baseUrl: baseUrl ?? null,
|
|
22
|
+
labelPrefix: labelPrefix ?? null,
|
|
17
23
|
declared: true,
|
|
24
|
+
enabled: false,
|
|
18
25
|
status: null,
|
|
19
26
|
state: null,
|
|
20
27
|
detail: null,
|
|
@@ -41,6 +48,12 @@ export class InMemoryTriggerSourceStore {
|
|
|
41
48
|
row.detail = result.detail ?? null;
|
|
42
49
|
row.updatedAt = new Date().toISOString();
|
|
43
50
|
}
|
|
51
|
+
async setTriggerSourceEnabled(name, enabled) {
|
|
52
|
+
const row = this.rows.get(name);
|
|
53
|
+
if (!row)
|
|
54
|
+
throw new Error(`Unknown trigger source: ${name}`);
|
|
55
|
+
row.enabled = enabled;
|
|
56
|
+
}
|
|
44
57
|
async deleteTriggerSource(name) {
|
|
45
58
|
this.rows.delete(name);
|
|
46
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { wireTrigger, wireTriggerSource } from './trigger-runner.js';
|
|
2
|
-
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, declaredTriggerSources, reconcileTriggerSources, teardownTriggerSources, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
|
-
export type { WebhookSourceOutcome } from './webhook-source-runner.js';
|
|
2
|
+
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, declaredTriggerSources, reconcileTriggerSources, enableTriggerSource, disableTriggerSource, reconcileWebhookRegistrations, teardownTriggerSources, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
|
+
export type { OrphanedWebhookRegistration, WebhookRegistration, WebhookRegistrations, WebhookSourceOutcome, } from './webhook-source-runner.js';
|
|
4
4
|
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME } from './webhook-source.types.js';
|
|
5
5
|
export type { CoreTriggerWebhookSource, TriggerEvent, WebhookCheckResult, WebhookLifecycleInput, WebhookReceiveResult, WebhookRequest, WebhookSetupResult, WebhookSourceJob, WebhookSourceMeta, WebhookSourceMethod, WebhookSourcesMeta, WebhookSourceState, WebhookTeardownInput, WebhookTeardownResult, } from './webhook-source.types.js';
|
|
6
6
|
export { PikkuTriggerService } from './pikku-trigger-service.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { wireTrigger, wireTriggerSource } from './trigger-runner.js';
|
|
2
|
-
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, declaredTriggerSources, reconcileTriggerSources, teardownTriggerSources, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
2
|
+
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, declaredTriggerSources, reconcileTriggerSources, enableTriggerSource, disableTriggerSource, reconcileWebhookRegistrations, teardownTriggerSources, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
3
|
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME } from './webhook-source.types.js';
|
|
4
4
|
export { PikkuTriggerService } from './pikku-trigger-service.js';
|
|
@@ -9,7 +9,8 @@ export declare const subscribedWebhookEvents: (source: string) => string[];
|
|
|
9
9
|
/**
|
|
10
10
|
* The body of a webhook source's route: `receive`, validate, and queue every
|
|
11
11
|
* event some trigger listens for. Events nobody listens for are answered and
|
|
12
|
-
* dropped, so the provider does not retry them forever.
|
|
12
|
+
* dropped, so the provider does not retry them forever. With a
|
|
13
|
+
* `triggerSourceStore`, a source nobody enabled answers 404.
|
|
13
14
|
*/
|
|
14
15
|
export declare const receiveWebhookSourceRequest: (sourceName: string, wire: {
|
|
15
16
|
http?: PikkuHTTP;
|
|
@@ -44,15 +45,61 @@ export declare const runWebhookSourceLifecycle: ({ action, previous, singletonSe
|
|
|
44
45
|
singletonServices?: CoreSingletonServices;
|
|
45
46
|
}) => Promise<WebhookSourceOutcome[]>;
|
|
46
47
|
/** The trigger sources this app declares, as the store is synced to. */
|
|
47
|
-
export declare const declaredTriggerSources: () => DeclaredTriggerSource[];
|
|
48
|
+
export declare const declaredTriggerSources: (address?: LifecycleInput) => DeclaredTriggerSource[];
|
|
48
49
|
/**
|
|
49
|
-
* Registers every
|
|
50
|
+
* Registers every enabled webhook source with its provider: `check`, then
|
|
50
51
|
* `setup` where it is missing or drifted, recording what was registered.
|
|
51
|
-
* Run after a deployment goes live.
|
|
52
|
+
* A source nobody enabled is skipped. Run after a deployment goes live.
|
|
52
53
|
*/
|
|
53
54
|
export declare const reconcileTriggerSources: ({ singletonServices, ...input }: LifecycleInput & {
|
|
54
55
|
singletonServices?: CoreSingletonServices;
|
|
55
56
|
}) => Promise<WebhookSourceOutcome[]>;
|
|
57
|
+
type SwitchInput = Partial<LifecycleInput> & {
|
|
58
|
+
name: string;
|
|
59
|
+
singletonServices?: CoreSingletonServices;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Turns a declared webhook source on and registers it with its provider, at
|
|
63
|
+
* the address given or the one the last deployment recorded.
|
|
64
|
+
*/
|
|
65
|
+
export declare const enableTriggerSource: ({ singletonServices, ...input }: SwitchInput) => Promise<WebhookSourceOutcome>;
|
|
66
|
+
/**
|
|
67
|
+
* Turns a webhook source off: it stops receiving at once, then is removed
|
|
68
|
+
* from its provider. A failed teardown leaves it off and records the error.
|
|
69
|
+
*/
|
|
70
|
+
export declare const disableTriggerSource: ({ singletonServices, ...input }: SwitchInput) => Promise<WebhookSourceOutcome>;
|
|
71
|
+
/** What one source registered with its provider, as a caller keeps it between runs. */
|
|
72
|
+
export type WebhookRegistration = {
|
|
73
|
+
url: string;
|
|
74
|
+
events: string[];
|
|
75
|
+
status: WebhookSourceOutcome['status'];
|
|
76
|
+
state?: WebhookSourceState;
|
|
77
|
+
/** The signing secrets `setup` stored, so a wiped credential store can be refilled without registering again. */
|
|
78
|
+
credentials?: Record<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
export type WebhookRegistrations = Record<string, WebhookRegistration>;
|
|
81
|
+
export type OrphanedWebhookRegistration = {
|
|
82
|
+
source: string;
|
|
83
|
+
url: string;
|
|
84
|
+
label: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Registers every declared webhook source against registrations the caller
|
|
88
|
+
* keeps rather than a `triggerSourceStore`, as `pikku dev` does in a file that
|
|
89
|
+
* outlives its database. A source whose url and events match what was
|
|
90
|
+
* registered is left alone without calling the provider, and its signing
|
|
91
|
+
* secrets are put back if the credential store lost them. Registrations no
|
|
92
|
+
* source declares any more are returned as orphans and kept, since only the
|
|
93
|
+
* code that set one up could tear it down.
|
|
94
|
+
*/
|
|
95
|
+
export declare const reconcileWebhookRegistrations: ({ registrations, singletonServices, ...input }: LifecycleInput & {
|
|
96
|
+
registrations: WebhookRegistrations;
|
|
97
|
+
singletonServices?: CoreSingletonServices;
|
|
98
|
+
}) => Promise<{
|
|
99
|
+
registrations: WebhookRegistrations;
|
|
100
|
+
outcomes: WebhookSourceOutcome[];
|
|
101
|
+
orphans: OrphanedWebhookRegistration[];
|
|
102
|
+
}>;
|
|
56
103
|
/**
|
|
57
104
|
* Removes the named webhook sources from their providers and forgets them.
|
|
58
105
|
* Run on the outgoing deployment for the sources the next release drops,
|
|
@@ -79,11 +79,16 @@ const validateEvents = async (source, events, logger) => {
|
|
|
79
79
|
/**
|
|
80
80
|
* The body of a webhook source's route: `receive`, validate, and queue every
|
|
81
81
|
* event some trigger listens for. Events nobody listens for are answered and
|
|
82
|
-
* dropped, so the provider does not retry them forever.
|
|
82
|
+
* dropped, so the provider does not retry them forever. With a
|
|
83
|
+
* `triggerSourceStore`, a source nobody enabled answers 404.
|
|
83
84
|
*/
|
|
84
85
|
export const receiveWebhookSourceRequest = async (sourceName, wire) => {
|
|
85
86
|
const singletonServices = getSingletonServices();
|
|
86
87
|
const meta = getSourceMeta(sourceName);
|
|
88
|
+
const store = singletonServices.triggerSourceStore;
|
|
89
|
+
if (store && !(await store.getTriggerSource(sourceName))?.enabled) {
|
|
90
|
+
return new Response(null, { status: 404 });
|
|
91
|
+
}
|
|
87
92
|
const source = pikkuState(null, 'trigger', 'webhookSources').get(sourceName);
|
|
88
93
|
const request = await readRequest(wire.http);
|
|
89
94
|
const result = meta.receive
|
|
@@ -152,9 +157,12 @@ export const dispatchWebhookSourceJob = async (job) => {
|
|
|
152
157
|
const recordAttempt = (singletonServices, receiptId, trigger, error) => singletonServices.incomingWebhookService
|
|
153
158
|
?.recordAttempt(receiptId, { trigger, ...(error ? { error } : {}) })
|
|
154
159
|
.catch((storeError) => singletonServices.logger.error(`Failed to record the attempt for webhook receipt ${receiptId}`, storeError));
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
const sourceAddress = (meta, { baseUrl, labelPrefix }) => ({
|
|
161
|
+
url: `${baseUrl.replace(/\/+$/, '')}${meta.route}`,
|
|
162
|
+
label: `${labelPrefix}:${meta.name}`,
|
|
163
|
+
});
|
|
164
|
+
const runSourceLifecycleStep = async (meta, action, lifecycleInput, previous, singletonServices) => {
|
|
165
|
+
const { url, label } = sourceAddress(meta, lifecycleInput);
|
|
158
166
|
const input = {
|
|
159
167
|
url,
|
|
160
168
|
label,
|
|
@@ -217,31 +225,165 @@ const requireTriggerSourceStore = (singletonServices) => {
|
|
|
217
225
|
return store;
|
|
218
226
|
};
|
|
219
227
|
/** The trigger sources this app declares, as the store is synced to. */
|
|
220
|
-
export const declaredTriggerSources = () => Object.keys(pikkuState(null, 'trigger', 'webhookSourceMeta')).map((name) => ({
|
|
228
|
+
export const declaredTriggerSources = (address) => Object.keys(pikkuState(null, 'trigger', 'webhookSourceMeta')).map((name) => ({
|
|
221
229
|
name,
|
|
222
230
|
kind: 'webhook',
|
|
231
|
+
...address,
|
|
223
232
|
}));
|
|
224
233
|
const detailOf = (outcome) => outcome.instructions ?? outcome.error ?? outcome.reason ?? null;
|
|
234
|
+
const registerTriggerSource = async (store, meta, input, previous, singletonServices) => {
|
|
235
|
+
const outcome = await runSourceLifecycleStep(meta, 'setup', input, previous, singletonServices);
|
|
236
|
+
await store.recordTriggerSource(meta.name, {
|
|
237
|
+
status: outcome.status,
|
|
238
|
+
...(outcome.state ? { state: outcome.state } : {}),
|
|
239
|
+
detail: detailOf(outcome),
|
|
240
|
+
});
|
|
241
|
+
return outcome;
|
|
242
|
+
};
|
|
225
243
|
/**
|
|
226
|
-
* Registers every
|
|
244
|
+
* Registers every enabled webhook source with its provider: `check`, then
|
|
227
245
|
* `setup` where it is missing or drifted, recording what was registered.
|
|
228
|
-
* Run after a deployment goes live.
|
|
246
|
+
* A source nobody enabled is skipped. Run after a deployment goes live.
|
|
229
247
|
*/
|
|
230
248
|
export const reconcileTriggerSources = async ({ singletonServices = getSingletonServices(), ...input }) => {
|
|
231
249
|
const store = requireTriggerSourceStore(singletonServices);
|
|
232
|
-
await store.syncTriggerSources(declaredTriggerSources(
|
|
250
|
+
await store.syncTriggerSources(declaredTriggerSources({
|
|
251
|
+
baseUrl: input.baseUrl,
|
|
252
|
+
labelPrefix: input.labelPrefix,
|
|
253
|
+
}));
|
|
233
254
|
const outcomes = [];
|
|
234
255
|
for (const meta of Object.values(pikkuState(null, 'trigger', 'webhookSourceMeta'))) {
|
|
235
256
|
const row = await store.getTriggerSource(meta.name);
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
if (!row?.enabled) {
|
|
258
|
+
outcomes.push({
|
|
259
|
+
source: meta.name,
|
|
260
|
+
url: sourceAddress(meta, input).url,
|
|
261
|
+
status: 'skipped',
|
|
262
|
+
reason: 'disabled',
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
outcomes.push(await registerTriggerSource(store, meta, input, row.state ?? undefined, singletonServices));
|
|
267
|
+
}
|
|
268
|
+
return outcomes;
|
|
269
|
+
};
|
|
270
|
+
/** The address given, or the one the last deployment synced onto the row. */
|
|
271
|
+
const switchAddress = (row, { baseUrl, labelPrefix, name }) => {
|
|
272
|
+
const address = {
|
|
273
|
+
baseUrl: baseUrl ?? row?.baseUrl,
|
|
274
|
+
labelPrefix: labelPrefix ?? row?.labelPrefix,
|
|
275
|
+
};
|
|
276
|
+
if (!address.baseUrl || !address.labelPrefix) {
|
|
277
|
+
throw new Error(`Trigger source '${name}' has no address yet: pass baseUrl and labelPrefix, or deploy so reconcile records them.`);
|
|
278
|
+
}
|
|
279
|
+
return address;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Turns a declared webhook source on and registers it with its provider, at
|
|
283
|
+
* the address given or the one the last deployment recorded.
|
|
284
|
+
*/
|
|
285
|
+
export const enableTriggerSource = async ({ singletonServices = getSingletonServices(), ...input }) => {
|
|
286
|
+
const store = requireTriggerSourceStore(singletonServices);
|
|
287
|
+
const meta = getSourceMeta(input.name);
|
|
288
|
+
await store.syncTriggerSources(declaredTriggerSources());
|
|
289
|
+
const row = await store.getTriggerSource(input.name);
|
|
290
|
+
const address = switchAddress(row, input);
|
|
291
|
+
await store.setTriggerSourceEnabled(input.name, true);
|
|
292
|
+
return registerTriggerSource(store, meta, address, row?.state ?? undefined, singletonServices);
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Turns a webhook source off: it stops receiving at once, then is removed
|
|
296
|
+
* from its provider. A failed teardown leaves it off and records the error.
|
|
297
|
+
*/
|
|
298
|
+
export const disableTriggerSource = async ({ singletonServices = getSingletonServices(), ...input }) => {
|
|
299
|
+
const store = requireTriggerSourceStore(singletonServices);
|
|
300
|
+
const meta = getSourceMeta(input.name);
|
|
301
|
+
const row = await store.getTriggerSource(input.name);
|
|
302
|
+
const address = switchAddress(row, input);
|
|
303
|
+
await store.setTriggerSourceEnabled(input.name, false);
|
|
304
|
+
const outcome = await runSourceLifecycleStep(meta, 'teardown', address, row?.state ?? undefined, singletonServices);
|
|
305
|
+
await store.recordTriggerSource(input.name, {
|
|
306
|
+
status: outcome.status,
|
|
307
|
+
...(outcome.status === 'failed' ? {} : { state: null }),
|
|
308
|
+
detail: detailOf(outcome),
|
|
309
|
+
});
|
|
310
|
+
return outcome;
|
|
311
|
+
};
|
|
312
|
+
/** The singleton credentials the package owning a source's `setup` declares: where its signing secret goes. */
|
|
313
|
+
const sourceCredentialNames = (meta) => {
|
|
314
|
+
if (!meta.setup)
|
|
315
|
+
return [];
|
|
316
|
+
const namespace = meta.setup.includes(':') ? meta.setup.split(':')[0] : '';
|
|
317
|
+
const packageName = pikkuState(null, 'addons', 'packages').get(namespace)?.package ?? null;
|
|
318
|
+
const declared = pikkuState(packageName, 'package', 'credentialsMeta') ?? {};
|
|
319
|
+
return Object.entries(declared)
|
|
320
|
+
.filter(([, credential]) => credential.type === 'singleton' && !credential.oauth2)
|
|
321
|
+
.map(([name]) => name);
|
|
322
|
+
};
|
|
323
|
+
const sameEvents = (a, b) => a.length === b.length && a.every((event) => b.includes(event));
|
|
324
|
+
/**
|
|
325
|
+
* Registers every declared webhook source against registrations the caller
|
|
326
|
+
* keeps rather than a `triggerSourceStore`, as `pikku dev` does in a file that
|
|
327
|
+
* outlives its database. A source whose url and events match what was
|
|
328
|
+
* registered is left alone without calling the provider, and its signing
|
|
329
|
+
* secrets are put back if the credential store lost them. Registrations no
|
|
330
|
+
* source declares any more are returned as orphans and kept, since only the
|
|
331
|
+
* code that set one up could tear it down.
|
|
332
|
+
*/
|
|
333
|
+
export const reconcileWebhookRegistrations = async ({ registrations, singletonServices = getSingletonServices(), ...input }) => {
|
|
334
|
+
const credentialService = singletonServices.credentialService;
|
|
335
|
+
const sources = pikkuState(null, 'trigger', 'webhookSourceMeta');
|
|
336
|
+
const next = {};
|
|
337
|
+
const outcomes = [];
|
|
338
|
+
for (const meta of Object.values(sources)) {
|
|
339
|
+
const { url } = sourceAddress(meta, input);
|
|
340
|
+
const events = subscribedWebhookEvents(meta.name);
|
|
341
|
+
const previous = registrations[meta.name];
|
|
342
|
+
const names = sourceCredentialNames(meta);
|
|
343
|
+
if (credentialService) {
|
|
344
|
+
for (const [name, value] of Object.entries(previous?.credentials ?? {})) {
|
|
345
|
+
if (!(await credentialService.has(name))) {
|
|
346
|
+
await credentialService.set(name, value);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (previous &&
|
|
351
|
+
previous.status !== 'failed' &&
|
|
352
|
+
previous.url === url &&
|
|
353
|
+
sameEvents(previous.events, events)) {
|
|
354
|
+
next[meta.name] = previous;
|
|
355
|
+
outcomes.push({ source: meta.name, url, status: 'unchanged' });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const outcome = await runSourceLifecycleStep(meta, 'setup', input, previous?.state, singletonServices);
|
|
359
|
+
const credentials = {};
|
|
360
|
+
for (const name of names) {
|
|
361
|
+
const value = await credentialService?.get(name);
|
|
362
|
+
if (value != null)
|
|
363
|
+
credentials[name] = value;
|
|
364
|
+
}
|
|
365
|
+
const state = outcome.state ?? previous?.state;
|
|
366
|
+
next[meta.name] = {
|
|
367
|
+
url,
|
|
368
|
+
events,
|
|
238
369
|
status: outcome.status,
|
|
239
|
-
...(
|
|
240
|
-
|
|
241
|
-
}
|
|
370
|
+
...(state ? { state } : {}),
|
|
371
|
+
...(Object.keys(credentials).length ? { credentials } : {}),
|
|
372
|
+
};
|
|
242
373
|
outcomes.push(outcome);
|
|
243
374
|
}
|
|
244
|
-
|
|
375
|
+
const orphans = [];
|
|
376
|
+
for (const [source, registration] of Object.entries(registrations)) {
|
|
377
|
+
if (sources[source])
|
|
378
|
+
continue;
|
|
379
|
+
next[source] = registration;
|
|
380
|
+
orphans.push({
|
|
381
|
+
source,
|
|
382
|
+
url: registration.url,
|
|
383
|
+
label: `${input.labelPrefix}:${source}`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return { registrations: next, outcomes, orphans };
|
|
245
387
|
};
|
|
246
388
|
/**
|
|
247
389
|
* Removes the named webhook sources from their providers and forgets them.
|
|
@@ -385,15 +385,15 @@ export class PikkuWorkflowService {
|
|
|
385
385
|
const shouldInline = options?.inline || !getSingletonServices()?.queueService;
|
|
386
386
|
return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire, workflowMeta);
|
|
387
387
|
}
|
|
388
|
-
const registrations = pikkuState(packageName, 'workflows', 'registrations');
|
|
389
|
-
const workflow = registrations.get(resolved?.resolvedName ?? name);
|
|
390
|
-
if (!workflow) {
|
|
391
|
-
throw new WorkflowNotFoundError(name);
|
|
392
|
-
}
|
|
393
388
|
if (!workflowMeta.graphHash) {
|
|
394
389
|
throw new Error(`Missing workflow graphHash for '${name}'`);
|
|
395
390
|
}
|
|
396
391
|
const shouldInline = options?.inline || !getSingletonServices()?.queueService;
|
|
392
|
+
// A queued run needs only the meta; the orchestrator's unit runs it.
|
|
393
|
+
const registrations = pikkuState(packageName, 'workflows', 'registrations');
|
|
394
|
+
if (shouldInline && !registrations.get(resolved?.resolvedName ?? name)) {
|
|
395
|
+
throw new WorkflowNotFoundError(name);
|
|
396
|
+
}
|
|
397
397
|
const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire, {
|
|
398
398
|
deterministic: workflowMeta.deterministic,
|
|
399
399
|
plannedSteps: workflowMeta.plannedSteps,
|
|
@@ -26,9 +26,10 @@ rotating it invalidates every credential at once.
|
|
|
26
26
|
|
|
27
27
|
This is what lets a holder be handed less than everything:
|
|
28
28
|
|
|
29
|
-
- The browser switcher gets
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
- The browser switcher gets no credential at all. It used to be handed
|
|
30
|
+
`VITE_DEV_ACTOR_SECRETS`, one per persona; it now names a persona by id at
|
|
31
|
+
`/sign-in/persona`, which the server gates with the actor opt-in and the
|
|
32
|
+
`devSwitcher` flag, so a bundle holds nothing worth taking.
|
|
32
33
|
- A run can be given `PIKKU_PERSONA_SECRETS` (`id=secret,…`, minted with
|
|
33
34
|
`pikku persona secret`) instead of the root, and then it can sign in as those
|
|
34
35
|
personas and no others. Asking for one outside the list throws naming the
|
package/package.json
CHANGED
package/src/public-surface.json
CHANGED
|
@@ -183,9 +183,12 @@
|
|
|
183
183
|
"PIKKU_INCOMING_WEBHOOK_QUEUE_NAME",
|
|
184
184
|
"PikkuTriggerService",
|
|
185
185
|
"declaredTriggerSources",
|
|
186
|
+
"disableTriggerSource",
|
|
186
187
|
"dispatchWebhookSourceJob",
|
|
188
|
+
"enableTriggerSource",
|
|
187
189
|
"receiveWebhookSourceRequest",
|
|
188
190
|
"reconcileTriggerSources",
|
|
191
|
+
"reconcileWebhookRegistrations",
|
|
189
192
|
"runWebhookSourceLifecycle",
|
|
190
193
|
"subscribedWebhookEvents",
|
|
191
194
|
"teardownTriggerSources",
|