@valbuild/server 0.116.0 → 0.117.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 +75 -0
- package/dist/declarations/src/index.d.ts +2 -1
- package/dist/declarations/src/tools/createValTools.d.ts +1 -1
- package/dist/declarations/src/tools/index.d.ts +2 -1
- package/dist/declarations/src/tools/types.d.ts +70 -14
- package/dist/valbuild-server.cjs.dev.js +166 -8
- package/dist/valbuild-server.cjs.prod.js +166 -8
- package/dist/valbuild-server.esm.js +164 -9
- package/package.json +5 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @valbuild/server
|
|
2
|
+
|
|
3
|
+
## 0.117.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`0ae7bac`](https://github.com/valbuild/val/commit/0ae7bac8a186460bc2b31f2ded89b00027bafb55)]:
|
|
8
|
+
- @valbuild/ui@0.117.1
|
|
9
|
+
|
|
10
|
+
## 0.117.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- [#582](https://github.com/valbuild/val/pull/582) [`fca3efa`](https://github.com/valbuild/val/commit/fca3efa389e2817401f55ea3dd184af7c611b807) Thanks [@freekh](https://github.com/freekh)! - Accept OAuth access tokens on the MCP endpoint, so editors can authorize as themselves
|
|
15
|
+
|
|
16
|
+
`initValMcp` takes an optional `oauth` config. Give it the authorization server's
|
|
17
|
+
URL and this endpoint's own URL, and every MCP call must then present an access
|
|
18
|
+
token that Val's authorization server issued:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const { valMcpAuthorize, valMcpTools, valMcpMetadata } = initValMcp(
|
|
22
|
+
valModules,
|
|
23
|
+
config,
|
|
24
|
+
{
|
|
25
|
+
oauth: {
|
|
26
|
+
issuer: "https://admin.val.build",
|
|
27
|
+
resource: "https://your-app.com/api/mcp",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The token is verified in your app — signature against the issuer's published
|
|
34
|
+
keys, plus issuer, audience and expiry — so the caller's identity is checked
|
|
35
|
+
rather than claimed. **Patches created over MCP now carry that profile as their
|
|
36
|
+
author**, which is what makes an edit made from a phone show up in the review
|
|
37
|
+
screen as somebody's rather than nobody's. Scopes are enforced too: a token
|
|
38
|
+
without `val:write` cannot reach a tool that writes.
|
|
39
|
+
|
|
40
|
+
Mount the discovery document so clients can find where to authorize:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// app/.well-known/oauth-protected-resource/route.ts
|
|
44
|
+
import { valMcpMetadata } from "../../../val/mcp";
|
|
45
|
+
export const { GET, OPTIONS } = valMcpMetadata!;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`valMcpMetadata` is `null` when no `oauth` config is given.
|
|
49
|
+
|
|
50
|
+
**Nothing changes if you leave `oauth` out.** Local development still works with
|
|
51
|
+
no authorization server, and an app already using a personal access token keeps
|
|
52
|
+
working as before.
|
|
53
|
+
|
|
54
|
+
One breaking change if you built your own host on `createValTools`:
|
|
55
|
+
`ValToolContext.auth` is now a tagged union, so `{ pat }` becomes
|
|
56
|
+
`{ type: "pat", pat }`. The new variant is
|
|
57
|
+
`{ type: "verified-profile", profileId, scopes }`, for a host that verified a
|
|
58
|
+
token itself.
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- [#579](https://github.com/valbuild/val/pull/579) [`b2812ae`](https://github.com/valbuild/val/commit/b2812ae4ee03e005ecead3365f49c625e536f94d) Thanks [@freekh](https://github.com/freekh)! - Every release now ships a changelog. Each package's `CHANGELOG.md` records what
|
|
63
|
+
changed under the version that shipped it — with a link to the pull request, the
|
|
64
|
+
commit and the author — and the same entry becomes the body of the GitHub
|
|
65
|
+
Release for the tag. The file is included in the npm tarball, so it is also
|
|
66
|
+
readable from an installed copy.
|
|
67
|
+
|
|
68
|
+
Up to now those changelogs were generated empty, and the GitHub Releases with
|
|
69
|
+
them, so there was no record of what any given version contained. Releases from
|
|
70
|
+
this one on have one; earlier versions stay blank.
|
|
71
|
+
|
|
72
|
+
- Updated dependencies [[`d94a40f`](https://github.com/valbuild/val/commit/d94a40f8bd11027636d183e293aced820b6f341f), [`b2812ae`](https://github.com/valbuild/val/commit/b2812ae4ee03e005ecead3365f49c625e536f94d)]:
|
|
73
|
+
- @valbuild/core@0.117.0
|
|
74
|
+
- @valbuild/shared@0.117.0
|
|
75
|
+
- @valbuild/ui@0.117.0
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { createService, Service } from "./Service.js";
|
|
2
2
|
export { createValApiRouter, createValServer, safeReadGit } from "./ValRouter.js";
|
|
3
3
|
export { createValTools } from "./tools/index.js";
|
|
4
|
-
export
|
|
4
|
+
export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, } from "./tools/index.js";
|
|
5
|
+
export type { ValScope, ValToolAuth, ValToolContext, ValToolDefinition, ValToolDefinitionJson, ValToolErrorCode, ValToolResult, ValTools, ValToolsOptions, } from "./tools/index.js";
|
|
5
6
|
export { initHandlerOptions, createValOps } from "./valServerConfig.js";
|
|
6
7
|
export { ValModuleLoader } from "./ValModuleLoader.js";
|
|
7
8
|
export { getCompilerOptions } from "./getCompilerOptions.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ValModules } from "@valbuild/core";
|
|
2
2
|
import type { ValServerConfig } from "../ValServer.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type ValTools } from "./types.js";
|
|
4
4
|
export type ValToolsOptions = ValServerConfig;
|
|
5
5
|
/**
|
|
6
6
|
* Val's server-side tool registry.
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { createValTools, type ValToolsOptions } from "./createValTools.js";
|
|
2
|
-
export
|
|
2
|
+
export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, } from "./types.js";
|
|
3
|
+
export type { ValScope, ValToolAuth, ValToolContext, ValToolDefinition, ValToolDefinitionJson, ValToolErrorCode, ValToolResult, ValTools, } from "./types.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuthorId } from "../ValOps.js";
|
|
1
2
|
import type { Json } from "@valbuild/core";
|
|
2
3
|
import type { z } from "zod";
|
|
3
4
|
/**
|
|
@@ -74,14 +75,56 @@ export type ValToolDefinitionJson = Omit<ValToolDefinition, "inputSchema"> & {
|
|
|
74
75
|
inputSchema: ReturnType<typeof z.toJSONSchema<z.ZodType>>;
|
|
75
76
|
};
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
+
* How the caller was established, and it is a union because there are two
|
|
79
|
+
* genuinely different answers — with different consequences downstream.
|
|
78
80
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
81
|
+
* The distinction that matters is **who checked**. A PAT is forwarded to the
|
|
82
|
+
* backend unchecked, because the app cannot resolve one; an access token is
|
|
83
|
+
* verified by the app itself, against a public key it does not hold and
|
|
84
|
+
* therefore cannot forge. The first is a credential being relayed. The second
|
|
85
|
+
* is a signature that has already been checked.
|
|
86
|
+
*/
|
|
87
|
+
export type ValToolAuth = {
|
|
88
|
+
type: "pat";
|
|
89
|
+
/**
|
|
90
|
+
* The caller's PAT. Never log this, never put it in a URL, and never let
|
|
91
|
+
* it reach a tool result.
|
|
92
|
+
*
|
|
93
|
+
* Relayed to the backend as-is: this app is not the authority on what the
|
|
94
|
+
* token may do, and the backend that is decides. Nothing is derived from
|
|
95
|
+
* it here — see `docs/plans/mcp.md` D.2.
|
|
96
|
+
*/
|
|
97
|
+
pat: string;
|
|
98
|
+
} | {
|
|
99
|
+
type: "verified-profile";
|
|
100
|
+
/**
|
|
101
|
+
* The profile the host **verified** — the `sub` of an access token whose
|
|
102
|
+
* signature, issuer, audience and expiry were all checked against the
|
|
103
|
+
* authorization server's published key.
|
|
104
|
+
*
|
|
105
|
+
* This field is the reason this type became a union, and an earlier
|
|
106
|
+
* version of this file argued no identity field should exist at all. That
|
|
107
|
+
* argument was about a specific case and stated too broadly: an id the
|
|
108
|
+
* host *asserts* on the strength of a credential it cannot check is an
|
|
109
|
+
* unverified claim dressed as a checked one, and that is still refused —
|
|
110
|
+
* it is why the `pat` variant carries no profile. An id the host
|
|
111
|
+
* *verified* cryptographically is a different thing, and it is the same
|
|
112
|
+
* standing the Studio has when it re-signs a session it established
|
|
113
|
+
* itself.
|
|
114
|
+
*/
|
|
115
|
+
profileId: AuthorId;
|
|
116
|
+
/**
|
|
117
|
+
* The token's granted scopes, as the authorization server issued them.
|
|
118
|
+
*
|
|
119
|
+
* Enforced here as well as by the backend, deliberately. Two checks on
|
|
120
|
+
* one grant is not redundancy for its own sake: this one can refuse a
|
|
121
|
+
* write before it is attempted, so a token that may only read never
|
|
122
|
+
* reaches the code that builds a patch.
|
|
123
|
+
*/
|
|
124
|
+
scopes: string[];
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Who is calling, established once per request by the host.
|
|
85
128
|
*
|
|
86
129
|
* `null` means local fs mode, where there is no credential to hold and patches
|
|
87
130
|
* are written with no author, exactly as the Studio does locally (D.1). In
|
|
@@ -90,16 +133,29 @@ export type ValToolDefinitionJson = Omit<ValToolDefinition, "inputSchema"> & {
|
|
|
90
133
|
* would turn a missing credential into full access.
|
|
91
134
|
*/
|
|
92
135
|
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;
|
|
136
|
+
auth: ValToolAuth | null;
|
|
100
137
|
/** Groups a run of related edits, when the host has such a notion. */
|
|
101
138
|
sessionId: string | null;
|
|
102
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* Brand a verified subject as an {@link AuthorId}.
|
|
142
|
+
*
|
|
143
|
+
* `AuthorId` is a branded string so that an id cannot be conjured from any
|
|
144
|
+
* string that happens to be lying around — which is exactly the mistake this
|
|
145
|
+
* type is guarding against. That makes one assertion unavoidable at the boundary
|
|
146
|
+
* where a real id enters the system, so it lives here, once, with a name that
|
|
147
|
+
* says what makes it legitimate: the caller has *verified* this subject, not
|
|
148
|
+
* received it.
|
|
149
|
+
*
|
|
150
|
+
* Do not reach for this to satisfy a type. If you are holding a string you did
|
|
151
|
+
* not verify, the honest value is `null`.
|
|
152
|
+
*/
|
|
153
|
+
export declare function authorIdFromVerifiedSubject(subject: string): AuthorId;
|
|
154
|
+
/** Read access. Every call needs it, the writes included. */
|
|
155
|
+
export declare const VAL_SCOPE_READ = "val:read";
|
|
156
|
+
/** Write access. Needed *in addition* by any tool not marked `readOnlyHint`. */
|
|
157
|
+
export declare const VAL_SCOPE_WRITE = "val:write";
|
|
158
|
+
export type ValScope = typeof VAL_SCOPE_READ | typeof VAL_SCOPE_WRITE;
|
|
103
159
|
export type ValToolResult = {
|
|
104
160
|
status: "ok";
|
|
105
161
|
data: Json;
|
|
@@ -11425,6 +11425,7 @@ function describeErrors(errors) {
|
|
|
11425
11425
|
* failing clearly.
|
|
11426
11426
|
*/
|
|
11427
11427
|
async function savePatch(deps, moduleFilePath, patch, onInvalid = "reject") {
|
|
11428
|
+
var _ctx$auth;
|
|
11428
11429
|
const {
|
|
11429
11430
|
ops,
|
|
11430
11431
|
ctx,
|
|
@@ -11460,13 +11461,24 @@ async function savePatch(deps, moduleFilePath, patch, onInvalid = "reject") {
|
|
|
11460
11461
|
unresolved = speculative.errors;
|
|
11461
11462
|
}
|
|
11462
11463
|
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11464
|
+
/**
|
|
11465
|
+
* Null on the PAT path, and the verified profile on the token path.
|
|
11466
|
+
*
|
|
11467
|
+
* The PAT case is unchanged and still deliberate: the app cannot resolve a
|
|
11468
|
+
* PAT, so any id it wrote here would be an unverified claim dressed up as a
|
|
11469
|
+
* checked one — and the request already carries the caller's own token, which
|
|
11470
|
+
* is a better answer to "who did this" than anything the app could assert.
|
|
11471
|
+
* Attributing that patch is the backend's job.
|
|
11472
|
+
*
|
|
11473
|
+
* The token case is the opposite situation, which is why it gets the opposite
|
|
11474
|
+
* answer. The host verified a signature over a key it does not hold, so the
|
|
11475
|
+
* profile is checked rather than claimed, and the backend has no token of its
|
|
11476
|
+
* own to attribute from — the call reaches it under the app's API key. If this
|
|
11477
|
+
* stayed null, every edit made through a signed-in editor's own session would
|
|
11478
|
+
* land with no author at all, which is worse than useless on a CMS whose
|
|
11479
|
+
* review screen is organised by who changed what.
|
|
11480
|
+
*/
|
|
11481
|
+
const authorId = ((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) === "verified-profile" ? ctx.auth.profileId : null;
|
|
11470
11482
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
11471
11483
|
const patchId = mintPatchId();
|
|
11472
11484
|
// Re-derived on the retry rather than reused: reusing the ref that just
|
|
@@ -11690,6 +11702,80 @@ function rejectFileOps(patch) {
|
|
|
11690
11702
|
};
|
|
11691
11703
|
}
|
|
11692
11704
|
|
|
11705
|
+
/**
|
|
11706
|
+
* The public surface of Val's server-side tool registry.
|
|
11707
|
+
*
|
|
11708
|
+
* Types only, deliberately: this file is the contract that the MCP hosts, the
|
|
11709
|
+
* CLI's stdio transport and the tools themselves are all written against, and
|
|
11710
|
+
* keeping it free of implementation means those can be built in any order
|
|
11711
|
+
* without one of them owning the shape.
|
|
11712
|
+
*
|
|
11713
|
+
* The design this implements is `docs/plans/mcp.md`, Part A. Two constraints
|
|
11714
|
+
* from it are load-bearing and easy to break by accident:
|
|
11715
|
+
*
|
|
11716
|
+
* 1. **Nothing here may import an MCP SDK.** That is what lets hosts other
|
|
11717
|
+
* than the template consume these tools, and it is not hypothetical
|
|
11718
|
+
* hygiene — the TypeScript SDK reorganised itself at v2.0.0, and a registry
|
|
11719
|
+
* coupled to it would have moved with it.
|
|
11720
|
+
* 2. **The result type is deliberately not MCP's `CallToolResult`.** Each host
|
|
11721
|
+
* adapts {@link ValToolResult} at its own edge, which is also where an
|
|
11722
|
+
* error becomes an in-band `isError` result the model can recover from
|
|
11723
|
+
* rather than a transport failure.
|
|
11724
|
+
*/
|
|
11725
|
+
|
|
11726
|
+
/** Why a tool call failed, in a form a host can map onto its own errors. */
|
|
11727
|
+
|
|
11728
|
+
/**
|
|
11729
|
+
* The same definition with `inputSchema` as JSON Schema, for hosts that want the
|
|
11730
|
+
* wire shape rather than a Standard Schema.
|
|
11731
|
+
*
|
|
11732
|
+
* Typed as whatever zod's own converter produces, so deriving it needs no cast
|
|
11733
|
+
* and no second hand-written description of the same input.
|
|
11734
|
+
*/
|
|
11735
|
+
|
|
11736
|
+
/**
|
|
11737
|
+
* How the caller was established, and it is a union because there are two
|
|
11738
|
+
* genuinely different answers — with different consequences downstream.
|
|
11739
|
+
*
|
|
11740
|
+
* The distinction that matters is **who checked**. A PAT is forwarded to the
|
|
11741
|
+
* backend unchecked, because the app cannot resolve one; an access token is
|
|
11742
|
+
* verified by the app itself, against a public key it does not hold and
|
|
11743
|
+
* therefore cannot forge. The first is a credential being relayed. The second
|
|
11744
|
+
* is a signature that has already been checked.
|
|
11745
|
+
*/
|
|
11746
|
+
|
|
11747
|
+
/**
|
|
11748
|
+
* Who is calling, established once per request by the host.
|
|
11749
|
+
*
|
|
11750
|
+
* `null` means local fs mode, where there is no credential to hold and patches
|
|
11751
|
+
* are written with no author, exactly as the Studio does locally (D.1). In
|
|
11752
|
+
* proxy mode `null` is refused rather than falling back to the app's own API
|
|
11753
|
+
* key: that key can do more than any single user, and quietly substituting it
|
|
11754
|
+
* would turn a missing credential into full access.
|
|
11755
|
+
*/
|
|
11756
|
+
|
|
11757
|
+
/**
|
|
11758
|
+
* Brand a verified subject as an {@link AuthorId}.
|
|
11759
|
+
*
|
|
11760
|
+
* `AuthorId` is a branded string so that an id cannot be conjured from any
|
|
11761
|
+
* string that happens to be lying around — which is exactly the mistake this
|
|
11762
|
+
* type is guarding against. That makes one assertion unavoidable at the boundary
|
|
11763
|
+
* where a real id enters the system, so it lives here, once, with a name that
|
|
11764
|
+
* says what makes it legitimate: the caller has *verified* this subject, not
|
|
11765
|
+
* received it.
|
|
11766
|
+
*
|
|
11767
|
+
* Do not reach for this to satisfy a type. If you are holding a string you did
|
|
11768
|
+
* not verify, the honest value is `null`.
|
|
11769
|
+
*/
|
|
11770
|
+
function authorIdFromVerifiedSubject(subject) {
|
|
11771
|
+
return subject;
|
|
11772
|
+
}
|
|
11773
|
+
|
|
11774
|
+
/** Read access. Every call needs it, the writes included. */
|
|
11775
|
+
const VAL_SCOPE_READ = "val:read";
|
|
11776
|
+
/** Write access. Needed *in addition* by any tool not marked `readOnlyHint`. */
|
|
11777
|
+
const VAL_SCOPE_WRITE = "val:write";
|
|
11778
|
+
|
|
11693
11779
|
/**
|
|
11694
11780
|
* How many callers' data layers to keep around in proxy mode.
|
|
11695
11781
|
*
|
|
@@ -11756,6 +11842,10 @@ function createValTools(valModules, options) {
|
|
|
11756
11842
|
message: describeZodError(parsed.error)
|
|
11757
11843
|
};
|
|
11758
11844
|
}
|
|
11845
|
+
const insufficient = refuseInsufficientScope(tool, ctx);
|
|
11846
|
+
if (insufficient) {
|
|
11847
|
+
return insufficient;
|
|
11848
|
+
}
|
|
11759
11849
|
const resolved = resolveOps(ctx);
|
|
11760
11850
|
if (resolved.status === "error") {
|
|
11761
11851
|
return resolved.result;
|
|
@@ -11840,6 +11930,13 @@ function createOpsResolver(valModules, options) {
|
|
|
11840
11930
|
// instance holds the token regardless — but it keeps credentials out of the
|
|
11841
11931
|
// key set, which is the thing that ends up in a heap dump or an error dump.
|
|
11842
11932
|
const byPatHash = new Map();
|
|
11933
|
+
/**
|
|
11934
|
+
* One instance for every verified caller, and unlike the PAT map that is
|
|
11935
|
+
* correct rather than a shortcut: this instance authenticates with the app's
|
|
11936
|
+
* own API key, so there is nothing per-caller in it to keep apart. Who did
|
|
11937
|
+
* what travels as the patch's `authorId` instead — see `writePath`.
|
|
11938
|
+
*/
|
|
11939
|
+
let sharedOps = null;
|
|
11843
11940
|
return ctx => {
|
|
11844
11941
|
if (!ctx.auth) {
|
|
11845
11942
|
return {
|
|
@@ -11847,10 +11944,32 @@ function createOpsResolver(valModules, options) {
|
|
|
11847
11944
|
result: {
|
|
11848
11945
|
status: "error",
|
|
11849
11946
|
code: "forbidden",
|
|
11850
|
-
message: "This Val project talks to the Val content backend, so every call needs the caller's own personal access token
|
|
11947
|
+
message: "This Val project talks to the Val content backend, so every call needs a credential: an access token from the Val authorization server, or the caller's own personal access token from `val login`."
|
|
11851
11948
|
}
|
|
11852
11949
|
};
|
|
11853
11950
|
}
|
|
11951
|
+
if (ctx.auth.type === "verified-profile") {
|
|
11952
|
+
if (!options.apiKey) {
|
|
11953
|
+
// Proxy mode is inferred from the api key being present, so this is
|
|
11954
|
+
// unreachable through `initHandlerOptions`. It stays because the
|
|
11955
|
+
// alternative to refusing is building ops with no credential at all.
|
|
11956
|
+
return {
|
|
11957
|
+
status: "error",
|
|
11958
|
+
result: {
|
|
11959
|
+
status: "error",
|
|
11960
|
+
code: "forbidden",
|
|
11961
|
+
message: "This Val project has no API key configured, so a verified access token cannot be exchanged for backend access."
|
|
11962
|
+
}
|
|
11963
|
+
};
|
|
11964
|
+
}
|
|
11965
|
+
if (!sharedOps) {
|
|
11966
|
+
sharedOps = createValOps(valModules, options);
|
|
11967
|
+
}
|
|
11968
|
+
return {
|
|
11969
|
+
status: "ok",
|
|
11970
|
+
ops: sharedOps
|
|
11971
|
+
};
|
|
11972
|
+
}
|
|
11854
11973
|
const key = node_crypto.createHash("sha256").update(ctx.auth.pat).digest("hex");
|
|
11855
11974
|
const cached = byPatHash.get(key);
|
|
11856
11975
|
if (cached) {
|
|
@@ -11963,6 +12082,42 @@ function describeZodError(error) {
|
|
|
11963
12082
|
}).join("; ");
|
|
11964
12083
|
}
|
|
11965
12084
|
|
|
12085
|
+
/**
|
|
12086
|
+
* Refuse a call the token was not granted, before anything is attempted.
|
|
12087
|
+
*
|
|
12088
|
+
* Derived from `readOnlyHint` rather than from a second list of tool names,
|
|
12089
|
+
* because a second list is a thing that drifts. The derivation also fails in
|
|
12090
|
+
* the safe direction: a tool that forgets the hint is treated as a write and
|
|
12091
|
+
* demands the wider scope, rather than a write slipping through as a read.
|
|
12092
|
+
*
|
|
12093
|
+
* Only the verified-token path is checked. A PAT carries no scopes here by
|
|
12094
|
+
* design — the backend resolves it and decides — so there is nothing to
|
|
12095
|
+
* enforce, and inventing a default would be this app claiming an authority it
|
|
12096
|
+
* does not have.
|
|
12097
|
+
*/
|
|
12098
|
+
function refuseInsufficientScope(tool, ctx) {
|
|
12099
|
+
var _ctx$auth, _tool$annotations;
|
|
12100
|
+
if (((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) !== "verified-profile") {
|
|
12101
|
+
return null;
|
|
12102
|
+
}
|
|
12103
|
+
// Read is needed by every call, including the writes: a tool that changes
|
|
12104
|
+
// content reads it first, and `ValToolAuth` says as much. Checking only the
|
|
12105
|
+
// wider scope would let a write-but-not-read token through here — today's
|
|
12106
|
+
// verifier refuses such a token before this point, but `createValTools` is
|
|
12107
|
+
// exported and another host may not.
|
|
12108
|
+
const needed = (_tool$annotations = tool.annotations) !== null && _tool$annotations !== void 0 && _tool$annotations.readOnlyHint ? [VAL_SCOPE_READ] : [VAL_SCOPE_READ, VAL_SCOPE_WRITE];
|
|
12109
|
+
const granted = ctx.auth.scopes;
|
|
12110
|
+
const missing = needed.filter(scope => !granted.includes(scope));
|
|
12111
|
+
if (missing.length === 0) {
|
|
12112
|
+
return null;
|
|
12113
|
+
}
|
|
12114
|
+
return {
|
|
12115
|
+
status: "error",
|
|
12116
|
+
code: "forbidden",
|
|
12117
|
+
message: `This access token does not have the ${missing.join(" and ")} scope, which ${tool.name} requires. Granted: ${granted.length > 0 ? granted.join(" ") : "(none)"}.`
|
|
12118
|
+
};
|
|
12119
|
+
}
|
|
12120
|
+
|
|
11966
12121
|
const JsFileLookupMapping = [
|
|
11967
12122
|
// NOTE: first one matching will be used
|
|
11968
12123
|
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
|
@@ -14150,6 +14305,8 @@ exports.DEFAULT_LOGIN_EXPIRES_IN_SECONDS = DEFAULT_LOGIN_EXPIRES_IN_SECONDS;
|
|
|
14150
14305
|
exports.DEFAULT_LOGIN_HOST = DEFAULT_LOGIN_HOST;
|
|
14151
14306
|
exports.DEFAULT_LOGIN_POLL_INTERVAL_SECONDS = DEFAULT_LOGIN_POLL_INTERVAL_SECONDS;
|
|
14152
14307
|
exports.Service = Service;
|
|
14308
|
+
exports.VAL_SCOPE_READ = VAL_SCOPE_READ;
|
|
14309
|
+
exports.VAL_SCOPE_WRITE = VAL_SCOPE_WRITE;
|
|
14153
14310
|
exports.ValFSHost = ValFSHost;
|
|
14154
14311
|
exports.ValLoginError = ValLoginError;
|
|
14155
14312
|
exports.ValModuleLoader = ValModuleLoader;
|
|
@@ -14157,6 +14314,7 @@ exports.ValOpsFS = ValOpsFS;
|
|
|
14157
14314
|
exports.ValOpsHttp = ValOpsHttp;
|
|
14158
14315
|
exports.ValSourceFileHandler = ValSourceFileHandler;
|
|
14159
14316
|
exports.analyzeValModule = analyzeValModule;
|
|
14317
|
+
exports.authorIdFromVerifiedSubject = authorIdFromVerifiedSubject;
|
|
14160
14318
|
exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
|
|
14161
14319
|
exports.checkRemoteRef = checkRemoteRef;
|
|
14162
14320
|
exports.classifyJsonValuesOp = classifyJsonValuesOp;
|
|
@@ -11425,6 +11425,7 @@ function describeErrors(errors) {
|
|
|
11425
11425
|
* failing clearly.
|
|
11426
11426
|
*/
|
|
11427
11427
|
async function savePatch(deps, moduleFilePath, patch, onInvalid = "reject") {
|
|
11428
|
+
var _ctx$auth;
|
|
11428
11429
|
const {
|
|
11429
11430
|
ops,
|
|
11430
11431
|
ctx,
|
|
@@ -11460,13 +11461,24 @@ async function savePatch(deps, moduleFilePath, patch, onInvalid = "reject") {
|
|
|
11460
11461
|
unresolved = speculative.errors;
|
|
11461
11462
|
}
|
|
11462
11463
|
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11464
|
+
/**
|
|
11465
|
+
* Null on the PAT path, and the verified profile on the token path.
|
|
11466
|
+
*
|
|
11467
|
+
* The PAT case is unchanged and still deliberate: the app cannot resolve a
|
|
11468
|
+
* PAT, so any id it wrote here would be an unverified claim dressed up as a
|
|
11469
|
+
* checked one — and the request already carries the caller's own token, which
|
|
11470
|
+
* is a better answer to "who did this" than anything the app could assert.
|
|
11471
|
+
* Attributing that patch is the backend's job.
|
|
11472
|
+
*
|
|
11473
|
+
* The token case is the opposite situation, which is why it gets the opposite
|
|
11474
|
+
* answer. The host verified a signature over a key it does not hold, so the
|
|
11475
|
+
* profile is checked rather than claimed, and the backend has no token of its
|
|
11476
|
+
* own to attribute from — the call reaches it under the app's API key. If this
|
|
11477
|
+
* stayed null, every edit made through a signed-in editor's own session would
|
|
11478
|
+
* land with no author at all, which is worse than useless on a CMS whose
|
|
11479
|
+
* review screen is organised by who changed what.
|
|
11480
|
+
*/
|
|
11481
|
+
const authorId = ((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) === "verified-profile" ? ctx.auth.profileId : null;
|
|
11470
11482
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
11471
11483
|
const patchId = mintPatchId();
|
|
11472
11484
|
// Re-derived on the retry rather than reused: reusing the ref that just
|
|
@@ -11690,6 +11702,80 @@ function rejectFileOps(patch) {
|
|
|
11690
11702
|
};
|
|
11691
11703
|
}
|
|
11692
11704
|
|
|
11705
|
+
/**
|
|
11706
|
+
* The public surface of Val's server-side tool registry.
|
|
11707
|
+
*
|
|
11708
|
+
* Types only, deliberately: this file is the contract that the MCP hosts, the
|
|
11709
|
+
* CLI's stdio transport and the tools themselves are all written against, and
|
|
11710
|
+
* keeping it free of implementation means those can be built in any order
|
|
11711
|
+
* without one of them owning the shape.
|
|
11712
|
+
*
|
|
11713
|
+
* The design this implements is `docs/plans/mcp.md`, Part A. Two constraints
|
|
11714
|
+
* from it are load-bearing and easy to break by accident:
|
|
11715
|
+
*
|
|
11716
|
+
* 1. **Nothing here may import an MCP SDK.** That is what lets hosts other
|
|
11717
|
+
* than the template consume these tools, and it is not hypothetical
|
|
11718
|
+
* hygiene — the TypeScript SDK reorganised itself at v2.0.0, and a registry
|
|
11719
|
+
* coupled to it would have moved with it.
|
|
11720
|
+
* 2. **The result type is deliberately not MCP's `CallToolResult`.** Each host
|
|
11721
|
+
* adapts {@link ValToolResult} at its own edge, which is also where an
|
|
11722
|
+
* error becomes an in-band `isError` result the model can recover from
|
|
11723
|
+
* rather than a transport failure.
|
|
11724
|
+
*/
|
|
11725
|
+
|
|
11726
|
+
/** Why a tool call failed, in a form a host can map onto its own errors. */
|
|
11727
|
+
|
|
11728
|
+
/**
|
|
11729
|
+
* The same definition with `inputSchema` as JSON Schema, for hosts that want the
|
|
11730
|
+
* wire shape rather than a Standard Schema.
|
|
11731
|
+
*
|
|
11732
|
+
* Typed as whatever zod's own converter produces, so deriving it needs no cast
|
|
11733
|
+
* and no second hand-written description of the same input.
|
|
11734
|
+
*/
|
|
11735
|
+
|
|
11736
|
+
/**
|
|
11737
|
+
* How the caller was established, and it is a union because there are two
|
|
11738
|
+
* genuinely different answers — with different consequences downstream.
|
|
11739
|
+
*
|
|
11740
|
+
* The distinction that matters is **who checked**. A PAT is forwarded to the
|
|
11741
|
+
* backend unchecked, because the app cannot resolve one; an access token is
|
|
11742
|
+
* verified by the app itself, against a public key it does not hold and
|
|
11743
|
+
* therefore cannot forge. The first is a credential being relayed. The second
|
|
11744
|
+
* is a signature that has already been checked.
|
|
11745
|
+
*/
|
|
11746
|
+
|
|
11747
|
+
/**
|
|
11748
|
+
* Who is calling, established once per request by the host.
|
|
11749
|
+
*
|
|
11750
|
+
* `null` means local fs mode, where there is no credential to hold and patches
|
|
11751
|
+
* are written with no author, exactly as the Studio does locally (D.1). In
|
|
11752
|
+
* proxy mode `null` is refused rather than falling back to the app's own API
|
|
11753
|
+
* key: that key can do more than any single user, and quietly substituting it
|
|
11754
|
+
* would turn a missing credential into full access.
|
|
11755
|
+
*/
|
|
11756
|
+
|
|
11757
|
+
/**
|
|
11758
|
+
* Brand a verified subject as an {@link AuthorId}.
|
|
11759
|
+
*
|
|
11760
|
+
* `AuthorId` is a branded string so that an id cannot be conjured from any
|
|
11761
|
+
* string that happens to be lying around — which is exactly the mistake this
|
|
11762
|
+
* type is guarding against. That makes one assertion unavoidable at the boundary
|
|
11763
|
+
* where a real id enters the system, so it lives here, once, with a name that
|
|
11764
|
+
* says what makes it legitimate: the caller has *verified* this subject, not
|
|
11765
|
+
* received it.
|
|
11766
|
+
*
|
|
11767
|
+
* Do not reach for this to satisfy a type. If you are holding a string you did
|
|
11768
|
+
* not verify, the honest value is `null`.
|
|
11769
|
+
*/
|
|
11770
|
+
function authorIdFromVerifiedSubject(subject) {
|
|
11771
|
+
return subject;
|
|
11772
|
+
}
|
|
11773
|
+
|
|
11774
|
+
/** Read access. Every call needs it, the writes included. */
|
|
11775
|
+
const VAL_SCOPE_READ = "val:read";
|
|
11776
|
+
/** Write access. Needed *in addition* by any tool not marked `readOnlyHint`. */
|
|
11777
|
+
const VAL_SCOPE_WRITE = "val:write";
|
|
11778
|
+
|
|
11693
11779
|
/**
|
|
11694
11780
|
* How many callers' data layers to keep around in proxy mode.
|
|
11695
11781
|
*
|
|
@@ -11756,6 +11842,10 @@ function createValTools(valModules, options) {
|
|
|
11756
11842
|
message: describeZodError(parsed.error)
|
|
11757
11843
|
};
|
|
11758
11844
|
}
|
|
11845
|
+
const insufficient = refuseInsufficientScope(tool, ctx);
|
|
11846
|
+
if (insufficient) {
|
|
11847
|
+
return insufficient;
|
|
11848
|
+
}
|
|
11759
11849
|
const resolved = resolveOps(ctx);
|
|
11760
11850
|
if (resolved.status === "error") {
|
|
11761
11851
|
return resolved.result;
|
|
@@ -11840,6 +11930,13 @@ function createOpsResolver(valModules, options) {
|
|
|
11840
11930
|
// instance holds the token regardless — but it keeps credentials out of the
|
|
11841
11931
|
// key set, which is the thing that ends up in a heap dump or an error dump.
|
|
11842
11932
|
const byPatHash = new Map();
|
|
11933
|
+
/**
|
|
11934
|
+
* One instance for every verified caller, and unlike the PAT map that is
|
|
11935
|
+
* correct rather than a shortcut: this instance authenticates with the app's
|
|
11936
|
+
* own API key, so there is nothing per-caller in it to keep apart. Who did
|
|
11937
|
+
* what travels as the patch's `authorId` instead — see `writePath`.
|
|
11938
|
+
*/
|
|
11939
|
+
let sharedOps = null;
|
|
11843
11940
|
return ctx => {
|
|
11844
11941
|
if (!ctx.auth) {
|
|
11845
11942
|
return {
|
|
@@ -11847,10 +11944,32 @@ function createOpsResolver(valModules, options) {
|
|
|
11847
11944
|
result: {
|
|
11848
11945
|
status: "error",
|
|
11849
11946
|
code: "forbidden",
|
|
11850
|
-
message: "This Val project talks to the Val content backend, so every call needs the caller's own personal access token
|
|
11947
|
+
message: "This Val project talks to the Val content backend, so every call needs a credential: an access token from the Val authorization server, or the caller's own personal access token from `val login`."
|
|
11851
11948
|
}
|
|
11852
11949
|
};
|
|
11853
11950
|
}
|
|
11951
|
+
if (ctx.auth.type === "verified-profile") {
|
|
11952
|
+
if (!options.apiKey) {
|
|
11953
|
+
// Proxy mode is inferred from the api key being present, so this is
|
|
11954
|
+
// unreachable through `initHandlerOptions`. It stays because the
|
|
11955
|
+
// alternative to refusing is building ops with no credential at all.
|
|
11956
|
+
return {
|
|
11957
|
+
status: "error",
|
|
11958
|
+
result: {
|
|
11959
|
+
status: "error",
|
|
11960
|
+
code: "forbidden",
|
|
11961
|
+
message: "This Val project has no API key configured, so a verified access token cannot be exchanged for backend access."
|
|
11962
|
+
}
|
|
11963
|
+
};
|
|
11964
|
+
}
|
|
11965
|
+
if (!sharedOps) {
|
|
11966
|
+
sharedOps = createValOps(valModules, options);
|
|
11967
|
+
}
|
|
11968
|
+
return {
|
|
11969
|
+
status: "ok",
|
|
11970
|
+
ops: sharedOps
|
|
11971
|
+
};
|
|
11972
|
+
}
|
|
11854
11973
|
const key = node_crypto.createHash("sha256").update(ctx.auth.pat).digest("hex");
|
|
11855
11974
|
const cached = byPatHash.get(key);
|
|
11856
11975
|
if (cached) {
|
|
@@ -11963,6 +12082,42 @@ function describeZodError(error) {
|
|
|
11963
12082
|
}).join("; ");
|
|
11964
12083
|
}
|
|
11965
12084
|
|
|
12085
|
+
/**
|
|
12086
|
+
* Refuse a call the token was not granted, before anything is attempted.
|
|
12087
|
+
*
|
|
12088
|
+
* Derived from `readOnlyHint` rather than from a second list of tool names,
|
|
12089
|
+
* because a second list is a thing that drifts. The derivation also fails in
|
|
12090
|
+
* the safe direction: a tool that forgets the hint is treated as a write and
|
|
12091
|
+
* demands the wider scope, rather than a write slipping through as a read.
|
|
12092
|
+
*
|
|
12093
|
+
* Only the verified-token path is checked. A PAT carries no scopes here by
|
|
12094
|
+
* design — the backend resolves it and decides — so there is nothing to
|
|
12095
|
+
* enforce, and inventing a default would be this app claiming an authority it
|
|
12096
|
+
* does not have.
|
|
12097
|
+
*/
|
|
12098
|
+
function refuseInsufficientScope(tool, ctx) {
|
|
12099
|
+
var _ctx$auth, _tool$annotations;
|
|
12100
|
+
if (((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) !== "verified-profile") {
|
|
12101
|
+
return null;
|
|
12102
|
+
}
|
|
12103
|
+
// Read is needed by every call, including the writes: a tool that changes
|
|
12104
|
+
// content reads it first, and `ValToolAuth` says as much. Checking only the
|
|
12105
|
+
// wider scope would let a write-but-not-read token through here — today's
|
|
12106
|
+
// verifier refuses such a token before this point, but `createValTools` is
|
|
12107
|
+
// exported and another host may not.
|
|
12108
|
+
const needed = (_tool$annotations = tool.annotations) !== null && _tool$annotations !== void 0 && _tool$annotations.readOnlyHint ? [VAL_SCOPE_READ] : [VAL_SCOPE_READ, VAL_SCOPE_WRITE];
|
|
12109
|
+
const granted = ctx.auth.scopes;
|
|
12110
|
+
const missing = needed.filter(scope => !granted.includes(scope));
|
|
12111
|
+
if (missing.length === 0) {
|
|
12112
|
+
return null;
|
|
12113
|
+
}
|
|
12114
|
+
return {
|
|
12115
|
+
status: "error",
|
|
12116
|
+
code: "forbidden",
|
|
12117
|
+
message: `This access token does not have the ${missing.join(" and ")} scope, which ${tool.name} requires. Granted: ${granted.length > 0 ? granted.join(" ") : "(none)"}.`
|
|
12118
|
+
};
|
|
12119
|
+
}
|
|
12120
|
+
|
|
11966
12121
|
const JsFileLookupMapping = [
|
|
11967
12122
|
// NOTE: first one matching will be used
|
|
11968
12123
|
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
|
@@ -14150,6 +14305,8 @@ exports.DEFAULT_LOGIN_EXPIRES_IN_SECONDS = DEFAULT_LOGIN_EXPIRES_IN_SECONDS;
|
|
|
14150
14305
|
exports.DEFAULT_LOGIN_HOST = DEFAULT_LOGIN_HOST;
|
|
14151
14306
|
exports.DEFAULT_LOGIN_POLL_INTERVAL_SECONDS = DEFAULT_LOGIN_POLL_INTERVAL_SECONDS;
|
|
14152
14307
|
exports.Service = Service;
|
|
14308
|
+
exports.VAL_SCOPE_READ = VAL_SCOPE_READ;
|
|
14309
|
+
exports.VAL_SCOPE_WRITE = VAL_SCOPE_WRITE;
|
|
14153
14310
|
exports.ValFSHost = ValFSHost;
|
|
14154
14311
|
exports.ValLoginError = ValLoginError;
|
|
14155
14312
|
exports.ValModuleLoader = ValModuleLoader;
|
|
@@ -14157,6 +14314,7 @@ exports.ValOpsFS = ValOpsFS;
|
|
|
14157
14314
|
exports.ValOpsHttp = ValOpsHttp;
|
|
14158
14315
|
exports.ValSourceFileHandler = ValSourceFileHandler;
|
|
14159
14316
|
exports.analyzeValModule = analyzeValModule;
|
|
14317
|
+
exports.authorIdFromVerifiedSubject = authorIdFromVerifiedSubject;
|
|
14160
14318
|
exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
|
|
14161
14319
|
exports.checkRemoteRef = checkRemoteRef;
|
|
14162
14320
|
exports.classifyJsonValuesOp = classifyJsonValuesOp;
|
|
@@ -11391,6 +11391,7 @@ function describeErrors(errors) {
|
|
|
11391
11391
|
* failing clearly.
|
|
11392
11392
|
*/
|
|
11393
11393
|
async function savePatch(deps, moduleFilePath, patch, onInvalid = "reject") {
|
|
11394
|
+
var _ctx$auth;
|
|
11394
11395
|
const {
|
|
11395
11396
|
ops,
|
|
11396
11397
|
ctx,
|
|
@@ -11426,13 +11427,24 @@ async function savePatch(deps, moduleFilePath, patch, onInvalid = "reject") {
|
|
|
11426
11427
|
unresolved = speculative.errors;
|
|
11427
11428
|
}
|
|
11428
11429
|
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11430
|
+
/**
|
|
11431
|
+
* Null on the PAT path, and the verified profile on the token path.
|
|
11432
|
+
*
|
|
11433
|
+
* The PAT case is unchanged and still deliberate: the app cannot resolve a
|
|
11434
|
+
* PAT, so any id it wrote here would be an unverified claim dressed up as a
|
|
11435
|
+
* checked one — and the request already carries the caller's own token, which
|
|
11436
|
+
* is a better answer to "who did this" than anything the app could assert.
|
|
11437
|
+
* Attributing that patch is the backend's job.
|
|
11438
|
+
*
|
|
11439
|
+
* The token case is the opposite situation, which is why it gets the opposite
|
|
11440
|
+
* answer. The host verified a signature over a key it does not hold, so the
|
|
11441
|
+
* profile is checked rather than claimed, and the backend has no token of its
|
|
11442
|
+
* own to attribute from — the call reaches it under the app's API key. If this
|
|
11443
|
+
* stayed null, every edit made through a signed-in editor's own session would
|
|
11444
|
+
* land with no author at all, which is worse than useless on a CMS whose
|
|
11445
|
+
* review screen is organised by who changed what.
|
|
11446
|
+
*/
|
|
11447
|
+
const authorId = ((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) === "verified-profile" ? ctx.auth.profileId : null;
|
|
11436
11448
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
11437
11449
|
const patchId = mintPatchId();
|
|
11438
11450
|
// Re-derived on the retry rather than reused: reusing the ref that just
|
|
@@ -11656,6 +11668,80 @@ function rejectFileOps(patch) {
|
|
|
11656
11668
|
};
|
|
11657
11669
|
}
|
|
11658
11670
|
|
|
11671
|
+
/**
|
|
11672
|
+
* The public surface of Val's server-side tool registry.
|
|
11673
|
+
*
|
|
11674
|
+
* Types only, deliberately: this file is the contract that the MCP hosts, the
|
|
11675
|
+
* CLI's stdio transport and the tools themselves are all written against, and
|
|
11676
|
+
* keeping it free of implementation means those can be built in any order
|
|
11677
|
+
* without one of them owning the shape.
|
|
11678
|
+
*
|
|
11679
|
+
* The design this implements is `docs/plans/mcp.md`, Part A. Two constraints
|
|
11680
|
+
* from it are load-bearing and easy to break by accident:
|
|
11681
|
+
*
|
|
11682
|
+
* 1. **Nothing here may import an MCP SDK.** That is what lets hosts other
|
|
11683
|
+
* than the template consume these tools, and it is not hypothetical
|
|
11684
|
+
* hygiene — the TypeScript SDK reorganised itself at v2.0.0, and a registry
|
|
11685
|
+
* coupled to it would have moved with it.
|
|
11686
|
+
* 2. **The result type is deliberately not MCP's `CallToolResult`.** Each host
|
|
11687
|
+
* adapts {@link ValToolResult} at its own edge, which is also where an
|
|
11688
|
+
* error becomes an in-band `isError` result the model can recover from
|
|
11689
|
+
* rather than a transport failure.
|
|
11690
|
+
*/
|
|
11691
|
+
|
|
11692
|
+
/** Why a tool call failed, in a form a host can map onto its own errors. */
|
|
11693
|
+
|
|
11694
|
+
/**
|
|
11695
|
+
* The same definition with `inputSchema` as JSON Schema, for hosts that want the
|
|
11696
|
+
* wire shape rather than a Standard Schema.
|
|
11697
|
+
*
|
|
11698
|
+
* Typed as whatever zod's own converter produces, so deriving it needs no cast
|
|
11699
|
+
* and no second hand-written description of the same input.
|
|
11700
|
+
*/
|
|
11701
|
+
|
|
11702
|
+
/**
|
|
11703
|
+
* How the caller was established, and it is a union because there are two
|
|
11704
|
+
* genuinely different answers — with different consequences downstream.
|
|
11705
|
+
*
|
|
11706
|
+
* The distinction that matters is **who checked**. A PAT is forwarded to the
|
|
11707
|
+
* backend unchecked, because the app cannot resolve one; an access token is
|
|
11708
|
+
* verified by the app itself, against a public key it does not hold and
|
|
11709
|
+
* therefore cannot forge. The first is a credential being relayed. The second
|
|
11710
|
+
* is a signature that has already been checked.
|
|
11711
|
+
*/
|
|
11712
|
+
|
|
11713
|
+
/**
|
|
11714
|
+
* Who is calling, established once per request by the host.
|
|
11715
|
+
*
|
|
11716
|
+
* `null` means local fs mode, where there is no credential to hold and patches
|
|
11717
|
+
* are written with no author, exactly as the Studio does locally (D.1). In
|
|
11718
|
+
* proxy mode `null` is refused rather than falling back to the app's own API
|
|
11719
|
+
* key: that key can do more than any single user, and quietly substituting it
|
|
11720
|
+
* would turn a missing credential into full access.
|
|
11721
|
+
*/
|
|
11722
|
+
|
|
11723
|
+
/**
|
|
11724
|
+
* Brand a verified subject as an {@link AuthorId}.
|
|
11725
|
+
*
|
|
11726
|
+
* `AuthorId` is a branded string so that an id cannot be conjured from any
|
|
11727
|
+
* string that happens to be lying around — which is exactly the mistake this
|
|
11728
|
+
* type is guarding against. That makes one assertion unavoidable at the boundary
|
|
11729
|
+
* where a real id enters the system, so it lives here, once, with a name that
|
|
11730
|
+
* says what makes it legitimate: the caller has *verified* this subject, not
|
|
11731
|
+
* received it.
|
|
11732
|
+
*
|
|
11733
|
+
* Do not reach for this to satisfy a type. If you are holding a string you did
|
|
11734
|
+
* not verify, the honest value is `null`.
|
|
11735
|
+
*/
|
|
11736
|
+
function authorIdFromVerifiedSubject(subject) {
|
|
11737
|
+
return subject;
|
|
11738
|
+
}
|
|
11739
|
+
|
|
11740
|
+
/** Read access. Every call needs it, the writes included. */
|
|
11741
|
+
const VAL_SCOPE_READ = "val:read";
|
|
11742
|
+
/** Write access. Needed *in addition* by any tool not marked `readOnlyHint`. */
|
|
11743
|
+
const VAL_SCOPE_WRITE = "val:write";
|
|
11744
|
+
|
|
11659
11745
|
/**
|
|
11660
11746
|
* How many callers' data layers to keep around in proxy mode.
|
|
11661
11747
|
*
|
|
@@ -11722,6 +11808,10 @@ function createValTools(valModules, options) {
|
|
|
11722
11808
|
message: describeZodError(parsed.error)
|
|
11723
11809
|
};
|
|
11724
11810
|
}
|
|
11811
|
+
const insufficient = refuseInsufficientScope(tool, ctx);
|
|
11812
|
+
if (insufficient) {
|
|
11813
|
+
return insufficient;
|
|
11814
|
+
}
|
|
11725
11815
|
const resolved = resolveOps(ctx);
|
|
11726
11816
|
if (resolved.status === "error") {
|
|
11727
11817
|
return resolved.result;
|
|
@@ -11806,6 +11896,13 @@ function createOpsResolver(valModules, options) {
|
|
|
11806
11896
|
// instance holds the token regardless — but it keeps credentials out of the
|
|
11807
11897
|
// key set, which is the thing that ends up in a heap dump or an error dump.
|
|
11808
11898
|
const byPatHash = new Map();
|
|
11899
|
+
/**
|
|
11900
|
+
* One instance for every verified caller, and unlike the PAT map that is
|
|
11901
|
+
* correct rather than a shortcut: this instance authenticates with the app's
|
|
11902
|
+
* own API key, so there is nothing per-caller in it to keep apart. Who did
|
|
11903
|
+
* what travels as the patch's `authorId` instead — see `writePath`.
|
|
11904
|
+
*/
|
|
11905
|
+
let sharedOps = null;
|
|
11809
11906
|
return ctx => {
|
|
11810
11907
|
if (!ctx.auth) {
|
|
11811
11908
|
return {
|
|
@@ -11813,10 +11910,32 @@ function createOpsResolver(valModules, options) {
|
|
|
11813
11910
|
result: {
|
|
11814
11911
|
status: "error",
|
|
11815
11912
|
code: "forbidden",
|
|
11816
|
-
message: "This Val project talks to the Val content backend, so every call needs the caller's own personal access token
|
|
11913
|
+
message: "This Val project talks to the Val content backend, so every call needs a credential: an access token from the Val authorization server, or the caller's own personal access token from `val login`."
|
|
11817
11914
|
}
|
|
11818
11915
|
};
|
|
11819
11916
|
}
|
|
11917
|
+
if (ctx.auth.type === "verified-profile") {
|
|
11918
|
+
if (!options.apiKey) {
|
|
11919
|
+
// Proxy mode is inferred from the api key being present, so this is
|
|
11920
|
+
// unreachable through `initHandlerOptions`. It stays because the
|
|
11921
|
+
// alternative to refusing is building ops with no credential at all.
|
|
11922
|
+
return {
|
|
11923
|
+
status: "error",
|
|
11924
|
+
result: {
|
|
11925
|
+
status: "error",
|
|
11926
|
+
code: "forbidden",
|
|
11927
|
+
message: "This Val project has no API key configured, so a verified access token cannot be exchanged for backend access."
|
|
11928
|
+
}
|
|
11929
|
+
};
|
|
11930
|
+
}
|
|
11931
|
+
if (!sharedOps) {
|
|
11932
|
+
sharedOps = createValOps(valModules, options);
|
|
11933
|
+
}
|
|
11934
|
+
return {
|
|
11935
|
+
status: "ok",
|
|
11936
|
+
ops: sharedOps
|
|
11937
|
+
};
|
|
11938
|
+
}
|
|
11820
11939
|
const key = createHash("sha256").update(ctx.auth.pat).digest("hex");
|
|
11821
11940
|
const cached = byPatHash.get(key);
|
|
11822
11941
|
if (cached) {
|
|
@@ -11929,6 +12048,42 @@ function describeZodError(error) {
|
|
|
11929
12048
|
}).join("; ");
|
|
11930
12049
|
}
|
|
11931
12050
|
|
|
12051
|
+
/**
|
|
12052
|
+
* Refuse a call the token was not granted, before anything is attempted.
|
|
12053
|
+
*
|
|
12054
|
+
* Derived from `readOnlyHint` rather than from a second list of tool names,
|
|
12055
|
+
* because a second list is a thing that drifts. The derivation also fails in
|
|
12056
|
+
* the safe direction: a tool that forgets the hint is treated as a write and
|
|
12057
|
+
* demands the wider scope, rather than a write slipping through as a read.
|
|
12058
|
+
*
|
|
12059
|
+
* Only the verified-token path is checked. A PAT carries no scopes here by
|
|
12060
|
+
* design — the backend resolves it and decides — so there is nothing to
|
|
12061
|
+
* enforce, and inventing a default would be this app claiming an authority it
|
|
12062
|
+
* does not have.
|
|
12063
|
+
*/
|
|
12064
|
+
function refuseInsufficientScope(tool, ctx) {
|
|
12065
|
+
var _ctx$auth, _tool$annotations;
|
|
12066
|
+
if (((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) !== "verified-profile") {
|
|
12067
|
+
return null;
|
|
12068
|
+
}
|
|
12069
|
+
// Read is needed by every call, including the writes: a tool that changes
|
|
12070
|
+
// content reads it first, and `ValToolAuth` says as much. Checking only the
|
|
12071
|
+
// wider scope would let a write-but-not-read token through here — today's
|
|
12072
|
+
// verifier refuses such a token before this point, but `createValTools` is
|
|
12073
|
+
// exported and another host may not.
|
|
12074
|
+
const needed = (_tool$annotations = tool.annotations) !== null && _tool$annotations !== void 0 && _tool$annotations.readOnlyHint ? [VAL_SCOPE_READ] : [VAL_SCOPE_READ, VAL_SCOPE_WRITE];
|
|
12075
|
+
const granted = ctx.auth.scopes;
|
|
12076
|
+
const missing = needed.filter(scope => !granted.includes(scope));
|
|
12077
|
+
if (missing.length === 0) {
|
|
12078
|
+
return null;
|
|
12079
|
+
}
|
|
12080
|
+
return {
|
|
12081
|
+
status: "error",
|
|
12082
|
+
code: "forbidden",
|
|
12083
|
+
message: `This access token does not have the ${missing.join(" and ")} scope, which ${tool.name} requires. Granted: ${granted.length > 0 ? granted.join(" ") : "(none)"}.`
|
|
12084
|
+
};
|
|
12085
|
+
}
|
|
12086
|
+
|
|
11932
12087
|
const JsFileLookupMapping = [
|
|
11933
12088
|
// NOTE: first one matching will be used
|
|
11934
12089
|
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
|
@@ -14108,4 +14263,4 @@ function readCapturedReport(snapshotDir) {
|
|
|
14108
14263
|
return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
14109
14264
|
}
|
|
14110
14265
|
|
|
14111
|
-
export { DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createService, createValApiRouter, createValModuleFileInspector, createValOps, createValServer, createValTools, currentFixHandlers, decodeJwtWithoutVerifying, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, findAndEvalValConfigFile, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, initHandlerOptions, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata, verifyJwt };
|
|
14266
|
+
export { DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, Service, VAL_SCOPE_READ, VAL_SCOPE_WRITE, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, authorIdFromVerifiedSubject, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createService, createValApiRouter, createValModuleFileInspector, createValOps, createValServer, createValTools, currentFixHandlers, decodeJwtWithoutVerifying, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, findAndEvalValConfigFile, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, initHandlerOptions, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata, verifyJwt };
|
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.
|
|
19
|
+
"version": "0.117.1",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
|
-
"@valbuild/core": "0.
|
|
33
|
-
"@valbuild/shared": "0.
|
|
34
|
-
"@valbuild/ui": "0.
|
|
32
|
+
"@valbuild/core": "0.117.0",
|
|
33
|
+
"@valbuild/shared": "0.117.0",
|
|
34
|
+
"@valbuild/ui": "0.117.1"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": "^20.19.0 || >=22"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
|
+
"CHANGELOG.md",
|
|
40
41
|
"dist"
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|