@pikku/core 0.12.91 → 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 +102 -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/persona-sign-in.d.ts +0 -2
- package/dist/services/persona-sign-in.js +15 -66
- 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/persona-sign-in.test.ts +28 -34
- package/src/services/persona-sign-in.ts +16 -88
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
+
## 0.12.93
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 4058c3a: `addFunction` accepts a config carrying its schemas. It typed the parameter with
|
|
6
|
+
two type arguments where the schema-carrying overloads of
|
|
7
|
+
`CorePikkuFunctionConfig` need five, so every generated scenario registration
|
|
8
|
+
failed to typecheck — invisible until a real project was compiled in CI.
|
|
9
|
+
- 4058c3a: authBearer, authCookie and authAPIKey now come from `#pikku/middleware`, so nothing needs `@pikku/core`
|
|
10
|
+
- 4058c3a: `pikku doc` takes several topics at once, so an agent that needs two exports
|
|
11
|
+
spends one round-trip rather than two.
|
|
12
|
+
|
|
13
|
+
A variadic positional validated at runtime but not in the types: `[files...]`
|
|
14
|
+
resolved to a key literally named `files...`, so declaring one was a type error.
|
|
15
|
+
|
|
16
|
+
- 4058c3a: `pikku doc` keeps a door screen to a door: exports, what each is for, and either
|
|
17
|
+
its signature or a pointer to its keys — never the keys themselves. `#pikku/function`
|
|
18
|
+
was 9.4k tokens and is now under 1k.
|
|
19
|
+
|
|
20
|
+
Error classes carry their registered HTTP status again. The scrape read only the
|
|
21
|
+
program, and a surveyed project consumes pikku as `.d.ts`, which has no statements —
|
|
22
|
+
so all 49 came back bare.
|
|
23
|
+
|
|
24
|
+
- 4058c3a: Give every export `pikku doc` lists a line saying what it is for, and gate it at zero
|
|
25
|
+
- 4058c3a: Point the doc's examples at real template source instead of restating it. An `@example snippet: name` names a `// @snippet start name` region in `templates/functions` or `templates/function-addon`, and the surface build resolves it — so every example the doc shows is code that compiled, and renaming an option breaks the build rather than the docs. `wireHTTP`, `wireChannel`, `wireScheduler`, `wireQueueWorker`, `defineSecret`, `defineVariable` and `addError` now carry one.
|
|
26
|
+
- 114c079: Answer 401, not 403, when a function requires a session and no session exists.
|
|
27
|
+
|
|
28
|
+
`MissingSessionError` has been in the error table at 401 since forever and was never thrown — the runner threw `ForbiddenError('Authentication required')` instead, so "you are not signed in" and "you are signed in but not allowed" both came back 403. The two mean opposite things to a client: the first is worth retrying after re-authenticating, the second never is.
|
|
29
|
+
|
|
30
|
+
That made pikku's own recovery unreachable. `HttpPersona` re-logs-in once on a 401 mid-run, for exactly the case its comment describes — a long run outliving its session. Against a stage using `betterAuthStatelessSession`, the signed cookie cache expires on the app's `cookieCache.maxAge` (5 minutes is the common setting), and from that moment every RPC in the run failed with 403 "Authentication required" while the retry watched for a 401 that could never arrive. A 32-minute scenario run failed everything after its first five minutes.
|
|
31
|
+
|
|
32
|
+
Permission and scope denials are untouched and stay 403.
|
|
33
|
+
|
|
34
|
+
- 4450b2a: Name the missing key when a secret is not found.
|
|
35
|
+
|
|
36
|
+
Every `SecretService` threw a bare `Requested secret not found`. In a deployed
|
|
37
|
+
runtime the stack is minified, so the message was the only evidence there was —
|
|
38
|
+
and it identified neither the key nor the service. Each implementation now names
|
|
39
|
+
the key it looked for; the better-auth middlewares that skip on an absent secret
|
|
40
|
+
match the prefix through one shared predicate instead of the whole string.
|
|
41
|
+
|
|
42
|
+
- 4058c3a: Every `@example` in the public surface now names a snippet from `examples/online-shop`,
|
|
43
|
+
and `@pikku/cli` ships the regions themselves as `snippets.json` beside `surface.json`.
|
|
44
|
+
|
|
45
|
+
One running application is the only source: the code a reader is shown is code that
|
|
46
|
+
compiles, migrates and passes `pikku` in CI, and it cannot drift from the API it
|
|
47
|
+
illustrates. 80 of the 85 app-entrypoint callables now carry an example, up from 34.
|
|
48
|
+
|
|
49
|
+
- 4058c3a: Say what each wiring key is for, and gate it so it stays said
|
|
50
|
+
|
|
51
|
+
The public surface doc listed keys as a name and a type. `schedule: string`
|
|
52
|
+
is a shape; what a caller needs is that it wants a cron expression. Written
|
|
53
|
+
as JSDoc where the type is declared, it reaches `pikku doc`, the IDE and the
|
|
54
|
+
console at once — 31% of keys carried one, now 64%.
|
|
55
|
+
|
|
56
|
+
`CoreHTTPFunctionWiring` was six near-identical union branches, so its keys
|
|
57
|
+
could not be documented once. It is now a shared object intersected with the
|
|
58
|
+
two unions that are genuinely correlated: `auth` with the kind of function it
|
|
59
|
+
admits, and the method with `sse` and `query`.
|
|
60
|
+
|
|
61
|
+
A test reads the shipped surface and holds three numbers: keys that say what
|
|
62
|
+
they are for can only go up, and references to a `Core*` internal or to a type
|
|
63
|
+
the doc never describes can only go down.
|
|
64
|
+
|
|
65
|
+
Drops `eventChannel` from HTTP wirings and `graph` from triggers; nothing read
|
|
66
|
+
either.
|
|
67
|
+
|
|
68
|
+
## 0.12.92
|
|
69
|
+
|
|
70
|
+
### Patch Changes
|
|
71
|
+
|
|
72
|
+
- b521f1b: Resolve the persona to impersonate during the fabric operator sign-in.
|
|
73
|
+
|
|
74
|
+
`POST /sign-in/fabric` now takes an optional
|
|
75
|
+
`actAs: { email, name?, create?, role? }`
|
|
76
|
+
and returns `actAs: { userId }` — the stage's own id for that address, looked up
|
|
77
|
+
before creating and created only when asked.
|
|
78
|
+
|
|
79
|
+
It has to happen there. Impersonation names a user id, a persona only knows an
|
|
80
|
+
email, and since better-auth's `admin()` plugin was dropped no HTTP endpoint
|
|
81
|
+
lists users — so the two calls the scenario runner made to resolve one
|
|
82
|
+
(`/auth/admin/list-users`, then `/auth/admin/create-user`) had nothing left to
|
|
83
|
+
reach and every deployed persona failed with `YOU_ARE_NOT_ALLOWED_TO_LIST_USERS`.
|
|
84
|
+
The adapter is already in hand on the sign-in request and the operator token has
|
|
85
|
+
already been verified, so the lookup is free and gated by the same check that
|
|
86
|
+
mints the session.
|
|
87
|
+
|
|
88
|
+
A created row gets a `role` only when the caller names one. pikku has no `role`
|
|
89
|
+
column of its own any more, but an app may still run better-auth's `admin()`
|
|
90
|
+
plugin and constrain that column, so the persona's first role is passed through
|
|
91
|
+
for those.
|
|
92
|
+
|
|
93
|
+
`OperatorSignInOptions.adminPath` is removed; nothing points at it any more.
|
|
94
|
+
|
|
95
|
+
A Fabric operator row now also satisfies the default impersonation gate.
|
|
96
|
+
`fabric()` grants the `admin` scope only when handed a `ScopeService`, and no
|
|
97
|
+
app template wires one — so the operator signed in holding nothing and every
|
|
98
|
+
impersonated request fell back to the operator's own session. The `fabric`
|
|
99
|
+
column is written by nothing but that sign-in, after an RS256 verification
|
|
100
|
+
against the stage's public key, so the row's existence is the authorization.
|
|
101
|
+
The scope half of the gate still fails closed.
|
|
102
|
+
|
|
1
103
|
## 0.12.91
|
|
2
104
|
|
|
3
105
|
### Patch Changes
|
|
@@ -6,14 +6,25 @@ export declare class PikkuError extends Error {
|
|
|
6
6
|
* survives serialization across a workflow step boundary and rehydration as a
|
|
7
7
|
* plain `Error`. Callers log the message alone for these, the full stack for
|
|
8
8
|
* everything else.
|
|
9
|
+
*
|
|
10
|
+
* @example snippet: isExpectedError
|
|
9
11
|
*/
|
|
10
12
|
export declare const isExpectedError: (error: unknown) => boolean;
|
|
11
13
|
export interface ErrorDetails {
|
|
14
|
+
/** The HTTP status this error answers with, instead of a 500. */
|
|
12
15
|
status: number;
|
|
16
|
+
/** What the caller is told. It leaves the process, so it must not name anything internal. */
|
|
13
17
|
message: string;
|
|
18
|
+
/** The JSON-RPC code an MCP client is given, where the HTTP status has no equivalent. */
|
|
14
19
|
mcpCode?: number;
|
|
15
20
|
}
|
|
16
21
|
export type PikkuErrorConstructor = new (...args: any[]) => Error;
|
|
22
|
+
/**
|
|
23
|
+
* Registers one of your own error classes with the HTTP status and message it
|
|
24
|
+
* should produce, so throwing it maps to a real response instead of a 500.
|
|
25
|
+
*
|
|
26
|
+
* @example snippet: addError
|
|
27
|
+
*/
|
|
17
28
|
export declare const addError: (error: any, { status, message, mcpCode }: ErrorDetails) => void;
|
|
18
29
|
export declare const addErrors: (errors: Array<[error: any, details: ErrorDetails]>) => void;
|
|
19
30
|
export declare const getErrorResponse: (error: Error) => {
|
|
@@ -11,9 +11,17 @@ export class PikkuError extends Error {
|
|
|
11
11
|
* survives serialization across a workflow step boundary and rehydration as a
|
|
12
12
|
* plain `Error`. Callers log the message alone for these, the full stack for
|
|
13
13
|
* everything else.
|
|
14
|
+
*
|
|
15
|
+
* @example snippet: isExpectedError
|
|
14
16
|
*/
|
|
15
17
|
export const isExpectedError = (error) => error instanceof PikkuError ||
|
|
16
18
|
error?.expected === true;
|
|
19
|
+
/**
|
|
20
|
+
* Registers one of your own error classes with the HTTP status and message it
|
|
21
|
+
* should produce, so throwing it maps to a real response instead of a 500.
|
|
22
|
+
*
|
|
23
|
+
* @example snippet: addError
|
|
24
|
+
*/
|
|
17
25
|
export const addError = (error, { status, message, mcpCode }) => {
|
|
18
26
|
pikkuState(null, 'misc', 'errors').set(error, mcpCode === undefined ? { status, message } : { status, message, mcpCode });
|
|
19
27
|
};
|
|
@@ -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
|
expected?: boolean;
|
|
6
9
|
[key: string]: any;
|
|
@@ -5,7 +5,7 @@ import type { CorePikkuFunctionConfig } from './functions.types.js';
|
|
|
5
5
|
import type { SessionService } from '../services/user-session-service.js';
|
|
6
6
|
import { PikkuCredentialWireService } from '../services/credential-wire-service.js';
|
|
7
7
|
import type { AddonInstance } from '../wirings/addon/addon-runner.js';
|
|
8
|
-
export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>, packageName?: string | null) => void;
|
|
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
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, }: {
|
|
@@ -7,7 +7,7 @@ import { 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';
|
|
10
|
-
import {
|
|
10
|
+
import { MissingSessionError, ReadonlySessionError } from '../errors/errors.js';
|
|
11
11
|
import { verifyScopes } from '../scopes.js';
|
|
12
12
|
import { PikkuCredentialWireService, createWireServicesCredentialWireProps, } from '../services/credential-wire-service.js';
|
|
13
13
|
import { defaultPikkuUserIdResolver } from '../services/pikku-user-id.js';
|
|
@@ -182,7 +182,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
182
182
|
funcConfig.auth === true ||
|
|
183
183
|
resolveAddonAuth(packageName, addonInstance?.namespace)) {
|
|
184
184
|
if (!session) {
|
|
185
|
-
throw new
|
|
185
|
+
throw new MissingSessionError('Authentication required');
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -191,7 +191,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
191
191
|
resolvedSingletonServices.logger.warn(`Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`);
|
|
192
192
|
}
|
|
193
193
|
if (!session) {
|
|
194
|
-
throw new
|
|
194
|
+
throw new MissingSessionError('Authentication required');
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
if (session?.readonly && !funcMeta.readonly) {
|
|
@@ -48,30 +48,38 @@ export type CorePermissionGroup<PikkuPermission = CorePikkuPermission<any>> = Re
|
|
|
48
48
|
*/
|
|
49
49
|
export type CorePikkuFunctionHook<Services = any, Data = any, Wire = any> = (services: Services, data: Data, wire: Wire) => Promise<void> | void;
|
|
50
50
|
export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any, any, any, any, any> | CorePikkuFunctionSessionless<any, any, any, any, any>, PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>, InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined, Scope extends string = string> = {
|
|
51
|
+
/** A human name for this function, shown wherever it is listed rather than called. */
|
|
51
52
|
title?: string;
|
|
53
|
+
/** What the function does. An agent choosing between tools reads this, so it is worth more care than a comment would be. */
|
|
52
54
|
description?: string;
|
|
53
55
|
/** Explicit logical name override; lets multiple exports share a versioned base */
|
|
54
56
|
override?: string;
|
|
57
|
+
/** Which version of this contract this export is. Two exports sharing an `override` and differing here are the same function at two versions. */
|
|
55
58
|
version?: number;
|
|
59
|
+
/** Filters this function in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
56
60
|
tags?: string[];
|
|
61
|
+
/** 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. */
|
|
57
62
|
expose?: boolean;
|
|
58
63
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* A last resort. Prefer `permissions`, which are declared, inspectable, and
|
|
64
|
-
* reusable; reach for this only when the check cannot be expressed as one.
|
|
65
|
-
*
|
|
66
|
-
* Purely declarative — it grants nothing, and asserting it falsely disables
|
|
67
|
-
* the audit that would have caught the mistake. Requires
|
|
68
|
-
* `allow.permissionsInBody` in `pikku.config.json`.
|
|
64
|
+
* Declares that the body does its own permission check, so the function is
|
|
65
|
+
* not open despite naming no session, scope or permission. It grants
|
|
66
|
+
* nothing — asserting it falsely just disables the audit that would have
|
|
67
|
+
* caught the mistake. Requires `allow.permissionsInBody` in the config.
|
|
69
68
|
*/
|
|
70
69
|
permissionsInBody?: boolean;
|
|
70
|
+
/** Publishes the function in this package's remote surface, which is what a `wireRemoteAddon` consumer gets a typed client for. */
|
|
71
71
|
remote?: boolean;
|
|
72
|
+
/** Offers the function to MCP clients as a tool, without a separate `wireMCPTool`. */
|
|
72
73
|
mcp?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Declares that the function only reads. It is enforced rather than
|
|
76
|
+
* decorative: a read-only session is refused any function without it, and an
|
|
77
|
+
* agent may call one without asking permission first.
|
|
78
|
+
*/
|
|
73
79
|
readonly?: boolean;
|
|
80
|
+
/** Where this function is deployed when the build can go either way. `auto` lets the analyser decide from what the function touches. */
|
|
74
81
|
deploy?: 'serverless' | 'server' | 'auto';
|
|
82
|
+
/** Under an agent's `explicit` approval policy, calling this pauses for a human to approve it. */
|
|
75
83
|
approvalRequired?: boolean;
|
|
76
84
|
/** When true, workflow steps calling this function are dispatched via the queue. No queue service configured is a hard error. Defaults to false (inline). */
|
|
77
85
|
workflowQueued?: boolean;
|
|
@@ -85,16 +93,19 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
85
93
|
* and report how much of the flow each surface actually covers.
|
|
86
94
|
*/
|
|
87
95
|
surfaces?: ScenarioSurface[];
|
|
96
|
+
/** Scenario steps only, and set by the definer rather than by hand: this step needs a persona, so the runner injects `wire.actor`. */
|
|
97
|
+
requiresActor?: boolean;
|
|
88
98
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
99
|
+
* Records every call in the audit log. `transactional` durability writes the
|
|
100
|
+
* entry in the same transaction as the work, so the two cannot disagree;
|
|
101
|
+
* `best-effort` does not hold the request up for it.
|
|
92
102
|
*/
|
|
93
|
-
requiresActor?: boolean;
|
|
94
103
|
audit?: boolean | {
|
|
95
104
|
durability?: 'best-effort' | 'transactional';
|
|
96
105
|
};
|
|
106
|
+
/** Builds the sentence a human is shown when asked to approve a call, from that call's own input. */
|
|
97
107
|
approvalDescription?: any;
|
|
108
|
+
/** The body. Its first parameter is the services it needs, destructured inline so the build can tree-shake the rest away. */
|
|
98
109
|
func: PikkuFunction;
|
|
99
110
|
/**
|
|
100
111
|
* Scenarios only: runs before the scenario body, with the scenario's own
|
|
@@ -113,20 +124,21 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
113
124
|
* as skipped rather than quietly omitted; naming it in `--flows` runs it.
|
|
114
125
|
*/
|
|
115
126
|
skip?: string;
|
|
127
|
+
/** Whether calling this requires a session, wherever it is wired. A wiring can be more permissive than the function, never less. */
|
|
116
128
|
auth?: boolean;
|
|
117
|
-
/**
|
|
118
|
-
* Scopes the session must hold; all are required (AND) and checked before
|
|
119
|
-
* `permissions`, which OR together — a scope can only narrow access.
|
|
120
|
-
* Narrowed to the generated `ScopeId` union, so an undeclared scope is a
|
|
121
|
-
* compile error. Requires a session — see
|
|
122
|
-
* {@link CorePikkuSessionlessFunctionConfig}.
|
|
123
|
-
*/
|
|
129
|
+
/** Scopes the session must hold. All are required, and checked before `permissions`, which OR together — a scope only narrows access. */
|
|
124
130
|
scopes?: Scope[];
|
|
131
|
+
/** Checks that run before the body. Grouped names OR together, so any one passing admits the caller; use `scopes` to require rather than offer. */
|
|
125
132
|
permissions?: CorePermissionGroup<PikkuPermission>;
|
|
133
|
+
/** Wraps this function wherever it is called from, unlike wiring middleware which only wraps one route into it. */
|
|
126
134
|
middleware?: PikkuMiddleware[];
|
|
135
|
+
/** The input schema, which is also the input type — there is no separate generic to keep in step with it. */
|
|
127
136
|
input?: InputSchema;
|
|
137
|
+
/** The output schema, which is also the return type. Naming a type here instead is what produces PKU463. */
|
|
128
138
|
output?: OutputSchema;
|
|
139
|
+
/** Presentation for this function as a node in a workflow graph. */
|
|
129
140
|
node?: CoreNodeConfig;
|
|
141
|
+
/** Error classes this may throw, so each one's registered HTTP status is used instead of a 500. */
|
|
130
142
|
errors?: Array<typeof PikkuError>;
|
|
131
143
|
};
|
|
132
144
|
/**
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads an API key from the request and JWT-decodes it into a session. Leaves
|
|
3
|
+
* an existing session alone, so it composes with other auth middleware.
|
|
4
|
+
*
|
|
5
|
+
* @example snippet: machineAuth
|
|
6
|
+
*/
|
|
1
7
|
export declare const authAPIKey: import("./middleware.types.js").CorePikkuMiddlewareFactory<{
|
|
8
|
+
/** Where to look: the `x-api-key` header, the `apiKey` query param, or both. */
|
|
2
9
|
source: "header" | "query" | "all";
|
|
3
10
|
}>;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { pikkuMiddleware, pikkuMiddlewareFactory, } from './middleware-factories.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads an API key from the request and JWT-decodes it into a session. Leaves
|
|
4
|
+
* an existing session alone, so it composes with other auth middleware.
|
|
5
|
+
*
|
|
6
|
+
* @example snippet: machineAuth
|
|
7
|
+
*/
|
|
2
8
|
export const authAPIKey = pikkuMiddlewareFactory(({ source }) => pikkuMiddleware(async ({ jwt: jwtService }, { http, setSession, session }, next) => {
|
|
3
9
|
if (!http?.request || !setSession || session) {
|
|
4
10
|
return next();
|
|
@@ -3,8 +3,11 @@ import type { CoreUserSession } from '../types/core.types.js';
|
|
|
3
3
|
* Validates a bearer token: JWT-decoded by default, or compared in constant
|
|
4
4
|
* time against a static `value` or a `secretId` resolved through the secrets
|
|
5
5
|
* service per request.
|
|
6
|
+
*
|
|
7
|
+
* @example snippet: machineAuth
|
|
6
8
|
*/
|
|
7
9
|
export declare const authBearer: import("./middleware.types.js").CorePikkuMiddlewareFactory<{
|
|
10
|
+
/** Omit to JWT-decode the token. Set it to accept one fixed token instead, matched in constant time. */
|
|
8
11
|
token?: {
|
|
9
12
|
value: string;
|
|
10
13
|
userSession: CoreUserSession;
|
|
@@ -13,6 +13,8 @@ const constantTimeEqual = (a, b) => {
|
|
|
13
13
|
* Validates a bearer token: JWT-decoded by default, or compared in constant
|
|
14
14
|
* time against a static `value` or a `secretId` resolved through the secrets
|
|
15
15
|
* service per request.
|
|
16
|
+
*
|
|
17
|
+
* @example snippet: machineAuth
|
|
16
18
|
*/
|
|
17
19
|
export const authBearer = pikkuMiddlewareFactory(({ token } = {}) => pikkuMiddleware(async ({ jwt: jwtService, secrets }, { http, setSession, session }, next) => {
|
|
18
20
|
if (!http?.request || !setSession || session) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { SerializeOptions } from 'cookie';
|
|
2
2
|
import type { RelativeTimeInput } from '../time-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads a JWT session from a cookie, and re-issues the cookie after the
|
|
5
|
+
* request whenever the session changed (e.g. after login).
|
|
6
|
+
*
|
|
7
|
+
* @example snippet: machineAuth
|
|
8
|
+
*/
|
|
3
9
|
export declare const authCookie: import("./middleware.types.js").CorePikkuMiddlewareFactory<{
|
|
10
|
+
/** Cookie name to read and write. */
|
|
4
11
|
name: string;
|
|
12
|
+
/** Serialize options merged over the defaults, which are httpOnly and sameSite lax. */
|
|
5
13
|
options: SerializeOptions;
|
|
14
|
+
/** How long the re-issued cookie lives, as a relative time such as `'7d'`. */
|
|
6
15
|
expiresIn: RelativeTimeInput;
|
|
7
16
|
}>;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { pikkuMiddleware, pikkuMiddlewareFactory, } from './middleware-factories.js';
|
|
2
2
|
import { getRelativeTimeOffsetFromNow } from '../time-utils.js';
|
|
3
|
-
/**
|
|
4
|
-
* Reads a JWT session from a cookie, and re-issues the cookie after the
|
|
5
|
-
* request whenever the session changed (e.g. after login).
|
|
6
|
-
*/
|
|
7
3
|
/** Standard JWT registered claims — present on a token even with no session. */
|
|
8
4
|
const JWT_REGISTERED_CLAIMS = new Set([
|
|
9
5
|
'iat',
|
|
@@ -24,6 +20,12 @@ const hasSessionIdentity = (session) => {
|
|
|
24
20
|
return false;
|
|
25
21
|
return Object.keys(session).some((key) => !JWT_REGISTERED_CLAIMS.has(key));
|
|
26
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Reads a JWT session from a cookie, and re-issues the cookie after the
|
|
25
|
+
* request whenever the session changed (e.g. after login).
|
|
26
|
+
*
|
|
27
|
+
* @example snippet: machineAuth
|
|
28
|
+
*/
|
|
27
29
|
export const authCookie = pikkuMiddlewareFactory(({ name, options, expiresIn }) => {
|
|
28
30
|
const mergedOptions = {
|
|
29
31
|
httpOnly: true,
|
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
* matching origin and otherwise sends no `Access-Control-Allow-Origin` at all,
|
|
5
5
|
* so the browser reports "origin not allowed" rather than an origin mismatch
|
|
6
6
|
* against whichever entry happened to be first.
|
|
7
|
+
*
|
|
8
|
+
* @example snippet: corsMiddleware
|
|
7
9
|
*/
|
|
8
10
|
export declare const cors: import("./middleware.types.js").CorePikkuMiddlewareFactory<{
|
|
11
|
+
/** Which origins may call. Defaults to `*`, which the browser rejects alongside `credentials: true` — name the origins instead. */
|
|
9
12
|
origin?: string | string[] | true;
|
|
13
|
+
/** Methods a cross-origin caller may use. Defaults to the common six; a method missing here fails preflight rather than the request. */
|
|
10
14
|
methods?: string[];
|
|
15
|
+
/** Request headers a caller may send. Defaults to content-type, authorization and x-api-key. */
|
|
11
16
|
headers?: string[];
|
|
17
|
+
/** Response headers the browser will let the caller's JavaScript read. Everything else is hidden from it even on a 200. */
|
|
12
18
|
exposeHeaders?: string[];
|
|
19
|
+
/** Whether cookies and auth headers ride along. Requires a named origin, never `*`. */
|
|
13
20
|
credentials?: boolean;
|
|
21
|
+
/** Seconds the browser may cache this preflight. Defaults to a day. */
|
|
14
22
|
maxAge?: number;
|
|
15
23
|
}>;
|
package/dist/middleware/cors.js
CHANGED
|
@@ -5,6 +5,8 @@ import { pikkuMiddleware, pikkuMiddlewareFactory, } from './middleware-factories
|
|
|
5
5
|
* matching origin and otherwise sends no `Access-Control-Allow-Origin` at all,
|
|
6
6
|
* so the browser reports "origin not allowed" rather than an origin mismatch
|
|
7
7
|
* against whichever entry happened to be first.
|
|
8
|
+
*
|
|
9
|
+
* @example snippet: corsMiddleware
|
|
8
10
|
*/
|
|
9
11
|
export const cors = pikkuMiddlewareFactory(({ origin = '*', methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], headers = ['Content-Type', 'Authorization', 'x-api-key'], exposeHeaders = [], credentials = false, maxAge = 86400, } = {}) => {
|
|
10
12
|
if (origin === '*' && credentials) {
|
|
@@ -4,6 +4,20 @@ import type { PikkuAgentMiddlewareHooks } from '../wirings/agent/agent.types.js'
|
|
|
4
4
|
import type { CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory } from './middleware.types.js';
|
|
5
5
|
export declare const pikkuMiddleware: <SingletonServices extends CoreSingletonServices = CoreSingletonServices, UserSession extends CoreUserSession = CoreUserSession>(middleware: CorePikkuMiddleware<SingletonServices, UserSession> | CorePikkuMiddlewareConfig<SingletonServices, UserSession>) => CorePikkuMiddleware<SingletonServices, UserSession>;
|
|
6
6
|
export declare const pikkuMiddlewareFactory: <In = any>(factory: CorePikkuMiddlewareFactory<In>) => CorePikkuMiddlewareFactory<In>;
|
|
7
|
+
/**
|
|
8
|
+
* Declares middleware for a channel — it runs around the connection and its
|
|
9
|
+
* messages rather than around a single request.
|
|
10
|
+
*/
|
|
7
11
|
export declare const pikkuChannelMiddleware: <SingletonServices extends CoreSingletonServices = CoreSingletonServices, Event = unknown>(middleware: CorePikkuChannelMiddleware<SingletonServices, Event>) => CorePikkuChannelMiddleware<SingletonServices, Event>;
|
|
12
|
+
/**
|
|
13
|
+
* Declares channel middleware that takes options, so one definition can be
|
|
14
|
+
* wired several times with different configuration.
|
|
15
|
+
*
|
|
16
|
+
* @example snippet: channelMiddlewareFactory
|
|
17
|
+
*/
|
|
8
18
|
export declare const pikkuChannelMiddlewareFactory: <In = any>(factory: CorePikkuChannelMiddlewareFactory<In>) => CorePikkuChannelMiddlewareFactory<In>;
|
|
19
|
+
/**
|
|
20
|
+
* Declares middleware for an agent run — hooks around the model call, its tool
|
|
21
|
+
* calls and the run's state.
|
|
22
|
+
*/
|
|
9
23
|
export declare const pikkuAgentMiddleware: <State extends Record<string, unknown> = Record<string, unknown>, SingletonServices extends CoreSingletonServices = CoreSingletonServices>(hooks: PikkuAgentMiddlewareHooks<State, SingletonServices>) => PikkuAgentMiddlewareHooks<State, SingletonServices>;
|
|
@@ -10,10 +10,24 @@ export const pikkuMiddleware = (middleware) => {
|
|
|
10
10
|
export const pikkuMiddlewareFactory = (factory) => {
|
|
11
11
|
return factory;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Declares middleware for a channel — it runs around the connection and its
|
|
15
|
+
* messages rather than around a single request.
|
|
16
|
+
*/
|
|
13
17
|
export const pikkuChannelMiddleware = (middleware) => {
|
|
14
18
|
return middleware;
|
|
15
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Declares channel middleware that takes options, so one definition can be
|
|
22
|
+
* wired several times with different configuration.
|
|
23
|
+
*
|
|
24
|
+
* @example snippet: channelMiddlewareFactory
|
|
25
|
+
*/
|
|
16
26
|
export const pikkuChannelMiddlewareFactory = (factory) => {
|
|
17
27
|
return factory;
|
|
18
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Declares middleware for an agent run — hooks around the model call, its tool
|
|
31
|
+
* calls and the run's state.
|
|
32
|
+
*/
|
|
19
33
|
export const pikkuAgentMiddleware = (hooks) => hooks;
|
|
@@ -23,7 +23,7 @@ export class LocalSecretService {
|
|
|
23
23
|
if (value) {
|
|
24
24
|
return createSecretValue(this.parseSecret(value));
|
|
25
25
|
}
|
|
26
|
-
throw new Error(
|
|
26
|
+
throw new Error(`Requested secret not found: ${key}`);
|
|
27
27
|
}
|
|
28
28
|
async setSecret(key, value) {
|
|
29
29
|
// Storing the wrapper would serialize it to '[secret]', so unwrap first —
|
|
@@ -59,8 +59,6 @@ export interface OperatorSignInOptions {
|
|
|
59
59
|
* for. Turn it on for throwaway stages.
|
|
60
60
|
*/
|
|
61
61
|
createMissing?: boolean;
|
|
62
|
-
/** Admin endpoint prefix under apiUrl. Default `/auth/admin`. */
|
|
63
|
-
adminPath?: string;
|
|
64
62
|
/** Fabric operator sign-in path under apiUrl. Default `/auth/sign-in/fabric`. */
|
|
65
63
|
signInPath?: string;
|
|
66
64
|
}
|
|
@@ -67,7 +67,15 @@ export const establishOperatorSession = async (fetchImpl, apiUrl, persona, optio
|
|
|
67
67
|
const res = await fetchImpl(`${apiUrl}${signInPath}`, {
|
|
68
68
|
method: 'POST',
|
|
69
69
|
headers: { 'content-type': 'application/json', ...extraHeaders },
|
|
70
|
-
body: JSON.stringify({
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
token,
|
|
72
|
+
actAs: {
|
|
73
|
+
email: persona.email,
|
|
74
|
+
name: persona.name,
|
|
75
|
+
create: options.createMissing ?? false,
|
|
76
|
+
...(persona.roles[0] ? { role: persona.roles[0] } : {}),
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
71
79
|
});
|
|
72
80
|
if (!res.ok) {
|
|
73
81
|
throw await failed('operator sign-in', persona.id, res);
|
|
@@ -76,72 +84,13 @@ export const establishOperatorSession = async (fetchImpl, apiUrl, persona, optio
|
|
|
76
84
|
if (setCookies.length === 0) {
|
|
77
85
|
throw new Error(`[scenario] operator sign-in for '${persona.id}' returned no session cookie`);
|
|
78
86
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.map((raw) => raw.split(';')[0])
|
|
85
|
-
.filter((pair) => Boolean(pair))
|
|
86
|
-
.join('; ');
|
|
87
|
-
const userId = await resolveUserId(fetchImpl, apiUrl, persona, options, {
|
|
88
|
-
...extraHeaders,
|
|
89
|
-
cookie: session,
|
|
90
|
-
});
|
|
91
|
-
return { setCookies, userId };
|
|
92
|
-
};
|
|
93
|
-
/**
|
|
94
|
-
* The target's own id for this persona's address, since impersonation names a
|
|
95
|
-
* user id and a persona only knows an email.
|
|
96
|
-
*
|
|
97
|
-
* Looked up before creating, so a persona that already exists is never
|
|
98
|
-
* duplicated and the run reads as "act as this person" rather than "make one".
|
|
99
|
-
*/
|
|
100
|
-
const resolveUserId = async (fetchImpl, apiUrl, persona, options, extraHeaders) => {
|
|
101
|
-
const adminPath = options.adminPath ?? '/auth/admin';
|
|
102
|
-
const query = new URLSearchParams({
|
|
103
|
-
filterField: 'email',
|
|
104
|
-
filterValue: persona.email,
|
|
105
|
-
filterOperator: 'eq',
|
|
106
|
-
limit: '1',
|
|
107
|
-
});
|
|
108
|
-
const found = await fetchImpl(`${apiUrl}${adminPath}/list-users?${query}`, {
|
|
109
|
-
headers: { accept: 'application/json', ...extraHeaders },
|
|
110
|
-
});
|
|
111
|
-
if (!found.ok) {
|
|
112
|
-
throw await failed('persona lookup', persona.id, found);
|
|
113
|
-
}
|
|
114
|
-
const listed = (await found.json().catch(() => null));
|
|
115
|
-
const existing = listed?.users?.find((u) => u.email === persona.email);
|
|
116
|
-
if (existing?.id) {
|
|
117
|
-
return String(existing.id);
|
|
118
|
-
}
|
|
119
|
-
if (!options.createMissing) {
|
|
120
|
-
throw new Error(`[scenario] no account on the target for persona '${persona.id}' (${persona.email}) — ` +
|
|
121
|
-
'provision it, or set createMissing on the operator credentials');
|
|
122
|
-
}
|
|
123
|
-
const created = await fetchImpl(`${apiUrl}${adminPath}/create-user`, {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: { 'content-type': 'application/json', ...extraHeaders },
|
|
126
|
-
body: JSON.stringify({
|
|
127
|
-
email: persona.email,
|
|
128
|
-
name: persona.name,
|
|
129
|
-
// Never used and never returned: the run impersonates rather than signs
|
|
130
|
-
// in, so the account is reachable only by someone already holding an
|
|
131
|
-
// operator token. A derivable password would undo exactly that.
|
|
132
|
-
password: globalThis.crypto.randomUUID(),
|
|
133
|
-
...(persona.roles[0] ? { role: persona.roles[0] } : {}),
|
|
134
|
-
}),
|
|
135
|
-
});
|
|
136
|
-
if (!created.ok) {
|
|
137
|
-
throw await failed('persona creation', persona.id, created);
|
|
138
|
-
}
|
|
139
|
-
const body = (await created.json().catch(() => null));
|
|
140
|
-
const id = body?.user?.id;
|
|
141
|
-
if (!id) {
|
|
142
|
-
throw new Error(`[scenario] creating persona '${persona.id}' returned no user id`);
|
|
87
|
+
const body = (await res.json().catch(() => null));
|
|
88
|
+
const userId = body?.actAs?.userId;
|
|
89
|
+
if (!userId) {
|
|
90
|
+
throw new Error(`[scenario] operator sign-in for '${persona.id}' returned no user to act as — ` +
|
|
91
|
+
'the target is running a @pikku/better-auth too old to resolve one');
|
|
143
92
|
}
|
|
144
|
-
return String(
|
|
93
|
+
return { setCookies, userId: String(userId) };
|
|
145
94
|
};
|
|
146
95
|
/**
|
|
147
96
|
* Sign a persona in on a DEPLOYED stage, by having a Fabric operator act as
|
|
@@ -33,6 +33,8 @@ export interface ScenarioHttpResponse<T = unknown> {
|
|
|
33
33
|
* `invokeRaw` returns this, and a step that has to reach past a persona — a
|
|
34
34
|
* route with no RPC, an identity no persona can hold — reaches for this rather
|
|
35
35
|
* than writing the same record by hand.
|
|
36
|
+
*
|
|
37
|
+
* @example snippet: scenarioPolling
|
|
36
38
|
*/
|
|
37
39
|
export declare const readScenarioHttpResponse: <T = unknown>(res: Response) => Promise<ScenarioHttpResponse<T>>;
|
|
38
40
|
/** How to send one JSON request, for `postScenarioJson`. */
|
|
@@ -59,6 +61,8 @@ export interface ScenarioJsonRequest {
|
|
|
59
61
|
* outright when the target answers an empty body or an HTML error page. A
|
|
60
62
|
* refusal is the expected outcome of a permissions scenario, so it has to
|
|
61
63
|
* survive as data.
|
|
64
|
+
*
|
|
65
|
+
* @example snippet: scenarioHttpStep
|
|
62
66
|
*/
|
|
63
67
|
export declare const postScenarioJson: <T = unknown>(url: string, { body, headers, method, fetch: send, }?: ScenarioJsonRequest) => Promise<ScenarioHttpResponse<T>>;
|
|
64
68
|
/** Per-call transport options. */
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* `invokeRaw` returns this, and a step that has to reach past a persona — a
|
|
6
6
|
* route with no RPC, an identity no persona can hold — reaches for this rather
|
|
7
7
|
* than writing the same record by hand.
|
|
8
|
+
*
|
|
9
|
+
* @example snippet: scenarioPolling
|
|
8
10
|
*/
|
|
9
11
|
export const readScenarioHttpResponse = async (res) => {
|
|
10
12
|
const text = res.status === 204 ? '' : await res.text().catch(() => '');
|
|
@@ -32,6 +34,8 @@ const parseJsonBody = (text) => {
|
|
|
32
34
|
* outright when the target answers an empty body or an HTML error page. A
|
|
33
35
|
* refusal is the expected outcome of a permissions scenario, so it has to
|
|
34
36
|
* survive as data.
|
|
37
|
+
*
|
|
38
|
+
* @example snippet: scenarioHttpStep
|
|
35
39
|
*/
|
|
36
40
|
export const postScenarioJson = async (url, { body, headers, method = 'POST', fetch: send = fetch, } = {}) => readScenarioHttpResponse(await send(url, {
|
|
37
41
|
method,
|
|
@@ -4,7 +4,6 @@ export type SecretValues<T> = {
|
|
|
4
4
|
[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>;
|