requestshield 0.1.6 → 0.1.8

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 (36) hide show
  1. package/README.md +177 -68
  2. package/config/.env.prod +1 -0
  3. package/package.json +1 -1
  4. package/skills/requestshield/SKILL.md +20 -17
  5. package/skills/requestshield/assets/AGENTS.codex.md +5 -3
  6. package/skills/requestshield/references/backend-java-core.md +3 -3
  7. package/skills/requestshield/references/backend-spring-boot.md +3 -3
  8. package/skills/requestshield/references/browser-manual.md +1 -1
  9. package/skills/requestshield/references/cli.md +92 -28
  10. package/skills/requestshield/references/integration-planning.md +10 -8
  11. package/skills/requestshield/references/troubleshooting.md +2 -2
  12. package/src/api-client.mjs +1 -1
  13. package/src/args.mjs +98 -106
  14. package/src/cli.mjs +69 -243
  15. package/src/command-registry.mjs +97 -0
  16. package/src/commands/agent-setup.mjs +24 -17
  17. package/src/commands/agent-status.mjs +60 -0
  18. package/src/commands/application-mutations.mjs +8 -4
  19. package/src/commands/apps-get.mjs +12 -4
  20. package/src/commands/apps-list.mjs +20 -13
  21. package/src/commands/contract.mjs +25 -0
  22. package/src/commands/keys-create.mjs +5 -2
  23. package/src/commands/mutation-support.mjs +26 -12
  24. package/src/commands/output.mjs +21 -0
  25. package/src/commands/secret-commands.mjs +13 -6
  26. package/src/commands/signin.mjs +17 -8
  27. package/src/commands/signout.mjs +7 -3
  28. package/src/commands/update-check.mjs +97 -43
  29. package/src/config.mjs +29 -3
  30. package/src/entrypoint.mjs +20 -13
  31. package/src/integration-contract-client.mjs +81 -0
  32. package/src/integration-contract.mjs +104 -0
  33. package/src/oauth-client.mjs +2 -2
  34. package/src/oauth-loopback.mjs +1 -1
  35. package/src/session-files.mjs +4 -4
  36. package/src/session-store.mjs +2 -2
package/src/config.mjs CHANGED
@@ -7,7 +7,7 @@ import { CliError } from "./errors.mjs";
7
7
 
8
8
  /** @typedef {'qat' | 'stg' | 'prod'} Profile */
9
9
  const PROFILES = new Set(["qat", "stg", "prod"]);
10
- const CONFIG_KEYS = new Set(["API_URL", "OAUTH_ISSUER", "OAUTH_CLIENT_ID", "OAUTH_AUTHORIZATION_ISSUER"]);
10
+ const CONFIG_KEYS = new Set(["API_URL", "DOCS_URL", "OAUTH_ISSUER", "OAUTH_CLIENT_ID", "OAUTH_AUTHORIZATION_ISSUER"]);
11
11
 
12
12
  export const DEFAULT_MAVEN_REPOSITORY =
13
13
  "https://sdk.intellifend.com/packages/maven";
@@ -73,6 +73,32 @@ export function getApiUrl(profile = "prod") {
73
73
  return configuredApiUrl(readProfileConfig(profile), profile);
74
74
  }
75
75
 
76
+ /** Public documentation selection is independent of API and OAuth settings.
77
+ * @param {Profile} [profile]
78
+ */
79
+ export function getDocsUrl(profile = "prod") {
80
+ let values;
81
+ try { values = readProfileConfig(profile); }
82
+ catch (error) {
83
+ throw new CliError(error instanceof CliError ? error.message : "Cannot read the RequestShield documentation configuration", {code: "CONTRACT_CONFIG_INVALID", exitCode: 2});
84
+ }
85
+ if (!values.DOCS_URL) {
86
+ throw new CliError(`Documentation is not configured for ${getCommandName(profile)}. Set DOCS_URL in config/.env.${profile}.`, {code: "CONTRACT_CONFIG_INVALID", exitCode: 2});
87
+ }
88
+ return validateDocsUrl(values.DOCS_URL);
89
+ }
90
+
91
+ /** @param {unknown} value */
92
+ export function validateDocsUrl(value) {
93
+ try {
94
+ if (typeof value !== "string" || !/^[\x21-\x7e]+$/.test(value) || /[\\?#]/.test(value)) throw new Error();
95
+ return validatedUrl(value, "DOCS_URL").replace(/\/+$/, "");
96
+ }
97
+ catch {
98
+ throw new CliError("DOCS_URL must be a valid HTTPS URL without credentials, query or fragment (HTTP is allowed only on loopback)", {code: "CONTRACT_CONFIG_INVALID", exitCode: 2});
99
+ }
100
+ }
101
+
76
102
  /** @param {Record<string, string | undefined>} values @param {Profile} profile */
77
103
  function configuredApiUrl(values, profile) {
78
104
  if (!values.API_URL) {
@@ -112,7 +138,7 @@ function readProfileConfig(profile) {
112
138
  throw invalidConfig(`Cannot read config/.env.${profile} for ${getCommandName(profile)}. Restore the package configuration file.`);
113
139
  }
114
140
  if (Object.keys(values).some(key => !CONFIG_KEYS.has(key))) {
115
- throw invalidConfig(`config/.env.${profile} accepts only API_URL, OAUTH_ISSUER, OAUTH_CLIENT_ID and OAUTH_AUTHORIZATION_ISSUER; do not store credentials in this file.`);
141
+ throw invalidConfig(`config/.env.${profile} accepts only API_URL, DOCS_URL, OAUTH_ISSUER, OAUTH_CLIENT_ID and OAUTH_AUTHORIZATION_ISSUER; do not store credentials in this file.`);
116
142
  }
117
143
  return values;
118
144
  }
@@ -120,7 +146,7 @@ function readProfileConfig(profile) {
120
146
  /** @param {unknown} value @returns {string} */
121
147
  export function validateBearerToken(value) {
122
148
  if (typeof value !== "string" || value.length > 16_384 || !/^[A-Za-z0-9._~+/-]+=*$/.test(value)) {
123
- throw new CliError("The access token is invalid; run `requestshield signin` again", {
149
+ throw new CliError("The access token is invalid; run `requestshield login` again", {
124
150
  code: "INVALID_ACCESS_TOKEN", exitCode: 3,
125
151
  });
126
152
  }
@@ -3,22 +3,29 @@ import { run } from "./cli.mjs";
3
3
  import { CliError } from "./errors.mjs";
4
4
  import { getCommandInvocation, getCommandName } from "./config.mjs";
5
5
 
6
- /** @param {import('./config.mjs').Profile} profile */
7
- export async function main(profile) {
8
- const command = getCommandName(profile);
9
- const invocation = getCommandInvocation(profile);
6
+ /** Render exactly one result document on JSON failures. Unknown failures are redacted.
7
+ * @param {string[]} argv @param {Parameters<typeof run>[1]} [deps] */
8
+ export async function execute(argv, deps = {}) {
9
+ const profile = deps.profile ?? "prod";
10
10
  try {
11
- await run(process.argv.slice(2), {profile});
11
+ await run(argv, deps);
12
+ return 0;
12
13
  } catch (error) {
13
- const message = error instanceof Error ? error.message : String(error);
14
- // Rewrite only fixed CLI guidance on stderr. Successful JSON and app names
15
- // pass through untouched, including names containing "requestshield".
16
- const guidance = message
17
- .replaceAll("`requestshield signin`", `\`${invocation} signin\``)
18
- .replaceAll("`requestshield signout`", `\`${invocation} signout\``)
14
+ const known = error instanceof CliError;
15
+ const invocation = getCommandInvocation(profile);
16
+ const message = (known ? error.message : "The command failed unexpectedly. Please try again.")
17
+ .replaceAll("`requestshield login`", `\`${invocation} login\``)
18
+ .replaceAll("`requestshield logout`", `\`${invocation} logout\``)
19
19
  .replaceAll("Usage: requestshield ", `Usage: ${invocation} `)
20
20
  .replace(/^ requestshield /gm, ` ${invocation} `);
21
- console.error(`${command}: ${guidance}`);
22
- process.exitCode = error instanceof CliError ? error.exitCode : 1;
21
+ const code = known ? error.code : "CLI_ERROR";
22
+ if (argv.includes("--json")) (deps.log ?? console.log)(JSON.stringify({error: {code, message}}, null, 2));
23
+ else (deps.warn ?? console.error)(`${getCommandName(profile)}: ${message}\nCode: ${code}`);
24
+ return known ? error.exitCode : 1;
23
25
  }
24
26
  }
27
+
28
+ /** @param {import('./config.mjs').Profile} profile */
29
+ export async function main(profile) {
30
+ process.exitCode = await execute(process.argv.slice(2), {profile});
31
+ }
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+ import { CliError } from "./errors.mjs";
3
+ import { validateDocsUrl } from "./config.mjs";
4
+ import { validateIntegrationContract } from "./integration-contract.mjs";
5
+
6
+ const MAX_BODY_BYTES = 1024 * 1024;
7
+
8
+ /** Fetch only public metadata. Never attach OAuth credentials or use Management APIs. */
9
+ export class IntegrationContractClient {
10
+ /** @param {{docsUrl: string, environment: import('./config.mjs').Profile, fetchImpl?: typeof fetch, timeoutMs?: number}} options */
11
+ constructor({docsUrl, environment, fetchImpl = fetch, timeoutMs = 15_000}) {
12
+ this.url = `${validateDocsUrl(docsUrl)}/integration-contract.json`;
13
+ this.environment = environment;
14
+ this.fetchImpl = fetchImpl;
15
+ this.timeoutMs = timeoutMs;
16
+ }
17
+
18
+ async getContract() {
19
+ const controller = new AbortController();
20
+ /** @type {ReadableStreamDefaultReader<Uint8Array> | undefined} */
21
+ let reader;
22
+ let expired = false;
23
+ /** @type {NodeJS.Timeout | undefined} */
24
+ let timer;
25
+ const deadline = new Promise((_, reject) => {
26
+ timer = setTimeout(() => {
27
+ expired = true;
28
+ controller.abort();
29
+ reject(timeout());
30
+ }, this.timeoutMs);
31
+ });
32
+ try {
33
+ const operation = (async () => {
34
+ const response = await this.fetchImpl(this.url, {
35
+ method: "GET", headers: {Accept: "application/json"},
36
+ credentials: "omit", redirect: "error", cache: "no-store", signal: controller.signal,
37
+ });
38
+ if (expired) { void response.body?.cancel().catch(() => {}); throw timeout(); }
39
+ if (!response.ok) {
40
+ void response.body?.cancel().catch(() => {});
41
+ throw new CliError(response.status === 404 ? "The public integration contract has not been published for this environment (HTTP 404)." : `The public integration contract request failed: HTTP ${response.status}.`, {code: response.status === 404 ? "CONTRACT_NOT_PUBLISHED" : "CONTRACT_FETCH_FAILED", exitCode: 1, httpStatus: response.status});
42
+ }
43
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
44
+ if (!mediaType || !/^application\/(?:json|[a-z0-9!#$&^_.+-]+\+json)$/.test(mediaType)
45
+ || Number(response.headers.get("content-length")) > MAX_BODY_BYTES) {
46
+ void response.body?.cancel().catch(() => {});
47
+ throw invalidResponse();
48
+ }
49
+ const chunks = [];
50
+ let size = 0;
51
+ if (response.body) {
52
+ reader = response.body.getReader();
53
+ while (true) {
54
+ const {done, value} = await reader.read();
55
+ if (expired) throw timeout();
56
+ if (done) break;
57
+ size += value.byteLength;
58
+ if (size > MAX_BODY_BYTES) throw invalidResponse();
59
+ chunks.push(value);
60
+ }
61
+ }
62
+ try {
63
+ const body = JSON.parse(new TextDecoder("utf-8", {fatal: true}).decode(Buffer.concat(chunks)));
64
+ return validateIntegrationContract(body, this.environment);
65
+ } catch { throw invalidResponse(); }
66
+ })();
67
+ return await Promise.race([operation, /** @type {Promise<never>} */ (deadline)]);
68
+ } catch (error) {
69
+ if (expired) throw timeout();
70
+ if (error instanceof CliError) throw error;
71
+ throw new CliError("Could not retrieve the public integration contract. Check connectivity and try again.", {code: "CONTRACT_FETCH_FAILED", exitCode: 1});
72
+ } finally {
73
+ clearTimeout(timer);
74
+ if (reader) void reader.cancel().catch(() => {});
75
+ controller.abort();
76
+ }
77
+ }
78
+ }
79
+
80
+ function timeout() { return new CliError("The public integration contract request timed out. Check connectivity and try again.", {code: "CONTRACT_FETCH_FAILED", exitCode: 1}); }
81
+ function invalidResponse() { return new CliError("The public integration contract is invalid, oversized or does not match this environment.", {code: "CONTRACT_INVALID", exitCode: 1}); }
@@ -0,0 +1,104 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {{groupId: string, artifactId: string}} MavenArtifact */
4
+ /** @typedef {{
5
+ * schemaVersion: 1, contractVersion: string, environment: 'qat' | 'stg' | 'prod',
6
+ * services: {challengeUrl: string},
7
+ * browser: {sdkVersion: string, scriptUrl: string, integrity: string,
8
+ * tokenHeader: 'X-IntelliFend-Token', availableModes: Array<'manual' | 'seamless'>,
9
+ * cspAdditions: {scriptSrc: string[], connectSrc: string[], workerSrc: string[]}},
10
+ * backend: {sdkVersion: string, minJdk: number, mavenRepository: string,
11
+ * core: MavenArtifact, springBoot3: MavenArtifact & {framework: 'Spring Boot 3 MVC'}},
12
+ * documentation: {browserSdk: string, javaSdk: string, domainsAndCsp: string}
13
+ * }} IntegrationContract */
14
+
15
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
16
+ const ENVIRONMENTS = new Set(['qat', 'stg', 'prod']);
17
+
18
+ /** Shared by the CLI reader and customer-docs build checks. This module performs
19
+ * no I/O and includes no release data; customer-docs owns the selected releases.
20
+ * Schema 1 rejects extra fields so unreviewed content never reaches CLI output.
21
+ * @param {unknown} value
22
+ * @param {string} expectedEnvironment
23
+ * @returns {{data: IntegrationContract}}
24
+ */
25
+ export function validateIntegrationContract(value, expectedEnvironment) {
26
+ const envelope = object(value, ['data']);
27
+ const data = object(envelope.data, ['schemaVersion', 'contractVersion', 'environment', 'services', 'browser', 'backend', 'documentation']);
28
+ check(data.schemaVersion === 1 && ENVIRONMENTS.has(expectedEnvironment) && data.environment === expectedEnvironment);
29
+ version(data.contractVersion);
30
+
31
+ const services = object(data.services, ['challengeUrl']);
32
+ const challenge = publicUrl(services.challengeUrl);
33
+ const browser = object(data.browser, ['sdkVersion', 'scriptUrl', 'integrity', 'tokenHeader', 'availableModes', 'cspAdditions']);
34
+ version(browser.sdkVersion);
35
+ const script = publicUrl(browser.scriptUrl);
36
+ check(script.pathname.endsWith(`/requestshield/v${browser.sdkVersion}/intellifend.js`));
37
+ check(typeof browser.integrity === 'string' && /^sha384-[A-Za-z0-9+/]{64}$/.test(browser.integrity));
38
+ check(browser.tokenHeader === 'X-IntelliFend-Token');
39
+ const modes = stringList(browser.availableModes, 2);
40
+ check(modes.includes('manual') && modes.every(mode => mode === 'manual' || mode === 'seamless'));
41
+
42
+ const csp = object(browser.cspAdditions, ['scriptSrc', 'connectSrc', 'workerSrc']);
43
+ const scriptSources = origins(csp.scriptSrc);
44
+ const connectSources = origins(csp.connectSrc);
45
+ check(scriptSources.includes(script.origin) && connectSources.includes(challenge.origin));
46
+ const workerSources = stringList(csp.workerSrc, 1);
47
+ check(workerSources.length === 1 && workerSources[0] === 'blob:');
48
+
49
+ const backend = object(data.backend, ['sdkVersion', 'minJdk', 'mavenRepository', 'core', 'springBoot3']);
50
+ version(backend.sdkVersion);
51
+ check(typeof backend.minJdk === 'number' && Number.isSafeInteger(backend.minJdk) && backend.minJdk >= 17 && backend.minJdk <= 100);
52
+ publicUrl(backend.mavenRepository);
53
+ const core = object(backend.core, ['groupId', 'artifactId']);
54
+ const starter = object(backend.springBoot3, ['groupId', 'artifactId', 'framework']);
55
+ for (const artifact of [core, starter]) {
56
+ check(typeof artifact.groupId === 'string' && /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/.test(artifact.groupId) && artifact.groupId.length <= 200);
57
+ check(typeof artifact.artifactId === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(artifact.artifactId));
58
+ }
59
+ check(starter.framework === 'Spring Boot 3 MVC');
60
+ const docs = object(data.documentation, ['browserSdk', 'javaSdk', 'domainsAndCsp']);
61
+ for (const url of Object.values(docs)) publicUrl(url);
62
+ return /** @type {{data: IntegrationContract}} */ (value);
63
+ }
64
+
65
+ /** @param {unknown} condition @returns {asserts condition} */
66
+ function check(condition) {
67
+ if (!condition) throw new Error('Invalid integration contract');
68
+ }
69
+
70
+ /** @param {unknown} value @param {string[]} keys @returns {Record<string, unknown>} */
71
+ function object(value, keys) {
72
+ check(value !== null && typeof value === 'object' && !Array.isArray(value));
73
+ const record = /** @type {Record<string, unknown>} */ (value);
74
+ check(Object.keys(record).length === keys.length && keys.every(key => Object.hasOwn(record, key)));
75
+ return record;
76
+ }
77
+
78
+ /** @param {unknown} value */
79
+ function version(value) {
80
+ check(typeof value === 'string' && value.length <= 64 && VERSION.test(value));
81
+ }
82
+
83
+ /** @param {unknown} value @returns {URL} */
84
+ function publicUrl(value) {
85
+ check(typeof value === 'string' && value.length <= 2048 && /^[\x21-\x7e]+$/.test(value) && !/[?#\\]/.test(value));
86
+ let url;
87
+ try { url = new URL(value); } catch { throw new Error('Invalid integration contract'); }
88
+ check(url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash);
89
+ return url;
90
+ }
91
+
92
+ /** @param {unknown} value @param {number} maximum @returns {string[]} */
93
+ function stringList(value, maximum) {
94
+ check(Array.isArray(value) && value.length >= 1 && value.length <= maximum);
95
+ check(value.every(item => typeof item === 'string') && new Set(value).size === value.length);
96
+ return /** @type {string[]} */ (value);
97
+ }
98
+
99
+ /** @param {unknown} value @returns {string[]} */
100
+ function origins(value) {
101
+ const list = stringList(value, 10);
102
+ for (const item of list) check(publicUrl(item).origin === item);
103
+ return list;
104
+ }
@@ -58,7 +58,7 @@ export class OAuthClient {
58
58
  return this.#credentials(body);
59
59
  } catch (error) {
60
60
  if (error instanceof CliError && ["OAUTH_INVALID_GRANT", "OAUTH_REJECTED"].includes(error.code)) throw error;
61
- throw new CliError("The refresh result is uncertain; run `requestshield signin` again", {code: "OAUTH_REFRESH_UNCERTAIN", exitCode: 3});
61
+ throw new CliError("The refresh result is uncertain; run `requestshield login` again", {code: "OAUTH_REFRESH_UNCERTAIN", exitCode: 3});
62
62
  }
63
63
  }
64
64
 
@@ -126,7 +126,7 @@ export class OAuthClient {
126
126
  } catch { throw invalidResponse(); }
127
127
  if (!response.ok) {
128
128
  if (body.error === "invalid_grant" || ["idp_refresh_token_invalid", "idp_authorization_code_invalid", "invalid_grant"].includes(String(body.error_type))) {
129
- throw new CliError("Authorization was rejected; run `requestshield signin` again", {code: "OAUTH_INVALID_GRANT", exitCode: 3});
129
+ throw new CliError("Authorization was rejected; run `requestshield login` again", {code: "OAUTH_INVALID_GRANT", exitCode: 3});
130
130
  }
131
131
  if (response.status >= 400 && response.status < 500) {
132
132
  throw new CliError("The identity provider rejected the request; check the OAuth configuration or sign in again", {code: "OAUTH_REJECTED", exitCode: 3});
@@ -68,7 +68,7 @@ export async function createLoopbackReceiver({state, issuer, signal}) {
68
68
  else socket.destroy();
69
69
  });
70
70
  const aborted = () => {
71
- rejectResult(new CliError("Sign-in was cancelled or expired; run `requestshield signin` again", {code: "SIGNIN_CANCELLED", exitCode: 3}));
71
+ rejectResult(new CliError("Sign-in was cancelled or expired; run `requestshield login` again", {code: "SIGNIN_CANCELLED", exitCode: 3}));
72
72
  if (pendingResponse && !pendingResponse.writableEnded) pendingResponse.writeHead(400).end("Sign-in was not completed. Return to your terminal.");
73
73
  server.close();
74
74
  server.closeAllConnections();
@@ -79,7 +79,7 @@ export class SessionFiles {
79
79
  } catch (error) {
80
80
  if (error instanceof CliError) throw error;
81
81
  if (errorCode(error) === "ENOENT") {
82
- throw new CliError("Not signed in. Run `requestshield signin` first.", { code: "NOT_SIGNED_IN", exitCode: 3 });
82
+ throw new CliError("Not signed in. Run `requestshield login` first.", { code: "NOT_SIGNED_IN", exitCode: 3 });
83
83
  }
84
84
  throw storageFailure();
85
85
  } finally { await handle?.close().catch(() => undefined); }
@@ -108,7 +108,7 @@ export class SessionFiles {
108
108
  return true;
109
109
  } catch (error) {
110
110
  if (removed) {
111
- throw new CliError("The local session was removed, but its durability could not be confirmed. Run `requestshield signout` again to confirm removal.", {
111
+ throw new CliError("The local session was removed, but its durability could not be confirmed. Run `requestshield logout` again to confirm removal.", {
112
112
  code: "SESSION_COMMIT_UNCERTAIN", exitCode: 7,
113
113
  });
114
114
  }
@@ -142,7 +142,7 @@ export class SessionFiles {
142
142
  await this.syncDirectory();
143
143
  } catch (error) {
144
144
  if (replaced) {
145
- throw new CliError("The session was replaced, but its durability could not be confirmed. Run `requestshield signin` again if the next command cannot load it.", {
145
+ throw new CliError("The session was replaced, but its durability could not be confirmed. Run `requestshield login` again if the next command cannot load it.", {
146
146
  code: "SESSION_COMMIT_UNCERTAIN", exitCode: 7,
147
147
  });
148
148
  }
@@ -209,5 +209,5 @@ function permissionFailure() {
209
209
  return new CliError("Could not protect the RequestShield session for the current operating-system user. No new credentials were written.", { code: "SESSION_PERMISSION_ERROR", exitCode: 7 });
210
210
  }
211
211
  function invalidFile() {
212
- return new CliError("The saved RequestShield session is invalid. Run `requestshield signin` again.", { code: "INVALID_SESSION", exitCode: 3 });
212
+ return new CliError("The saved RequestShield session is invalid. Run `requestshield login` again.", { code: "INVALID_SESSION", exitCode: 3 });
213
213
  }
@@ -180,12 +180,12 @@ function checkedCredentials(value) {
180
180
  }
181
181
 
182
182
  function invalidSession() {
183
- return new CliError("The saved RequestShield session is invalid or belongs to different OAuth configuration. Run `requestshield signin` again.", {
183
+ return new CliError("The saved RequestShield session is invalid or belongs to different OAuth configuration. Run `requestshield login` again.", {
184
184
  code: "INVALID_SESSION", exitCode: 3,
185
185
  });
186
186
  }
187
187
  function signinRequired() {
188
- return new CliError("The RequestShield session could not be safely refreshed. Run `requestshield signin` again.", {
188
+ return new CliError("The RequestShield session could not be safely refreshed. Run `requestshield login` again.", {
189
189
  code: "SESSION_SIGNIN_REQUIRED", exitCode: 3,
190
190
  });
191
191
  }