@coffre/core 0.4.1 → 0.4.3
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/dist/dotenv.d.ts +6 -1
- package/dist/dotenv.js +29 -12
- package/dist/mcp.d.ts +10 -5
- package/dist/mcp.js +17 -12
- package/dist/schemas.d.ts +1 -1
- package/package.json +2 -2
- package/src/dotenv.ts +37 -18
- package/src/mcp.ts +16 -11
package/dist/dotenv.d.ts
CHANGED
|
@@ -17,9 +17,14 @@ export type ParsedEntry = {
|
|
|
17
17
|
value: string;
|
|
18
18
|
line: number;
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* A line that could not be parsed: its number, why, and its key when it has
|
|
22
|
+
* a valid one. Never the line's text: a line without its "=", or with a key
|
|
23
|
+
* that is not one, may be a secret pasted on its own, `c2stbGl2ZS0xMjM0NQ==`.
|
|
24
|
+
*/
|
|
20
25
|
export type ParseProblem = {
|
|
21
26
|
line: number;
|
|
22
|
-
|
|
27
|
+
key?: string;
|
|
23
28
|
reason: string;
|
|
24
29
|
};
|
|
25
30
|
export type ParseResult = {
|
package/dist/dotenv.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
//#region src/dotenv.ts
|
|
2
|
+
/**
|
|
3
|
+
* A secret's key, as the API's `secretKey` (`schemas.ts`) has it: a plain
|
|
4
|
+
* pattern, so that the browser's import does not carry zod. The tests hold
|
|
5
|
+
* the two in agreement.
|
|
6
|
+
*/
|
|
2
7
|
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
3
8
|
function parseDotenv(input) {
|
|
4
9
|
const entries = [];
|
|
@@ -14,7 +19,6 @@ function parseDotenv(input) {
|
|
|
14
19
|
if (equals === -1) {
|
|
15
20
|
problems.push({
|
|
16
21
|
line: lineNumber,
|
|
17
|
-
text: trimmed,
|
|
18
22
|
reason: "no \"=\" on this line"
|
|
19
23
|
});
|
|
20
24
|
continue;
|
|
@@ -23,7 +27,6 @@ function parseDotenv(input) {
|
|
|
23
27
|
if (!KEY_RE.test(key)) {
|
|
24
28
|
problems.push({
|
|
25
29
|
line: lineNumber,
|
|
26
|
-
text: key,
|
|
27
30
|
reason: "key must match ^[A-Za-z_][A-Za-z0-9_]*$"
|
|
28
31
|
});
|
|
29
32
|
continue;
|
|
@@ -35,7 +38,7 @@ function parseDotenv(input) {
|
|
|
35
38
|
if (closing === -1) {
|
|
36
39
|
problems.push({
|
|
37
40
|
line: lineNumber,
|
|
38
|
-
|
|
41
|
+
key,
|
|
39
42
|
reason: "unterminated double quote (multi-line values are not supported)"
|
|
40
43
|
});
|
|
41
44
|
continue;
|
|
@@ -44,7 +47,7 @@ function parseDotenv(input) {
|
|
|
44
47
|
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
45
48
|
problems.push({
|
|
46
49
|
line: lineNumber,
|
|
47
|
-
|
|
50
|
+
key,
|
|
48
51
|
reason: "unexpected text after the closing quote"
|
|
49
52
|
});
|
|
50
53
|
continue;
|
|
@@ -54,7 +57,7 @@ function parseDotenv(input) {
|
|
|
54
57
|
if (closing === -1) {
|
|
55
58
|
problems.push({
|
|
56
59
|
line: lineNumber,
|
|
57
|
-
|
|
60
|
+
key,
|
|
58
61
|
reason: "unterminated single quote (multi-line values are not supported)"
|
|
59
62
|
});
|
|
60
63
|
continue;
|
|
@@ -63,20 +66,34 @@ function parseDotenv(input) {
|
|
|
63
66
|
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
64
67
|
problems.push({
|
|
65
68
|
line: lineNumber,
|
|
66
|
-
|
|
69
|
+
key,
|
|
67
70
|
reason: "unexpected text after the closing quote"
|
|
68
71
|
});
|
|
69
72
|
continue;
|
|
70
73
|
}
|
|
71
74
|
} else {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
value =
|
|
75
|
+
const text = withoutExport.slice(equals + 1);
|
|
76
|
+
const comment = text.search(/\s#/);
|
|
77
|
+
value = (comment === -1 ? text : text.slice(0, comment)).trim();
|
|
78
|
+
if (/^=+$/.test(value)) {
|
|
79
|
+
problems.push({
|
|
80
|
+
line: lineNumber,
|
|
81
|
+
reason: "a value pasted on its own, not KEY=value: only \"=\" follows what would be its key"
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (value === "") {
|
|
86
|
+
problems.push({
|
|
87
|
+
line: lineNumber,
|
|
88
|
+
reason: "no value: write KEY=\"\" for an empty one"
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
75
92
|
}
|
|
76
93
|
if (value.includes("\0")) {
|
|
77
94
|
problems.push({
|
|
78
95
|
line: lineNumber,
|
|
79
|
-
|
|
96
|
+
key,
|
|
80
97
|
reason: "value contains a NUL byte, which cannot be passed in an environment"
|
|
81
98
|
});
|
|
82
99
|
continue;
|
|
@@ -84,7 +101,7 @@ function parseDotenv(input) {
|
|
|
84
101
|
if (seen.has(key)) {
|
|
85
102
|
problems.push({
|
|
86
103
|
line: lineNumber,
|
|
87
|
-
|
|
104
|
+
key,
|
|
88
105
|
reason: "duplicate key in this file"
|
|
89
106
|
});
|
|
90
107
|
continue;
|
|
@@ -167,7 +184,7 @@ function assertEnvironmentEntry(key, value) {
|
|
|
167
184
|
if (value.includes("\0")) throw new Error(`${key} contains a NUL byte`);
|
|
168
185
|
}
|
|
169
186
|
function quoteDotenv(value) {
|
|
170
|
-
if (PLAIN_VALUE.test(value)) return value;
|
|
187
|
+
if (PLAIN_VALUE.test(value) && !/^=*$/.test(value)) return value;
|
|
171
188
|
if (!/['\n\r]/.test(value)) return `'${value}'`;
|
|
172
189
|
return `"${value.replace(/[\\"\n\r]/g, (character) => {
|
|
173
190
|
switch (character) {
|
package/dist/mcp.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 4 and 5). Plain functions over strings, for the server and the consent
|
|
6
6
|
* page alike.
|
|
7
7
|
*/
|
|
8
|
-
/** In the order the consent page lists them; `
|
|
9
|
-
export declare const MCP_SCOPES: readonly ["
|
|
8
|
+
/** In the order the consent page lists them; `read` is always granted. */
|
|
9
|
+
export declare const MCP_SCOPES: readonly ["read", "write", "reveal", "manage-access"];
|
|
10
10
|
export type McpScope = (typeof MCP_SCOPES)[number];
|
|
11
11
|
export declare const MCP_SCOPE_INFO: Record<McpScope, {
|
|
12
12
|
label: string;
|
|
@@ -15,7 +15,7 @@ export declare const MCP_SCOPE_INFO: Record<McpScope, {
|
|
|
15
15
|
export declare function isMcpScope(value: string): value is McpScope;
|
|
16
16
|
/**
|
|
17
17
|
* The scopes a space-separated `scope` asks for, in catalogue order, with
|
|
18
|
-
* `
|
|
18
|
+
* `read` always among them; and the ones coffre does not know, which the
|
|
19
19
|
* caller refuses as `invalid_scope`.
|
|
20
20
|
*/
|
|
21
21
|
export declare function parseScopes(value: string | null | undefined): {
|
|
@@ -24,8 +24,13 @@ export declare function parseScopes(value: string | null | undefined): {
|
|
|
24
24
|
};
|
|
25
25
|
/** Scopes as one string, in catalogue order: how a connection stores them and a token answer says them. */
|
|
26
26
|
export declare function scopeString(scopes: Iterable<McpScope>): string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* A person's choice at consent: what they ticked, whatever the client asked
|
|
29
|
+
* for, and never without `read`. What a client asks for only decides what
|
|
30
|
+
* starts ticked: clients ask for what the resource metadata names, `read`,
|
|
31
|
+
* and not every one asks for more later (docs/design/mcp.md, section 5).
|
|
32
|
+
*/
|
|
33
|
+
export declare function grantedScopes(chosen: readonly string[]): McpScope[];
|
|
29
34
|
/**
|
|
30
35
|
* Whether a connection granted `granted` supersedes an earlier one of the
|
|
31
36
|
* same client that holds `held`: it grants all of that and more, as a
|
package/dist/mcp.js
CHANGED
|
@@ -5,24 +5,24 @@
|
|
|
5
5
|
* 4 and 5). Plain functions over strings, for the server and the consent
|
|
6
6
|
* page alike.
|
|
7
7
|
*/
|
|
8
|
-
/** In the order the consent page lists them; `
|
|
8
|
+
/** In the order the consent page lists them; `read` is always granted. */
|
|
9
9
|
const MCP_SCOPES = [
|
|
10
|
-
"
|
|
10
|
+
"read",
|
|
11
11
|
"write",
|
|
12
|
-
"
|
|
12
|
+
"reveal",
|
|
13
13
|
"manage-access"
|
|
14
14
|
];
|
|
15
15
|
const MCP_SCOPE_INFO = {
|
|
16
|
-
|
|
17
|
-
label: "
|
|
16
|
+
read: {
|
|
17
|
+
label: "Read",
|
|
18
18
|
description: "Projects, environments, key names, history, access and the audit log. Never a value."
|
|
19
19
|
},
|
|
20
20
|
write: {
|
|
21
21
|
label: "Write",
|
|
22
22
|
description: "Set, generate, rename, archive and restore secrets; create projects and environments. Each change waits for your approval on coffre."
|
|
23
23
|
},
|
|
24
|
-
|
|
25
|
-
label: "
|
|
24
|
+
reveal: {
|
|
25
|
+
label: "Reveal values",
|
|
26
26
|
description: "Secret values, sent to the app. They become part of its conversation, wherever it keeps it."
|
|
27
27
|
},
|
|
28
28
|
"manage-access": {
|
|
@@ -37,14 +37,14 @@ function isMcpScope(value) {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* The scopes a space-separated `scope` asks for, in catalogue order, with
|
|
40
|
-
* `
|
|
40
|
+
* `read` always among them; and the ones coffre does not know, which the
|
|
41
41
|
* caller refuses as `invalid_scope`.
|
|
42
42
|
*/
|
|
43
43
|
function parseScopes(value) {
|
|
44
44
|
const asked = new Set((value ?? "").split(" ").filter((scope) => scope !== "" && !IGNORED_SCOPES.has(scope)));
|
|
45
45
|
const unknown = [...asked].filter((scope) => !isMcpScope(scope));
|
|
46
46
|
return {
|
|
47
|
-
scopes: MCP_SCOPES.filter((scope) => scope === "
|
|
47
|
+
scopes: MCP_SCOPES.filter((scope) => scope === "read" || asked.has(scope)),
|
|
48
48
|
unknown
|
|
49
49
|
};
|
|
50
50
|
}
|
|
@@ -53,10 +53,15 @@ function scopeString(scopes) {
|
|
|
53
53
|
const held = new Set(scopes);
|
|
54
54
|
return MCP_SCOPES.filter((scope) => held.has(scope)).join(" ");
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* A person's choice at consent: what they ticked, whatever the client asked
|
|
58
|
+
* for, and never without `read`. What a client asks for only decides what
|
|
59
|
+
* starts ticked: clients ask for what the resource metadata names, `read`,
|
|
60
|
+
* and not every one asks for more later (docs/design/mcp.md, section 5).
|
|
61
|
+
*/
|
|
62
|
+
function grantedScopes(chosen) {
|
|
58
63
|
const picked = new Set(chosen);
|
|
59
|
-
return MCP_SCOPES.filter((scope) => scope === "
|
|
64
|
+
return MCP_SCOPES.filter((scope) => scope === "read" || picked.has(scope));
|
|
60
65
|
}
|
|
61
66
|
/**
|
|
62
67
|
* Whether a connection granted `granted` supersedes an earlier one of the
|
package/dist/schemas.d.ts
CHANGED
|
@@ -29,8 +29,8 @@ export declare const principalType: z.ZodEnum<{
|
|
|
29
29
|
}>;
|
|
30
30
|
export declare const principalId: z.ZodString;
|
|
31
31
|
export declare const instanceRole: z.ZodEnum<{
|
|
32
|
-
user: "user";
|
|
33
32
|
owner: "owner";
|
|
33
|
+
user: "user";
|
|
34
34
|
}>;
|
|
35
35
|
export declare const grantId: z.ZodString;
|
|
36
36
|
export declare const emailAddress: z.ZodString;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "What coffre's packages share: roles, envelopes, identity, the audit chain and the vault's contract.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@types/node": "26.1.1",
|
|
85
85
|
"tsdown": "0.23.0",
|
|
86
86
|
"typescript": "5.9.3",
|
|
87
|
-
"@coffre/conformance": "0.4.
|
|
87
|
+
"@coffre/conformance": "0.4.3"
|
|
88
88
|
},
|
|
89
89
|
"scripts": {
|
|
90
90
|
"build": "tsdown",
|
package/src/dotenv.ts
CHANGED
|
@@ -13,13 +13,23 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
export type ParsedEntry = { key: string; value: string; line: number };
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A line that could not be parsed: its number, why, and its key when it has
|
|
18
|
+
* a valid one. Never the line's text: a line without its "=", or with a key
|
|
19
|
+
* that is not one, may be a secret pasted on its own, `c2stbGl2ZS0xMjM0NQ==`.
|
|
20
|
+
*/
|
|
21
|
+
export type ParseProblem = { line: number; key?: string; reason: string };
|
|
17
22
|
|
|
18
23
|
export type ParseResult = {
|
|
19
24
|
entries: ParsedEntry[];
|
|
20
25
|
problems: ParseProblem[];
|
|
21
26
|
};
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A secret's key, as the API's `secretKey` (`schemas.ts`) has it: a plain
|
|
30
|
+
* pattern, so that the browser's import does not carry zod. The tests hold
|
|
31
|
+
* the two in agreement.
|
|
32
|
+
*/
|
|
23
33
|
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
24
34
|
|
|
25
35
|
export function parseDotenv(input: string): ParseResult {
|
|
@@ -45,17 +55,13 @@ export function parseDotenv(input: string): ParseResult {
|
|
|
45
55
|
|
|
46
56
|
const equals = withoutExport.indexOf('=');
|
|
47
57
|
if (equals === -1) {
|
|
48
|
-
problems.push({ line: lineNumber,
|
|
58
|
+
problems.push({ line: lineNumber, reason: 'no "=" on this line' });
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
const key = withoutExport.slice(0, equals).trim();
|
|
53
63
|
if (!KEY_RE.test(key)) {
|
|
54
|
-
problems.push({
|
|
55
|
-
line: lineNumber,
|
|
56
|
-
text: key,
|
|
57
|
-
reason: 'key must match ^[A-Za-z_][A-Za-z0-9_]*$',
|
|
58
|
-
});
|
|
64
|
+
problems.push({ line: lineNumber, reason: 'key must match ^[A-Za-z_][A-Za-z0-9_]*$' });
|
|
59
65
|
continue;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -67,7 +73,7 @@ export function parseDotenv(input: string): ParseResult {
|
|
|
67
73
|
if (closing === -1) {
|
|
68
74
|
problems.push({
|
|
69
75
|
line: lineNumber,
|
|
70
|
-
|
|
76
|
+
key,
|
|
71
77
|
reason: 'unterminated double quote (multi-line values are not supported)',
|
|
72
78
|
});
|
|
73
79
|
continue;
|
|
@@ -79,7 +85,7 @@ export function parseDotenv(input: string): ParseResult {
|
|
|
79
85
|
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
80
86
|
problems.push({
|
|
81
87
|
line: lineNumber,
|
|
82
|
-
|
|
88
|
+
key,
|
|
83
89
|
reason: 'unexpected text after the closing quote',
|
|
84
90
|
});
|
|
85
91
|
continue;
|
|
@@ -89,7 +95,7 @@ export function parseDotenv(input: string): ParseResult {
|
|
|
89
95
|
if (closing === -1) {
|
|
90
96
|
problems.push({
|
|
91
97
|
line: lineNumber,
|
|
92
|
-
|
|
98
|
+
key,
|
|
93
99
|
reason: 'unterminated single quote (multi-line values are not supported)',
|
|
94
100
|
});
|
|
95
101
|
continue;
|
|
@@ -99,16 +105,28 @@ export function parseDotenv(input: string): ParseResult {
|
|
|
99
105
|
if (trailingIsSignificant(rest.slice(closing + 1))) {
|
|
100
106
|
problems.push({
|
|
101
107
|
line: lineNumber,
|
|
102
|
-
|
|
108
|
+
key,
|
|
103
109
|
reason: 'unexpected text after the closing quote',
|
|
104
110
|
});
|
|
105
111
|
continue;
|
|
106
112
|
}
|
|
107
113
|
} else {
|
|
108
|
-
// Unquoted:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
// Unquoted: a "#" after whitespace starts a comment, found before trimming, as dotenv does:
|
|
115
|
+
// `KEY= # later` has no value, `KEY=a #b` is `a`; `KEY=#fff` and `KEY=a#b` keep theirs.
|
|
116
|
+
const text = withoutExport.slice(equals + 1);
|
|
117
|
+
const comment = text.search(/\s#/);
|
|
118
|
+
value = (comment === -1 ? text : text.slice(0, comment)).trim();
|
|
119
|
+
// A secret pasted on its own line, its base64 padding taken for the "=": `c2stbGl2ZS0xMjM0NTY3OA==`
|
|
120
|
+
// leaves "=" after it, `c2stbGl2ZS0xMjM0NTY3ODk=` nothing. Its text would be the key: never named.
|
|
121
|
+
// An empty value is meant only quoted, `KEY=""`; unquoted, it can't be told from that padding.
|
|
122
|
+
if (/^=+$/.test(value)) {
|
|
123
|
+
problems.push({ line: lineNumber, reason: 'a value pasted on its own, not KEY=value: only "=" follows what would be its key' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (value === '') {
|
|
127
|
+
problems.push({ line: lineNumber, reason: 'no value: write KEY="" for an empty one' });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
// A NUL byte cannot survive being put in a process environment: execve
|
|
@@ -118,14 +136,14 @@ export function parseDotenv(input: string): ParseResult {
|
|
|
118
136
|
if (value.includes('\u0000')) {
|
|
119
137
|
problems.push({
|
|
120
138
|
line: lineNumber,
|
|
121
|
-
|
|
139
|
+
key,
|
|
122
140
|
reason: 'value contains a NUL byte, which cannot be passed in an environment',
|
|
123
141
|
});
|
|
124
142
|
continue;
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
if (seen.has(key)) {
|
|
128
|
-
problems.push({ line: lineNumber,
|
|
146
|
+
problems.push({ line: lineNumber, key, reason: 'duplicate key in this file' });
|
|
129
147
|
continue;
|
|
130
148
|
}
|
|
131
149
|
seen.add(key);
|
|
@@ -212,7 +230,8 @@ export function assertEnvironmentEntry(key: string, value: string): void {
|
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
function quoteDotenv(value: string): string {
|
|
215
|
-
|
|
233
|
+
// Empty, or only "=", a value is quoted: unquoted, the parser refuses it as a pasted secret's padding.
|
|
234
|
+
if (PLAIN_VALUE.test(value) && !/^=*$/.test(value)) return value;
|
|
216
235
|
if (!/['\n\r]/.test(value)) return `'${value}'`;
|
|
217
236
|
const escaped = value.replace(/[\\"\n\r]/g, (character) => {
|
|
218
237
|
switch (character) {
|
package/src/mcp.ts
CHANGED
|
@@ -5,22 +5,22 @@
|
|
|
5
5
|
* page alike.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** In the order the consent page lists them; `
|
|
9
|
-
export const MCP_SCOPES = ['
|
|
8
|
+
/** In the order the consent page lists them; `read` is always granted. */
|
|
9
|
+
export const MCP_SCOPES = ['read', 'write', 'reveal', 'manage-access'] as const;
|
|
10
10
|
|
|
11
11
|
export type McpScope = (typeof MCP_SCOPES)[number];
|
|
12
12
|
|
|
13
13
|
export const MCP_SCOPE_INFO: Record<McpScope, { label: string; description: string }> = {
|
|
14
|
-
|
|
15
|
-
label: '
|
|
14
|
+
read: {
|
|
15
|
+
label: 'Read',
|
|
16
16
|
description: 'Projects, environments, key names, history, access and the audit log. Never a value.',
|
|
17
17
|
},
|
|
18
18
|
write: {
|
|
19
19
|
label: 'Write',
|
|
20
20
|
description: 'Set, generate, rename, archive and restore secrets; create projects and environments. Each change waits for your approval on coffre.',
|
|
21
21
|
},
|
|
22
|
-
|
|
23
|
-
label: '
|
|
22
|
+
reveal: {
|
|
23
|
+
label: 'Reveal values',
|
|
24
24
|
description: 'Secret values, sent to the app. They become part of its conversation, wherever it keeps it.',
|
|
25
25
|
},
|
|
26
26
|
'manage-access': {
|
|
@@ -38,13 +38,13 @@ export function isMcpScope(value: string): value is McpScope {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* The scopes a space-separated `scope` asks for, in catalogue order, with
|
|
41
|
-
* `
|
|
41
|
+
* `read` always among them; and the ones coffre does not know, which the
|
|
42
42
|
* caller refuses as `invalid_scope`.
|
|
43
43
|
*/
|
|
44
44
|
export function parseScopes(value: string | null | undefined): { scopes: McpScope[]; unknown: string[] } {
|
|
45
45
|
const asked = new Set((value ?? '').split(' ').filter((scope) => scope !== '' && !IGNORED_SCOPES.has(scope)));
|
|
46
46
|
const unknown = [...asked].filter((scope) => !isMcpScope(scope));
|
|
47
|
-
return { scopes: MCP_SCOPES.filter((scope) => scope === '
|
|
47
|
+
return { scopes: MCP_SCOPES.filter((scope) => scope === 'read' || asked.has(scope)), unknown };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Scopes as one string, in catalogue order: how a connection stores them and a token answer says them. */
|
|
@@ -53,10 +53,15 @@ export function scopeString(scopes: Iterable<McpScope>): string {
|
|
|
53
53
|
return MCP_SCOPES.filter((scope) => held.has(scope)).join(' ');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* A person's choice at consent: what they ticked, whatever the client asked
|
|
58
|
+
* for, and never without `read`. What a client asks for only decides what
|
|
59
|
+
* starts ticked: clients ask for what the resource metadata names, `read`,
|
|
60
|
+
* and not every one asks for more later (docs/design/mcp.md, section 5).
|
|
61
|
+
*/
|
|
62
|
+
export function grantedScopes(chosen: readonly string[]): McpScope[] {
|
|
58
63
|
const picked = new Set(chosen);
|
|
59
|
-
return MCP_SCOPES.filter((scope) => scope === '
|
|
64
|
+
return MCP_SCOPES.filter((scope) => scope === 'read' || picked.has(scope));
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
/**
|