@pikku/core 0.12.92 → 0.12.93
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 +67 -0
- package/dist/errors/error-handler.d.ts +11 -0
- package/dist/errors/error-handler.js +8 -0
- package/dist/errors/serialized-error.d.ts +3 -0
- package/dist/function/function-runner.d.ts +1 -1
- package/dist/function/function-runner.js +3 -3
- package/dist/function/functions.types.d.ts +33 -21
- package/dist/middleware/auth-apikey.d.ts +7 -0
- package/dist/middleware/auth-apikey.js +6 -0
- package/dist/middleware/auth-bearer.d.ts +3 -0
- package/dist/middleware/auth-bearer.js +2 -0
- package/dist/middleware/auth-cookie.d.ts +9 -0
- package/dist/middleware/auth-cookie.js +6 -4
- package/dist/middleware/cors.d.ts +8 -0
- package/dist/middleware/cors.js +2 -0
- package/dist/middleware/middleware-factories.d.ts +14 -0
- package/dist/middleware/middleware-factories.js +14 -0
- package/dist/services/local-secrets.js +1 -1
- package/dist/services/personas-service.d.ts +4 -0
- package/dist/services/personas-service.js +4 -0
- package/dist/services/secret-service.d.ts +0 -1
- package/dist/testing/service-tests/secret-service-tests.js +1 -1
- package/dist/types/core.types.d.ts +5 -0
- package/dist/wirings/addon/wire-addon.d.ts +17 -15
- package/dist/wirings/addon/wire-addon.js +6 -0
- package/dist/wirings/addon/wire-remote-addon.d.ts +4 -0
- package/dist/wirings/addon/wire-remote-addon.js +4 -0
- package/dist/wirings/agent/agent-prepare.js +2 -2
- package/dist/wirings/agent/agent.types.d.ts +31 -27
- package/dist/wirings/agent-scorer/agent-scorer.d.ts +4 -0
- package/dist/wirings/agent-scorer/agent-scorer.js +4 -0
- package/dist/wirings/channel/channel.types.d.ts +11 -0
- package/dist/wirings/cli/cli.types.d.ts +43 -1
- package/dist/wirings/cli/define-cli-commands.d.ts +6 -1
- package/dist/wirings/cli/define-cli-commands.js +6 -1
- package/dist/wirings/credential/credential.types.d.ts +7 -0
- package/dist/wirings/gateway/gateway.types.d.ts +23 -0
- package/dist/wirings/http/http.types.d.ts +51 -37
- package/dist/wirings/mcp/mcp.types.d.ts +27 -0
- package/dist/wirings/queue/queue.types.d.ts +6 -0
- package/dist/wirings/scheduler/scheduler.types.d.ts +5 -0
- package/dist/wirings/secret/secret.types.d.ts +9 -14
- package/dist/wirings/secret/secret.types.js +7 -0
- package/dist/wirings/trigger/trigger.types.d.ts +4 -1
- package/dist/wirings/variable/variable.types.d.ts +14 -13
- package/dist/wirings/variable/variable.types.js +7 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +7 -0
- package/dist/wirings/workflow/scenario-cookie-jar.d.ts +6 -0
- package/dist/wirings/workflow/scenario-cookie-jar.js +6 -0
- package/dist/wirings/workflow/scenario-poll.d.ts +6 -0
- package/dist/wirings/workflow/scenario-poll.js +6 -0
- package/dist/wirings/workflow/scenario-step-guards.d.ts +6 -0
- package/dist/wirings/workflow/scenario-step-guards.js +6 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +4 -0
- package/dist/wirings/workflow/workflow-errors.js +4 -0
- package/package.json +1 -1
- package/src/app-leaf-surface.test.ts +2 -2
- package/src/errors/error-handler.ts +11 -0
- package/src/errors/serialized-error.ts +3 -0
- package/src/function/function-runner.ts +4 -4
- package/src/function/functions.types.ts +33 -21
- package/src/middleware/auth-apikey.ts +7 -0
- package/src/middleware/auth-bearer.ts +3 -0
- package/src/middleware/auth-cookie.ts +9 -4
- package/src/middleware/cors.ts +8 -0
- package/src/middleware/middleware-factories.ts +14 -0
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +1 -1
- package/src/services/personas-service.ts +4 -0
- package/src/services/secret-service.ts +0 -1
- package/src/testing/service-tests/secret-service-tests.ts +1 -1
- package/src/types/core.types.ts +5 -0
- package/src/wirings/addon/addon-auth-tags.test.ts +4 -4
- package/src/wirings/addon/wire-addon.ts +17 -15
- package/src/wirings/addon/wire-remote-addon.ts +4 -0
- package/src/wirings/agent/agent-authorization.test.ts +7 -3
- package/src/wirings/agent/agent-prepare.ts +2 -2
- package/src/wirings/agent/agent.types.ts +31 -27
- package/src/wirings/agent-scorer/agent-scorer.ts +4 -0
- package/src/wirings/channel/channel.types.ts +11 -0
- package/src/wirings/cli/cli.types.ts +44 -1
- package/src/wirings/cli/define-cli-commands.ts +6 -1
- package/src/wirings/credential/credential.types.ts +7 -0
- package/src/wirings/gateway/gateway-authorization.test.ts +3 -3
- package/src/wirings/gateway/gateway.types.ts +23 -0
- package/src/wirings/http/http-runner-addon-ref.test.ts +1 -1
- package/src/wirings/http/http.types.ts +94 -69
- package/src/wirings/mcp/mcp.types.ts +27 -0
- package/src/wirings/queue/queue.types.ts +6 -0
- package/src/wirings/scheduler/scheduler.types.ts +5 -0
- package/src/wirings/secret/secret.types.ts +9 -14
- package/src/wirings/trigger/trigger.types.ts +4 -1
- package/src/wirings/variable/variable.types.ts +14 -13
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/scenario-cookie-jar.ts +6 -0
- package/src/wirings/workflow/scenario-poll.ts +6 -0
- package/src/wirings/workflow/scenario-step-guards.ts +6 -0
- package/src/wirings/workflow/workflow-errors.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -7,10 +7,15 @@ export type ScheduledTasksMeta<UserSession extends CoreUserSession = any> = Reco
|
|
|
7
7
|
session?: UserSession;
|
|
8
8
|
}>;
|
|
9
9
|
export type CoreScheduledTask<PikkuFunctionConfig = CorePikkuFunctionConfig<CorePikkuFunctionSessionless<void, void>>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
|
|
10
|
+
/** Unique across the project. It is how the task is addressed in logs, in `pikku meta`, and by a scheduler service asked to run it now. */
|
|
10
11
|
name: string;
|
|
12
|
+
/** A five-field cron expression: minute, hour, day of month, month, day of week. `0 9 * * 1` is 09:00 every Monday. Interpreted in the deployment's timezone, not the author's. */
|
|
11
13
|
schedule: string;
|
|
14
|
+
/** The function to run. It receives no session and no input: a scheduled task has no caller, so it must be sessionless. */
|
|
12
15
|
func: PikkuFunctionConfig;
|
|
16
|
+
/** Filters this task in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
13
17
|
tags?: string[];
|
|
18
|
+
/** Wraps every execution. There is no request to read from, so this is for tracing, locking and teardown rather than auth. */
|
|
14
19
|
middleware?: PikkuMiddleware[];
|
|
15
20
|
};
|
|
16
21
|
export interface PikkuScheduledTask {
|
|
@@ -4,21 +4,9 @@ export type CoreSecret<T = unknown> = {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
secretId: string;
|
|
6
6
|
schema: T;
|
|
7
|
-
/**
|
|
8
|
-
* A secret is REQUIRED by default because `getSecret` throws when the value is
|
|
9
|
-
* missing — a deploy without it is a runtime failure waiting to happen, so the
|
|
10
|
-
* deploy gate refuses it. Mark a secret `optional` when absence is a supported
|
|
11
|
-
* state the app handles (a feature that simply stays off): the generated map
|
|
12
|
-
* emits it as an optional property and `getSecret` resolves `undefined`
|
|
13
|
-
* instead of throwing.
|
|
14
|
-
*/
|
|
7
|
+
/** Required by default: this says absence is a supported state, and `getSecret` resolves `undefined` rather than throwing. */
|
|
15
8
|
optional?: boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Where a user goes to obtain this value — a provider's API-key page, a setup
|
|
18
|
-
* guide, an internal runbook. Consoles and deploy UIs surface it beside a
|
|
19
|
-
* missing value. The same field on every credential/variable definition means
|
|
20
|
-
* the same thing.
|
|
21
|
-
*/
|
|
9
|
+
/** Where a user goes to obtain this value, surfaced beside a missing one. */
|
|
22
10
|
docsUrl?: string;
|
|
23
11
|
/** Rotation cadence as a duration string, e.g. `'1d'`, `'30day'`, `'1w'`. */
|
|
24
12
|
rotationPeriod?: string;
|
|
@@ -59,4 +47,11 @@ export type SecretDefinitionMeta = {
|
|
|
59
47
|
};
|
|
60
48
|
export type SecretDefinitionsMeta = Record<string, SecretDefinitionMeta>;
|
|
61
49
|
export type SecretDefinitions = SecretDefinitionMeta[];
|
|
50
|
+
/**
|
|
51
|
+
* Declares a secret this project needs, with the shape of its value. The CLI
|
|
52
|
+
* collects every declaration into `CredentialsMap`, which is what makes
|
|
53
|
+
* `secrets.getSecret('NAME')` return the right type instead of `unknown`.
|
|
54
|
+
*
|
|
55
|
+
* @example snippet: secrets
|
|
56
|
+
*/
|
|
62
57
|
export declare const defineSecret: <T>(_config: CoreSecret<T>) => void;
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declares a secret this project needs, with the shape of its value. The CLI
|
|
3
|
+
* collects every declaration into `CredentialsMap`, which is what makes
|
|
4
|
+
* `secrets.getSecret('NAME')` return the right type instead of `unknown`.
|
|
5
|
+
*
|
|
6
|
+
* @example snippet: secrets
|
|
7
|
+
*/
|
|
1
8
|
export const defineSecret = (_config) => { };
|
|
@@ -28,11 +28,14 @@ export type CorePikkuTriggerFunctionConfig<TInput = unknown, TOutput = unknown,
|
|
|
28
28
|
};
|
|
29
29
|
export declare const pikkuTriggerFunc: <TInput = unknown, TOutput = unknown, Services extends CoreSingletonServices = CoreSingletonServices, InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined>(triggerOrConfig: CorePikkuTriggerFunction<TInput, TOutput, Services> | CorePikkuTriggerFunctionConfig<TInput, TOutput, Services, InputSchema, OutputSchema>) => CorePikkuTriggerFunctionConfig<TInput, TOutput, Services, InputSchema, OutputSchema>;
|
|
30
30
|
export interface CoreTrigger<PikkuFunctionConfig = any> {
|
|
31
|
+
/** What a `wireTriggerSource` points at to fire this trigger. It is the contract between the two, so both must spell it the same. */
|
|
31
32
|
name: string;
|
|
33
|
+
/** The function to run each time the trigger fires. */
|
|
32
34
|
func: PikkuFunctionConfig;
|
|
35
|
+
/** What firing this trigger means, for whoever is reading the wiring rather than writing it. */
|
|
33
36
|
description?: string;
|
|
37
|
+
/** Filters this trigger in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
34
38
|
tags?: string[];
|
|
35
|
-
graph?: true;
|
|
36
39
|
}
|
|
37
40
|
export interface TriggerInstance {
|
|
38
41
|
name: string;
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
export type CoreVariable<T = unknown> = {
|
|
2
|
+
/** How the variable is asked for in code. Generated into `VariablesMap`, so it is what `variables.get` autocompletes. */
|
|
2
3
|
name: string;
|
|
4
|
+
/** The name shown to whoever configures the deployment. */
|
|
3
5
|
displayName: string;
|
|
6
|
+
/** What the value does, for the person setting it rather than the one reading it. */
|
|
4
7
|
description?: string;
|
|
8
|
+
/** The environment variable this reads, which is the name that has to exist on the host. */
|
|
5
9
|
variableId: string;
|
|
10
|
+
/** The shape of the value. It arrives as a string, so this is also what parses it. */
|
|
6
11
|
schema: T;
|
|
7
|
-
/**
|
|
8
|
-
* A variable is REQUIRED by default, and marking it `optional` is how a
|
|
9
|
-
* declaration says its absence is a supported state. Same flag, same
|
|
10
|
-
* polarity and same meaning as `CoreSecret.optional` — one word to learn
|
|
11
|
-
* rather than two with opposite senses.
|
|
12
|
-
*
|
|
13
|
-
* Defaulting to required rather than following `variables.get`'s
|
|
14
|
-
* `T | undefined` return is deliberate. That signature describes what a
|
|
15
|
-
* caller must HANDLE, not whether a deployment is correct without the value:
|
|
16
|
-
* an undefined feature flag is fine, an undefined API base URL is an outage
|
|
17
|
-
* that the type system cannot tell apart. Declaring the difference is the
|
|
18
|
-
* point of the flag, and the safe default for an undeclared one is to ask.
|
|
19
|
-
*/
|
|
12
|
+
/** Required by default: this says the deployment is still correct without it. */
|
|
20
13
|
optional?: boolean;
|
|
14
|
+
/** Where to go to work out what to set this to. */
|
|
21
15
|
docsUrl?: string;
|
|
22
16
|
};
|
|
23
17
|
export type VariableDefinitionMeta = {
|
|
@@ -32,4 +26,11 @@ export type VariableDefinitionMeta = {
|
|
|
32
26
|
};
|
|
33
27
|
export type VariableDefinitionsMeta = Record<string, VariableDefinitionMeta>;
|
|
34
28
|
export type VariableDefinitions = VariableDefinitionMeta[];
|
|
29
|
+
/**
|
|
30
|
+
* Declares an environment variable this project needs, with the shape of its
|
|
31
|
+
* value. The CLI collects every declaration into `VariablesMap`, which is what
|
|
32
|
+
* makes `variables.get('NAME')` return the right type instead of `unknown`.
|
|
33
|
+
*
|
|
34
|
+
* @example snippet: variables
|
|
35
|
+
*/
|
|
35
36
|
export declare const defineVariable: <T>(_config: CoreVariable<T>) => void;
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declares an environment variable this project needs, with the shape of its
|
|
3
|
+
* value. The CLI collects every declaration into `VariablesMap`, which is what
|
|
4
|
+
* makes `variables.get('NAME')` return the right type instead of `unknown`.
|
|
5
|
+
*
|
|
6
|
+
* @example snippet: variables
|
|
7
|
+
*/
|
|
1
8
|
export const defineVariable = (_config) => { };
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export interface PikkuWorkflowGraphConfig<FuncMap extends Record<string, string>, T> {
|
|
2
|
+
/** Keeps the graph in the codebase but out of the build. */
|
|
2
3
|
disabled?: true;
|
|
4
|
+
/** Unique across the project. It is how the graph is started and how its runs are grouped. */
|
|
3
5
|
name?: string;
|
|
6
|
+
/** What the graph does, for whoever is reading it rather than editing it. */
|
|
4
7
|
description?: string;
|
|
8
|
+
/** Filters this graph in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
5
9
|
tags?: string[];
|
|
10
|
+
/** The graph's steps, keyed by node id, each naming a function. `pikku meta` lists the names available here. */
|
|
6
11
|
nodes: FuncMap;
|
|
12
|
+
/** Per-node settings — retries, timeouts, the edges between them. */
|
|
7
13
|
config?: T;
|
|
14
|
+
/** Free text carried onto the rendered graph, for a reader who needs the reasoning the shape cannot show. */
|
|
8
15
|
notes?: string[];
|
|
9
16
|
}
|
|
10
17
|
export interface PikkuWorkflowGraphResult {
|
|
@@ -3,4 +3,10 @@ export interface ScenarioCookieJar {
|
|
|
3
3
|
clear(): void;
|
|
4
4
|
readonly empty: boolean;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* A cookie store for a scenario run, so a step that signs in leaves the session
|
|
8
|
+
* cookie behind for the steps after it.
|
|
9
|
+
*
|
|
10
|
+
* @example snippet: scenarioCookieJar
|
|
11
|
+
*/
|
|
6
12
|
export declare const createCookieJar: (apiUrl: string) => ScenarioCookieJar;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cookie store for a scenario run, so a step that signs in leaves the session
|
|
3
|
+
* cookie behind for the steps after it.
|
|
4
|
+
*
|
|
5
|
+
* @example snippet: scenarioCookieJar
|
|
6
|
+
*/
|
|
1
7
|
export const createCookieJar = (apiUrl) => {
|
|
2
8
|
const jar = new Map();
|
|
3
9
|
const origin = new URL(apiUrl).origin;
|
|
@@ -2,4 +2,10 @@ export interface PollOptions {
|
|
|
2
2
|
timeoutMs?: number;
|
|
3
3
|
intervalMs?: number;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Retries an assertion until it passes or the timeout runs out — for the
|
|
7
|
+
* eventually-consistent parts of a scenario (a queued job, a projection).
|
|
8
|
+
*
|
|
9
|
+
* @example snippet: scenarioPolling
|
|
10
|
+
*/
|
|
5
11
|
export declare const pollUntil: <T>(attempt: () => Promise<T | undefined> | T | undefined, { timeoutMs, intervalMs }?: PollOptions) => Promise<T | undefined>;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retries an assertion until it passes or the timeout runs out — for the
|
|
3
|
+
* eventually-consistent parts of a scenario (a queued job, a projection).
|
|
4
|
+
*
|
|
5
|
+
* @example snippet: scenarioPolling
|
|
6
|
+
*/
|
|
1
7
|
export const pollUntil = async (attempt, { timeoutMs = 15_000, intervalMs = 250 } = {}) => {
|
|
2
8
|
const deadline = Date.now() + timeoutMs;
|
|
3
9
|
for (;;) {
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { PikkuScenarioStepWire, ScenarioEnvironment } from './scenario-step.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The environment the current scenario run targets, or a throw explaining that
|
|
4
|
+
* the run carries none. Use it in a step that needs the target's URLs.
|
|
5
|
+
*
|
|
6
|
+
* @example snippet: scenarioHttpStep
|
|
7
|
+
*/
|
|
2
8
|
export declare const requireScenarioEnv: (scenarioStep: PikkuScenarioStepWire | undefined) => ScenarioEnvironment;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment the current scenario run targets, or a throw explaining that
|
|
3
|
+
* the run carries none. Use it in a step that needs the target's URLs.
|
|
4
|
+
*
|
|
5
|
+
* @example snippet: scenarioHttpStep
|
|
6
|
+
*/
|
|
1
7
|
export const requireScenarioEnv = (scenarioStep) => {
|
|
2
8
|
const env = scenarioStep?.env;
|
|
3
9
|
if (!env) {
|
|
@@ -9,6 +9,10 @@ export declare class WorkflowAsyncException extends Error {
|
|
|
9
9
|
readonly stepName: string;
|
|
10
10
|
constructor(runId: string, stepName: string);
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown inside a workflow step when the run has been cancelled, so the step
|
|
14
|
+
* stops rather than finishing work nobody wants.
|
|
15
|
+
*/
|
|
12
16
|
export declare class WorkflowCancelledException extends Error {
|
|
13
17
|
readonly runId: string;
|
|
14
18
|
readonly reason?: string | undefined;
|
|
@@ -13,6 +13,10 @@ export class WorkflowAsyncException extends Error {
|
|
|
13
13
|
this.name = 'WorkflowAsyncException';
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Thrown inside a workflow step when the run has been cancelled, so the step
|
|
18
|
+
* stops rather than finishing work nobody wants.
|
|
19
|
+
*/
|
|
16
20
|
export class WorkflowCancelledException extends Error {
|
|
17
21
|
runId;
|
|
18
22
|
reason;
|
package/package.json
CHANGED
|
@@ -124,12 +124,12 @@ const barrelSpecifiers = (file: string) => {
|
|
|
124
124
|
* it otherwise never imports.
|
|
125
125
|
*/
|
|
126
126
|
const namesOwnedByALeaf = new Map<string, string>([
|
|
127
|
-
['defineCredential', '#pikku/
|
|
127
|
+
['defineCredential', '#pikku/auth'],
|
|
128
128
|
['defineSecret', '#pikku/secrets'],
|
|
129
129
|
['defineVariable', '#pikku/variables'],
|
|
130
130
|
['defineScope', '#pikku/scopes'],
|
|
131
131
|
['defineSystemRole', '#pikku/scopes'],
|
|
132
|
-
['cors', '#pikku/
|
|
132
|
+
['cors', '#pikku/middleware'],
|
|
133
133
|
['InvalidOriginError', '#pikku/error'],
|
|
134
134
|
])
|
|
135
135
|
|
|
@@ -13,19 +13,30 @@ export class PikkuError extends Error {
|
|
|
13
13
|
* survives serialization across a workflow step boundary and rehydration as a
|
|
14
14
|
* plain `Error`. Callers log the message alone for these, the full stack for
|
|
15
15
|
* everything else.
|
|
16
|
+
*
|
|
17
|
+
* @example snippet: isExpectedError
|
|
16
18
|
*/
|
|
17
19
|
export const isExpectedError = (error: unknown): boolean =>
|
|
18
20
|
error instanceof PikkuError ||
|
|
19
21
|
(error as { expected?: unknown } | null)?.expected === true
|
|
20
22
|
|
|
21
23
|
export interface ErrorDetails {
|
|
24
|
+
/** The HTTP status this error answers with, instead of a 500. */
|
|
22
25
|
status: number
|
|
26
|
+
/** What the caller is told. It leaves the process, so it must not name anything internal. */
|
|
23
27
|
message: string
|
|
28
|
+
/** The JSON-RPC code an MCP client is given, where the HTTP status has no equivalent. */
|
|
24
29
|
mcpCode?: number
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export type PikkuErrorConstructor = new (...args: any[]) => Error
|
|
28
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Registers one of your own error classes with the HTTP status and message it
|
|
36
|
+
* should produce, so throwing it maps to a real response instead of a 500.
|
|
37
|
+
*
|
|
38
|
+
* @example snippet: addError
|
|
39
|
+
*/
|
|
29
40
|
export const addError = (
|
|
30
41
|
error: any,
|
|
31
42
|
{ status, message, mcpCode }: ErrorDetails
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export interface SerializedError {
|
|
2
|
+
/** What went wrong, carried across a boundary that cannot carry an Error. */
|
|
2
3
|
message: string
|
|
4
|
+
/** Present only where the failure was unexpected; a deliberate error is logged by its message alone. */
|
|
3
5
|
stack?: string
|
|
6
|
+
/** The error class's registered name, which is what a caller matches on rather than the message. */
|
|
4
7
|
code?: string
|
|
5
8
|
// Set for a deliberate PikkuError; survives step-boundary rehydration so
|
|
6
9
|
// the workflow runner logs the message alone rather than a stack trace.
|
|
@@ -29,7 +29,7 @@ import type { CorePikkuFunctionConfig } from './functions.types.js'
|
|
|
29
29
|
import { parseVersionedId } from '../version.js'
|
|
30
30
|
import type { SessionService } from '../services/user-session-service.js'
|
|
31
31
|
import { PikkuSessionService } from '../services/user-session-service.js'
|
|
32
|
-
import {
|
|
32
|
+
import { MissingSessionError, ReadonlySessionError } from '../errors/errors.js'
|
|
33
33
|
import { verifyScopes } from '../scopes.js'
|
|
34
34
|
import {
|
|
35
35
|
PikkuCredentialWireService,
|
|
@@ -83,7 +83,7 @@ async function resolveSession(
|
|
|
83
83
|
|
|
84
84
|
export const addFunction = (
|
|
85
85
|
funcName: string,
|
|
86
|
-
funcConfig: CorePikkuFunctionConfig<any, any>,
|
|
86
|
+
funcConfig: CorePikkuFunctionConfig<any, any, any, any, any>,
|
|
87
87
|
packageName: string | null = null
|
|
88
88
|
) => {
|
|
89
89
|
pikkuState(packageName, 'function', 'functions').set(funcName, funcConfig)
|
|
@@ -329,7 +329,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
329
329
|
resolveAddonAuth(packageName, addonInstance?.namespace)
|
|
330
330
|
) {
|
|
331
331
|
if (!session) {
|
|
332
|
-
throw new
|
|
332
|
+
throw new MissingSessionError('Authentication required')
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
} else {
|
|
@@ -339,7 +339,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
339
339
|
)
|
|
340
340
|
}
|
|
341
341
|
if (!session) {
|
|
342
|
-
throw new
|
|
342
|
+
throw new MissingSessionError('Authentication required')
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
|
|
@@ -193,30 +193,38 @@ export type CorePikkuFunctionConfig<
|
|
|
193
193
|
OutputSchema extends StandardSchemaV1 | undefined = undefined,
|
|
194
194
|
Scope extends string = string,
|
|
195
195
|
> = {
|
|
196
|
+
/** A human name for this function, shown wherever it is listed rather than called. */
|
|
196
197
|
title?: string
|
|
198
|
+
/** What the function does. An agent choosing between tools reads this, so it is worth more care than a comment would be. */
|
|
197
199
|
description?: string
|
|
198
200
|
/** Explicit logical name override; lets multiple exports share a versioned base */
|
|
199
201
|
override?: string
|
|
202
|
+
/** Which version of this contract this export is. Two exports sharing an `override` and differing here are the same function at two versions. */
|
|
200
203
|
version?: number
|
|
204
|
+
/** Filters this function in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
201
205
|
tags?: string[]
|
|
206
|
+
/** Makes the function callable from outside as `POST /rpc/<name>`. Without a session requirement, a permission or an addon gate, that means callable by anyone. */
|
|
202
207
|
expose?: boolean
|
|
203
208
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* A last resort. Prefer `permissions`, which are declared, inspectable, and
|
|
209
|
-
* reusable; reach for this only when the check cannot be expressed as one.
|
|
210
|
-
*
|
|
211
|
-
* Purely declarative — it grants nothing, and asserting it falsely disables
|
|
212
|
-
* the audit that would have caught the mistake. Requires
|
|
213
|
-
* `allow.permissionsInBody` in `pikku.config.json`.
|
|
209
|
+
* Declares that the body does its own permission check, so the function is
|
|
210
|
+
* not open despite naming no session, scope or permission. It grants
|
|
211
|
+
* nothing — asserting it falsely just disables the audit that would have
|
|
212
|
+
* caught the mistake. Requires `allow.permissionsInBody` in the config.
|
|
214
213
|
*/
|
|
215
214
|
permissionsInBody?: boolean
|
|
215
|
+
/** Publishes the function in this package's remote surface, which is what a `wireRemoteAddon` consumer gets a typed client for. */
|
|
216
216
|
remote?: boolean
|
|
217
|
+
/** Offers the function to MCP clients as a tool, without a separate `wireMCPTool`. */
|
|
217
218
|
mcp?: boolean
|
|
219
|
+
/**
|
|
220
|
+
* Declares that the function only reads. It is enforced rather than
|
|
221
|
+
* decorative: a read-only session is refused any function without it, and an
|
|
222
|
+
* agent may call one without asking permission first.
|
|
223
|
+
*/
|
|
218
224
|
readonly?: boolean
|
|
225
|
+
/** Where this function is deployed when the build can go either way. `auto` lets the analyser decide from what the function touches. */
|
|
219
226
|
deploy?: 'serverless' | 'server' | 'auto'
|
|
227
|
+
/** Under an agent's `explicit` approval policy, calling this pauses for a human to approve it. */
|
|
220
228
|
approvalRequired?: boolean
|
|
221
229
|
/** When true, workflow steps calling this function are dispatched via the queue. No queue service configured is a hard error. Defaults to false (inline). */
|
|
222
230
|
workflowQueued?: boolean
|
|
@@ -230,18 +238,21 @@ export type CorePikkuFunctionConfig<
|
|
|
230
238
|
* and report how much of the flow each surface actually covers.
|
|
231
239
|
*/
|
|
232
240
|
surfaces?: ScenarioSurface[]
|
|
241
|
+
/** Scenario steps only, and set by the definer rather than by hand: this step needs a persona, so the runner injects `wire.actor`. */
|
|
242
|
+
requiresActor?: boolean
|
|
233
243
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
244
|
+
* Records every call in the audit log. `transactional` durability writes the
|
|
245
|
+
* entry in the same transaction as the work, so the two cannot disagree;
|
|
246
|
+
* `best-effort` does not hold the request up for it.
|
|
237
247
|
*/
|
|
238
|
-
requiresActor?: boolean
|
|
239
248
|
audit?:
|
|
240
249
|
| boolean
|
|
241
250
|
| {
|
|
242
251
|
durability?: 'best-effort' | 'transactional'
|
|
243
252
|
}
|
|
253
|
+
/** Builds the sentence a human is shown when asked to approve a call, from that call's own input. */
|
|
244
254
|
approvalDescription?: any
|
|
255
|
+
/** The body. Its first parameter is the services it needs, destructured inline so the build can tree-shake the rest away. */
|
|
245
256
|
func: PikkuFunction
|
|
246
257
|
/**
|
|
247
258
|
* Scenarios only: runs before the scenario body, with the scenario's own
|
|
@@ -260,20 +271,21 @@ export type CorePikkuFunctionConfig<
|
|
|
260
271
|
* as skipped rather than quietly omitted; naming it in `--flows` runs it.
|
|
261
272
|
*/
|
|
262
273
|
skip?: string
|
|
274
|
+
/** Whether calling this requires a session, wherever it is wired. A wiring can be more permissive than the function, never less. */
|
|
263
275
|
auth?: boolean
|
|
264
|
-
/**
|
|
265
|
-
* Scopes the session must hold; all are required (AND) and checked before
|
|
266
|
-
* `permissions`, which OR together — a scope can only narrow access.
|
|
267
|
-
* Narrowed to the generated `ScopeId` union, so an undeclared scope is a
|
|
268
|
-
* compile error. Requires a session — see
|
|
269
|
-
* {@link CorePikkuSessionlessFunctionConfig}.
|
|
270
|
-
*/
|
|
276
|
+
/** Scopes the session must hold. All are required, and checked before `permissions`, which OR together — a scope only narrows access. */
|
|
271
277
|
scopes?: Scope[]
|
|
278
|
+
/** Checks that run before the body. Grouped names OR together, so any one passing admits the caller; use `scopes` to require rather than offer. */
|
|
272
279
|
permissions?: CorePermissionGroup<PikkuPermission>
|
|
280
|
+
/** Wraps this function wherever it is called from, unlike wiring middleware which only wraps one route into it. */
|
|
273
281
|
middleware?: PikkuMiddleware[]
|
|
282
|
+
/** The input schema, which is also the input type — there is no separate generic to keep in step with it. */
|
|
274
283
|
input?: InputSchema
|
|
284
|
+
/** The output schema, which is also the return type. Naming a type here instead is what produces PKU463. */
|
|
275
285
|
output?: OutputSchema
|
|
286
|
+
/** Presentation for this function as a node in a workflow graph. */
|
|
276
287
|
node?: CoreNodeConfig
|
|
288
|
+
/** Error classes this may throw, so each one's registered HTTP status is used instead of a 500. */
|
|
277
289
|
errors?: Array<typeof PikkuError>
|
|
278
290
|
}
|
|
279
291
|
|
|
@@ -2,7 +2,14 @@ import {
|
|
|
2
2
|
pikkuMiddleware,
|
|
3
3
|
pikkuMiddlewareFactory,
|
|
4
4
|
} from './middleware-factories.js'
|
|
5
|
+
/**
|
|
6
|
+
* Reads an API key from the request and JWT-decodes it into a session. Leaves
|
|
7
|
+
* an existing session alone, so it composes with other auth middleware.
|
|
8
|
+
*
|
|
9
|
+
* @example snippet: machineAuth
|
|
10
|
+
*/
|
|
5
11
|
export const authAPIKey = pikkuMiddlewareFactory<{
|
|
12
|
+
/** Where to look: the `x-api-key` header, the `apiKey` query param, or both. */
|
|
6
13
|
source: 'header' | 'query' | 'all'
|
|
7
14
|
}>(({ source }) =>
|
|
8
15
|
pikkuMiddleware(
|
|
@@ -17,8 +17,11 @@ const constantTimeEqual = (a: string, b: string): boolean => {
|
|
|
17
17
|
* Validates a bearer token: JWT-decoded by default, or compared in constant
|
|
18
18
|
* time against a static `value` or a `secretId` resolved through the secrets
|
|
19
19
|
* service per request.
|
|
20
|
+
*
|
|
21
|
+
* @example snippet: machineAuth
|
|
20
22
|
*/
|
|
21
23
|
export const authBearer = pikkuMiddlewareFactory<{
|
|
24
|
+
/** Omit to JWT-decode the token. Set it to accept one fixed token instead, matched in constant time. */
|
|
22
25
|
token?:
|
|
23
26
|
| {
|
|
24
27
|
value: string
|
|
@@ -6,10 +6,6 @@ import {
|
|
|
6
6
|
import type { RelativeTimeInput } from '../time-utils.js'
|
|
7
7
|
import { getRelativeTimeOffsetFromNow } from '../time-utils.js'
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Reads a JWT session from a cookie, and re-issues the cookie after the
|
|
11
|
-
* request whenever the session changed (e.g. after login).
|
|
12
|
-
*/
|
|
13
9
|
/** Standard JWT registered claims — present on a token even with no session. */
|
|
14
10
|
const JWT_REGISTERED_CLAIMS = new Set([
|
|
15
11
|
'iat',
|
|
@@ -31,9 +27,18 @@ const hasSessionIdentity = (session: unknown): boolean => {
|
|
|
31
27
|
return Object.keys(session).some((key) => !JWT_REGISTERED_CLAIMS.has(key))
|
|
32
28
|
}
|
|
33
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Reads a JWT session from a cookie, and re-issues the cookie after the
|
|
32
|
+
* request whenever the session changed (e.g. after login).
|
|
33
|
+
*
|
|
34
|
+
* @example snippet: machineAuth
|
|
35
|
+
*/
|
|
34
36
|
export const authCookie = pikkuMiddlewareFactory<{
|
|
37
|
+
/** Cookie name to read and write. */
|
|
35
38
|
name: string
|
|
39
|
+
/** Serialize options merged over the defaults, which are httpOnly and sameSite lax. */
|
|
36
40
|
options: SerializeOptions
|
|
41
|
+
/** How long the re-issued cookie lives, as a relative time such as `'7d'`. */
|
|
37
42
|
expiresIn: RelativeTimeInput
|
|
38
43
|
}>(({ name, options, expiresIn }) => {
|
|
39
44
|
const mergedOptions: SerializeOptions = {
|
package/src/middleware/cors.ts
CHANGED
|
@@ -8,13 +8,21 @@ import {
|
|
|
8
8
|
* matching origin and otherwise sends no `Access-Control-Allow-Origin` at all,
|
|
9
9
|
* so the browser reports "origin not allowed" rather than an origin mismatch
|
|
10
10
|
* against whichever entry happened to be first.
|
|
11
|
+
*
|
|
12
|
+
* @example snippet: corsMiddleware
|
|
11
13
|
*/
|
|
12
14
|
export const cors = pikkuMiddlewareFactory<{
|
|
15
|
+
/** Which origins may call. Defaults to `*`, which the browser rejects alongside `credentials: true` — name the origins instead. */
|
|
13
16
|
origin?: string | string[] | true
|
|
17
|
+
/** Methods a cross-origin caller may use. Defaults to the common six; a method missing here fails preflight rather than the request. */
|
|
14
18
|
methods?: string[]
|
|
19
|
+
/** Request headers a caller may send. Defaults to content-type, authorization and x-api-key. */
|
|
15
20
|
headers?: string[]
|
|
21
|
+
/** Response headers the browser will let the caller's JavaScript read. Everything else is hidden from it even on a 200. */
|
|
16
22
|
exposeHeaders?: string[]
|
|
23
|
+
/** Whether cookies and auth headers ride along. Requires a named origin, never `*`. */
|
|
17
24
|
credentials?: boolean
|
|
25
|
+
/** Seconds the browser may cache this preflight. Defaults to a day. */
|
|
18
26
|
maxAge?: number
|
|
19
27
|
}>(
|
|
20
28
|
({
|
|
@@ -39,6 +39,10 @@ export const pikkuMiddlewareFactory = <In = any>(
|
|
|
39
39
|
return factory
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Declares middleware for a channel — it runs around the connection and its
|
|
44
|
+
* messages rather than around a single request.
|
|
45
|
+
*/
|
|
42
46
|
export const pikkuChannelMiddleware = <
|
|
43
47
|
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
44
48
|
Event = unknown,
|
|
@@ -48,12 +52,22 @@ export const pikkuChannelMiddleware = <
|
|
|
48
52
|
return middleware
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Declares channel middleware that takes options, so one definition can be
|
|
57
|
+
* wired several times with different configuration.
|
|
58
|
+
*
|
|
59
|
+
* @example snippet: channelMiddlewareFactory
|
|
60
|
+
*/
|
|
51
61
|
export const pikkuChannelMiddlewareFactory = <In = any>(
|
|
52
62
|
factory: CorePikkuChannelMiddlewareFactory<In>
|
|
53
63
|
): CorePikkuChannelMiddlewareFactory<In> => {
|
|
54
64
|
return factory
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Declares middleware for an agent run — hooks around the model call, its tool
|
|
69
|
+
* calls and the run's state.
|
|
70
|
+
*/
|
|
57
71
|
export const pikkuAgentMiddleware = <
|
|
58
72
|
State extends Record<string, unknown> = Record<string, unknown>,
|
|
59
73
|
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
@@ -22,7 +22,7 @@ describe('LocalSecretService', () => {
|
|
|
22
22
|
const vars = new LocalVariablesService({})
|
|
23
23
|
const service = new LocalSecretService(vars)
|
|
24
24
|
await assert.rejects(() => service.getSecret('MISSING'), {
|
|
25
|
-
message: 'Requested secret not found',
|
|
25
|
+
message: 'Requested secret not found: MISSING',
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
28
|
|
|
@@ -44,7 +44,7 @@ describe('LocalSecretService', () => {
|
|
|
44
44
|
const vars = new LocalVariablesService({})
|
|
45
45
|
const service = new LocalSecretService(vars)
|
|
46
46
|
await assert.rejects(() => service.getSecret('MISSING'), {
|
|
47
|
-
message: 'Requested secret not found',
|
|
47
|
+
message: 'Requested secret not found: MISSING',
|
|
48
48
|
})
|
|
49
49
|
})
|
|
50
50
|
|
|
@@ -32,7 +32,7 @@ export class LocalSecretService implements SecretService {
|
|
|
32
32
|
if (value) {
|
|
33
33
|
return createSecretValue(this.parseSecret<T>(value))
|
|
34
34
|
}
|
|
35
|
-
throw new Error(
|
|
35
|
+
throw new Error(`Requested secret not found: ${key}`)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
public async setSecret(key: string, value: unknown): Promise<void> {
|
|
@@ -38,6 +38,8 @@ export interface ScenarioHttpResponse<T = unknown> {
|
|
|
38
38
|
* `invokeRaw` returns this, and a step that has to reach past a persona — a
|
|
39
39
|
* route with no RPC, an identity no persona can hold — reaches for this rather
|
|
40
40
|
* than writing the same record by hand.
|
|
41
|
+
*
|
|
42
|
+
* @example snippet: scenarioPolling
|
|
41
43
|
*/
|
|
42
44
|
export const readScenarioHttpResponse = async <T = unknown>(
|
|
43
45
|
res: Response
|
|
@@ -84,6 +86,8 @@ export interface ScenarioJsonRequest {
|
|
|
84
86
|
* outright when the target answers an empty body or an HTML error page. A
|
|
85
87
|
* refusal is the expected outcome of a permissions scenario, so it has to
|
|
86
88
|
* survive as data.
|
|
89
|
+
*
|
|
90
|
+
* @example snippet: scenarioHttpStep
|
|
87
91
|
*/
|
|
88
92
|
export const postScenarioJson = async <T = unknown>(
|
|
89
93
|
url: string,
|
|
@@ -4,7 +4,6 @@ import type { SecretValue } from '../classification/secret-value.js'
|
|
|
4
4
|
export type SecretValues<T> = { [K in keyof T]: SecretValue<T[K]> }
|
|
5
5
|
|
|
6
6
|
export interface SecretService {
|
|
7
|
-
/** Throws if the secret is not found. Unwrap the result with `.reveal()`. */
|
|
8
7
|
getSecret<T = string>(key: string): Promise<SecretValue<T>>
|
|
9
8
|
/** Answers for any key, including a disallowed one — it must not throw. */
|
|
10
9
|
hasSecret(key: string): Promise<boolean>
|
|
@@ -44,7 +44,7 @@ export const defineSecretServiceTests = (
|
|
|
44
44
|
test('getSecret throws for missing key', async () => {
|
|
45
45
|
const service = await factory({ key: kek })
|
|
46
46
|
await assert.rejects(() => service.getSecret('nonexistent'), {
|
|
47
|
-
message: 'Requested secret not found',
|
|
47
|
+
message: 'Requested secret not found: nonexistent',
|
|
48
48
|
})
|
|
49
49
|
})
|
|
50
50
|
|