synomem 0.7.2 → 0.9.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.
- package/CHANGELOG.md +38 -0
- package/README.md +47 -68
- package/dist/backend.d.ts +18 -6
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +55 -41
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts +20 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1394 -1281
- package/dist/cli.js.map +1 -1
- package/dist/configure.d.ts +12 -46
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +51 -192
- package/dist/configure.js.map +1 -1
- package/dist/credentials.d.ts +73 -33
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +167 -43
- package/dist/credentials.js.map +1 -1
- package/dist/discover.d.ts +10 -13
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +45 -30
- package/dist/discover.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -0
- package/dist/errors.js.map +1 -1
- package/dist/import.d.ts +3 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/import.js +3 -0
- package/dist/import.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +18 -7
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +402 -183
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.d.ts +5 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +27 -105
- package/dist/mcp-server.js.map +1 -1
- package/dist/oauth.d.ts +31 -33
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +178 -125
- package/dist/oauth.js.map +1 -1
- package/dist/profiles.d.ts +243 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +465 -0
- package/dist/profiles.js.map +1 -0
- package/dist/project.d.ts +8 -39
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +36 -94
- package/dist/project.js.map +1 -1
- package/dist/remote.d.ts +23 -15
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +54 -49
- package/dist/remote.js.map +1 -1
- package/dist/resolvers.d.ts +47 -0
- package/dist/resolvers.d.ts.map +1 -0
- package/dist/resolvers.js +255 -0
- package/dist/resolvers.js.map +1 -0
- package/dist/service.d.ts +2 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/skill-install.d.ts +4 -6
- package/dist/skill-install.d.ts.map +1 -1
- package/dist/skill-install.js +13 -12
- package/dist/skill-install.js.map +1 -1
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +173 -196
- package/docs/mcp.md +69 -65
- package/package.json +1 -1
- package/skills/synomem/SKILL.md +30 -4
- package/skills/synomem/references/examples.md +14 -0
- package/src/backend.ts +66 -64
- package/src/cli.ts +2137 -2163
- package/src/configure.ts +62 -241
- package/src/credentials.ts +208 -84
- package/src/discover.ts +60 -36
- package/src/errors.ts +5 -0
- package/src/import.ts +5 -0
- package/src/index.ts +14 -12
- package/src/mcp/index.ts +473 -194
- package/src/mcp-server.ts +32 -114
- package/src/oauth.ts +229 -130
- package/src/profiles.ts +644 -0
- package/src/project.ts +42 -108
- package/src/remote.ts +69 -58
- package/src/resolvers.ts +299 -0
- package/src/service.ts +2 -0
- package/src/skill-install.ts +17 -18
- package/src/types.ts +46 -0
package/src/project.ts
CHANGED
|
@@ -1,60 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-project
|
|
2
|
+
* Per-project profile selection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Opening an agent in a repository should be enough to act as the right
|
|
5
|
+
* identity: a `.synomem/project.json` found by walking up from the working
|
|
6
|
+
* directory may name a profile (or an MCP preset) from the user's own
|
|
7
|
+
* `profiles.json`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* So it is a file in the project, found by walking up from the working
|
|
15
|
-
* directory — the same shape as `.git`, `.nvmrc` or `.npmrc`, and for the same
|
|
16
|
-
* reason: the directory somebody is working in is the thing that knows which
|
|
17
|
-
* project this is.
|
|
18
|
-
*
|
|
19
|
-
* `.synomem/config.json` holds a POINTER, never a store:
|
|
20
|
-
*
|
|
21
|
-
* { "workspace": "lumina", "actor": "claude" }
|
|
22
|
-
*
|
|
23
|
-
* The database stays under the Synomem home. Putting one in the repository
|
|
24
|
-
* would mean an append-only event log inside somebody's git history, committed
|
|
25
|
-
* by accident the first time they ran `git add -A`.
|
|
9
|
+
* It may name ONLY that. A cloned repository is untrusted input, so the file
|
|
10
|
+
* cannot carry a credential, an API origin, a context id, or an actor — it can
|
|
11
|
+
* point at a profile the user already created, and nothing else. An unknown
|
|
12
|
+
* key is refused rather than ignored, so an old `{ "workspace", "actor" }`
|
|
13
|
+
* pointer fails loudly instead of silently doing nothing.
|
|
26
14
|
*/
|
|
27
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
28
16
|
import { dirname, join, parse, resolve } from 'node:path';
|
|
29
|
-
import { resolveHome } from './config.js';
|
|
30
17
|
import { z } from 'zod';
|
|
31
18
|
import { SynomemError } from './errors.js';
|
|
32
|
-
import { localWorkspaceHome, workspaceNameSchema } from './workspaces.js';
|
|
33
19
|
|
|
34
20
|
export const PROJECT_DIRECTORY = '.synomem';
|
|
35
|
-
|
|
21
|
+
/*
|
|
22
|
+
* `project.json`, never `config.json`: a Synomem home (a local store) keeps its
|
|
23
|
+
* own policy in `.synomem/config.json`, and the walk up from a project directory
|
|
24
|
+
* passes through the user's home — so a shared name would read the store's
|
|
25
|
+
* config as a project pointer and break every command run beneath it.
|
|
26
|
+
*/
|
|
27
|
+
export const PROJECT_CONFIG_FILE = 'project.json';
|
|
28
|
+
|
|
29
|
+
const nameSchema = z
|
|
30
|
+
.string()
|
|
31
|
+
.trim()
|
|
32
|
+
.regex(/^[a-z0-9][a-z0-9._-]{0,62}$/i, 'must be a profile or preset name');
|
|
36
33
|
|
|
37
|
-
export const projectConfigSchema = z
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
export const projectConfigSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
profile: nameSchema.optional(),
|
|
37
|
+
preset: nameSchema.optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict()
|
|
40
|
+
.refine((value) => !(value.profile && value.preset), {
|
|
41
|
+
message: 'name a profile or a preset, not both',
|
|
42
|
+
});
|
|
43
43
|
|
|
44
44
|
export type ProjectConfig = z.infer<typeof projectConfigSchema>;
|
|
45
45
|
|
|
46
46
|
export interface ProjectSelection extends ProjectConfig {
|
|
47
|
-
/** The
|
|
48
|
-
|
|
47
|
+
/** The file that supplied the selection, so errors can say where it came from. */
|
|
48
|
+
path: string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* The nearest project
|
|
53
|
-
*
|
|
54
|
-
* Walking up stops at the filesystem root, and never treats the Synomem home
|
|
55
|
-
* itself as a project: `~/.synomem/config.json` is a real Synomem config with a
|
|
56
|
-
* different shape, and reading it as a project pointer would silently apply a
|
|
57
|
-
* home's settings to every command run anywhere under the home directory.
|
|
52
|
+
* The nearest project file at or above `from`. The Synomem home itself is
|
|
53
|
+
* never treated as a project directory.
|
|
58
54
|
*/
|
|
59
55
|
export function findProjectSelection(
|
|
60
56
|
from: string = process.cwd(),
|
|
@@ -63,12 +59,11 @@ export function findProjectSelection(
|
|
|
63
59
|
const stopAt = parse(resolve(from)).root;
|
|
64
60
|
let directory = resolve(from);
|
|
65
61
|
for (;;) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const isHome = home !== undefined && resolve(home) === join(directory, PROJECT_DIRECTORY);
|
|
62
|
+
const folder = join(directory, PROJECT_DIRECTORY);
|
|
63
|
+
const candidate = join(folder, PROJECT_CONFIG_FILE);
|
|
64
|
+
const isHome = home !== undefined && resolve(home) === folder;
|
|
70
65
|
if (!isHome && existsSync(candidate)) {
|
|
71
|
-
return { ...readProjectConfig(candidate),
|
|
66
|
+
return { ...readProjectConfig(candidate), path: candidate };
|
|
72
67
|
}
|
|
73
68
|
if (directory === stopAt) return undefined;
|
|
74
69
|
const parent = dirname(directory);
|
|
@@ -88,7 +83,9 @@ function readProjectConfig(path: string): ProjectConfig {
|
|
|
88
83
|
if (!parsed.success) {
|
|
89
84
|
throw new SynomemError(
|
|
90
85
|
'CONFIG_INVALID',
|
|
91
|
-
`${path} is not a valid Synomem project file: ${
|
|
86
|
+
`${path} is not a valid Synomem project file (it may name only a "profile" or a "preset"): ${
|
|
87
|
+
parsed.error.issues[0]?.message ?? 'unknown problem'
|
|
88
|
+
}`,
|
|
92
89
|
);
|
|
93
90
|
}
|
|
94
91
|
return parsed.data;
|
|
@@ -103,66 +100,3 @@ export function writeProjectSelection(directory: string, config: ProjectConfig):
|
|
|
103
100
|
writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
104
101
|
return path;
|
|
105
102
|
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Which workspace to act in, and as whom.
|
|
109
|
-
*
|
|
110
|
-
* One resolver, used by both the CLI and the stdio MCP server, so the two
|
|
111
|
-
* cannot disagree about precedence. Order, most specific first:
|
|
112
|
-
*
|
|
113
|
-
* 1. `--workspace` / `--actor` — said on this invocation
|
|
114
|
-
* 2. `SYNOMEM_WORKSPACE` / `SYNOMEM_ACTOR_ID` — set for this process
|
|
115
|
-
* 3. `.synomem/config.json` in the project, found by walking up from the
|
|
116
|
-
* working directory
|
|
117
|
-
* 4. the root home, which is the default workspace
|
|
118
|
-
*
|
|
119
|
-
* The project file is third rather than first because a flag someone typed
|
|
120
|
-
* should always beat a file they may have forgotten is there.
|
|
121
|
-
*/
|
|
122
|
-
export function resolveWorkspaceSelection(input: {
|
|
123
|
-
flag?: string;
|
|
124
|
-
actorFlag?: string;
|
|
125
|
-
env?: NodeJS.ProcessEnv;
|
|
126
|
-
cwd?: string;
|
|
127
|
-
explicitRoot?: string;
|
|
128
|
-
}): { home: string; workspace?: string; actor?: string; source: string } {
|
|
129
|
-
const env = input.env ?? process.env;
|
|
130
|
-
const root = resolveHome(input.explicitRoot);
|
|
131
|
-
|
|
132
|
-
if (input.flag) {
|
|
133
|
-
return {
|
|
134
|
-
home: localWorkspaceHome(input.flag, input.explicitRoot),
|
|
135
|
-
workspace: input.flag,
|
|
136
|
-
...(input.actorFlag ? { actor: input.actorFlag } : {}),
|
|
137
|
-
source: 'the --workspace option',
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const fromEnv = env.SYNOMEM_WORKSPACE?.trim();
|
|
142
|
-
if (fromEnv) {
|
|
143
|
-
return {
|
|
144
|
-
home: localWorkspaceHome(fromEnv, input.explicitRoot),
|
|
145
|
-
workspace: fromEnv,
|
|
146
|
-
...((input.actorFlag ?? env.SYNOMEM_ACTOR_ID?.trim())
|
|
147
|
-
? { actor: input.actorFlag ?? env.SYNOMEM_ACTOR_ID?.trim() }
|
|
148
|
-
: {}),
|
|
149
|
-
source: 'SYNOMEM_WORKSPACE',
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const project = findProjectSelection(input.cwd ?? process.cwd(), root);
|
|
154
|
-
if (project?.workspace) {
|
|
155
|
-
return {
|
|
156
|
-
home: localWorkspaceHome(project.workspace, input.explicitRoot),
|
|
157
|
-
workspace: project.workspace,
|
|
158
|
-
...((input.actorFlag ?? project.actor) ? { actor: input.actorFlag ?? project.actor } : {}),
|
|
159
|
-
source: join(project.directory, PROJECT_DIRECTORY, PROJECT_CONFIG_FILE),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
home: root,
|
|
165
|
-
...((input.actorFlag ?? project?.actor) ? { actor: input.actorFlag ?? project?.actor } : {}),
|
|
166
|
-
source: 'the default workspace',
|
|
167
|
-
};
|
|
168
|
-
}
|
package/src/remote.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { actorSchema } from './schemas.js';
|
|
2
|
-
import {
|
|
2
|
+
import { errorCodes, SynomemError, type SynomemErrorCode } from './errors.js';
|
|
3
3
|
import type { SynomemService, SynomemServiceCapabilities, SynomemServiceInfo } from './service.js';
|
|
4
4
|
import type {
|
|
5
5
|
ActorIdentity,
|
|
@@ -31,26 +31,40 @@ export interface SynomemCredentialProvider {
|
|
|
31
31
|
getAccessToken(signal?: AbortSignal): Promise<string | undefined>;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Where a remote service gets its bearer: an OAuth access token or a `syn_` access key
|
|
36
|
+
* (identity contract §6.1). It may refresh; it must never be logged.
|
|
37
|
+
*/
|
|
38
|
+
export interface RemoteCredentialSource {
|
|
39
|
+
bearer(): Promise<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type RemoteCredential =
|
|
43
|
+
RemoteCredentialSource | SynomemCredentialProvider | (() => Promise<string | undefined>);
|
|
44
|
+
|
|
34
45
|
export interface RemoteSynomemOptions {
|
|
35
46
|
baseUrl: string;
|
|
47
|
+
/** The workspace in the route. Must equal the selected context's workspace. */
|
|
36
48
|
workspaceId: string;
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The stable target this service acts as. Sent as `Synomem-Context-Id` on every request.
|
|
51
|
+
* Omit only for a fixed credential, whose single pinned context the API selects itself.
|
|
52
|
+
*/
|
|
53
|
+
contextId?: string;
|
|
54
|
+
credential: RemoteCredential;
|
|
39
55
|
fetch?: typeof fetch;
|
|
40
56
|
signal?: AbortSignal;
|
|
41
57
|
timeoutMs?: number;
|
|
42
58
|
maximumResponseBytes?: number;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
assertActor?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Normalizes any accepted credential shape into one bearer lookup. */
|
|
62
|
+
export function bearerFrom(
|
|
63
|
+
credential: RemoteCredential,
|
|
64
|
+
): (signal?: AbortSignal) => Promise<string | undefined> {
|
|
65
|
+
if (typeof credential === 'function') return () => credential();
|
|
66
|
+
if ('bearer' in credential) return () => credential.bearer();
|
|
67
|
+
return (signal) => credential.getAccessToken(signal);
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
interface ApiErrorEnvelope {
|
|
@@ -110,6 +124,14 @@ function queryString(input: object): string {
|
|
|
110
124
|
return encoded ? `?${encoded}` : '';
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
/** Codes a caller must be able to branch on, never collapsed into AUTH_* by status. */
|
|
128
|
+
const contextCodes = new Set<SynomemErrorCode>([
|
|
129
|
+
'CONTEXT_REQUIRED',
|
|
130
|
+
'CONTEXT_FORBIDDEN',
|
|
131
|
+
'CONTEXT_AMBIGUOUS',
|
|
132
|
+
'REAUTHORIZATION_REQUIRED',
|
|
133
|
+
]);
|
|
134
|
+
|
|
113
135
|
function knownErrorCode(value: string): SynomemErrorCode {
|
|
114
136
|
return errorCodes.some((code) => code === value)
|
|
115
137
|
? (value as SynomemErrorCode)
|
|
@@ -157,17 +179,19 @@ export function environmentCredentialProvider(
|
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
export class RemoteSynomemService implements SynomemService {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
182
|
+
/**
|
|
183
|
+
* The actor this service acts as, learned from the server during `init()` — never asserted
|
|
184
|
+
* by the caller. Before `init()` it is a placeholder that no request uses.
|
|
185
|
+
*/
|
|
186
|
+
actor: ActorIdentity = { kind: 'system', id: 'unbound' };
|
|
187
|
+
readonly contextId?: string;
|
|
163
188
|
private readonly baseUrl: URL;
|
|
164
189
|
private readonly workspaceId: string;
|
|
165
|
-
private readonly
|
|
190
|
+
private readonly bearer: (signal?: AbortSignal) => Promise<string | undefined>;
|
|
166
191
|
private readonly fetchImplementation: typeof fetch;
|
|
167
192
|
private readonly signal?: AbortSignal;
|
|
168
193
|
private readonly timeoutMs: number;
|
|
169
194
|
private readonly maximumResponseBytes: number;
|
|
170
|
-
private readonly assertActor: boolean;
|
|
171
195
|
private initialized = false;
|
|
172
196
|
private cachedCapabilities?: SynomemServiceCapabilities;
|
|
173
197
|
|
|
@@ -540,13 +564,13 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
540
564
|
throw new SynomemError('CONFIG_INVALID', 'Remote Synomem workspaceId is required.');
|
|
541
565
|
}
|
|
542
566
|
this.workspaceId = options.workspaceId;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
567
|
+
if (options.contextId !== undefined) {
|
|
568
|
+
if (!/^[A-Za-z0-9_-]{1,100}$/.test(options.contextId)) {
|
|
569
|
+
throw new SynomemError('CONFIG_INVALID', 'Remote Synomem contextId is malformed.');
|
|
570
|
+
}
|
|
571
|
+
this.contextId = options.contextId;
|
|
547
572
|
}
|
|
548
|
-
this.
|
|
549
|
-
this.assertActor = options.assertActor ?? true;
|
|
573
|
+
this.bearer = bearerFrom(options.credential);
|
|
550
574
|
this.fetchImplementation = options.fetch ?? fetch;
|
|
551
575
|
this.signal = options.signal;
|
|
552
576
|
this.timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
@@ -572,30 +596,21 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
572
596
|
throw new SynomemError('REMOTE_PROTOCOL', 'The configured server is not a remote backend.');
|
|
573
597
|
}
|
|
574
598
|
const binding = this.cachedCapabilities.binding;
|
|
575
|
-
|
|
576
|
-
// asserted" — historically the CLI default for commands that run before
|
|
577
|
-
// any identity exists (`doctor`, `agent list`, `agent create`, …). No
|
|
578
|
-
// credential the server authenticates ever reports that kind back, so
|
|
579
|
-
// asserting it here would make every one of those commands fail against
|
|
580
|
-
// a remote backend no matter who is actually calling — exactly the
|
|
581
|
-
// bootstrap deadlock the server-side half of this already fixed.
|
|
582
|
-
// `assertActor: false` is the same bypass for a fallback of any other
|
|
583
|
-
// kind: a caller whose "actor" is only a historical guess (never a real
|
|
584
|
-
// --actor, SYNOMEM_ACTOR_ID, or project binding) that should adopt
|
|
585
|
-
// whatever the credential actually names rather than fail outright.
|
|
586
|
-
// Either way, only a caller that named a real identity gets it enforced.
|
|
587
|
-
const assertedRealActor = this.assertActor && this.actor.kind !== 'system';
|
|
588
|
-
if (
|
|
589
|
-
binding.workspaceId !== this.workspaceId ||
|
|
590
|
-
(assertedRealActor &&
|
|
591
|
-
(binding.actor.kind !== this.actor.kind || binding.actor.id !== this.actor.id))
|
|
592
|
-
) {
|
|
599
|
+
if (binding.workspaceId !== this.workspaceId) {
|
|
593
600
|
throw new SynomemError(
|
|
594
|
-
'
|
|
595
|
-
'The
|
|
601
|
+
'CONTEXT_FORBIDDEN',
|
|
602
|
+
'The selected Synomem context does not belong to the workspace this service addresses.',
|
|
596
603
|
);
|
|
597
604
|
}
|
|
598
|
-
if (
|
|
605
|
+
if (this.contextId && binding.contextId && binding.contextId !== this.contextId) {
|
|
606
|
+
throw new SynomemError('CONTEXT_FORBIDDEN', 'The server bound a different context.');
|
|
607
|
+
}
|
|
608
|
+
const actor = actorSchema.safeParse(binding.actor);
|
|
609
|
+
if (!actor.success) {
|
|
610
|
+
throw new SynomemError('REMOTE_PROTOCOL', 'The server reported an invalid bound actor.');
|
|
611
|
+
}
|
|
612
|
+
// The server decides who this is; the client only learns it (plan §5 rule 7).
|
|
613
|
+
this.actor = actor.data;
|
|
599
614
|
this.initialized = true;
|
|
600
615
|
}
|
|
601
616
|
|
|
@@ -698,7 +713,7 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
698
713
|
body?: object,
|
|
699
714
|
idempotencyKey?: string,
|
|
700
715
|
): Promise<T> {
|
|
701
|
-
const accessToken = await this.
|
|
716
|
+
const accessToken = await this.bearer(this.signal);
|
|
702
717
|
if (!accessToken) {
|
|
703
718
|
throw new SynomemError('AUTH_REQUIRED', 'Remote Synomem authentication is required.');
|
|
704
719
|
}
|
|
@@ -721,15 +736,9 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
721
736
|
headers: {
|
|
722
737
|
accept: 'application/json',
|
|
723
738
|
authorization: `Bearer ${accessToken}`,
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
* key: an access key authenticates the member who owns it, not a
|
|
728
|
-
* machine or a workspace, so which workspace and which agent are
|
|
729
|
-
* meant have to be named on every request.
|
|
730
|
-
*/
|
|
731
|
-
'synomem-workspace-id': this.workspaceId,
|
|
732
|
-
...(this.actor.kind === 'agent' ? { 'synomem-agent-id': this.actor.id } : {}),
|
|
739
|
+
// The ONLY selector (identity contract §3). The server authorizes it against the
|
|
740
|
+
// credential's grant on every request; knowing an id grants nothing.
|
|
741
|
+
...(this.contextId ? { 'synomem-context-id': this.contextId } : {}),
|
|
733
742
|
...(body ? { 'content-type': 'application/json' } : {}),
|
|
734
743
|
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
|
|
735
744
|
},
|
|
@@ -760,8 +769,10 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
760
769
|
if (envelope.ok !== false || !envelope.error || typeof envelope.error.message !== 'string') {
|
|
761
770
|
throw new SynomemError('REMOTE_PROTOCOL', 'Remote Synomem returned an invalid error.');
|
|
762
771
|
}
|
|
763
|
-
const
|
|
764
|
-
|
|
772
|
+
const reported = knownErrorCode(envelope.error.code);
|
|
773
|
+
const code = contextCodes.has(reported)
|
|
774
|
+
? reported
|
|
775
|
+
: response.status === 401
|
|
765
776
|
? 'AUTH_REQUIRED'
|
|
766
777
|
: response.status === 403
|
|
767
778
|
? 'AUTH_FORBIDDEN'
|