@valbuild/server 0.114.0 → 0.116.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.
@@ -5,4 +5,4 @@ import { ValidationError } from "@valbuild/core";
5
5
  * There is no schema in hand here — a `ValidationError` carries only the value
6
6
  * it flagged — so the shape is all there is to go on.
7
7
  */
8
- export declare function getValidationErrorFileRef(validationError: ValidationError): any;
8
+ export declare function getValidationErrorFileRef(validationError: ValidationError): string | null;
@@ -1,5 +1,8 @@
1
1
  export { createService, Service } from "./Service.js";
2
2
  export { createValApiRouter, createValServer, safeReadGit } from "./ValRouter.js";
3
+ export { createValTools } from "./tools/index.js";
4
+ export type { ValToolContext, ValToolDefinition, ValToolDefinitionJson, ValToolErrorCode, ValToolResult, ValTools, ValToolsOptions, } from "./tools/index.js";
5
+ export { initHandlerOptions, createValOps } from "./valServerConfig.js";
3
6
  export { ValModuleLoader } from "./ValModuleLoader.js";
4
7
  export { getCompilerOptions } from "./getCompilerOptions.js";
5
8
  export { ValSourceFileHandler } from "./ValSourceFileHandler.js";
@@ -25,8 +28,8 @@ export { checkRemoteRef, downloadFileFromRemote, getCachedRemoteFileDir, getCach
25
28
  export { hasRemoteFileSchema } from "@valbuild/core";
26
29
  export { getFileExt } from "./getFileExt.js";
27
30
  export { evalValConfigFile, findAndEvalValConfigFile, } from "./evalValConfigFile.js";
28
- export { startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, } from "./login.js";
29
- export type { ValLoginErrorCode, ValLoginResult, ValLoginSession, } from "./login.js";
31
+ export { startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, } from "./login.js";
32
+ export type { ValLoginErrorCode, ValLoginResult, ValDeviceAuthorization, } from "./login.js";
30
33
  export { createModulePathMap, createJsonEntryPathMap, getModulePathRange, } from "./modulePathMap.js";
31
34
  export { findJsonEntryFilePath } from "./jsonEntryLocation.js";
32
35
  export { classifyJsonValuesOp, rebaseContentOp } from "./patch/jsonValuesPatch.js";
@@ -1,5 +1,15 @@
1
1
  /**
2
- * The Val login device flow, as reusable primitives.
2
+ * The Val login flow, as reusable primitives.
3
+ *
4
+ * This is an RFC 8628 device authorization grant. The shape that matters:
5
+ * {@link ValDeviceAuthorization.deviceCode} is a secret this process holds and
6
+ * polls with, while {@link ValDeviceAuthorization.userCode} is the short string
7
+ * the human reads out of the terminal and types into a browser. Only the device
8
+ * code can collect a token.
9
+ *
10
+ * Keep them apart. Show the user code; never print, log or put the device code
11
+ * in a URL. An earlier version of this flow used one value for both jobs, which
12
+ * meant anyone who saw the verification link could collect the token it led to.
3
13
  *
4
14
  * The CLI wraps these with terminal output, and `@valbuild/language-server`
5
15
  * wraps them with LSP `window/showDocument` and progress reporting. Neither the
@@ -16,6 +26,10 @@ export type ValLoginErrorCode =
16
26
  | "unexpected-response"
17
27
  /** The server returned a 5xx. */
18
28
  | "server-error"
29
+ /** The user declined the login in the browser. */
30
+ | "access-denied"
31
+ /** The login was not approved before the code expired. */
32
+ | "expired"
19
33
  /** The user did not complete the login within the allotted time. */
20
34
  | "timeout"
21
35
  /** The caller aborted the flow. */
@@ -25,12 +39,23 @@ export declare class ValLoginError extends Error {
25
39
  readonly details?: string | undefined;
26
40
  constructor(code: ValLoginErrorCode, message: string, details?: string | undefined);
27
41
  }
28
- /** A login attempt that is waiting for the user to confirm in a browser. */
29
- export type ValLoginSession = {
30
- /** Opaque token used to poll for confirmation. */
31
- nonce: string;
32
- /** URL the user must open to confirm the login. */
33
- url: string;
42
+ /** A login attempt that is waiting for the user to approve it in a browser. */
43
+ export type ValDeviceAuthorization = {
44
+ /**
45
+ * Secret. Polls for the token. Do not display, log or transmit anywhere but
46
+ * the token endpoint.
47
+ */
48
+ deviceCode: string;
49
+ /** Short code for the user to compare and type. Safe to display. */
50
+ userCode: string;
51
+ /** Where the user goes to enter {@link userCode}. */
52
+ verificationUri: string;
53
+ /** {@link verificationUri} with the code prefilled, for convenience. */
54
+ verificationUriComplete: string;
55
+ /** Seconds until the code stops being approvable. */
56
+ expiresInSeconds: number;
57
+ /** Minimum seconds between polls, per the server. */
58
+ intervalSeconds: number;
34
59
  };
35
60
  export type ValLoginResult = {
36
61
  profile: {
@@ -39,24 +64,26 @@ export type ValLoginResult = {
39
64
  pat: string;
40
65
  };
41
66
  /**
42
- * Begin a login attempt. The caller is responsible for getting
43
- * {@link ValLoginSession.url} in front of the user.
67
+ * Begin a login attempt. The caller is responsible for getting the user code
68
+ * and verification URL in front of the user.
44
69
  */
45
70
  export declare function startValLogin(options?: {
46
71
  host?: string;
47
- }): Promise<ValLoginSession>;
48
- export declare const DEFAULT_LOGIN_MAX_DURATION: number;
49
- export declare const DEFAULT_LOGIN_POLL_INTERVAL = 1000;
72
+ deviceName?: string;
73
+ }): Promise<ValDeviceAuthorization>;
74
+ /** Fallbacks for a server that omits the optional RFC 8628 timing fields. */
75
+ export declare const DEFAULT_LOGIN_EXPIRES_IN_SECONDS = 600;
76
+ export declare const DEFAULT_LOGIN_POLL_INTERVAL_SECONDS = 5;
50
77
  /**
51
- * Poll until the user confirms the login in their browser.
78
+ * Poll until the user approves the login in their browser.
52
79
  *
53
80
  * Accepts an `AbortSignal` so an editor can cancel the flow when the user
54
- * dismisses the prompt, instead of leaving a poll loop running for 5 minutes.
81
+ * dismisses the prompt, instead of leaving a poll loop running to expiry.
55
82
  */
56
- export declare function awaitValLoginConfirmation(nonce: string, options?: {
83
+ export declare function awaitValLoginConfirmation(authorization: ValDeviceAuthorization, options?: {
57
84
  host?: string;
85
+ /** Defaults to the authorization's own `expiresInSeconds`. */
58
86
  maxDurationMs?: number;
59
- pollIntervalMs?: number;
60
87
  signal?: AbortSignal;
61
88
  /** Wall clock, injectable for tests. */
62
89
  now?: () => number;
@@ -1,4 +1,4 @@
1
- import { PatchId } from "@valbuild/core";
1
+ import { ModuleFilePath, PatchId } from "@valbuild/core";
2
2
  import { z } from "zod";
3
3
  import type { AuthorId, BaseSha } from "./ValOps.js";
4
4
  import { PatchLogEntry, PatchLogProblem } from "./patchLog.js";
@@ -32,14 +32,14 @@ export declare const FSPatch: z.ZodObject<{
32
32
  patch: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
33
33
  op: z.ZodLiteral<"add">;
34
34
  path: z.ZodArray<z.ZodString>;
35
- value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
35
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
36
36
  }, z.core.$strict>, z.ZodObject<{
37
37
  op: z.ZodLiteral<"remove">;
38
38
  path: z.ZodTuple<[z.ZodString], z.ZodString>;
39
39
  }, z.core.$strict>, z.ZodObject<{
40
40
  op: z.ZodLiteral<"replace">;
41
41
  path: z.ZodArray<z.ZodString>;
42
- value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
42
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
43
43
  }, z.core.$strict>, z.ZodObject<{
44
44
  op: z.ZodLiteral<"move">;
45
45
  from: z.ZodTuple<[z.ZodString], z.ZodString>;
@@ -51,15 +51,15 @@ export declare const FSPatch: z.ZodObject<{
51
51
  }, z.core.$strict>, z.ZodObject<{
52
52
  op: z.ZodLiteral<"test">;
53
53
  path: z.ZodArray<z.ZodString>;
54
- value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
54
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
55
55
  }, z.core.$strict>, z.ZodObject<{
56
56
  op: z.ZodLiteral<"file">;
57
57
  path: z.ZodArray<z.ZodString>;
58
58
  filePath: z.ZodString;
59
- value: z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>;
59
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
60
60
  remote: z.ZodBoolean;
61
61
  nestedFilePath: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
- metadata: z.ZodOptional<z.ZodType<JSONValueT, unknown, z.core.$ZodTypeInternals<JSONValueT, unknown>>>;
62
+ metadata: z.ZodOptional<z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>>;
63
63
  }, z.core.$strict>], "op">>;
64
64
  patchId: z.ZodString;
65
65
  baseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
@@ -0,0 +1,17 @@
1
+ import type { ValModules } from "@valbuild/core";
2
+ import type { ValServerConfig } from "../ValServer.js";
3
+ import type { ValTools } from "./types.js";
4
+ export type ValToolsOptions = ValServerConfig;
5
+ /**
6
+ * Val's server-side tool registry.
7
+ *
8
+ * This is the piece Val did not have: the Studio's chat tools are defined *and
9
+ * executed in the browser*, against its client stores, so nothing here could be
10
+ * re-exposed. These tools run against {@link ValOps} instead, which is what lets
11
+ * an MCP server — or a stdio transport, or anything else — drive Val content
12
+ * without a browser.
13
+ *
14
+ * Transport-agnostic on purpose: nothing under `tools/` imports an MCP SDK. A
15
+ * host adapts {@link ValToolResult} at its own edge.
16
+ */
17
+ export declare function createValTools(valModules: ValModules, options: ValToolsOptions): ValTools;
@@ -0,0 +1,2 @@
1
+ export { createValTools, type ValToolsOptions } from "./createValTools.js";
2
+ export type { ValToolContext, ValToolDefinition, ValToolDefinitionJson, ValToolErrorCode, ValToolResult, ValTools, } from "./types.js";
@@ -0,0 +1,116 @@
1
+ import type { Json } from "@valbuild/core";
2
+ import type { z } from "zod";
3
+ /**
4
+ * The public surface of Val's server-side tool registry.
5
+ *
6
+ * Types only, deliberately: this file is the contract that the MCP hosts, the
7
+ * CLI's stdio transport and the tools themselves are all written against, and
8
+ * keeping it free of implementation means those can be built in any order
9
+ * without one of them owning the shape.
10
+ *
11
+ * The design this implements is `docs/plans/mcp.md`, Part A. Two constraints
12
+ * from it are load-bearing and easy to break by accident:
13
+ *
14
+ * 1. **Nothing here may import an MCP SDK.** That is what lets hosts other
15
+ * than the template consume these tools, and it is not hypothetical
16
+ * hygiene — the TypeScript SDK reorganised itself at v2.0.0, and a registry
17
+ * coupled to it would have moved with it.
18
+ * 2. **The result type is deliberately not MCP's `CallToolResult`.** Each host
19
+ * adapts {@link ValToolResult} at its own edge, which is also where an
20
+ * error becomes an in-band `isError` result the model can recover from
21
+ * rather than a transport failure.
22
+ */
23
+ /** Why a tool call failed, in a form a host can map onto its own errors. */
24
+ export type ValToolErrorCode =
25
+ /** No tool of that name is registered. */
26
+ "unknown-tool"
27
+ /** The arguments did not satisfy the tool's `inputSchema`. */
28
+ | "invalid-args"
29
+ /** The path, module or key the arguments name does not exist. */
30
+ | "not-found"
31
+ /** The caller is not allowed to do this — wrong credential, or none. */
32
+ | "forbidden"
33
+ /**
34
+ * The write was rejected because applying it would leave content invalid.
35
+ * Nothing was stored. See `docs/plans/mcp.md` Part C on speculative
36
+ * validation.
37
+ */
38
+ | "validation-failed"
39
+ /**
40
+ * Another writer moved the patch chain first. Callers may re-derive the
41
+ * parent ref and retry once; the registry does that itself before
42
+ * surfacing this.
43
+ */
44
+ | "conflict"
45
+ /** The tool exists but is not available in this mode (e.g. fs vs http). */
46
+ | "unsupported"
47
+ /** Anything else, including an upstream failure. */
48
+ | "internal";
49
+ export type ValToolDefinition = {
50
+ name: string;
51
+ title?: string;
52
+ description: string;
53
+ /** zod v4 — a Standard Schema, which is what the MCP SDKs consume. */
54
+ inputSchema: z.ZodType;
55
+ /**
56
+ * MCP tool annotations. Hints, not enforcement: a host may show them to a
57
+ * user or use them to decide what to auto-approve, so they have to be
58
+ * honest about what the tool does.
59
+ */
60
+ annotations?: {
61
+ readOnlyHint?: boolean;
62
+ destructiveHint?: boolean;
63
+ idempotentHint?: boolean;
64
+ };
65
+ };
66
+ /**
67
+ * The same definition with `inputSchema` as JSON Schema, for hosts that want the
68
+ * wire shape rather than a Standard Schema.
69
+ *
70
+ * Typed as whatever zod's own converter produces, so deriving it needs no cast
71
+ * and no second hand-written description of the same input.
72
+ */
73
+ export type ValToolDefinitionJson = Omit<ValToolDefinition, "inputSchema"> & {
74
+ inputSchema: ReturnType<typeof z.toJSONSchema<z.ZodType>>;
75
+ };
76
+ /**
77
+ * Who is calling, established once per request by the host.
78
+ *
79
+ * There is deliberately no identity field here, only the credential. In proxy
80
+ * mode the caller's own personal access token authenticates every downstream
81
+ * call, so the backend decides who the caller is (`docs/plans/mcp.md` D.2). An
82
+ * `authorId` alongside it would have to be filled in by the host, and the host
83
+ * has no way to verify a PAT — so the field would be an unverified claim that
84
+ * looks like a checked one, which is the confused-deputy shape D.6 rejects.
85
+ *
86
+ * `null` means local fs mode, where there is no credential to hold and patches
87
+ * are written with no author, exactly as the Studio does locally (D.1). In
88
+ * proxy mode `null` is refused rather than falling back to the app's own API
89
+ * key: that key can do more than any single user, and quietly substituting it
90
+ * would turn a missing credential into full access.
91
+ */
92
+ export type ValToolContext = {
93
+ auth: {
94
+ /**
95
+ * The caller's PAT. Never log this, never put it in a URL, and never let
96
+ * it reach a tool result.
97
+ */
98
+ pat: string;
99
+ } | null;
100
+ /** Groups a run of related edits, when the host has such a notion. */
101
+ sessionId: string | null;
102
+ };
103
+ export type ValToolResult = {
104
+ status: "ok";
105
+ data: Json;
106
+ } | {
107
+ status: "error";
108
+ code: ValToolErrorCode;
109
+ message: string;
110
+ };
111
+ export type ValTools = {
112
+ list(): ValToolDefinition[];
113
+ listJsonSchema(): ValToolDefinitionJson[];
114
+ call(name: string, args: unknown, ctx: ValToolContext): Promise<ValToolResult>;
115
+ dispose(): Promise<void>;
116
+ };
@@ -0,0 +1,81 @@
1
+ import { ValConfig, ValModules } from "@valbuild/core";
2
+ import type { ValServerConfig } from "./ValServer.js";
3
+ import type { ValApiOptions } from "./ValRouter.js";
4
+ import { ValOpsFS } from "./ValOpsFS.js";
5
+ import { ValOpsHttp } from "./ValOpsHttp.js";
6
+ /**
7
+ * Resolving how Val is configured, and building the data layer from it.
8
+ *
9
+ * Both live here rather than inside `createValApiRouter` because the MCP tool
10
+ * registry needs exactly the same answers: which mode we are in, which
11
+ * credential to use, and which `ValOps` implementation that implies. Two copies
12
+ * of this would drift, and the failure would be quiet — a registry that decides
13
+ * it is in fs mode while the Studio decides it is in proxy mode reads different
14
+ * content from the same project.
15
+ *
16
+ * The type-only import from `./ValRouter` is deliberate: it keeps `ValApiOptions`
17
+ * where its documentation lives without creating a runtime cycle.
18
+ *
19
+ * The credential-bearing URL check at the bottom of this file lives here for the
20
+ * same reason: it is a check on the URLs `initHandlerOptions` resolves, and its
21
+ * only caller is that function. Leaving it in `./ValRouter` would have meant
22
+ * importing it back from there, which is the runtime cycle the paragraph above
23
+ * exists to avoid.
24
+ */
25
+ export declare const DEFAULT_VAL_BUILD_URL = "https://admin.val.build";
26
+ /**
27
+ * Resolve options plus environment into a concrete {@link ValServerConfig}.
28
+ *
29
+ * Moved verbatim out of `createValApiRouter`; the precedence rules are load
30
+ * bearing, so this is the one place they are written down. Note that "proxy"
31
+ * mode is inferred when `VAL_API_KEY` or `VAL_SECRET` is present and no mode was
32
+ * given, which is why a project can be pushed into proxy mode by setting an env
33
+ * var alone.
34
+ */
35
+ export declare function initHandlerOptions(route: string, opts: ValApiOptions, config: ValConfig): Promise<ValServerConfig>;
36
+ /**
37
+ * Build the data layer a {@link ValServerConfig} calls for.
38
+ *
39
+ * `auth` decides *whose* credential the http backend sees. Left out, it is the
40
+ * app's own API key — which is what the Studio wants, because there the app has
41
+ * already verified a session cookie and is acting on the user's behalf under its
42
+ * own authority.
43
+ *
44
+ * A caller acting for a user it has *not* authenticated itself must pass that
45
+ * user's personal access token instead, so the backend is the one that decides
46
+ * what the caller may do. That is the whole of `docs/plans/mcp.md` D.2: the app
47
+ * stops being an authority and goes back to being a pipe. Passing the app's API
48
+ * key on such a request is the D.6 confused deputy, and it is worth being blunt
49
+ * about why it is tempting — it works, and it works for every project the key
50
+ * can reach, including the ones the caller cannot.
51
+ */
52
+ export declare function createValOps(valModules: ValModules, options: ValServerConfig, auth?: {
53
+ pat: string;
54
+ }): ValOpsFS | ValOpsHttp;
55
+ /**
56
+ * Hosts we send credentials to, and what each one puts at risk. They differ:
57
+ * only `valBuildUrl` hands back the app token that becomes the session cookie,
58
+ * so a single shared sentence would overstate one and understate the other.
59
+ */
60
+ type CredentialBearingUrl = "valBuildUrl" | "valContentUrl";
61
+ /**
62
+ * Returns a warning if `url` would send credentials somewhere they can be read
63
+ * off the wire, or null if it is fine.
64
+ *
65
+ * Both URLs default to https, but each is overridable - `opts.valBuildUrl` /
66
+ * `VAL_BUILD_URL`, `opts.valContentUrl` / `VAL_CONTENT_URL` - and neither
67
+ * override has ever been scheme-checked. Point one at a plain http host and the
68
+ * api key goes out in clear text, and whatever comes back is whatever the
69
+ * network says: for `valBuildUrl` that includes the app token this server
70
+ * re-signs into the session cookie.
71
+ *
72
+ * Loopback over http is exempt: that is a val.build running on the developer's
73
+ * own machine, and there is no network to be on the wrong side of.
74
+ *
75
+ * This warns rather than throws. Both overrides are set by the operator, not by
76
+ * an attacker, so this is a misconfiguration to surface - not untrusted input to
77
+ * reject - and refusing to boot would break anyone deliberately pointing at an
78
+ * internal http host today.
79
+ */
80
+ export declare function insecureUrlWarning(name: CredentialBearingUrl, url: string): string | null;
81
+ export {};