@skaile/forge-app-actions 0.2.1 → 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.
Files changed (51) hide show
  1. package/README.md +155 -2
  2. package/bin/forge-actions.mjs +274 -0
  3. package/dist/module.d.mts +24 -0
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +78 -1
  6. package/dist/runtime/chat/ai-sdk.d.ts +23 -0
  7. package/dist/runtime/chat/ai-sdk.js +25 -0
  8. package/dist/runtime/chat/index.d.ts +2 -6
  9. package/dist/runtime/chat/index.js +2 -4
  10. package/dist/runtime/client/index.d.ts +57 -0
  11. package/dist/runtime/client/index.js +60 -0
  12. package/dist/runtime/internal/bearer.d.ts +6 -0
  13. package/dist/runtime/internal/bearer.js +5 -0
  14. package/dist/runtime/internal/manifest.d.ts +17 -0
  15. package/dist/runtime/internal/manifest.js +0 -0
  16. package/dist/runtime/internal/tool-name.d.ts +6 -0
  17. package/dist/runtime/internal/tool-name.js +4 -0
  18. package/dist/runtime/server/http/create-handler.d.ts +50 -0
  19. package/dist/runtime/server/http/create-handler.js +135 -0
  20. package/dist/runtime/server/http/index.d.ts +8 -0
  21. package/dist/runtime/server/http/index.js +1 -0
  22. package/dist/runtime/server/http/route.d.ts +16 -0
  23. package/dist/runtime/server/http/route.js +28 -0
  24. package/dist/runtime/server/index.d.ts +2 -0
  25. package/dist/runtime/server/index.js +2 -0
  26. package/dist/runtime/server/mcp/create-handler.d.ts +31 -0
  27. package/dist/runtime/server/mcp/create-handler.js +80 -0
  28. package/dist/runtime/server/mcp/index.d.ts +7 -0
  29. package/dist/runtime/server/mcp/index.js +1 -0
  30. package/dist/runtime/server/mcp/route.d.ts +16 -0
  31. package/dist/runtime/server/mcp/route.js +57 -0
  32. package/dist/runtime/server/utils/invoke-action.d.ts +19 -0
  33. package/dist/runtime/server/utils/invoke-action.js +32 -0
  34. package/dist/runtime/server/utils/server-actions.d.ts +87 -0
  35. package/dist/runtime/server/utils/server-actions.js +75 -0
  36. package/package.json +33 -2
  37. package/src/runtime/chat/ai-sdk.ts +53 -0
  38. package/src/runtime/chat/index.ts +2 -10
  39. package/src/runtime/client/index.ts +131 -0
  40. package/src/runtime/internal/bearer.ts +11 -0
  41. package/src/runtime/internal/manifest.ts +19 -0
  42. package/src/runtime/internal/tool-name.ts +10 -0
  43. package/src/runtime/server/http/create-handler.ts +220 -0
  44. package/src/runtime/server/http/index.ts +9 -0
  45. package/src/runtime/server/http/route.ts +54 -0
  46. package/src/runtime/server/index.ts +2 -0
  47. package/src/runtime/server/mcp/create-handler.ts +134 -0
  48. package/src/runtime/server/mcp/index.ts +8 -0
  49. package/src/runtime/server/mcp/route.ts +90 -0
  50. package/src/runtime/server/utils/invoke-action.ts +71 -0
  51. package/src/runtime/server/utils/server-actions.ts +198 -0
@@ -0,0 +1,198 @@
1
+ // server/utils/server-actions.ts
2
+ //
3
+ // NOTE: this file must live *directly* in `runtime/server/utils/`. The Nuxt
4
+ // module auto-imports that directory NON-recursively (see
5
+ // `client-capability-bridge.ts`), so a subdirectory would silently never reach
6
+ // consumers.
7
+ //
8
+ // Server actions are headless: they run in Nitro, have no palette row and no
9
+ // browser round-trip. They are the unit the `./mcp` projection exposes as MCP
10
+ // tools. Browser actions (`defineForgeAction`) are unaffected.
11
+
12
+ import * as z from "zod";
13
+
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. */
25
+ name: string;
26
+ /** What the transport's authorize hook set as `principal` (MCP: `forge-app-actions:mcp:authorize`). */
27
+ principal: P;
28
+ /** Host event (under Nitro an `H3Event`); apps pass it on to their server utils. */
29
+ event: unknown;
30
+ /** What the transport's authorize hook set as `deps`, e.g. injected service clients. */
31
+ deps: D;
32
+ }
33
+
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
+ > {
47
+ /** Without the `app.` prefix, e.g. `trainings.create`; the name becomes `app.<id>`. An id starting with `app.` throws. */
48
+ id: Id;
49
+ /** Description the model reads. Required; empty throws. */
50
+ description: string;
51
+ /** Zod schema of the input. Defaults to `z.object({})`. */
52
+ input?: T;
53
+ /** Documentation only, as with `defineForgeAction`. Defaults to `"effect"`. */
54
+ kind?: "effect" | "query";
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;
65
+ }
66
+
67
+ export interface ServerAction<Id extends string = string, I = unknown, O = unknown> {
68
+ id: Id;
69
+ name: string;
70
+ description: string;
71
+ kind: "effect" | "query";
72
+ readonly audience: readonly ServerActionAudience[];
73
+ readonly inputZod: z.ZodType;
74
+ /** `z.toJSONSchema(inputZod, { target: "draft-07", io: "input" })` without `$schema`. */
75
+ inputSchema: Record<string, unknown>;
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>;
84
+ }
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
+
92
+ /** The derived JSON Schema without its top-level `$schema` draft marker; everything else stays. */
93
+ function withoutSchemaKey(schema: Record<string, unknown>): Record<string, unknown> {
94
+ const { $schema: _draft, ...rest } = schema;
95
+ return rest;
96
+ }
97
+
98
+ export function defineServerAction<
99
+ const Id extends string,
100
+ T extends z.ZodType = z.ZodObject<Record<string, never>>,
101
+ P = unknown,
102
+ D = unknown,
103
+ R = unknown,
104
+ >(args: DefineServerActionArgs<Id, T, P, D, R>): ServerAction<Id, z.input<T>, Awaited<R>> {
105
+ const { id } = args;
106
+ if (!id || id.trim().length === 0) {
107
+ throw new Error("defineServerAction: an action needs a non-empty id.");
108
+ }
109
+ if (id.startsWith("app.")) {
110
+ throw new Error(
111
+ `defineServerAction("${id}"): the id must not carry the \`app.\` prefix — it is added ` +
112
+ `automatically to derive the action name. Use "${id.slice(4)}".`,
113
+ );
114
+ }
115
+ if (!args.description || args.description.trim().length === 0) {
116
+ throw new Error(
117
+ `defineServerAction("${id}"): a server action must declare a description — the model reads it.`,
118
+ );
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)]);
127
+
128
+ const inputZod: z.ZodType = args.input ?? z.object({});
129
+ const inputSchema = withoutSchemaKey(
130
+ z.toJSONSchema(inputZod, { target: "draft-07", io: "input" }) as Record<string, unknown>,
131
+ );
132
+ const name = `app.${id}`;
133
+
134
+ return {
135
+ id,
136
+ name,
137
+ description: args.description,
138
+ kind: args.kind ?? "effect",
139
+ audience,
140
+ inputZod,
141
+ inputSchema,
142
+ async run(input, ctx) {
143
+ const parsed = inputZod.parse(input) as z.infer<T>;
144
+ const context = { ...ctx, name } as ServerActionContext<P, D>;
145
+ await args.authorize?.(parsed, context);
146
+ return await args.run(parsed, context);
147
+ },
148
+ };
149
+ }
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
+
173
+ export class ServerActionRegistry {
174
+ private readonly actions = new Map<string, ServerAction>();
175
+
176
+ register(action: ServerAction): void {
177
+ if (this.actions.has(action.name)) {
178
+ throw new Error(`Server action already registered: ${action.name}`);
179
+ }
180
+ this.actions.set(action.name, action);
181
+ }
182
+
183
+ registerAll(actions: readonly ServerAction[]): void {
184
+ for (const action of actions) this.register(action);
185
+ }
186
+
187
+ /** In registration order. */
188
+ list(): ServerAction[] {
189
+ return [...this.actions.values()];
190
+ }
191
+
192
+ get(name: string): ServerAction | undefined {
193
+ return this.actions.get(name);
194
+ }
195
+ }
196
+
197
+ /** Process-wide singleton, like `capabilityRegistry`. */
198
+ export const serverActionRegistry = new ServerActionRegistry();