@skaile/forge-app-actions 0.3.0 → 0.4.0
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/README.md +111 -4
- package/bin/forge-actions.mjs +274 -0
- package/dist/module.d.mts +10 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +55 -16
- package/dist/runtime/chat/ai-sdk.d.ts +23 -0
- package/dist/runtime/chat/ai-sdk.js +25 -0
- package/dist/runtime/client/index.d.ts +57 -0
- package/dist/runtime/client/index.js +60 -0
- package/dist/runtime/internal/bearer.d.ts +6 -0
- package/dist/runtime/internal/bearer.js +5 -0
- package/dist/runtime/internal/manifest.d.ts +17 -0
- package/dist/runtime/internal/manifest.js +0 -0
- package/dist/runtime/server/http/create-handler.d.ts +50 -0
- package/dist/runtime/server/http/create-handler.js +135 -0
- package/dist/runtime/server/http/index.d.ts +8 -0
- package/dist/runtime/server/http/index.js +1 -0
- package/dist/runtime/server/http/route.d.ts +16 -0
- package/dist/runtime/server/http/route.js +28 -0
- package/dist/runtime/server/index.d.ts +1 -0
- package/dist/runtime/server/index.js +1 -0
- package/dist/runtime/server/mcp/create-handler.d.ts +8 -6
- package/dist/runtime/server/mcp/create-handler.js +8 -26
- package/dist/runtime/server/mcp/route.d.ts +2 -0
- package/dist/runtime/server/mcp/route.js +7 -10
- package/dist/runtime/server/utils/invoke-action.d.ts +19 -0
- package/dist/runtime/server/utils/invoke-action.js +32 -0
- package/dist/runtime/server/utils/server-actions.d.ts +57 -12
- package/dist/runtime/server/utils/server-actions.js +20 -2
- package/package.json +26 -2
- package/src/runtime/chat/ai-sdk.ts +53 -0
- package/src/runtime/client/index.ts +131 -0
- package/src/runtime/internal/bearer.ts +11 -0
- package/src/runtime/internal/manifest.ts +19 -0
- package/src/runtime/server/http/create-handler.ts +220 -0
- package/src/runtime/server/http/index.ts +9 -0
- package/src/runtime/server/http/route.ts +54 -0
- package/src/runtime/server/index.ts +1 -0
- package/src/runtime/server/mcp/create-handler.ts +16 -43
- package/src/runtime/server/mcp/route.ts +8 -11
- package/src/runtime/server/utils/invoke-action.ts +71 -0
- package/src/runtime/server/utils/server-actions.ts +95 -14
|
@@ -8,17 +8,18 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
8
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
9
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
import * as z from "zod";
|
|
12
11
|
import { toolNameOf } from "../../internal/tool-name";
|
|
13
|
-
import
|
|
12
|
+
import { invokeAction } from "../utils/invoke-action";
|
|
13
|
+
import type { ServerAction, ServerActionScope } from "../utils/server-actions";
|
|
14
14
|
|
|
15
15
|
export { toolNameOf };
|
|
16
16
|
|
|
17
17
|
export interface ActionsMcpServerOptions {
|
|
18
|
-
actions
|
|
18
|
+
/** Served as given; narrow them with `forAudience(actions, "llm")` first to honor audiences. */
|
|
19
|
+
actions: readonly ServerAction[];
|
|
19
20
|
serverInfo: { name: string; version: string };
|
|
20
|
-
/** `principal` and `
|
|
21
|
-
context:
|
|
21
|
+
/** `principal`, `event` and `deps` handed to every action as `ctx`; its `name` is derived per action. */
|
|
22
|
+
context: ServerActionScope;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type ToolInputSchema = { type: "object"; [key: string]: unknown };
|
|
@@ -27,33 +28,11 @@ const text = (value: string) => [{ type: "text" as const, text: value }];
|
|
|
27
28
|
|
|
28
29
|
const failure = (message: string) => ({ isError: true as const, content: text(message) });
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
* A ZodError by shape, not `instanceof`: the schema belongs to the app, whose
|
|
32
|
-
* zod instance may differ from the one bundled with this package.
|
|
33
|
-
*/
|
|
34
|
-
function isZodError(error: unknown): error is z.core.$ZodError {
|
|
35
|
-
const candidate = error as { name?: unknown; issues?: unknown } | null;
|
|
36
|
-
return (
|
|
37
|
-
typeof candidate?.name === "string" &&
|
|
38
|
-
/^\$?ZodError$/.test(candidate.name) &&
|
|
39
|
-
Array.isArray(candidate.issues)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** The message to show the model: an H3 error carries it in `statusMessage`. */
|
|
44
|
-
function messageOf(error: unknown): string {
|
|
45
|
-
if (error && typeof error === "object") {
|
|
46
|
-
const { statusMessage, message } = error as { statusMessage?: unknown; message?: unknown };
|
|
47
|
-
if (typeof statusMessage === "string" && statusMessage) return statusMessage;
|
|
48
|
-
if (typeof message === "string") return message;
|
|
49
|
-
}
|
|
50
|
-
return String(error);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
31
|
/**
|
|
54
32
|
* An MCP server whose tools are the given server actions, named
|
|
55
|
-
* `toolNameOf(action.name)`. Every call
|
|
56
|
-
*
|
|
33
|
+
* `toolNameOf(action.name)`. Every call goes through `invokeAction` with `context`;
|
|
34
|
+
* a failed call (invalid input, denied by the action's `authorize`, handler error)
|
|
35
|
+
* is an `isError` result carrying the outcome's message, never an HTTP error.
|
|
57
36
|
*/
|
|
58
37
|
export function createActionsMcpServer(options: ActionsMcpServerOptions): Server {
|
|
59
38
|
const byTool = new Map<string, ServerAction>();
|
|
@@ -81,20 +60,14 @@ export function createActionsMcpServer(options: ActionsMcpServerOptions): Server
|
|
|
81
60
|
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
|
82
61
|
const action = byTool.get(params.name);
|
|
83
62
|
if (!action) return failure(`unknown tool: ${params.name}`);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (result !== null && typeof result === "object" && !Array.isArray(result)) {
|
|
91
|
-
return { content, structuredContent: result as Record<string, unknown> };
|
|
92
|
-
}
|
|
93
|
-
return { content };
|
|
94
|
-
} catch (error) {
|
|
95
|
-
if (isZodError(error)) return failure(`invalid input: ${z.prettifyError(error)}`);
|
|
96
|
-
return failure(messageOf(error));
|
|
63
|
+
const outcome = await invokeAction(action, params.arguments ?? {}, options.context);
|
|
64
|
+
if (!outcome.ok) return failure(outcome.message);
|
|
65
|
+
const { result } = outcome;
|
|
66
|
+
const content = text(JSON.stringify(result) ?? "null");
|
|
67
|
+
if (result !== null && typeof result === "object" && !Array.isArray(result)) {
|
|
68
|
+
return { content, structuredContent: result as Record<string, unknown> };
|
|
97
69
|
}
|
|
70
|
+
return { content };
|
|
98
71
|
});
|
|
99
72
|
|
|
100
73
|
return server;
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
setResponseStatus,
|
|
13
13
|
} from "h3";
|
|
14
14
|
import { useNitroApp, useRuntimeConfig } from "nitropack/runtime";
|
|
15
|
-
import {
|
|
15
|
+
import { bearerOf } from "../../internal/bearer";
|
|
16
|
+
import { forAudience, serverActionRegistry } from "../utils/server-actions";
|
|
16
17
|
import { createActionsMcpHttpHandler } from "./create-handler";
|
|
17
18
|
|
|
18
19
|
/** What the `forge-app-actions:mcp:authorize` hook receives and may change. */
|
|
@@ -24,13 +25,8 @@ export interface McpAuthorizeDecision {
|
|
|
24
25
|
allowed: boolean;
|
|
25
26
|
/** What the actions see as `ctx.principal`. */
|
|
26
27
|
principal?: unknown;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const BEARER = /^Bearer\s+(\S.*)$/i;
|
|
30
|
-
|
|
31
|
-
function bearerOf(event: H3Event): string | null {
|
|
32
|
-
const header = getRequestHeader(event, "authorization");
|
|
33
|
-
return header?.match(BEARER)?.[1]?.trim() || null;
|
|
28
|
+
/** What the actions see as `ctx.deps`, e.g. injected service clients. */
|
|
29
|
+
deps?: unknown;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
/** The Streamable HTTP MCP endpoint over the registered server actions. */
|
|
@@ -47,9 +43,10 @@ export default defineEventHandler(async (event) => {
|
|
|
47
43
|
|
|
48
44
|
const decision: McpAuthorizeDecision = {
|
|
49
45
|
event,
|
|
50
|
-
bearer: bearerOf(event),
|
|
46
|
+
bearer: bearerOf(getRequestHeader(event, "authorization")),
|
|
51
47
|
allowed: false,
|
|
52
48
|
principal: undefined,
|
|
49
|
+
deps: undefined,
|
|
53
50
|
};
|
|
54
51
|
try {
|
|
55
52
|
// The hook's typing comes from the module's type template, which is absent
|
|
@@ -86,8 +83,8 @@ export default defineEventHandler(async (event) => {
|
|
|
86
83
|
};
|
|
87
84
|
// The handler writes the response itself; h3 sees `res.writableEnded` and sends nothing more.
|
|
88
85
|
await createActionsMcpHttpHandler({
|
|
89
|
-
actions: serverActionRegistry.list(),
|
|
86
|
+
actions: forAudience(serverActionRegistry.list(), "llm"),
|
|
90
87
|
serverInfo: { name: mcp.name, version: mcp.version },
|
|
91
|
-
context: { principal: decision.principal, event },
|
|
88
|
+
context: { principal: decision.principal, event, deps: decision.deps },
|
|
92
89
|
})(event.node.req, event.node.res, parsed);
|
|
93
90
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// server/utils/invoke-action.ts
|
|
2
|
+
//
|
|
3
|
+
// Directly in `runtime/server/utils/` for the same reason as `server-actions.ts`.
|
|
4
|
+
// Transport-neutral: no H3, MCP or AI SDK import, so every projection (MCP, HTTP,
|
|
5
|
+
// chat tools) classifies failures the same way.
|
|
6
|
+
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import type { ServerAction, ServerActionScope } from "./server-actions";
|
|
9
|
+
|
|
10
|
+
/** What a projection maps to its own error shape: `status` is HTTP-style, whatever the transport. */
|
|
11
|
+
export type ActionOutcome =
|
|
12
|
+
| { ok: true; result: unknown }
|
|
13
|
+
| { ok: false; status: number; message: string; issues?: unknown[] };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A ZodError by shape, not `instanceof`: the schema belongs to the app, whose
|
|
17
|
+
* zod instance may differ from the one bundled with this package.
|
|
18
|
+
*/
|
|
19
|
+
function isZodError(error: unknown): error is z.core.$ZodError {
|
|
20
|
+
const candidate = error as { name?: unknown; issues?: unknown } | null;
|
|
21
|
+
return (
|
|
22
|
+
typeof candidate?.name === "string" &&
|
|
23
|
+
/^\$?ZodError$/.test(candidate.name) &&
|
|
24
|
+
Array.isArray(candidate.issues)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The message to show the caller: an H3 error carries it in `statusMessage`. */
|
|
29
|
+
function messageOf(error: unknown): string {
|
|
30
|
+
if (error && typeof error === "object") {
|
|
31
|
+
const { statusMessage, message } = error as { statusMessage?: unknown; message?: unknown };
|
|
32
|
+
if (typeof statusMessage === "string" && statusMessage) return statusMessage;
|
|
33
|
+
if (typeof message === "string") return message;
|
|
34
|
+
}
|
|
35
|
+
return String(error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An explicit HTTP-style status on the error (H3 errors carry `statusCode`), if it is a valid one. */
|
|
39
|
+
function statusOf(error: unknown): number | undefined {
|
|
40
|
+
const { statusCode } = (error ?? {}) as { statusCode?: unknown };
|
|
41
|
+
return typeof statusCode === "number" && Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600
|
|
42
|
+
? statusCode
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs `action` and classifies the result: schema error → 400 with
|
|
48
|
+
* `invalid input: …` and its `issues`; an error with a numeric `statusCode` →
|
|
49
|
+
* that status; anything else → 500. Calls `action.run` only: parsing and the
|
|
50
|
+
* action's `authorize` happen there, exactly once, so this never reparses or
|
|
51
|
+
* authorizes a second time.
|
|
52
|
+
*/
|
|
53
|
+
export async function invokeAction(
|
|
54
|
+
action: ServerAction,
|
|
55
|
+
input: unknown,
|
|
56
|
+
ctx: ServerActionScope,
|
|
57
|
+
): Promise<ActionOutcome> {
|
|
58
|
+
try {
|
|
59
|
+
return { ok: true, result: await action.run(input, ctx) };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (isZodError(error)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
status: 400,
|
|
65
|
+
message: `invalid input: ${z.prettifyError(error)}`,
|
|
66
|
+
issues: error.issues,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { ok: false, status: statusOf(error) ?? 500, message: messageOf(error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -11,39 +11,84 @@
|
|
|
11
11
|
|
|
12
12
|
import * as z from "zod";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/** Who may be offered an action: people/CLIs (`user`) and/or models (`llm`). Exposure control, not authorization. */
|
|
15
|
+
export type ServerActionAudience = "user" | "llm";
|
|
16
|
+
|
|
17
|
+
const AUDIENCES: readonly ServerActionAudience[] = ["user", "llm"];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What an action sees. The core fixes no principal shape: `P` is whatever the
|
|
21
|
+
* app's transport hook resolved, `D` whatever dependencies it injects.
|
|
22
|
+
*/
|
|
23
|
+
export interface ServerActionContext<P = unknown, D = unknown> {
|
|
24
|
+
/** Canonical capability name `app.<id>`, derived by `run`; never taken from the caller. */
|
|
16
25
|
name: string;
|
|
17
|
-
/** What the `forge-app-actions:mcp:authorize`
|
|
26
|
+
/** What the transport's authorize hook set as `principal` (MCP: `forge-app-actions:mcp:authorize`). */
|
|
18
27
|
principal: P;
|
|
19
28
|
/** Host event (under Nitro an `H3Event`); apps pass it on to their server utils. */
|
|
20
29
|
event: unknown;
|
|
30
|
+
/** What the transport's authorize hook set as `deps`, e.g. injected service clients. */
|
|
31
|
+
deps: D;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The caller-supplied part of a context. `run` derives `name` itself and
|
|
36
|
+
* ignores one passed here.
|
|
37
|
+
*/
|
|
38
|
+
export type ServerActionScope<P = unknown, D = unknown> = Omit<ServerActionContext<P, D>, "name">;
|
|
39
|
+
|
|
40
|
+
export interface DefineServerActionArgs<
|
|
41
|
+
Id extends string,
|
|
42
|
+
T extends z.ZodType,
|
|
43
|
+
P = unknown,
|
|
44
|
+
D = unknown,
|
|
45
|
+
R = unknown,
|
|
46
|
+
> {
|
|
24
47
|
/** Without the `app.` prefix, e.g. `trainings.create`; the name becomes `app.<id>`. An id starting with `app.` throws. */
|
|
25
|
-
id:
|
|
48
|
+
id: Id;
|
|
26
49
|
/** Description the model reads. Required; empty throws. */
|
|
27
50
|
description: string;
|
|
28
51
|
/** Zod schema of the input. Defaults to `z.object({})`. */
|
|
29
52
|
input?: T;
|
|
30
53
|
/** Documentation only, as with `defineForgeAction`. Defaults to `"effect"`. */
|
|
31
54
|
kind?: "effect" | "query";
|
|
32
|
-
|
|
55
|
+
/** Who is offered the action. Defaults to both; an empty array throws. */
|
|
56
|
+
audience?: readonly ServerActionAudience[];
|
|
57
|
+
/**
|
|
58
|
+
* Runs after the input parsed and before the handler, with the canonical
|
|
59
|
+
* context. Throw to deny (an error with a numeric `statusCode` keeps it);
|
|
60
|
+
* the handler then does not run. Read the current authority here, not from a
|
|
61
|
+
* value captured earlier.
|
|
62
|
+
*/
|
|
63
|
+
authorize?: (input: z.infer<T>, ctx: ServerActionContext<P, D>) => void | Promise<void>;
|
|
64
|
+
run: (input: z.infer<T>, ctx: ServerActionContext<P, D>) => R;
|
|
33
65
|
}
|
|
34
66
|
|
|
35
|
-
export interface ServerAction {
|
|
36
|
-
id:
|
|
67
|
+
export interface ServerAction<Id extends string = string, I = unknown, O = unknown> {
|
|
68
|
+
id: Id;
|
|
37
69
|
name: string;
|
|
38
70
|
description: string;
|
|
39
71
|
kind: "effect" | "query";
|
|
72
|
+
readonly audience: readonly ServerActionAudience[];
|
|
40
73
|
readonly inputZod: z.ZodType;
|
|
41
74
|
/** `z.toJSONSchema(inputZod, { target: "draft-07", io: "input" })` without `$schema`. */
|
|
42
75
|
inputSchema: Record<string, unknown>;
|
|
43
|
-
/**
|
|
44
|
-
|
|
76
|
+
/** Type-level only: never set at runtime. Read it through `ActionInput` / `ActionOutput`. */
|
|
77
|
+
readonly types?: { input: I; output: O };
|
|
78
|
+
/**
|
|
79
|
+
* Parses `input` through `inputZod` once (a `ZodError` propagates), awaits
|
|
80
|
+
* `authorize` (a rejection propagates and nothing ran), then calls the handler.
|
|
81
|
+
* The context's `name` is always `app.<id>`; a `name` in `ctx` is ignored.
|
|
82
|
+
*/
|
|
83
|
+
run(input: unknown, ctx: ServerActionScope & { name?: string }): Promise<unknown>;
|
|
45
84
|
}
|
|
46
85
|
|
|
86
|
+
/** The input type an action accepts (`z.input` of its schema). */
|
|
87
|
+
export type ActionInput<A> = A extends ServerAction<string, infer I, unknown> ? I : never;
|
|
88
|
+
|
|
89
|
+
/** The output type an action resolves to. */
|
|
90
|
+
export type ActionOutput<A> = A extends ServerAction<string, unknown, infer O> ? O : never;
|
|
91
|
+
|
|
47
92
|
/** The derived JSON Schema without its top-level `$schema` draft marker; everything else stays. */
|
|
48
93
|
function withoutSchemaKey(schema: Record<string, unknown>): Record<string, unknown> {
|
|
49
94
|
const { $schema: _draft, ...rest } = schema;
|
|
@@ -51,9 +96,12 @@ function withoutSchemaKey(schema: Record<string, unknown>): Record<string, unkno
|
|
|
51
96
|
}
|
|
52
97
|
|
|
53
98
|
export function defineServerAction<
|
|
99
|
+
const Id extends string,
|
|
54
100
|
T extends z.ZodType = z.ZodObject<Record<string, never>>,
|
|
55
101
|
P = unknown,
|
|
56
|
-
|
|
102
|
+
D = unknown,
|
|
103
|
+
R = unknown,
|
|
104
|
+
>(args: DefineServerActionArgs<Id, T, P, D, R>): ServerAction<Id, z.input<T>, Awaited<R>> {
|
|
57
105
|
const { id } = args;
|
|
58
106
|
if (!id || id.trim().length === 0) {
|
|
59
107
|
throw new Error("defineServerAction: an action needs a non-empty id.");
|
|
@@ -69,26 +117,59 @@ export function defineServerAction<
|
|
|
69
117
|
`defineServerAction("${id}"): a server action must declare a description — the model reads it.`,
|
|
70
118
|
);
|
|
71
119
|
}
|
|
120
|
+
const requested = args.audience ?? AUDIENCES;
|
|
121
|
+
if (requested.length === 0 || requested.some((entry) => !AUDIENCES.includes(entry))) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`defineServerAction("${id}"): audience must be a non-empty subset of ${JSON.stringify(AUDIENCES)}.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const audience = Object.freeze([...new Set(requested)]);
|
|
72
127
|
|
|
73
128
|
const inputZod: z.ZodType = args.input ?? z.object({});
|
|
74
129
|
const inputSchema = withoutSchemaKey(
|
|
75
130
|
z.toJSONSchema(inputZod, { target: "draft-07", io: "input" }) as Record<string, unknown>,
|
|
76
131
|
);
|
|
132
|
+
const name = `app.${id}`;
|
|
77
133
|
|
|
78
134
|
return {
|
|
79
135
|
id,
|
|
80
|
-
name
|
|
136
|
+
name,
|
|
81
137
|
description: args.description,
|
|
82
138
|
kind: args.kind ?? "effect",
|
|
139
|
+
audience,
|
|
83
140
|
inputZod,
|
|
84
141
|
inputSchema,
|
|
85
142
|
async run(input, ctx) {
|
|
86
143
|
const parsed = inputZod.parse(input) as z.infer<T>;
|
|
87
|
-
|
|
144
|
+
const context = { ...ctx, name } as ServerActionContext<P, D>;
|
|
145
|
+
await args.authorize?.(parsed, context);
|
|
146
|
+
return await args.run(parsed, context);
|
|
88
147
|
},
|
|
89
148
|
};
|
|
90
149
|
}
|
|
91
150
|
|
|
151
|
+
/**
|
|
152
|
+
* `defineServerAction` with the app's principal and deps types bound once, so
|
|
153
|
+
* handlers and `authorize` callbacks are typed without annotating every definition.
|
|
154
|
+
*/
|
|
155
|
+
export function createServerActionDefiner<P = unknown, D = unknown>() {
|
|
156
|
+
return <
|
|
157
|
+
const Id extends string,
|
|
158
|
+
T extends z.ZodType = z.ZodObject<Record<string, never>>,
|
|
159
|
+
R = unknown,
|
|
160
|
+
>(
|
|
161
|
+
args: DefineServerActionArgs<Id, T, P, D, R>,
|
|
162
|
+
): ServerAction<Id, z.input<T>, Awaited<R>> => defineServerAction<Id, T, P, D, R>(args);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The actions offered to `audience`, in order. Element types are kept, so a typed catalogue stays typed. */
|
|
166
|
+
export function forAudience<A extends ServerAction>(
|
|
167
|
+
actions: readonly A[],
|
|
168
|
+
audience: ServerActionAudience,
|
|
169
|
+
): A[] {
|
|
170
|
+
return actions.filter((action) => action.audience.includes(audience));
|
|
171
|
+
}
|
|
172
|
+
|
|
92
173
|
export class ServerActionRegistry {
|
|
93
174
|
private readonly actions = new Map<string, ServerAction>();
|
|
94
175
|
|
|
@@ -99,7 +180,7 @@ export class ServerActionRegistry {
|
|
|
99
180
|
this.actions.set(action.name, action);
|
|
100
181
|
}
|
|
101
182
|
|
|
102
|
-
registerAll(actions: ServerAction[]): void {
|
|
183
|
+
registerAll(actions: readonly ServerAction[]): void {
|
|
103
184
|
for (const action of actions) this.register(action);
|
|
104
185
|
}
|
|
105
186
|
|