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.
Files changed (93) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +47 -68
  3. package/dist/backend.d.ts +18 -6
  4. package/dist/backend.d.ts.map +1 -1
  5. package/dist/backend.js +55 -41
  6. package/dist/backend.js.map +1 -1
  7. package/dist/cli.d.ts +20 -33
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +1391 -1321
  10. package/dist/cli.js.map +1 -1
  11. package/dist/configure.d.ts +12 -46
  12. package/dist/configure.d.ts.map +1 -1
  13. package/dist/configure.js +51 -192
  14. package/dist/configure.js.map +1 -1
  15. package/dist/credentials.d.ts +73 -33
  16. package/dist/credentials.d.ts.map +1 -1
  17. package/dist/credentials.js +167 -43
  18. package/dist/credentials.js.map +1 -1
  19. package/dist/discover.d.ts +8 -35
  20. package/dist/discover.d.ts.map +1 -1
  21. package/dist/discover.js +42 -38
  22. package/dist/discover.js.map +1 -1
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +4 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/import.d.ts +3 -0
  28. package/dist/import.d.ts.map +1 -1
  29. package/dist/import.js +3 -0
  30. package/dist/import.js.map +1 -1
  31. package/dist/index.d.ts +10 -8
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +6 -5
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/index.d.ts +18 -18
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +402 -231
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp-server.d.ts +5 -1
  40. package/dist/mcp-server.d.ts.map +1 -1
  41. package/dist/mcp-server.js +27 -105
  42. package/dist/mcp-server.js.map +1 -1
  43. package/dist/oauth.d.ts +31 -33
  44. package/dist/oauth.d.ts.map +1 -1
  45. package/dist/oauth.js +178 -125
  46. package/dist/oauth.js.map +1 -1
  47. package/dist/profiles.d.ts +243 -0
  48. package/dist/profiles.d.ts.map +1 -0
  49. package/dist/profiles.js +465 -0
  50. package/dist/profiles.js.map +1 -0
  51. package/dist/project.d.ts +8 -39
  52. package/dist/project.d.ts.map +1 -1
  53. package/dist/project.js +36 -94
  54. package/dist/project.js.map +1 -1
  55. package/dist/remote.d.ts +24 -17
  56. package/dist/remote.d.ts.map +1 -1
  57. package/dist/remote.js +54 -52
  58. package/dist/remote.js.map +1 -1
  59. package/dist/resolvers.d.ts +47 -0
  60. package/dist/resolvers.d.ts.map +1 -0
  61. package/dist/resolvers.js +255 -0
  62. package/dist/resolvers.js.map +1 -0
  63. package/dist/service.d.ts +3 -2
  64. package/dist/service.d.ts.map +1 -1
  65. package/dist/skill-install.d.ts +4 -6
  66. package/dist/skill-install.d.ts.map +1 -1
  67. package/dist/skill-install.js +13 -12
  68. package/dist/skill-install.js.map +1 -1
  69. package/dist/types.d.ts +45 -15
  70. package/dist/types.d.ts.map +1 -1
  71. package/docs/cli.md +173 -196
  72. package/docs/mcp.md +69 -65
  73. package/package.json +1 -1
  74. package/skills/synomem/SKILL.md +29 -26
  75. package/skills/synomem/references/examples.md +13 -11
  76. package/src/backend.ts +66 -64
  77. package/src/cli.ts +2131 -2222
  78. package/src/configure.ts +62 -241
  79. package/src/credentials.ts +208 -84
  80. package/src/discover.ts +53 -59
  81. package/src/errors.ts +4 -0
  82. package/src/import.ts +5 -0
  83. package/src/index.ts +15 -19
  84. package/src/mcp/index.ts +473 -277
  85. package/src/mcp-server.ts +32 -114
  86. package/src/oauth.ts +229 -130
  87. package/src/profiles.ts +644 -0
  88. package/src/project.ts +42 -108
  89. package/src/remote.ts +69 -63
  90. package/src/resolvers.ts +299 -0
  91. package/src/service.ts +2 -7
  92. package/src/skill-install.ts +17 -18
  93. package/src/types.ts +40 -15
package/src/project.ts CHANGED
@@ -1,60 +1,56 @@
1
1
  /**
2
- * Per-project workspace selection.
2
+ * Per-project profile selection.
3
3
  *
4
- * The goal is that opening an agent in a repository is enough: every note,
5
- * memo, task, todo and post it writes lands in that repository's workspace,
6
- * with nobody naming the workspace again for the rest of the session.
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
- * That cannot be a command typed into a running session. The stdio MCP server
9
- * is bound to a home and an actor when the harness launches it, so a later
10
- * instruction has nothing to retarget. It also should not be one global
11
- * "current workspace": two agents open in two repositories would fight over
12
- * it, which is exactly the case this exists to serve.
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
- export const PROJECT_CONFIG_FILE = 'config.json';
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.object({
38
- /** The local workspace this project's records belong in. */
39
- workspace: workspaceNameSchema.optional(),
40
- /** The agent this project's records are written by, when it is always one. */
41
- actor: z.string().trim().min(1).max(63).optional(),
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 directory whose `.synomem` was used, so tools can say where it came from. */
48
- directory: string;
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 configuration at or above `from`.
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 candidate = join(directory, PROJECT_DIRECTORY, PROJECT_CONFIG_FILE);
67
- // `<home>/config.json` is the home's own config, not a project pointer, and
68
- // the home is never a project directory.
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), directory };
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: ${parsed.error.issues[0]?.message ?? 'unknown problem'}`,
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 { asSynomemError, errorCodes, SynomemError, type SynomemErrorCode } from './errors.js';
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
- expectedActor: ActorIdentity;
39
- credentialProvider: SynomemCredentialProvider;
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
- * False when `expectedActor` is only a historical CLI fallback guess (a
46
- * caller named no real identity via `--actor`, `SYNOMEM_ACTOR_ID`, or a
47
- * project binding), not an identity the caller actually asserted. Defaults
48
- * to true, so every existing caller keeps its current strict behavior.
49
- * `kind: 'system'` already bypasses the match unconditionally (its own
50
- * historical bootstrap placeholder); this is the same bypass for a
51
- * fallback of any other kind, without pretending that fallback IS a real
52
- * identity.
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
- // Not `readonly`: `init()` resolves it from the server's response when the
162
- // caller didn't assert a real identity up front (see `assertedRealActor`).
163
- actor: ActorIdentity;
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 credentialProvider: SynomemCredentialProvider;
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
- try {
545
- this.actor = actorSchema.parse(options.expectedActor);
546
- } catch (error) {
547
- throw asSynomemError(error);
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.credentialProvider = options.credentialProvider;
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
- // `kind: 'system'` is this client's own placeholder for "no actor
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
- 'AUTH_FORBIDDEN',
596
- 'The authenticated Synomem actor does not match the configured actor.',
601
+ 'CONTEXT_FORBIDDEN',
602
+ 'The selected Synomem context does not belong to the workspace this service addresses.',
597
603
  );
598
604
  }
599
- if (!assertedRealActor) this.actor = binding.actor;
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.credentialProvider.getAccessToken(this.signal);
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
- * Harmless for an OAuth actor token (its workspace and identity are
731
- * already bound into the token itself), and required for an access
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 code =
769
- response.status === 401
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'