synomem 0.8.0 → 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 -33
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1391 -1321
- 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 +8 -35
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +42 -38
- 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 +4 -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 +10 -8
- 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 -18
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +402 -231
- 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 +24 -17
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +54 -52
- 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 +3 -2
- 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 +45 -15
- 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 +29 -26
- package/skills/synomem/references/examples.md +13 -11
- package/src/backend.ts +66 -64
- package/src/cli.ts +2131 -2222
- package/src/configure.ts +62 -241
- package/src/credentials.ts +208 -84
- package/src/discover.ts +53 -59
- package/src/errors.ts +4 -0
- package/src/import.ts +5 -0
- package/src/index.ts +15 -19
- package/src/mcp/index.ts +473 -277
- 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 -63
- package/src/resolvers.ts +299 -0
- package/src/service.ts +2 -7
- package/src/skill-install.ts +17 -18
- package/src/types.ts +40 -15
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,9 +1,8 @@
|
|
|
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,
|
|
6
|
-
WorkspaceIdentity,
|
|
7
6
|
ChangesInput,
|
|
8
7
|
BindRuntimeInput,
|
|
9
8
|
CreateAgentInput,
|
|
@@ -32,26 +31,40 @@ export interface SynomemCredentialProvider {
|
|
|
32
31
|
getAccessToken(signal?: AbortSignal): Promise<string | undefined>;
|
|
33
32
|
}
|
|
34
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
|
+
|
|
35
45
|
export interface RemoteSynomemOptions {
|
|
36
46
|
baseUrl: string;
|
|
47
|
+
/** The workspace in the route. Must equal the selected context's workspace. */
|
|
37
48
|
workspaceId: string;
|
|
38
|
-
|
|
39
|
-
|
|
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;
|
|
40
55
|
fetch?: typeof fetch;
|
|
41
56
|
signal?: AbortSignal;
|
|
42
57
|
timeoutMs?: number;
|
|
43
58
|
maximumResponseBytes?: number;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
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);
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
interface ApiErrorEnvelope {
|
|
@@ -111,6 +124,14 @@ function queryString(input: object): string {
|
|
|
111
124
|
return encoded ? `?${encoded}` : '';
|
|
112
125
|
}
|
|
113
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
|
+
|
|
114
135
|
function knownErrorCode(value: string): SynomemErrorCode {
|
|
115
136
|
return errorCodes.some((code) => code === value)
|
|
116
137
|
? (value as SynomemErrorCode)
|
|
@@ -158,17 +179,19 @@ export function environmentCredentialProvider(
|
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
export class RemoteSynomemService implements SynomemService {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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;
|
|
164
188
|
private readonly baseUrl: URL;
|
|
165
189
|
private readonly workspaceId: string;
|
|
166
|
-
private readonly
|
|
190
|
+
private readonly bearer: (signal?: AbortSignal) => Promise<string | undefined>;
|
|
167
191
|
private readonly fetchImplementation: typeof fetch;
|
|
168
192
|
private readonly signal?: AbortSignal;
|
|
169
193
|
private readonly timeoutMs: number;
|
|
170
194
|
private readonly maximumResponseBytes: number;
|
|
171
|
-
private readonly assertActor: boolean;
|
|
172
195
|
private initialized = false;
|
|
173
196
|
private cachedCapabilities?: SynomemServiceCapabilities;
|
|
174
197
|
|
|
@@ -541,13 +564,13 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
541
564
|
throw new SynomemError('CONFIG_INVALID', 'Remote Synomem workspaceId is required.');
|
|
542
565
|
}
|
|
543
566
|
this.workspaceId = options.workspaceId;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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;
|
|
548
572
|
}
|
|
549
|
-
this.
|
|
550
|
-
this.assertActor = options.assertActor ?? true;
|
|
573
|
+
this.bearer = bearerFrom(options.credential);
|
|
551
574
|
this.fetchImplementation = options.fetch ?? fetch;
|
|
552
575
|
this.signal = options.signal;
|
|
553
576
|
this.timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
@@ -573,30 +596,21 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
573
596
|
throw new SynomemError('REMOTE_PROTOCOL', 'The configured server is not a remote backend.');
|
|
574
597
|
}
|
|
575
598
|
const binding = this.cachedCapabilities.binding;
|
|
576
|
-
|
|
577
|
-
// asserted" — historically the CLI default for commands that run before
|
|
578
|
-
// any identity exists (`doctor`, `agent list`, `agent create`, …). No
|
|
579
|
-
// credential the server authenticates ever reports that kind back, so
|
|
580
|
-
// asserting it here would make every one of those commands fail against
|
|
581
|
-
// a remote backend no matter who is actually calling — exactly the
|
|
582
|
-
// bootstrap deadlock the server-side half of this already fixed.
|
|
583
|
-
// `assertActor: false` is the same bypass for a fallback of any other
|
|
584
|
-
// kind: a caller whose "actor" is only a historical guess (never a real
|
|
585
|
-
// --actor, SYNOMEM_ACTOR_ID, or project binding) that should adopt
|
|
586
|
-
// whatever the credential actually names rather than fail outright.
|
|
587
|
-
// Either way, only a caller that named a real identity gets it enforced.
|
|
588
|
-
const assertedRealActor = this.assertActor && this.actor.kind !== 'system';
|
|
589
|
-
if (
|
|
590
|
-
binding.workspaceId !== this.workspaceId ||
|
|
591
|
-
(assertedRealActor &&
|
|
592
|
-
(binding.actor.kind !== this.actor.kind || binding.actor.id !== this.actor.id))
|
|
593
|
-
) {
|
|
599
|
+
if (binding.workspaceId !== this.workspaceId) {
|
|
594
600
|
throw new SynomemError(
|
|
595
|
-
'
|
|
596
|
-
'The
|
|
601
|
+
'CONTEXT_FORBIDDEN',
|
|
602
|
+
'The selected Synomem context does not belong to the workspace this service addresses.',
|
|
597
603
|
);
|
|
598
604
|
}
|
|
599
|
-
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;
|
|
600
614
|
this.initialized = true;
|
|
601
615
|
}
|
|
602
616
|
|
|
@@ -642,10 +656,6 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
642
656
|
return { backend: 'remote', baseUrl: this.baseUrl.href, workspaceId: this.workspaceId };
|
|
643
657
|
}
|
|
644
658
|
|
|
645
|
-
async workspaces(): Promise<WorkspaceIdentity> {
|
|
646
|
-
return this.request<WorkspaceIdentity>('GET', '../../identity');
|
|
647
|
-
}
|
|
648
|
-
|
|
649
659
|
getCanonicalEvent(id: string) {
|
|
650
660
|
return this.request<Awaited<ReturnType<SynomemService['getCanonicalEvent']>>>(
|
|
651
661
|
'GET',
|
|
@@ -703,7 +713,7 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
703
713
|
body?: object,
|
|
704
714
|
idempotencyKey?: string,
|
|
705
715
|
): Promise<T> {
|
|
706
|
-
const accessToken = await this.
|
|
716
|
+
const accessToken = await this.bearer(this.signal);
|
|
707
717
|
if (!accessToken) {
|
|
708
718
|
throw new SynomemError('AUTH_REQUIRED', 'Remote Synomem authentication is required.');
|
|
709
719
|
}
|
|
@@ -726,15 +736,9 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
726
736
|
headers: {
|
|
727
737
|
accept: 'application/json',
|
|
728
738
|
authorization: `Bearer ${accessToken}`,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
* key: an access key authenticates the member who owns it, not a
|
|
733
|
-
* machine or a workspace, so which workspace and which agent are
|
|
734
|
-
* meant have to be named on every request.
|
|
735
|
-
*/
|
|
736
|
-
'synomem-workspace-id': this.workspaceId,
|
|
737
|
-
...(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 } : {}),
|
|
738
742
|
...(body ? { 'content-type': 'application/json' } : {}),
|
|
739
743
|
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
|
|
740
744
|
},
|
|
@@ -765,8 +769,10 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
765
769
|
if (envelope.ok !== false || !envelope.error || typeof envelope.error.message !== 'string') {
|
|
766
770
|
throw new SynomemError('REMOTE_PROTOCOL', 'Remote Synomem returned an invalid error.');
|
|
767
771
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
772
|
+
const reported = knownErrorCode(envelope.error.code);
|
|
773
|
+
const code = contextCodes.has(reported)
|
|
774
|
+
? reported
|
|
775
|
+
: response.status === 401
|
|
770
776
|
? 'AUTH_REQUIRED'
|
|
771
777
|
: response.status === 403
|
|
772
778
|
? 'AUTH_FORBIDDEN'
|