@forgecart/cli 2.202610071755.0 → 2.202610082355.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 (26) hide show
  1. package/dist/src/authorization/cli-authorization-flow.d.ts +55 -0
  2. package/dist/src/authorization/cli-authorization-flow.js +175 -0
  3. package/dist/src/authorization/cli-authorization-flow.js.map +1 -0
  4. package/dist/src/authorization/cli-authorization.client.d.ts +36 -0
  5. package/dist/src/authorization/cli-authorization.client.js +169 -0
  6. package/dist/src/authorization/cli-authorization.client.js.map +1 -0
  7. package/dist/src/authorization/cli-credential-check.d.ts +42 -0
  8. package/dist/src/authorization/cli-credential-check.js +157 -0
  9. package/dist/src/authorization/cli-credential-check.js.map +1 -0
  10. package/dist/src/authorization/cli-unauthorized-refusal.d.ts +21 -0
  11. package/dist/src/authorization/cli-unauthorized-refusal.js +33 -0
  12. package/dist/src/authorization/cli-unauthorized-refusal.js.map +1 -0
  13. package/dist/src/authorization/type/cli-authorization.types.d.ts +55 -0
  14. package/dist/src/authorization/type/cli-authorization.types.js +24 -0
  15. package/dist/src/authorization/type/cli-authorization.types.js.map +1 -0
  16. package/dist/src/commands/init.d.ts +41 -14
  17. package/dist/src/commands/init.js +131 -23
  18. package/dist/src/commands/init.js.map +1 -1
  19. package/dist/src/exit-ladder.d.ts +8 -1
  20. package/dist/src/exit-ladder.js +7 -0
  21. package/dist/src/exit-ladder.js.map +1 -1
  22. package/package.json +1 -1
  23. package/templates/storefront/README.md +6 -2
  24. package/templates/storefront/src/server/forgecart/forgecart-client.factory.ts +11 -7
  25. package/templates/storefront-shadcn/README.md +6 -2
  26. package/templates/storefront-shadcn/src/server/forgecart/forgecart-client.factory.ts +11 -7
@@ -0,0 +1,55 @@
1
+ import type { CliAuthorizationClient } from './cli-authorization.client.js';
2
+ import { type CliCredentials } from './type/cli-authorization.types.js';
3
+ /**
4
+ * The browser authorization `forgecart init` runs when its credential check
5
+ * comes up short (#2772; record `refs/design/cli-improvements`, screen `init`).
6
+ *
7
+ * It opens an authorization on the admin API, prints the dashboard URL and the
8
+ * user code — BEFORE any browser is attempted, for the reader whose browser
9
+ * never opens — then opens the browser when a person is at a terminal, and
10
+ * polls at the cadence the request named until the authorization settles or
11
+ * the wait ends: the window, or `--timeout` when that comes first. The wait is
12
+ * quiet — no countdown, no interval, no attempt count — so an approval on the
13
+ * third poll reads exactly as one on the first. The device code is the poll's
14
+ * secret and is never printed.
15
+ */
16
+ export declare class CliAuthorizationFlow {
17
+ private readonly client;
18
+ constructor(client: CliAuthorizationClient);
19
+ /**
20
+ * Authorize this machine and answer the four credentials the approval
21
+ * delivers. A denial, an expiry or an elapsed wait ends the run on exit 3.
22
+ *
23
+ * @param report the credential check's report, printed first.
24
+ * @param timeoutSec `--timeout`: the most the wait may last — absent, the authorization's window.
25
+ */
26
+ authorize(report: readonly string[], timeoutSec: number | undefined): Promise<CliCredentials>;
27
+ /** Poll until the authorization settles or the deadline passes. */
28
+ private awaitApproval;
29
+ /** The credentials an APPROVED answer delivers; null while PENDING. DENIED and EXPIRED end the run. */
30
+ private settle;
31
+ /** The window's close — or `--timeout` from now, when that comes first. */
32
+ private deadlineOf;
33
+ /** What the run prints once the authorization is open, for a terminal or for a log. */
34
+ private handOff;
35
+ /** A person at a terminal: the browser opens, and the comparison is asked for where they stand. */
36
+ private interactiveHandOff;
37
+ /** No terminal: no browser, the URL and the code for whoever reads the log, and the bounded wait. */
38
+ private headlessHandOff;
39
+ /** The wait, quiet: `--timeout` is the one number it echoes, because the caller passed it. */
40
+ private waitingLine;
41
+ /**
42
+ * Best effort by design: over ssh, in a container or on a locked-down laptop
43
+ * nothing opens, and the URL and the code are already on the screen for
44
+ * exactly that reader — so a launcher that cannot start changes nothing about
45
+ * the run, and the CLI never waits on the browser it started.
46
+ */
47
+ private openBrowser;
48
+ private browserLauncher;
49
+ /**
50
+ * A browser is opened only for a person: stdin and stdout both a terminal.
51
+ * Read through the terminal types the question prompt reads them with — on a
52
+ * stream that is no terminal `isTTY` is absent, not false.
53
+ */
54
+ private isTerminal;
55
+ }
@@ -0,0 +1,175 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { hostname } from 'node:os';
3
+ import { setTimeout as sleep } from 'node:timers/promises';
4
+ import { print } from '../output.js';
5
+ import { readPackageVersion } from '../version.js';
6
+ import { CliUnauthorizedReason, CliUnauthorizedRefusal } from './cli-unauthorized-refusal.js';
7
+ import { CliAuthorizationState, } from './type/cli-authorization.types.js';
8
+ /**
9
+ * The browser authorization `forgecart init` runs when its credential check
10
+ * comes up short (#2772; record `refs/design/cli-improvements`, screen `init`).
11
+ *
12
+ * It opens an authorization on the admin API, prints the dashboard URL and the
13
+ * user code — BEFORE any browser is attempted, for the reader whose browser
14
+ * never opens — then opens the browser when a person is at a terminal, and
15
+ * polls at the cadence the request named until the authorization settles or
16
+ * the wait ends: the window, or `--timeout` when that comes first. The wait is
17
+ * quiet — no countdown, no interval, no attempt count — so an approval on the
18
+ * third poll reads exactly as one on the first. The device code is the poll's
19
+ * secret and is never printed.
20
+ */
21
+ export class CliAuthorizationFlow {
22
+ client;
23
+ constructor(client) {
24
+ this.client = client;
25
+ }
26
+ /**
27
+ * Authorize this machine and answer the four credentials the approval
28
+ * delivers. A denial, an expiry or an elapsed wait ends the run on exit 3.
29
+ *
30
+ * @param report the credential check's report, printed first.
31
+ * @param timeoutSec `--timeout`: the most the wait may last — absent, the authorization's window.
32
+ */
33
+ async authorize(report, timeoutSec) {
34
+ for (const line of report)
35
+ print(line);
36
+ const authorization = await this.client.request({
37
+ deviceName: hostname(),
38
+ cliVersion: readPackageVersion(),
39
+ });
40
+ const interactive = this.isTerminal();
41
+ for (const line of this.handOff(authorization, timeoutSec, interactive))
42
+ print(line);
43
+ if (interactive)
44
+ this.openBrowser(authorization.verificationUrl);
45
+ const credentials = await this.awaitApproval(authorization, timeoutSec);
46
+ print('');
47
+ print(' ✓ Authorized');
48
+ return credentials;
49
+ }
50
+ /** Poll until the authorization settles or the deadline passes. */
51
+ async awaitApproval(authorization, timeoutSec) {
52
+ const deadline = this.deadlineOf(authorization, timeoutSec);
53
+ const intervalMs = authorization.pollIntervalSec * 1000;
54
+ for (;;) {
55
+ const remainingMs = deadline.at - Date.now();
56
+ if (remainingMs <= 0)
57
+ throw new CliUnauthorizedRefusal(deadline.reason);
58
+ await sleep(Math.min(intervalMs, remainingMs));
59
+ const credentials = this.settle(await this.client.status(authorization.deviceCode));
60
+ if (credentials !== null)
61
+ return credentials;
62
+ }
63
+ }
64
+ /** The credentials an APPROVED answer delivers; null while PENDING. DENIED and EXPIRED end the run. */
65
+ settle(status) {
66
+ if (status.state === CliAuthorizationState.DENIED) {
67
+ throw new CliUnauthorizedRefusal(CliUnauthorizedReason.DENIED);
68
+ }
69
+ if (status.state === CliAuthorizationState.EXPIRED) {
70
+ throw new CliUnauthorizedRefusal(CliUnauthorizedReason.EXPIRED);
71
+ }
72
+ if (status.state === CliAuthorizationState.PENDING)
73
+ return null;
74
+ if (status.credentials === null) {
75
+ throw new Error('The admin API answered APPROVED without the credentials an approval delivers.');
76
+ }
77
+ return status.credentials;
78
+ }
79
+ /** The window's close — or `--timeout` from now, when that comes first. */
80
+ deadlineOf(authorization, timeoutSec) {
81
+ const window = {
82
+ at: Date.parse(authorization.expiresAt),
83
+ reason: CliUnauthorizedReason.EXPIRED,
84
+ };
85
+ if (timeoutSec === undefined)
86
+ return window;
87
+ const timeoutAt = Date.now() + timeoutSec * 1000;
88
+ if (timeoutAt >= window.at)
89
+ return window;
90
+ return { at: timeoutAt, reason: CliUnauthorizedReason.TIMED_OUT };
91
+ }
92
+ /** What the run prints once the authorization is open, for a terminal or for a log. */
93
+ handOff(authorization, timeoutSec, interactive) {
94
+ if (interactive)
95
+ return this.interactiveHandOff(authorization, timeoutSec);
96
+ return this.headlessHandOff(authorization, timeoutSec);
97
+ }
98
+ /** A person at a terminal: the browser opens, and the comparison is asked for where they stand. */
99
+ interactiveHandOff(authorization, timeoutSec) {
100
+ return [
101
+ '',
102
+ ' This machine is not authorized for any store yet.',
103
+ '',
104
+ ' Opening your browser to authorize it…',
105
+ '',
106
+ ` ${authorization.verificationUrl}`,
107
+ '',
108
+ ' If the browser did not open, visit that URL and check the code reads',
109
+ '',
110
+ ` ${authorization.userCode}`,
111
+ '',
112
+ ` ${this.waitingLine(timeoutSec)} · press Ctrl-C to abort`,
113
+ '',
114
+ ' Not you? The code is shown in the browser — approve only if it matches',
115
+ ` ${authorization.userCode}. Ctrl-C aborts and nothing is written.`,
116
+ ];
117
+ }
118
+ /** No terminal: no browser, the URL and the code for whoever reads the log, and the bounded wait. */
119
+ headlessHandOff(authorization, timeoutSec) {
120
+ return [
121
+ '',
122
+ ' No TTY — not opening a browser.',
123
+ '',
124
+ ' Authorize this machine at',
125
+ '',
126
+ ` ${authorization.verificationUrl}`,
127
+ '',
128
+ ' and check the code reads',
129
+ '',
130
+ ` ${authorization.userCode}`,
131
+ '',
132
+ ' — or supply the four keys in the environment and re-run.',
133
+ '',
134
+ ` ${this.waitingLine(timeoutSec)}`,
135
+ ];
136
+ }
137
+ /** The wait, quiet: `--timeout` is the one number it echoes, because the caller passed it. */
138
+ waitingLine(timeoutSec) {
139
+ if (timeoutSec === undefined)
140
+ return 'Waiting for approval…';
141
+ return `Waiting for approval… (--timeout ${String(timeoutSec)}s)`;
142
+ }
143
+ /**
144
+ * Best effort by design: over ssh, in a container or on a locked-down laptop
145
+ * nothing opens, and the URL and the code are already on the screen for
146
+ * exactly that reader — so a launcher that cannot start changes nothing about
147
+ * the run, and the CLI never waits on the browser it started.
148
+ */
149
+ openBrowser(url) {
150
+ const { command, args } = this.browserLauncher(url);
151
+ const launcher = spawn(command, args, { stdio: 'ignore', detached: true });
152
+ launcher.once('error', () => {
153
+ // The hand-off printed above is the fallback for a launcher that cannot start.
154
+ });
155
+ launcher.unref();
156
+ }
157
+ browserLauncher(url) {
158
+ if (process.platform === 'darwin')
159
+ return { command: 'open', args: [url] };
160
+ if (process.platform === 'win32')
161
+ return { command: 'explorer.exe', args: [url] };
162
+ return { command: 'xdg-open', args: [url] };
163
+ }
164
+ /**
165
+ * A browser is opened only for a person: stdin and stdout both a terminal.
166
+ * Read through the terminal types the question prompt reads them with — on a
167
+ * stream that is no terminal `isTTY` is absent, not false.
168
+ */
169
+ isTerminal() {
170
+ const keyboard = process.stdin;
171
+ const display = process.stdout;
172
+ return keyboard.isTTY === true && display.isTTY === true;
173
+ }
174
+ }
175
+ //# sourceMappingURL=cli-authorization-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-authorization-flow.js","sourceRoot":"","sources":["../../../src/authorization/cli-authorization-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { type CliAuthorizationRequestInput, type CliAuthorizationRequestResult, type CliAuthorizationStatusResult } from './type/cli-authorization.types.js';
2
+ /**
3
+ * The admin API's two Public CLI authorization operations, over GraphQL on
4
+ * HTTP — the CLI ships no WebSocket client, and both operations are single
5
+ * request/response exchanges. Every answer is read field by field: a result
6
+ * the CLI cannot read is an error naming the endpoint, never a guess.
7
+ */
8
+ export declare class CliAuthorizationClient {
9
+ private readonly endpoint;
10
+ /** @param endpoint the admin API's GraphQL URL. */
11
+ constructor(endpoint: string);
12
+ /** Open an authorization for this machine. */
13
+ request(input: CliAuthorizationRequestInput): Promise<CliAuthorizationRequestResult>;
14
+ /**
15
+ * Poll the authorization the device code names. A typed refusal the server
16
+ * answers instead — denied, expired, unknown — ends the run on exit 3.
17
+ */
18
+ status(deviceCode: string): Promise<CliAuthorizationStatusResult>;
19
+ /** POST one operation and answer its root field's object. */
20
+ private send;
21
+ private post;
22
+ /** The response's GraphQL body — an answer that is not one names its HTTP status. */
23
+ private readBody;
24
+ /** A GraphQL error ends the call: a CLI authorization refusal on exit 3, anything else as an error. */
25
+ private assertNoErrors;
26
+ /** The `ErrorCode` a GraphQL error carries in its extensions. */
27
+ private errorCode;
28
+ /** The credentials an answer carries — null on every answer but APPROVED. */
29
+ private readCredentials;
30
+ private readString;
31
+ private isState;
32
+ private unreadable;
33
+ private notGraphQlMessage;
34
+ /** What failed to connect: fetch's own message names nothing, its cause names the socket error. */
35
+ private failureDetail;
36
+ }
@@ -0,0 +1,169 @@
1
+ import { isJsonObject } from '../json.js';
2
+ import { CliUnauthorizedReason, CliUnauthorizedRefusal } from './cli-unauthorized-refusal.js';
3
+ import { CliAuthorizationState, } from './type/cli-authorization.types.js';
4
+ /** The SDK's `CliAuthorizationRequest` operation, verbatim — `forgecart init` holds no credential yet. */
5
+ const REQUEST_DOCUMENT = `mutation CliAuthorizationRequest($input: CliAuthorizationRequestInput!) {
6
+ cliAuthorizationRequest(input: $input) {
7
+ deviceCode
8
+ userCode
9
+ verificationUrl
10
+ expiresAt
11
+ pollIntervalSec
12
+ }
13
+ }`;
14
+ /** The SDK's `CliAuthorizationStatus` operation, verbatim — the poll, keyed on the device code. */
15
+ const STATUS_DOCUMENT = `mutation CliAuthorizationStatus($deviceCode: String!) {
16
+ cliAuthorizationStatus(deviceCode: $deviceCode) {
17
+ state
18
+ credentials {
19
+ channelToken
20
+ adminSecret
21
+ shopApiUrl
22
+ adminApiUrl
23
+ }
24
+ }
25
+ }`;
26
+ /**
27
+ * The typed refusals a poll can answer, by the end each one means for the CLI.
28
+ * An unknown code is an authorization the server no longer holds — its window
29
+ * closed and the sweep took it — so it ends exactly as an expired one does.
30
+ */
31
+ const REFUSAL_REASONS = {
32
+ CLI_AUTHORIZATION_DENIED: CliUnauthorizedReason.DENIED,
33
+ CLI_AUTHORIZATION_EXPIRED: CliUnauthorizedReason.EXPIRED,
34
+ CLI_AUTHORIZATION_CODE_UNKNOWN: CliUnauthorizedReason.EXPIRED,
35
+ };
36
+ /**
37
+ * The admin API's two Public CLI authorization operations, over GraphQL on
38
+ * HTTP — the CLI ships no WebSocket client, and both operations are single
39
+ * request/response exchanges. Every answer is read field by field: a result
40
+ * the CLI cannot read is an error naming the endpoint, never a guess.
41
+ */
42
+ export class CliAuthorizationClient {
43
+ endpoint;
44
+ /** @param endpoint the admin API's GraphQL URL. */
45
+ constructor(endpoint) {
46
+ this.endpoint = endpoint;
47
+ }
48
+ /** Open an authorization for this machine. */
49
+ async request(input) {
50
+ const result = await this.send(REQUEST_DOCUMENT, { input }, 'cliAuthorizationRequest');
51
+ const expiresAt = this.readString(result, 'expiresAt', 'cliAuthorizationRequest');
52
+ const pollIntervalSec = result.pollIntervalSec;
53
+ if (Number.isNaN(Date.parse(expiresAt)))
54
+ throw this.unreadable('cliAuthorizationRequest');
55
+ if (typeof pollIntervalSec !== 'number' || pollIntervalSec <= 0) {
56
+ throw this.unreadable('cliAuthorizationRequest');
57
+ }
58
+ return {
59
+ deviceCode: this.readString(result, 'deviceCode', 'cliAuthorizationRequest'),
60
+ userCode: this.readString(result, 'userCode', 'cliAuthorizationRequest'),
61
+ verificationUrl: this.readString(result, 'verificationUrl', 'cliAuthorizationRequest'),
62
+ expiresAt,
63
+ pollIntervalSec,
64
+ };
65
+ }
66
+ /**
67
+ * Poll the authorization the device code names. A typed refusal the server
68
+ * answers instead — denied, expired, unknown — ends the run on exit 3.
69
+ */
70
+ async status(deviceCode) {
71
+ const result = await this.send(STATUS_DOCUMENT, { deviceCode }, 'cliAuthorizationStatus');
72
+ const state = result.state;
73
+ if (!this.isState(state))
74
+ throw this.unreadable('cliAuthorizationStatus');
75
+ return { state, credentials: this.readCredentials(result.credentials) };
76
+ }
77
+ /** POST one operation and answer its root field's object. */
78
+ async send(document, variables, field) {
79
+ const response = await this.post(document, variables);
80
+ const body = await this.readBody(response);
81
+ this.assertNoErrors(body.errors, field);
82
+ const data = body.data;
83
+ const result = isJsonObject(data) ? data[field] : undefined;
84
+ if (!isJsonObject(result))
85
+ throw this.unreadable(field);
86
+ return result;
87
+ }
88
+ async post(document, variables) {
89
+ try {
90
+ return await fetch(this.endpoint, {
91
+ method: 'POST',
92
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
93
+ body: JSON.stringify({ query: document, variables }),
94
+ });
95
+ }
96
+ catch (error) {
97
+ throw new Error(`Could not reach the admin API at ${this.endpoint}: ${this.failureDetail(error)}.`, { cause: error });
98
+ }
99
+ }
100
+ /** The response's GraphQL body — an answer that is not one names its HTTP status. */
101
+ async readBody(response) {
102
+ const text = await response.text();
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(text);
106
+ }
107
+ catch (error) {
108
+ throw new Error(this.notGraphQlMessage(response.status), { cause: error });
109
+ }
110
+ if (!isJsonObject(parsed))
111
+ throw new Error(this.notGraphQlMessage(response.status));
112
+ return parsed;
113
+ }
114
+ /** A GraphQL error ends the call: a CLI authorization refusal on exit 3, anything else as an error. */
115
+ assertNoErrors(errors, field) {
116
+ if (!Array.isArray(errors) || errors.length === 0)
117
+ return;
118
+ const first = errors[0];
119
+ const code = this.errorCode(first);
120
+ const reason = code === undefined ? undefined : REFUSAL_REASONS[code];
121
+ if (reason !== undefined)
122
+ throw new CliUnauthorizedRefusal(reason);
123
+ const message = isJsonObject(first) && typeof first.message === 'string' ? first.message : '';
124
+ throw new Error(`The admin API at ${this.endpoint} refused ${field}: ${code ?? message}.`);
125
+ }
126
+ /** The `ErrorCode` a GraphQL error carries in its extensions. */
127
+ errorCode(error) {
128
+ if (!isJsonObject(error) || !isJsonObject(error.extensions))
129
+ return undefined;
130
+ const code = error.extensions.code;
131
+ return typeof code === 'string' ? code : undefined;
132
+ }
133
+ /** The credentials an answer carries — null on every answer but APPROVED. */
134
+ readCredentials(value) {
135
+ if (value === null || value === undefined)
136
+ return null;
137
+ if (!isJsonObject(value))
138
+ throw this.unreadable('cliAuthorizationStatus');
139
+ return {
140
+ channelToken: this.readString(value, 'channelToken', 'cliAuthorizationStatus'),
141
+ adminSecret: this.readString(value, 'adminSecret', 'cliAuthorizationStatus'),
142
+ shopApiUrl: this.readString(value, 'shopApiUrl', 'cliAuthorizationStatus'),
143
+ adminApiUrl: this.readString(value, 'adminApiUrl', 'cliAuthorizationStatus'),
144
+ };
145
+ }
146
+ readString(record, key, field) {
147
+ const value = record[key];
148
+ if (typeof value !== 'string' || value === '')
149
+ throw this.unreadable(field);
150
+ return value;
151
+ }
152
+ isState(value) {
153
+ return Object.values(CliAuthorizationState).includes(value);
154
+ }
155
+ unreadable(field) {
156
+ return new Error(`The admin API at ${this.endpoint} answered a ${field} result the CLI cannot read.`);
157
+ }
158
+ notGraphQlMessage(status) {
159
+ return `The admin API at ${this.endpoint} answered HTTP ${String(status)} without a GraphQL response.`;
160
+ }
161
+ /** What failed to connect: fetch's own message names nothing, its cause names the socket error. */
162
+ failureDetail(error) {
163
+ const cause = error instanceof Error ? error.cause : undefined;
164
+ if (cause instanceof Error)
165
+ return cause.message;
166
+ return error instanceof Error ? error.message : String(error);
167
+ }
168
+ }
169
+ //# sourceMappingURL=cli-authorization.client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-authorization.client.js","sourceRoot":"","sources":["../../../src/authorization/cli-authorization.client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { CliCredentialKey, type CliCredentials } from './type/cli-authorization.types.js';
2
+ /**
3
+ * `forgecart init`'s credential check (#2772): each of the four keys from the
4
+ * environment, then the target's `.env`, then its `.forgecart/config.json`
5
+ * (`channelToken`, `apiUrl`) — the first non-empty value wins. With all four
6
+ * found, init asks nothing; with any absent, it authorizes in the browser, and
7
+ * the check's {@link report} is the first thing that run prints.
8
+ */
9
+ export declare class CliCredentialCheck {
10
+ private readonly environment;
11
+ private readonly dotenv;
12
+ private readonly config;
13
+ private constructor();
14
+ /** Look for the four keys for the storefront at `targetDir`. */
15
+ static read(targetDir: string, env?: NodeJS.ProcessEnv): Promise<CliCredentialCheck>;
16
+ /** The value the check found for `key`: the environment's, then `.env`'s, then `config.json`'s. */
17
+ value(key: CliCredentialKey): string | undefined;
18
+ /** All four credentials — undefined while any one is absent. */
19
+ credentials(): CliCredentials | undefined;
20
+ /**
21
+ * The check as a failed run prints it (record `refs/design/cli-improvements`,
22
+ * `init?case=missing`): one row per place it looked, each naming what that
23
+ * place does not carry. Values are never printed — only key names.
24
+ */
25
+ report(): string[];
26
+ private row;
27
+ /** The unset keys, two to a line, the last line closed by `— unset`. */
28
+ private unsetLines;
29
+ /**
30
+ * The four keys `.env` assigns. The FIRST assignment of a key is the one read
31
+ * — the line `init`'s merge refreshes — and an empty value is no value.
32
+ */
33
+ private static readDotenv;
34
+ /** The channel token and shop API URL `.forgecart/config.json` records. */
35
+ private static readConfig;
36
+ private static parseConfig;
37
+ private static unreadableConfigMessage;
38
+ /** A file that may legitimately not exist; any other failure propagates. */
39
+ private static readIfPresent;
40
+ private static unquote;
41
+ private static isCredentialKey;
42
+ }
@@ -0,0 +1,157 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isJsonObject } from '../json.js';
4
+ import { CliCredentialKey } from './type/cli-authorization.types.js';
5
+ /** The report's source column: `.forgecart/config.json`, and the gutter after it. */
6
+ const SOURCE_COLUMN_WIDTH = 26;
7
+ /** One `.env` assignment — the key shape `init`'s merge manages. */
8
+ const DOTENV_ASSIGNMENT = /^([A-Z][A-Z0-9_]*)=(.*)$/;
9
+ /** A value wrapped in one pair of matching quotes, as `.env` files may write it. */
10
+ const QUOTED_VALUE = /^(["'])(.*)\1$/;
11
+ /** The `.forgecart/config.json` field each credential it carries is read from. */
12
+ const CONFIG_FIELDS = [
13
+ [CliCredentialKey.CHANNEL_TOKEN, 'channelToken'],
14
+ [CliCredentialKey.SHOP_API_URL, 'apiUrl'],
15
+ ];
16
+ /**
17
+ * `forgecart init`'s credential check (#2772): each of the four keys from the
18
+ * environment, then the target's `.env`, then its `.forgecart/config.json`
19
+ * (`channelToken`, `apiUrl`) — the first non-empty value wins. With all four
20
+ * found, init asks nothing; with any absent, it authorizes in the browser, and
21
+ * the check's {@link report} is the first thing that run prints.
22
+ */
23
+ export class CliCredentialCheck {
24
+ environment;
25
+ dotenv;
26
+ config;
27
+ constructor(environment, dotenv, config) {
28
+ this.environment = environment;
29
+ this.dotenv = dotenv;
30
+ this.config = config;
31
+ }
32
+ /** Look for the four keys for the storefront at `targetDir`. */
33
+ static async read(targetDir, env = process.env) {
34
+ const environment = new Map();
35
+ for (const key of Object.values(CliCredentialKey)) {
36
+ const value = env[key]?.trim();
37
+ if (value)
38
+ environment.set(key, value);
39
+ }
40
+ return new CliCredentialCheck(environment, await CliCredentialCheck.readDotenv(join(targetDir, '.env')), await CliCredentialCheck.readConfig(join(targetDir, '.forgecart', 'config.json')));
41
+ }
42
+ /** The value the check found for `key`: the environment's, then `.env`'s, then `config.json`'s. */
43
+ value(key) {
44
+ return this.environment.get(key) ?? this.dotenv?.get(key) ?? this.config?.get(key);
45
+ }
46
+ /** All four credentials — undefined while any one is absent. */
47
+ credentials() {
48
+ const channelToken = this.value(CliCredentialKey.CHANNEL_TOKEN);
49
+ const adminSecret = this.value(CliCredentialKey.ADMIN_SECRET);
50
+ const shopApiUrl = this.value(CliCredentialKey.SHOP_API_URL);
51
+ const adminApiUrl = this.value(CliCredentialKey.ADMIN_API_URL);
52
+ if (!channelToken || !adminSecret || !shopApiUrl || !adminApiUrl)
53
+ return undefined;
54
+ return { channelToken, adminSecret, shopApiUrl, adminApiUrl };
55
+ }
56
+ /**
57
+ * The check as a failed run prints it (record `refs/design/cli-improvements`,
58
+ * `init?case=missing`): one row per place it looked, each naming what that
59
+ * place does not carry. Values are never printed — only key names.
60
+ */
61
+ report() {
62
+ return [
63
+ '',
64
+ ' Checking credentials…',
65
+ '',
66
+ ...this.row('.env', this.dotenv),
67
+ ...this.row('environment', this.environment),
68
+ ...this.row('.forgecart/config.json', this.config),
69
+ ];
70
+ }
71
+ row(label, source) {
72
+ const gutter = ` ${label.padEnd(SOURCE_COLUMN_WIDTH)}`;
73
+ if (source === null)
74
+ return [`${gutter}no such file`];
75
+ const unset = Object.values(CliCredentialKey).filter((key) => !source.has(key));
76
+ const continuation = ' '.repeat(gutter.length);
77
+ return this.unsetLines(unset).map((line, index) => `${index === 0 ? gutter : continuation}${line}`);
78
+ }
79
+ /** The unset keys, two to a line, the last line closed by `— unset`. */
80
+ unsetLines(keys) {
81
+ const lines = [];
82
+ for (let start = 0; start < keys.length; start += 2) {
83
+ lines.push(keys.slice(start, start + 2).join(', '));
84
+ }
85
+ return lines.map((line, index) => index === lines.length - 1 ? `${line} — unset` : `${line},`);
86
+ }
87
+ /**
88
+ * The four keys `.env` assigns. The FIRST assignment of a key is the one read
89
+ * — the line `init`'s merge refreshes — and an empty value is no value.
90
+ */
91
+ static async readDotenv(path) {
92
+ const raw = await CliCredentialCheck.readIfPresent(path);
93
+ if (raw === null)
94
+ return null;
95
+ const found = new Map();
96
+ for (const line of raw.split('\n')) {
97
+ const assignment = DOTENV_ASSIGNMENT.exec(line);
98
+ if (assignment === null)
99
+ continue;
100
+ const [, key, rawValue] = assignment;
101
+ if (!CliCredentialCheck.isCredentialKey(key) || found.has(key))
102
+ continue;
103
+ const value = CliCredentialCheck.unquote(rawValue.trim());
104
+ if (value)
105
+ found.set(key, value);
106
+ }
107
+ return found;
108
+ }
109
+ /** The channel token and shop API URL `.forgecart/config.json` records. */
110
+ static async readConfig(path) {
111
+ const raw = await CliCredentialCheck.readIfPresent(path);
112
+ if (raw === null)
113
+ return null;
114
+ const config = CliCredentialCheck.parseConfig(raw, path);
115
+ const found = new Map();
116
+ for (const [key, field] of CONFIG_FIELDS) {
117
+ const value = config[field];
118
+ if (typeof value === 'string' && value.trim())
119
+ found.set(key, value.trim());
120
+ }
121
+ return found;
122
+ }
123
+ static parseConfig(raw, path) {
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(raw);
127
+ }
128
+ catch (error) {
129
+ throw new Error(CliCredentialCheck.unreadableConfigMessage(path), { cause: error });
130
+ }
131
+ if (!isJsonObject(parsed))
132
+ throw new Error(CliCredentialCheck.unreadableConfigMessage(path));
133
+ return parsed;
134
+ }
135
+ static unreadableConfigMessage(path) {
136
+ return `${path} is not a JSON object, so the credentials it records cannot be read.`;
137
+ }
138
+ /** A file that may legitimately not exist; any other failure propagates. */
139
+ static async readIfPresent(path) {
140
+ try {
141
+ return await readFile(path, 'utf8');
142
+ }
143
+ catch (error) {
144
+ if (error.code === 'ENOENT')
145
+ return null;
146
+ throw error;
147
+ }
148
+ }
149
+ static unquote(value) {
150
+ const quoted = QUOTED_VALUE.exec(value);
151
+ return quoted === null ? value : quoted[2];
152
+ }
153
+ static isCredentialKey(key) {
154
+ return Object.values(CliCredentialKey).includes(key);
155
+ }
156
+ }
157
+ //# sourceMappingURL=cli-credential-check.js.map