@youkno/edge-cli 1.21.339 → 1.21.341

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/README.md CHANGED
@@ -52,6 +52,8 @@ Login creates **two** sessions from the one sign-in: a shell session that drives
52
52
 
53
53
  Prefer the API-only token anywhere a token leaves the CLI (Postman, scripts, a colleague's terminal): a `--scope shell` token can run any shell command your account is allowed to, against whatever environment it was issued for. If you logged in before this existed, `edge-cli token` will tell you to log in again rather than hand back the shell-capable one.
54
54
 
55
+ To sign in as a member of staff of a particular company, pass its client ID (pin): `edge-cli --client 101 login`. Both sessions are then exchanged for your row in that company, which is where roles such as shell access live once staff accounts are split per company. Without `--client` you sign in as yourself, as before. Only the flag on the `login` command counts. `CLIENT_ID` in `.edge-api` does not, and the REPL's `/login` ignores `--client`: it re-authenticates whatever identity the current session already is, keeping its company when the stored login is `email#clientId`. So a `--client` given for shop context never switches identity. The company session is stored beside your own login as `email#clientId` (for example `ops@example.com#101`), never over it, and becomes the default. `edge-cli login:use <email>` switches back to yourself. A stored session keeps the identity it was created with, so to change company, log in again with a different `--client`.
56
+
55
57
  ```bash
56
58
  edge-cli login:list # see saved accounts
57
59
  edge-cli login:use <email> # switch default account
@@ -105,7 +107,7 @@ edge-cli product:default --clear
105
107
  | `--product <name>` | Override product (default from `~/.edge-api`) |
106
108
  | `--env <env>` | `prod`, `devel`, or `devlocal` (default: `prod`) |
107
109
  | `--base-url <url>` | Override API URL |
108
- | `--client <clientId>` | Client ID for client-scoped operations |
110
+ | `--client <clientId>` | Client ID for client-scoped operations; with `login`, the company to sign in as staff of |
109
111
  | `--config-path <path>` | Additional config file path |
110
112
 
111
113
  ## Help
@@ -13,7 +13,8 @@ function registerLogin(program) {
13
13
  const parent = this.parent;
14
14
  const opts = (parent?.opts() ?? {});
15
15
  const cfg = await (0, config_1.resolveEffectiveConfig)(opts);
16
- const email = await (0, auth_1.login)(cfg);
16
+ // --client selects the company to sign in as, here and only here: the explicit login.
17
+ const email = await (0, auth_1.login)({ ...cfg, exchangeClientId: opts.clientId?.trim() || undefined });
17
18
  process.stdout.write(`Successfully authenticated as ${email}\n`);
18
19
  if (this.opts().shell) {
19
20
  await (0, shell_1.repl)(cfg, {});
@@ -110,7 +110,9 @@ const REPL_COMMANDS = new Map([
110
110
  // since resolveAccessToken re-reads the token store on every command, the very next line
111
111
  // picks up the new session with nothing to invalidate.
112
112
  run: async (cfg) => {
113
- const email = await (0, auth_1.login)(cfg);
113
+ // Re-authenticates the identity the session already is: a company login (`email#101`)
114
+ // stays that company's. Taken from the stored default, never from a shop-context --client.
115
+ const email = await (0, auth_1.login)({ ...cfg, exchangeClientId: (0, auth_1.defaultLoginClientId)(cfg) });
114
116
  process.stdout.write(`Successfully authenticated as ${email}\n`);
115
117
  }
116
118
  }],
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ program
18
18
  .option("-p, --product <product>", "Product key (youkno, stash, alleaves, ...)")
19
19
  .option("--env <env>", "Environment (devlocal|devel|prod)", "prod")
20
20
  .option("--base-url <url>", "Override API base URL")
21
- .option("--client <clientId>", "Client ID")
21
+ .option("--client <clientId>", "Client ID (with login: the company to sign in as staff of)")
22
22
  .option("--config-path <path>", "Additional edge-api style config file")
23
23
  .hook("preAction", (command) => {
24
24
  const env = command.opts().env;
package/dist/lib/auth.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ShellAuthMethodError = void 0;
7
7
  exports.listLogins = listLogins;
8
8
  exports.setDefaultLogin = setDefaultLogin;
9
+ exports.defaultLoginClientId = defaultLoginClientId;
9
10
  exports.logout = logout;
10
11
  exports.login = login;
11
12
  exports.resolveAccessToken = resolveAccessToken;
@@ -115,6 +116,24 @@ function setDefaultLogin(cfg, email) {
115
116
  scope.default = email;
116
117
  writeDb(db);
117
118
  }
119
+ /**
120
+ * The key a login is stored under: the email for the person's own session, `email#clientId` for a
121
+ * session exchanged into a company with `--client`, so one never overwrites the other.
122
+ */
123
+ function accountKeyFor(email, clientId) {
124
+ return clientId ? `${email}#${clientId}` : email;
125
+ }
126
+ /**
127
+ * The client ID the default stored login was exchanged into, from its `email#clientId` key, or
128
+ * undefined for the person's own login. Lets the REPL's `/login` re-authenticate the same identity
129
+ * it is running as -- taken from the stored session, never from a `--client` given for shop context.
130
+ */
131
+ function defaultLoginClientId(cfg) {
132
+ const key = getScope(readDb(), cfg).default ?? "";
133
+ const hash = key.lastIndexOf("#");
134
+ const clientId = hash > 0 ? key.slice(hash + 1) : "";
135
+ return /^[A-Za-z0-9_-]+$/.test(clientId) ? clientId : undefined;
136
+ }
118
137
  function authStateHeader(cfg) {
119
138
  return `${cfg.product}.${cfg.env}`;
120
139
  }
@@ -221,6 +240,36 @@ function asShellAuthMethodError(err, hint) {
221
240
  const detail = err.problem.detail ?? err.problem.title ?? "Shell access requires a different sign-in method.";
222
241
  return new ShellAuthMethodError(`${detail}${hint}`);
223
242
  }
243
+ /**
244
+ * The server's `errorCode`s for an exchange whose subject cannot be selected. Mirror
245
+ * `ApiErrorCode.NOT_A_MEMBER`, `VULGUS_NOT_SELECTABLE` and `VIRTUAL_CREDENTIAL_REFUSED`.
246
+ */
247
+ const ERROR_CODE_NOT_A_MEMBER = 403012;
248
+ const ERROR_CODE_VULGUS_NOT_SELECTABLE = 400006;
249
+ const ERROR_CODE_VIRTUAL_CREDENTIAL_REFUSED = 400007;
250
+ /**
251
+ * A `--client` refusal as an error that says what to do, or undefined when this failure is
252
+ * something else. Matched on errorCode, never on the prose.
253
+ */
254
+ function asCompanySelectionError(err, cfg) {
255
+ if (!(err instanceof AuthEndpointError)) {
256
+ return undefined;
257
+ }
258
+ switch (err.problem?.errorCode) {
259
+ case ERROR_CODE_NOT_A_MEMBER:
260
+ return new Error(`Login failed: you are not a member of client ${cfg.exchangeClientId} ` +
261
+ `on ${cfg.product}. Check --client, or omit it to sign in as yourself.`, { cause: err });
262
+ case ERROR_CODE_VULGUS_NOT_SELECTABLE:
263
+ return new Error(`Login failed: --client ${cfg.exchangeClientId} is the Vulgus company, ` +
264
+ `which is your own account. Omit --client.`, { cause: err });
265
+ case ERROR_CODE_VIRTUAL_CREDENTIAL_REFUSED:
266
+ return new Error("Login failed: the console handed over a company session's Firebase token. " +
267
+ "It must hand over the person's own Firebase token, not the company session's; " +
268
+ "sign out of the console, sign in again, and retry.", { cause: err });
269
+ default:
270
+ return undefined;
271
+ }
272
+ }
224
273
  async function requestTokenEndpoint(cfg, endpoint, payload) {
225
274
  const url = `${cfg.baseUrl.replace(/\/$/, "")}/api/v1/auth/${endpoint}`;
226
275
  const response = await fetch(url, {
@@ -240,13 +289,24 @@ async function requestTokenEndpoint(cfg, endpoint, payload) {
240
289
  }
241
290
  return (await response.json());
242
291
  }
243
- async function authExchangeFirebaseToken(cfg, firebaseToken, scope = EXCHANGE_SCOPE_SHELL) {
244
- const resp = await requestTokenEndpoint(cfg, "exchange", {
292
+ /**
293
+ * The body of a token exchange. `clientId` is sent only when `--client` was given, so the server
294
+ * makes the person's row in that company the subject (after the virtual-users migration, staff of
295
+ * the Operations company hold SHELL_ACCESS there, not on their own row). Absent, the exchange is
296
+ * exactly what it was before the field existed. Like `scope`, it is sent on exchange only: a refresh
297
+ * re-mints whatever subject the session was created for.
298
+ */
299
+ function buildExchangePayload(cfg, firebaseToken, scope, deviceId) {
300
+ return {
245
301
  firebaseToken,
246
- deviceId: authDeviceId(),
302
+ deviceId,
247
303
  deviceName: DEFAULT_DEVICE_NAME,
248
- ...(scope === null ? {} : { scope })
249
- });
304
+ ...(scope === null ? {} : { scope }),
305
+ ...(cfg.exchangeClientId ? { clientId: cfg.exchangeClientId } : {})
306
+ };
307
+ }
308
+ async function authExchangeFirebaseToken(cfg, firebaseToken, scope = EXCHANGE_SCOPE_SHELL) {
309
+ const resp = await requestTokenEndpoint(cfg, "exchange", buildExchangePayload(cfg, firebaseToken, scope, authDeviceId()));
250
310
  if (!resp?.accessToken) {
251
311
  throw new Error("Token exchange did not return accessToken");
252
312
  }
@@ -293,6 +353,10 @@ async function exchangeCallbackToken(cfg, callbackToken) {
293
353
  if (refusal) {
294
354
  throw refusal;
295
355
  }
356
+ const companyRefusal = asCompanySelectionError(err, cfg);
357
+ if (companyRefusal) {
358
+ throw companyRefusal;
359
+ }
296
360
  const reason = err instanceof Error ? err.message : String(err);
297
361
  // The identity hint only fits a 401. On a network error or a 5xx, telling someone to check
298
362
  // which account they used is misdirection -- the same species of misleading report this
@@ -530,10 +594,17 @@ async function login(cfg) {
530
594
  let firebaseToken;
531
595
  if (parsed.firebaseToken) {
532
596
  firebaseToken = parsed.firebaseToken;
533
- tokenResp = await authExchangeFirebaseToken(cfg, firebaseToken);
597
+ tokenResp = await exchangeCallbackToken(cfg, firebaseToken);
534
598
  }
535
599
  else if (parsed.accessToken && parsed.refreshToken) {
536
- // Already a first-party pair; nothing to exchange.
600
+ // Already a first-party pair; nothing to exchange -- and so nothing to apply --client to.
601
+ // Refused rather than stored: the pair is for whichever identity the console chose, and
602
+ // saving it under a login that asked for a company would sign in as the wrong one.
603
+ if (cfg.exchangeClientId) {
604
+ throw new Error(`Login failed: --client ${cfg.exchangeClientId} could not be applied because the ` +
605
+ "console returned a finished session instead of a Firebase token to exchange. " +
606
+ "Log in without --client, or from a console that returns a Firebase token.");
607
+ }
537
608
  tokenResp = {
538
609
  accessToken: parsed.accessToken,
539
610
  refreshToken: parsed.refreshToken,
@@ -550,8 +621,11 @@ async function login(cfg) {
550
621
  throw new Error("Authentication callback did not include usable token data.");
551
622
  }
552
623
  const email = parsed.email ?? extractEmailFromToken(tokenResp.accessToken);
553
- storeTokens(cfg, email, tokenResp, await exchangeApiSession(cfg, firebaseToken));
554
- return email;
624
+ // A company session is stored beside the person's own, never over it: `email#clientId`. It
625
+ // becomes the default like any fresh login; `login:use <email>` switches back.
626
+ const accountKey = accountKeyFor(email, cfg.exchangeClientId);
627
+ storeTokens(cfg, accountKey, tokenResp, await exchangeApiSession(cfg, firebaseToken));
628
+ return accountKey;
555
629
  }
556
630
  /**
557
631
  * Exchange the same Firebase token a second time for an API-only session.
@@ -578,7 +652,8 @@ async function exchangeApiSession(cfg, firebaseToken) {
578
652
  return resp;
579
653
  }
580
654
  catch (err) {
581
- const reason = err instanceof Error ? err.message : String(err);
655
+ const reason = asCompanySelectionError(err, cfg)?.message
656
+ ?? (err instanceof Error ? err.message : String(err));
582
657
  process.stderr.write(`Warning: could not create an API-only session (${reason}).\n` +
583
658
  `'edge-cli token' will need --scope shell until the next successful login.\n`);
584
659
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youkno/edge-cli",
3
- "version": "1.21.339",
3
+ "version": "1.21.341",
4
4
  "description": "Cross-platform Edge CLI",
5
5
  "bin": "dist/index.js",
6
6
  "publishConfig": {
@@ -14,7 +14,8 @@ export function registerLogin(program: Command): void {
14
14
  const parent = this.parent;
15
15
  const opts = (parent?.opts() ?? {}) as CliOptions;
16
16
  const cfg = await resolveEffectiveConfig(opts);
17
- const email = await login(cfg);
17
+ // --client selects the company to sign in as, here and only here: the explicit login.
18
+ const email = await login({ ...cfg, exchangeClientId: opts.clientId?.trim() || undefined });
18
19
  process.stdout.write(`Successfully authenticated as ${email}\n`);
19
20
  if (this.opts<{ shell?: boolean }>().shell) {
20
21
  await repl(cfg, {});
@@ -4,7 +4,7 @@ import readline from "node:readline";
4
4
 
5
5
  import { Command } from "commander";
6
6
 
7
- import { login, resolveAuthHeader, resolveAuthHeaderFast } from "../lib/auth";
7
+ import { defaultLoginClientId, login, resolveAuthHeader, resolveAuthHeaderFast } from "../lib/auth";
8
8
  import { resolveEffectiveConfig } from "../lib/config";
9
9
  import { baseHeaders, checkedText } from "../lib/request";
10
10
  import { CliOptions, EffectiveConfig } from "../lib/types";
@@ -154,7 +154,9 @@ const REPL_COMMANDS = new Map<string, ReplCommand>([
154
154
  // since resolveAccessToken re-reads the token store on every command, the very next line
155
155
  // picks up the new session with nothing to invalidate.
156
156
  run: async (cfg) => {
157
- const email = await login(cfg);
157
+ // Re-authenticates the identity the session already is: a company login (`email#101`)
158
+ // stays that company's. Taken from the stored default, never from a shop-context --client.
159
+ const email = await login({ ...cfg, exchangeClientId: defaultLoginClientId(cfg) });
158
160
  process.stdout.write(`Successfully authenticated as ${email}\n`);
159
161
  }
160
162
  }],
package/src/index.ts CHANGED
@@ -21,7 +21,7 @@ program
21
21
  .option("-p, --product <product>", "Product key (youkno, stash, alleaves, ...)")
22
22
  .option("--env <env>", "Environment (devlocal|devel|prod)", "prod")
23
23
  .option("--base-url <url>", "Override API base URL")
24
- .option("--client <clientId>", "Client ID")
24
+ .option("--client <clientId>", "Client ID (with login: the company to sign in as staff of)")
25
25
  .option("--config-path <path>", "Additional edge-api style config file")
26
26
  .hook("preAction", (command) => {
27
27
  const env = command.opts().env as string;
package/src/lib/auth.ts CHANGED
@@ -153,6 +153,26 @@ export function setDefaultLogin(cfg: EffectiveConfig, email: string): void {
153
153
  writeDb(db);
154
154
  }
155
155
 
156
+ /**
157
+ * The key a login is stored under: the email for the person's own session, `email#clientId` for a
158
+ * session exchanged into a company with `--client`, so one never overwrites the other.
159
+ */
160
+ function accountKeyFor(email: string, clientId: string | undefined): string {
161
+ return clientId ? `${email}#${clientId}` : email;
162
+ }
163
+
164
+ /**
165
+ * The client ID the default stored login was exchanged into, from its `email#clientId` key, or
166
+ * undefined for the person's own login. Lets the REPL's `/login` re-authenticate the same identity
167
+ * it is running as -- taken from the stored session, never from a `--client` given for shop context.
168
+ */
169
+ export function defaultLoginClientId(cfg: EffectiveConfig): string | undefined {
170
+ const key = getScope(readDb(), cfg).default ?? "";
171
+ const hash = key.lastIndexOf("#");
172
+ const clientId = hash > 0 ? key.slice(hash + 1) : "";
173
+ return /^[A-Za-z0-9_-]+$/.test(clientId) ? clientId : undefined;
174
+ }
175
+
156
176
  function authStateHeader(cfg: EffectiveConfig): string {
157
177
  return `${cfg.product}.${cfg.env}`;
158
178
  }
@@ -273,6 +293,38 @@ function asShellAuthMethodError(err: unknown, hint: string): ShellAuthMethodErro
273
293
  return new ShellAuthMethodError(`${detail}${hint}`);
274
294
  }
275
295
 
296
+ /**
297
+ * The server's `errorCode`s for an exchange whose subject cannot be selected. Mirror
298
+ * `ApiErrorCode.NOT_A_MEMBER`, `VULGUS_NOT_SELECTABLE` and `VIRTUAL_CREDENTIAL_REFUSED`.
299
+ */
300
+ const ERROR_CODE_NOT_A_MEMBER = 403012;
301
+ const ERROR_CODE_VULGUS_NOT_SELECTABLE = 400006;
302
+ const ERROR_CODE_VIRTUAL_CREDENTIAL_REFUSED = 400007;
303
+
304
+ /**
305
+ * A `--client` refusal as an error that says what to do, or undefined when this failure is
306
+ * something else. Matched on errorCode, never on the prose.
307
+ */
308
+ function asCompanySelectionError(err: unknown, cfg: EffectiveConfig): Error | undefined {
309
+ if (!(err instanceof AuthEndpointError)) {
310
+ return undefined;
311
+ }
312
+ switch (err.problem?.errorCode) {
313
+ case ERROR_CODE_NOT_A_MEMBER:
314
+ return new Error(`Login failed: you are not a member of client ${cfg.exchangeClientId} ` +
315
+ `on ${cfg.product}. Check --client, or omit it to sign in as yourself.`, { cause: err });
316
+ case ERROR_CODE_VULGUS_NOT_SELECTABLE:
317
+ return new Error(`Login failed: --client ${cfg.exchangeClientId} is the Vulgus company, ` +
318
+ `which is your own account. Omit --client.`, { cause: err });
319
+ case ERROR_CODE_VIRTUAL_CREDENTIAL_REFUSED:
320
+ return new Error("Login failed: the console handed over a company session's Firebase token. " +
321
+ "It must hand over the person's own Firebase token, not the company session's; " +
322
+ "sign out of the console, sign in again, and retry.", { cause: err });
323
+ default:
324
+ return undefined;
325
+ }
326
+ }
327
+
276
328
  async function requestTokenEndpoint(
277
329
  cfg: EffectiveConfig,
278
330
  endpoint: "exchange" | "refresh" | "logout",
@@ -304,17 +356,35 @@ async function requestTokenEndpoint(
304
356
  return (await response.json()) as TokenResponse;
305
357
  }
306
358
 
359
+ /**
360
+ * The body of a token exchange. `clientId` is sent only when `--client` was given, so the server
361
+ * makes the person's row in that company the subject (after the virtual-users migration, staff of
362
+ * the Operations company hold SHELL_ACCESS there, not on their own row). Absent, the exchange is
363
+ * exactly what it was before the field existed. Like `scope`, it is sent on exchange only: a refresh
364
+ * re-mints whatever subject the session was created for.
365
+ */
366
+ function buildExchangePayload(
367
+ cfg: Pick<EffectiveConfig, "exchangeClientId">,
368
+ firebaseToken: string,
369
+ scope: string | null,
370
+ deviceId: string
371
+ ): Record<string, unknown> {
372
+ return {
373
+ firebaseToken,
374
+ deviceId,
375
+ deviceName: DEFAULT_DEVICE_NAME,
376
+ ...(scope === null ? {} : { scope }),
377
+ ...(cfg.exchangeClientId ? { clientId: cfg.exchangeClientId } : {})
378
+ };
379
+ }
380
+
307
381
  async function authExchangeFirebaseToken(
308
382
  cfg: EffectiveConfig,
309
383
  firebaseToken: string,
310
384
  scope: string | null = EXCHANGE_SCOPE_SHELL
311
385
  ): Promise<TokenResponse> {
312
- const resp = await requestTokenEndpoint(cfg, "exchange", {
313
- firebaseToken,
314
- deviceId: authDeviceId(),
315
- deviceName: DEFAULT_DEVICE_NAME,
316
- ...(scope === null ? {} : { scope })
317
- });
386
+ const resp = await requestTokenEndpoint(cfg, "exchange",
387
+ buildExchangePayload(cfg, firebaseToken, scope, authDeviceId()));
318
388
  if (!resp?.accessToken) {
319
389
  throw new Error("Token exchange did not return accessToken");
320
390
  }
@@ -363,6 +433,10 @@ async function exchangeCallbackToken(cfg: EffectiveConfig, callbackToken: string
363
433
  if (refusal) {
364
434
  throw refusal;
365
435
  }
436
+ const companyRefusal = asCompanySelectionError(err, cfg);
437
+ if (companyRefusal) {
438
+ throw companyRefusal;
439
+ }
366
440
 
367
441
  const reason = err instanceof Error ? err.message : String(err);
368
442
  // The identity hint only fits a 401. On a network error or a 5xx, telling someone to check
@@ -635,9 +709,16 @@ export async function login(cfg: EffectiveConfig): Promise<string> {
635
709
  let firebaseToken: string | undefined;
636
710
  if (parsed.firebaseToken) {
637
711
  firebaseToken = parsed.firebaseToken;
638
- tokenResp = await authExchangeFirebaseToken(cfg, firebaseToken);
712
+ tokenResp = await exchangeCallbackToken(cfg, firebaseToken);
639
713
  } else if (parsed.accessToken && parsed.refreshToken) {
640
- // Already a first-party pair; nothing to exchange.
714
+ // Already a first-party pair; nothing to exchange -- and so nothing to apply --client to.
715
+ // Refused rather than stored: the pair is for whichever identity the console chose, and
716
+ // saving it under a login that asked for a company would sign in as the wrong one.
717
+ if (cfg.exchangeClientId) {
718
+ throw new Error(`Login failed: --client ${cfg.exchangeClientId} could not be applied because the ` +
719
+ "console returned a finished session instead of a Firebase token to exchange. " +
720
+ "Log in without --client, or from a console that returns a Firebase token.");
721
+ }
641
722
  tokenResp = {
642
723
  accessToken: parsed.accessToken,
643
724
  refreshToken: parsed.refreshToken,
@@ -655,8 +736,11 @@ export async function login(cfg: EffectiveConfig): Promise<string> {
655
736
  }
656
737
 
657
738
  const email = parsed.email ?? extractEmailFromToken(tokenResp.accessToken);
658
- storeTokens(cfg, email, tokenResp, await exchangeApiSession(cfg, firebaseToken));
659
- return email;
739
+ // A company session is stored beside the person's own, never over it: `email#clientId`. It
740
+ // becomes the default like any fresh login; `login:use <email>` switches back.
741
+ const accountKey = accountKeyFor(email, cfg.exchangeClientId);
742
+ storeTokens(cfg, accountKey, tokenResp, await exchangeApiSession(cfg, firebaseToken));
743
+ return accountKey;
660
744
  }
661
745
 
662
746
  /**
@@ -688,7 +772,8 @@ async function exchangeApiSession(
688
772
  }
689
773
  return resp;
690
774
  } catch (err) {
691
- const reason = err instanceof Error ? err.message : String(err);
775
+ const reason = asCompanySelectionError(err, cfg)?.message
776
+ ?? (err instanceof Error ? err.message : String(err));
692
777
  process.stderr.write(
693
778
  `Warning: could not create an API-only session (${reason}).\n` +
694
779
  `'edge-cli token' will need --scope shell until the next successful login.\n`
package/src/lib/types.ts CHANGED
@@ -29,5 +29,14 @@ export type EffectiveConfig = {
29
29
  baseUrl: string;
30
30
  consoleBaseUrl?: string;
31
31
  clientId?: string;
32
+ /**
33
+ * The company to sign in as a member of staff of, sent as `clientId` on the token exchange.
34
+ * Never set by `resolveEffectiveConfig`: only the explicit `login` command fills it, from an
35
+ * explicit `--client`. Not from `CLIENT_ID` in a config file (that exists for client-scoped
36
+ * commands such as `upload-users`), and not for the REPL's `/login`, so a `--client` given for
37
+ * shop context never switches which identity a session signs in as. Unset means the person's
38
+ * own row, as before.
39
+ */
40
+ exchangeClientId?: string;
32
41
  configFiles: string[];
33
42
  };