@valbuild/server 0.120.0 → 0.120.1

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.120.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#590](https://github.com/valbuild/val/pull/590) [`6f318d4`](https://github.com/valbuild/val/commit/6f318d406295b772e721bf463283f47e2822e996) Thanks [@freekh](https://github.com/freekh)! - MCP: survive a signing-key rotation, and say what a refused local token means.
8
+
9
+ `@valbuild/next` caches the authorization server's JWKS for five minutes. Until
10
+ now a token signed with a key that arrived inside that window was refused
11
+ outright, so every warm instance rejected valid tokens until the cache expired —
12
+ a rotation on the issuer's side showed up as an outage on yours.
13
+
14
+ A token naming a key the cache does not hold now provokes one refetch of the key
15
+ set, at most once per issuer every 30 seconds. The rate limit matters because the
16
+ key id comes from the token: without it, unknown key ids would be a way to make
17
+ your app call its issuer once per request. It limits how often a fetch is
18
+ _started_, so requests that arrive while one is already running join it — which
19
+ is the normal shape of a rotation, where many requests meet the new key at once.
20
+
21
+ Separately, an MCP call that presents an access token to a project running in
22
+ local filesystem mode is still refused — there is nothing to authenticate against
23
+ — but the message now names the cause, which is that the project has an `oauth`
24
+ issuer configured (often `VAL_OAUTH_ISSUER` in a local `.env`) and should not
25
+ have one for local development.
26
+
3
27
  ## 0.120.0
4
28
 
5
29
  ### Patch Changes
@@ -1,6 +1,8 @@
1
1
  import type { ValModules } from "@valbuild/core";
2
2
  import type { ValServerConfig } from "../ValServer.js";
3
- import { type ValTools } from "./types.js";
3
+ import type { ValOps } from "../ValOps.js";
4
+ import type { ValToolState } from "./defineTool.js";
5
+ import { type ValToolResult, type ValTools } from "./types.js";
4
6
  export type ValToolsOptions = ValServerConfig;
5
7
  /**
6
8
  * Val's server-side tool registry.
@@ -15,3 +17,25 @@ export type ValToolsOptions = ValServerConfig;
15
17
  * host adapts {@link ValToolResult} at its own edge.
16
18
  */
17
19
  export declare function createValTools(valModules: ValModules, options: ValToolsOptions): ValTools;
20
+ /**
21
+ * The content as the caller should see it, loaded once per call.
22
+ *
23
+ * Pending patches are applied, because an agent looking at a project mid-edit
24
+ * should see what the Studio would show rather than the last published state.
25
+ *
26
+ * Deliberately not cached across calls. In fs mode a save recomputes the base
27
+ * sha within the same process, so a cached view would go stale silently — and
28
+ * the cost of being wrong here is an agent writing a patch against content that
29
+ * has already moved.
30
+ *
31
+ * Exported for `toolsFixture`, not for consumers: `tools/index.ts` re-exports by
32
+ * name, so this stays inside the package. A test that assembled its own state
33
+ * would be asserting against a view no real call ever sees.
34
+ */
35
+ export declare function loadState(ops: ValOps): Promise<{
36
+ status: "ok";
37
+ state: ValToolState;
38
+ } | {
39
+ status: "error";
40
+ result: ValToolResult;
41
+ }>;
@@ -0,0 +1,69 @@
1
+ import type { Json, ModuleFilePath, PatchId, SerializedSchema } from "@valbuild/core";
2
+ import type { z } from "zod";
3
+ import type { ValOps, OrderedPatches, PatchAnalysis, Schemas, Sources } from "../ValOps.js";
4
+ import type { ValToolContext, ValToolDefinition, ValToolErrorCode, ValToolResult } from "./types.js";
5
+ /**
6
+ * How a tool is written, and what it is handed.
7
+ *
8
+ * Tools are defined with {@link defineTool} so that the handler's `args` are
9
+ * inferred from the tool's own `inputSchema`. Without that the array of tools
10
+ * would have to be typed at its widest and every handler would start by
11
+ * re-narrowing `unknown`, which is exactly where a tool and its schema drift
12
+ * apart unnoticed.
13
+ */
14
+ /** Everything a tool is allowed to reach. Deliberately narrow. */
15
+ export type ValToolDeps = {
16
+ /**
17
+ * The data layer for *this call*. In proxy mode it is authenticated as the
18
+ * caller, so a tool must never reach for an ambient instance instead.
19
+ */
20
+ ops: ValOps;
21
+ ctx: ValToolContext;
22
+ /**
23
+ * The content as the caller should see it: base sources with pending patches
24
+ * applied, plus the schemas and the patch analysis that produced them.
25
+ *
26
+ * Loaded once per call and shared, because a tool that re-derived it would
27
+ * both pay for it again and risk reading a different revision than the one it
28
+ * validated against.
29
+ */
30
+ state: ValToolState;
31
+ };
32
+ export type ValToolState = {
33
+ schemas: Schemas;
34
+ serializedSchemas: Record<ModuleFilePath, SerializedSchema>;
35
+ sources: Sources;
36
+ patches: OrderedPatches;
37
+ analysis: PatchAnalysis;
38
+ /**
39
+ * Pending patches that would not apply, by the module they belong to.
40
+ *
41
+ * A module listed here has `sources` that silently lack those changes, so its
42
+ * content is not what publishing would produce. Reads still return it — it is
43
+ * what the Studio would show too, and a project has to stay diagnosable — but
44
+ * a write to such a module is refused, because it would be based on a state
45
+ * that does not exist.
46
+ */
47
+ unappliedPatches: Record<ModuleFilePath, {
48
+ patchId: PatchId;
49
+ skipped: boolean;
50
+ error: {
51
+ message: string;
52
+ };
53
+ }[]>;
54
+ };
55
+ export type ValToolImpl = ValToolDefinition & {
56
+ handler: (args: unknown, deps: ValToolDeps) => Promise<ValToolResult>;
57
+ };
58
+ /**
59
+ * Declare a tool, binding its handler to its input schema.
60
+ *
61
+ * The handler receives already-parsed arguments: the registry validates against
62
+ * `inputSchema` before calling, so a handler never sees input its schema would
63
+ * have rejected.
64
+ */
65
+ export declare function defineTool<S extends z.ZodType>(definition: Omit<ValToolDefinition, "inputSchema"> & {
66
+ inputSchema: S;
67
+ }, handler: (args: z.infer<S>, deps: ValToolDeps) => Promise<ValToolResult>): ValToolImpl;
68
+ export declare function ok(data: Json): ValToolResult;
69
+ export declare function err(code: ValToolErrorCode, message: string): ValToolResult;
@@ -11935,12 +11935,19 @@ function createOpsResolver(valModules, options) {
11935
11935
  // credential should not silently get local filesystem access instead —
11936
11936
  // and the difference matters, because fs mode writes straight to disk
11937
11937
  // with no backend permission check at all.
11938
+ //
11939
+ // The two credentials get different messages because they arrive here
11940
+ // for different reasons. A PAT is something the caller chose to send. A
11941
+ // verified access token is not: it only exists because this app
11942
+ // advertised an authorization server, so the developer seeing this did
11943
+ // not do anything wrong — a config file did, and naming it is the
11944
+ // difference between a two-minute fix and an afternoon.
11938
11945
  return {
11939
11946
  status: "error",
11940
11947
  result: {
11941
11948
  status: "error",
11942
11949
  code: "unsupported",
11943
- message: "This Val project is running in local filesystem mode, where there is nothing to authenticate against. Do not send a credential."
11950
+ message: ctx.auth.type === "verified-profile" ? "This Val project is running in local filesystem mode, so there is nothing to authenticate against, but it is configured with an `oauth` issuer and is therefore asking clients for an access token it cannot use. Remove the `oauth` config (or `VAL_OAUTH_ISSUER` from your local `.env`) for local development." : "This Val project is running in local filesystem mode, where there is nothing to authenticate against. Do not send a credential."
11944
11951
  }
11945
11952
  };
11946
11953
  }
@@ -12036,6 +12043,10 @@ function createOpsResolver(valModules, options) {
12036
12043
  * sha within the same process, so a cached view would go stale silently — and
12037
12044
  * the cost of being wrong here is an agent writing a patch against content that
12038
12045
  * has already moved.
12046
+ *
12047
+ * Exported for `toolsFixture`, not for consumers: `tools/index.ts` re-exports by
12048
+ * name, so this stays inside the package. A test that assembled its own state
12049
+ * would be asserting against a view no real call ever sees.
12039
12050
  */
12040
12051
  async function loadState(ops) {
12041
12052
  const patches = await ops.fetchPatches({
@@ -11935,12 +11935,19 @@ function createOpsResolver(valModules, options) {
11935
11935
  // credential should not silently get local filesystem access instead —
11936
11936
  // and the difference matters, because fs mode writes straight to disk
11937
11937
  // with no backend permission check at all.
11938
+ //
11939
+ // The two credentials get different messages because they arrive here
11940
+ // for different reasons. A PAT is something the caller chose to send. A
11941
+ // verified access token is not: it only exists because this app
11942
+ // advertised an authorization server, so the developer seeing this did
11943
+ // not do anything wrong — a config file did, and naming it is the
11944
+ // difference between a two-minute fix and an afternoon.
11938
11945
  return {
11939
11946
  status: "error",
11940
11947
  result: {
11941
11948
  status: "error",
11942
11949
  code: "unsupported",
11943
- message: "This Val project is running in local filesystem mode, where there is nothing to authenticate against. Do not send a credential."
11950
+ message: ctx.auth.type === "verified-profile" ? "This Val project is running in local filesystem mode, so there is nothing to authenticate against, but it is configured with an `oauth` issuer and is therefore asking clients for an access token it cannot use. Remove the `oauth` config (or `VAL_OAUTH_ISSUER` from your local `.env`) for local development." : "This Val project is running in local filesystem mode, where there is nothing to authenticate against. Do not send a credential."
11944
11951
  }
11945
11952
  };
11946
11953
  }
@@ -12036,6 +12043,10 @@ function createOpsResolver(valModules, options) {
12036
12043
  * sha within the same process, so a cached view would go stale silently — and
12037
12044
  * the cost of being wrong here is an agent writing a patch against content that
12038
12045
  * has already moved.
12046
+ *
12047
+ * Exported for `toolsFixture`, not for consumers: `tools/index.ts` re-exports by
12048
+ * name, so this stays inside the package. A test that assembled its own state
12049
+ * would be asserting against a view no real call ever sees.
12039
12050
  */
12040
12051
  async function loadState(ops) {
12041
12052
  const patches = await ops.fetchPatches({
@@ -11901,12 +11901,19 @@ function createOpsResolver(valModules, options) {
11901
11901
  // credential should not silently get local filesystem access instead —
11902
11902
  // and the difference matters, because fs mode writes straight to disk
11903
11903
  // with no backend permission check at all.
11904
+ //
11905
+ // The two credentials get different messages because they arrive here
11906
+ // for different reasons. A PAT is something the caller chose to send. A
11907
+ // verified access token is not: it only exists because this app
11908
+ // advertised an authorization server, so the developer seeing this did
11909
+ // not do anything wrong — a config file did, and naming it is the
11910
+ // difference between a two-minute fix and an afternoon.
11904
11911
  return {
11905
11912
  status: "error",
11906
11913
  result: {
11907
11914
  status: "error",
11908
11915
  code: "unsupported",
11909
- message: "This Val project is running in local filesystem mode, where there is nothing to authenticate against. Do not send a credential."
11916
+ message: ctx.auth.type === "verified-profile" ? "This Val project is running in local filesystem mode, so there is nothing to authenticate against, but it is configured with an `oauth` issuer and is therefore asking clients for an access token it cannot use. Remove the `oauth` config (or `VAL_OAUTH_ISSUER` from your local `.env`) for local development." : "This Val project is running in local filesystem mode, where there is nothing to authenticate against. Do not send a credential."
11910
11917
  }
11911
11918
  };
11912
11919
  }
@@ -12002,6 +12009,10 @@ function createOpsResolver(valModules, options) {
12002
12009
  * sha within the same process, so a cached view would go stale silently — and
12003
12010
  * the cost of being wrong here is an agent writing a patch against content that
12004
12011
  * has already moved.
12012
+ *
12013
+ * Exported for `toolsFixture`, not for consumers: `tools/index.ts` re-exports by
12014
+ * name, so this stays inside the package. A test that assembled its own state
12015
+ * would be asserting against a view no real call ever sees.
12005
12016
  */
12006
12017
  async function loadState(ops) {
12007
12018
  const patches = await ops.fetchPatches({
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "types": "dist/valbuild-server.cjs.d.ts",
19
- "version": "0.120.0",
19
+ "version": "0.120.1",
20
20
  "devDependencies": {
21
21
  "@prettier/sync": "^0.6.1",
22
22
  "@types/jest": "^30.0.0"
@@ -29,9 +29,9 @@
29
29
  "typescript": "^6.0.3",
30
30
  "zod": "^4.4.3",
31
31
  "zod-validation-error": "^5.0.0",
32
- "@valbuild/shared": "0.120.0",
33
32
  "@valbuild/core": "0.120.0",
34
- "@valbuild/ui": "0.120.0"
33
+ "@valbuild/ui": "0.120.0",
34
+ "@valbuild/shared": "0.120.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": "^20.19.0 || >=22"