@pikku/core 0.12.128 → 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 +22 -0
- package/dist/function/function-meta.types.d.ts +14 -0
- package/dist/function/function-runner.d.ts +6 -1
- package/dist/function/function-runner.js +18 -14
- package/dist/services/trigger-source-store.d.ts +19 -2
- package/dist/services/trigger-source-store.js +14 -1
- package/dist/wirings/channel/channel-common.d.ts +3 -1
- package/dist/wirings/channel/channel-common.js +2 -1
- package/dist/wirings/channel/channel-handler.d.ts +1 -1
- package/dist/wirings/channel/channel-handler.js +2 -1
- package/dist/wirings/channel/local/local-channel-runner.js +3 -1
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -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/internals/index.md +1 -0
- package/knowledge/decisions/internals/the-runner-resolves-wire-services-itself.md +31 -0
- 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,25 @@
|
|
|
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
|
+
|
|
17
|
+
## 0.12.129
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 1ab6eb4: Fix app functions reached through `rpc.invoke`/`rpc.exposed` (including the generated `/rpc/:rpcName` endpoint) running without their wire services. Since `singletonServicesOnly`, the forwarding `rpcCaller` built none and the callee inherited that, so any service from `pikkuWireServices` was `undefined`. The runner now resolves the callee's factory itself. Channels pass their per-connection set as the new `wireServices` option, which is reused and left open.
|
|
22
|
+
|
|
1
23
|
## 0.12.128
|
|
2
24
|
|
|
3
25
|
### 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>;
|
|
@@ -8,9 +8,14 @@ import type { AddonInstance } from '../wirings/addon/addon-runner.js';
|
|
|
8
8
|
export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any, any, any, any>, packageName?: string | null) => void;
|
|
9
9
|
export declare const getFunctionNames: (packageName?: string | null) => string[];
|
|
10
10
|
export declare const getAllFunctionNames: () => string[];
|
|
11
|
-
export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, coerceDataFromSchema, wire, sessionService, credentialWireService, packageName, addonInstance, }: {
|
|
11
|
+
export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, wireServices: providedWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, coerceDataFromSchema, wire, sessionService, credentialWireService, packageName, addonInstance, }: {
|
|
12
12
|
singletonServices: CoreSingletonServices;
|
|
13
13
|
createWireServices?: CreateWireServices;
|
|
14
|
+
/**
|
|
15
|
+
* Wire services the caller already built and still owns, such as a
|
|
16
|
+
* channel's per-connection set. Used as-is and never closed here.
|
|
17
|
+
*/
|
|
18
|
+
wireServices?: Record<string, unknown>;
|
|
14
19
|
data: () => Promise<In> | In;
|
|
15
20
|
auth?: boolean;
|
|
16
21
|
inheritedMiddleware?: MiddlewareMetadata[];
|
|
@@ -3,7 +3,7 @@ import { runMiddleware, combineMiddleware } from '../middleware-runner.js';
|
|
|
3
3
|
import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../wirings/channel/channel-middleware-runner.js';
|
|
4
4
|
import { runPermissions } from '../permissions.js';
|
|
5
5
|
import { withoutSecrets } from '../services/secretless.js';
|
|
6
|
-
import { pikkuState } from '../pikku-state.js';
|
|
6
|
+
import { getCreateWireServices, pikkuState } from '../pikku-state.js';
|
|
7
7
|
import { applyDefaultsFromSchema, coerceTopLevelDataFromSchema, validateSchema, } from '../schema.js';
|
|
8
8
|
import { parseVersionedId } from '../version.js';
|
|
9
9
|
import { PikkuSessionService } from '../services/user-session-service.js';
|
|
@@ -64,7 +64,7 @@ export const getAllFunctionNames = () => {
|
|
|
64
64
|
}
|
|
65
65
|
return functions;
|
|
66
66
|
};
|
|
67
|
-
export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, coerceDataFromSchema, wire, sessionService, credentialWireService, packageName = null, addonInstance, }) => {
|
|
67
|
+
export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServices, createWireServices, wireServices: providedWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, coerceDataFromSchema, wire, sessionService, credentialWireService, packageName = null, addonInstance, }) => {
|
|
68
68
|
wire.wireType ??= wireType;
|
|
69
69
|
wire.wireId ??= wireId;
|
|
70
70
|
const funcMap = pikkuState(packageName, 'function', 'functions');
|
|
@@ -118,13 +118,14 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
118
118
|
const resolvedSingletonServices = funcPackageName
|
|
119
119
|
? await getOrCreatePackageSingletonServices(funcPackageName, singletonServices, addonInstance)
|
|
120
120
|
: singletonServices;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
// knowledge: decisions/internals/the-runner-resolves-wire-services-itself.md
|
|
122
|
+
const packageCreateWireServices = funcPackageName
|
|
123
|
+
? pikkuState(funcPackageName, 'package', 'factories')?.createWireServices
|
|
124
|
+
: undefined;
|
|
125
|
+
const reusedWireServices = packageCreateWireServices
|
|
126
|
+
? undefined
|
|
127
|
+
: providedWireServices;
|
|
128
|
+
const resolvedCreateWireServices = packageCreateWireServices ?? createWireServices ?? getCreateWireServices();
|
|
128
129
|
const allChannelMiddleware = combineChannelMiddleware(wireType, wireId, {
|
|
129
130
|
wireInheritedChannelMiddleware: inheritedChannelMiddleware,
|
|
130
131
|
wireChannelMiddleware,
|
|
@@ -256,11 +257,14 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
256
257
|
let invocationAuditLog;
|
|
257
258
|
let invocationAnalytics;
|
|
258
259
|
try {
|
|
259
|
-
|
|
260
|
+
if (!funcMeta.singletonServicesOnly && !reusedWireServices) {
|
|
261
|
+
wireServices = (await resolvedCreateWireServices?.(resolvedSingletonServices, invocationWire));
|
|
262
|
+
}
|
|
263
|
+
const invocationWireServices = funcMeta.singletonServicesOnly
|
|
260
264
|
? undefined
|
|
261
|
-
: (
|
|
262
|
-
let services =
|
|
263
|
-
? { ...resolvedSingletonServices, ...
|
|
265
|
+
: (reusedWireServices ?? wireServices);
|
|
266
|
+
let services = invocationWireServices && Object.keys(invocationWireServices).length > 0
|
|
267
|
+
? { ...resolvedSingletonServices, ...invocationWireServices }
|
|
264
268
|
: resolvedSingletonServices;
|
|
265
269
|
// knowledge: decisions/internals/core-function-runner-restores-the-wire-fields-it-overwrites.md
|
|
266
270
|
if (resolvedAuditConfig &&
|
|
@@ -286,7 +290,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
286
290
|
const callerPackageName = funcPackageName;
|
|
287
291
|
Object.defineProperty(invocationWire, 'rpc', {
|
|
288
292
|
get() {
|
|
289
|
-
const rpc = rpcService.getContextRPCService(
|
|
293
|
+
const rpc = rpcService.getContextRPCService(resolvedSingletonServices, invocationWire, { sessionService }, 0, callerPackageName);
|
|
290
294
|
Object.defineProperty(invocationWire, 'rpc', {
|
|
291
295
|
value: rpc,
|
|
292
296
|
writable: true,
|
|
@@ -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
|
}
|
|
@@ -2,7 +2,7 @@ import type { CoreSingletonServices } from '../../types/core.types.js';
|
|
|
2
2
|
import type { MiddlewareMetadata } from '../../middleware/middleware.types.js';
|
|
3
3
|
import type { CoreChannel, ChannelMessageMeta } from './channel.types.js';
|
|
4
4
|
import { type PikkuSessionService } from '../../services/user-session-service.js';
|
|
5
|
-
export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, }: {
|
|
5
|
+
export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, }: {
|
|
6
6
|
channelConfig: CoreChannel<unknown, any>;
|
|
7
7
|
meta: ChannelMessageMeta;
|
|
8
8
|
lifecycleConfig: any;
|
|
@@ -16,4 +16,6 @@ export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta,
|
|
|
16
16
|
* just connected.
|
|
17
17
|
*/
|
|
18
18
|
userSession?: PikkuSessionService<any>;
|
|
19
|
+
/** The connection's wire services, reused rather than rebuilt per call. */
|
|
20
|
+
wireServices?: Record<string, unknown>;
|
|
19
21
|
}) => Promise<unknown>;
|
|
@@ -2,7 +2,7 @@ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
|
|
|
2
2
|
import { runPikkuFunc } from '../../function/function-runner.js';
|
|
3
3
|
import { createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
|
|
4
4
|
import { combineChannelMiddleware, wrapChannelWithMiddleware, } from './channel-middleware-runner.js';
|
|
5
|
-
export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, }) => {
|
|
5
|
+
export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, }) => {
|
|
6
6
|
const lifecycleMiddleware = typeof lifecycleConfig === 'object' && 'middleware' in lifecycleConfig
|
|
7
7
|
? lifecycleConfig.middleware || []
|
|
8
8
|
: [];
|
|
@@ -25,6 +25,7 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
|
|
|
25
25
|
const runLifecycle = async () => {
|
|
26
26
|
return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
|
|
27
27
|
singletonServices: services,
|
|
28
|
+
wireServices,
|
|
28
29
|
data: () => data,
|
|
29
30
|
wire,
|
|
30
31
|
sessionService: userSession,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CoreServices, CoreUserSession } from '../../types/core.types.js';
|
|
2
2
|
import type { BinaryData, CoreChannel, PikkuChannelHandler } from './channel.types.js';
|
|
3
3
|
import type { SessionService } from '../../services/user-session-service.js';
|
|
4
|
-
export declare const processMessageHandlers: (services: CoreServices, channelConfig: CoreChannel<any, any>, channelHandler: PikkuChannelHandler, userSession?: SessionService<CoreUserSession>) => {
|
|
4
|
+
export declare const processMessageHandlers: (services: CoreServices, channelConfig: CoreChannel<any, any>, channelHandler: PikkuChannelHandler, userSession?: SessionService<CoreUserSession>, wireServices?: Record<string, unknown>) => {
|
|
5
5
|
onMessage: (rawData: unknown) => Promise<unknown>;
|
|
6
6
|
onBinaryMessage: ((data: BinaryData) => Promise<BinaryData | void>) | undefined;
|
|
7
7
|
};
|
|
@@ -37,7 +37,7 @@ const validateAuth = (requiresSession, session, onMessage) => {
|
|
|
37
37
|
}
|
|
38
38
|
return true;
|
|
39
39
|
};
|
|
40
|
-
export const processMessageHandlers = (services, channelConfig, channelHandler, userSession) => {
|
|
40
|
+
export const processMessageHandlers = (services, channelConfig, channelHandler, userSession, wireServices) => {
|
|
41
41
|
const logger = services.logger;
|
|
42
42
|
const requiresSession = channelConfig.auth !== false;
|
|
43
43
|
const processMessage = async (data, onMessage, routingProperty, routerValue) => {
|
|
@@ -76,6 +76,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
|
|
|
76
76
|
const wireChannelMiddleware = channelConfig.channelMiddleware || [];
|
|
77
77
|
return await runPikkuFunc('channel', cacheKey, pikkuFuncId, {
|
|
78
78
|
singletonServices: services,
|
|
79
|
+
wireServices,
|
|
79
80
|
data: () => data,
|
|
80
81
|
inheritedMiddleware,
|
|
81
82
|
wireMiddleware,
|
|
@@ -89,6 +89,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
89
89
|
data: openingData,
|
|
90
90
|
channelMiddlewareMeta: meta.channelMiddleware,
|
|
91
91
|
userSession,
|
|
92
|
+
wireServices,
|
|
92
93
|
});
|
|
93
94
|
if (result !== undefined) {
|
|
94
95
|
await channel.send(result);
|
|
@@ -117,6 +118,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
117
118
|
channel,
|
|
118
119
|
channelMiddlewareMeta: meta.channelMiddleware,
|
|
119
120
|
userSession,
|
|
121
|
+
wireServices,
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
catch (e) {
|
|
@@ -131,7 +133,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
131
133
|
}
|
|
132
134
|
await closeServices();
|
|
133
135
|
});
|
|
134
|
-
const { onMessage, onBinaryMessage } = processMessageHandlers(services, channelConfig, channelHandler, userSession);
|
|
136
|
+
const { onMessage, onBinaryMessage } = processMessageHandlers(services, channelConfig, channelHandler, userSession, wireServices);
|
|
135
137
|
channelHandler.registerOnMessage(async (data) => {
|
|
136
138
|
try {
|
|
137
139
|
const result = await onMessage(data);
|
|
@@ -80,6 +80,7 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
|
|
|
80
80
|
data: openingData,
|
|
81
81
|
channelMiddlewareMeta: meta.channelMiddleware,
|
|
82
82
|
userSession,
|
|
83
|
+
wireServices,
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
86
|
const pikkuUserId = userSession.getPikkuUserId();
|
|
@@ -150,6 +151,7 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
|
|
|
150
151
|
channel,
|
|
151
152
|
channelMiddlewareMeta: meta.channelMiddleware,
|
|
152
153
|
userSession,
|
|
154
|
+
wireServices,
|
|
153
155
|
});
|
|
154
156
|
}
|
|
155
157
|
catch (e) {
|
|
@@ -197,7 +199,7 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
|
|
|
197
199
|
};
|
|
198
200
|
let response;
|
|
199
201
|
try {
|
|
200
|
-
const { onMessage } = processMessageHandlers(services, channelConfig, channelHandler, userSession);
|
|
202
|
+
const { onMessage } = processMessageHandlers(services, channelConfig, channelHandler, userSession, wireServices);
|
|
201
203
|
response = await onMessage(data);
|
|
202
204
|
}
|
|
203
205
|
catch (e) {
|
|
@@ -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,
|
|
@@ -62,6 +62,7 @@ caller is entitled to assume.
|
|
|
62
62
|
- [A column's at-rest form is an axis of its own](core-column-form-is-an-axis-of-its-own.md) — How a value is stored is independent of how sensitive it is, so form carries a required nominal brand on writes while classification stays optional on reads
|
|
63
63
|
- [The data-classification brand is an optional property](core-data-classification-brand-is-an-optional-property.md) — Making **classification** required would break ordinary Kysely operands, so the brand only constrains values flowing out
|
|
64
64
|
- [The function runner restores the wire fields it overwrites](core-function-runner-restores-the-wire-fields-it-overwrites.md) — One wire object is reused across nested calls, so functionId, audit, addonNamespace and rpc are saved and put back in a finally
|
|
65
|
+
- [The function runner resolves wire services itself](the-runner-resolves-wire-services-itself.md) — A function's wire services come from its own package's factory or the app's registered one, never borrowed from whoever called it
|
|
65
66
|
- [Hot reload merges generated meta and never replaces it](core-hot-reload-merges-generated-meta-never-replaces-it.md) — Reloading codegen output must preserve runtime-registered meta, which no generated JSON contains
|
|
66
67
|
- [Hot reload owns its module registry instead of re-importing](core-hot-reload-owns-its-module-registry.md) — Dev reload transpiles to CJS and runs modules through vm.compileFunction, because the native ESM loader map cannot be evicted
|
|
67
68
|
- [Middleware order is resolution scope first, then priority](core-middleware-order-is-scope-then-priority.md) — Middleware is collected global to function, then stably sorted by priority, deduped, frozen and cached per wire
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The function runner resolves wire services itself
|
|
4
|
+
description: A function's wire services come from its own package's factory or the app's registered one, never borrowed from whoever called it
|
|
5
|
+
tags: core
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The function runner resolves wire services itself
|
|
9
|
+
|
|
10
|
+
`runPikkuFunc` picks the wire-services factory for the function it is about to
|
|
11
|
+
run: the function's addon package factory if it has one, else a factory the
|
|
12
|
+
transport passed explicitly, else the app's registered one
|
|
13
|
+
(`getCreateWireServices()`). A caller that passes nothing still gets the
|
|
14
|
+
function its wire services.
|
|
15
|
+
|
|
16
|
+
Before this, only the transports passed a factory. `rpc.invoke`, `rpc.exposed`,
|
|
17
|
+
gateway handlers and webhook sources passed none, so an app function reached
|
|
18
|
+
through them ran on whatever services its caller had already merged. That held
|
|
19
|
+
until `singletonServicesOnly` let a caller skip building wire services: the
|
|
20
|
+
generated `rpcCaller` behind `/rpc/:rpcName` reads no service, was marked, and
|
|
21
|
+
every app function behind it lost `events` and every other wire service. Nested
|
|
22
|
+
RPC therefore gets `resolvedSingletonServices`, not the caller's merged set.
|
|
23
|
+
|
|
24
|
+
Channels are the one caller that legitimately shares wire services: they build
|
|
25
|
+
one set per connection. They hand it over as `wireServices`, which the runner
|
|
26
|
+
uses as-is and does not close, because the connection owns its lifetime.
|
|
27
|
+
|
|
28
|
+
**What this rules out:** passing a caller's merged services down as
|
|
29
|
+
`singletonServices` to carry wire services into a nested call, and deciding in
|
|
30
|
+
one function's meta whether a different function gets wire services.
|
|
31
|
+
`singletonServicesOnly` only ever describes the function it is set on.
|
|
@@ -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",
|