@nevermined-io/cli 1.3.2 → 1.4.1

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 (63) hide show
  1. package/README.md +5 -5
  2. package/dist/base-command.d.ts +11 -1
  3. package/dist/base-command.d.ts.map +1 -1
  4. package/dist/base-command.js +14 -0
  5. package/dist/base-command.js.map +1 -1
  6. package/dist/commands/agents/register-agent-and-plan.d.ts.map +1 -1
  7. package/dist/commands/agents/register-agent-and-plan.js +5 -2
  8. package/dist/commands/agents/register-agent-and-plan.js.map +1 -1
  9. package/dist/commands/agents/register-agent.d.ts.map +1 -1
  10. package/dist/commands/agents/register-agent.js +5 -2
  11. package/dist/commands/agents/register-agent.js.map +1 -1
  12. package/dist/commands/agents/update-agent-metadata.d.ts.map +1 -1
  13. package/dist/commands/agents/update-agent-metadata.js +5 -2
  14. package/dist/commands/agents/update-agent-metadata.js.map +1 -1
  15. package/dist/commands/cards/delegate.d.ts +26 -0
  16. package/dist/commands/cards/delegate.d.ts.map +1 -0
  17. package/dist/commands/cards/delegate.js +87 -0
  18. package/dist/commands/cards/delegate.js.map +1 -0
  19. package/dist/commands/cards/enroll.d.ts +25 -0
  20. package/dist/commands/cards/enroll.d.ts.map +1 -0
  21. package/dist/commands/cards/enroll.js +88 -0
  22. package/dist/commands/cards/enroll.js.map +1 -0
  23. package/dist/commands/cards/setup.d.ts +28 -0
  24. package/dist/commands/cards/setup.d.ts.map +1 -0
  25. package/dist/commands/cards/setup.js +113 -0
  26. package/dist/commands/cards/setup.js.map +1 -0
  27. package/dist/commands/login.js +1 -26
  28. package/dist/commands/login.js.map +1 -1
  29. package/dist/commands/organizations/get-my-memberships.d.ts +15 -0
  30. package/dist/commands/organizations/get-my-memberships.d.ts.map +1 -0
  31. package/dist/commands/organizations/get-my-memberships.js +25 -0
  32. package/dist/commands/organizations/get-my-memberships.js.map +1 -0
  33. package/dist/commands/organizations/get-organization-activity.d.ts +18 -0
  34. package/dist/commands/organizations/get-organization-activity.d.ts.map +1 -0
  35. package/dist/commands/organizations/get-organization-activity.js +32 -0
  36. package/dist/commands/organizations/get-organization-activity.js.map +1 -0
  37. package/dist/commands/plans/{set-proof-required.d.ts → set-onchain-mirror.d.ts} +4 -4
  38. package/dist/commands/plans/{set-proof-required.d.ts.map → set-onchain-mirror.d.ts.map} +1 -1
  39. package/dist/commands/plans/{set-proof-required.js → set-onchain-mirror.js} +7 -7
  40. package/dist/commands/plans/{set-proof-required.js.map → set-onchain-mirror.js.map} +1 -1
  41. package/dist/generator/command-generator.d.ts.map +1 -1
  42. package/dist/generator/command-generator.js +4 -2
  43. package/dist/generator/command-generator.js.map +1 -1
  44. package/dist/generator/generate.js +2 -2
  45. package/dist/generator/sync-check.js +2 -2
  46. package/dist/utils/browser.d.ts +14 -0
  47. package/dist/utils/browser.d.ts.map +1 -0
  48. package/dist/utils/browser.js +41 -0
  49. package/dist/utils/browser.js.map +1 -0
  50. package/dist/utils/orgs.d.ts +36 -0
  51. package/dist/utils/orgs.d.ts.map +1 -0
  52. package/dist/utils/orgs.js +65 -0
  53. package/dist/utils/orgs.js.map +1 -0
  54. package/dist/utils/widget-redirect-flow.d.ts +83 -0
  55. package/dist/utils/widget-redirect-flow.d.ts.map +1 -0
  56. package/dist/utils/widget-redirect-flow.js +259 -0
  57. package/dist/utils/widget-redirect-flow.js.map +1 -0
  58. package/oclif.manifest.json +431 -146
  59. package/package.json +4 -3
  60. package/dist/commands/plans/redeem-credits.d.ts +0 -21
  61. package/dist/commands/plans/redeem-credits.d.ts.map +0 -1
  62. package/dist/commands/plans/redeem-credits.js +0 -35
  63. package/dist/commands/plans/redeem-credits.js.map +0 -1
@@ -0,0 +1,65 @@
1
+ import { createInterface } from 'readline';
2
+ /**
3
+ * Resolve which organisation a top-level CLI widget flow should be
4
+ * scoped to.
5
+ *
6
+ * Rules (mirror the acceptance criteria in issue #1671):
7
+ * - `--org <id>` wins outright (we don't double-check membership — the
8
+ * backend's `POST /widgets/session/self` will reject if the user
9
+ * isn't a member).
10
+ * - Otherwise call `payments.organizations.getMyMemberships()`:
11
+ * - 0 → exit with the "card setup is only available to org members"
12
+ * message.
13
+ * - 1 → use it silently.
14
+ * - 2+ → if the terminal is interactive, prompt the user to pick;
15
+ * otherwise exit non-zero with "Pass --org <id>".
16
+ *
17
+ * Errors thrown by this helper are formatted to be safe to print
18
+ * directly with `BaseCommand.handleError`.
19
+ */
20
+ export async function resolveOrgIdInteractive(opts) {
21
+ if (opts.flagOrgId) {
22
+ return { orgId: opts.flagOrgId, orgName: '' };
23
+ }
24
+ const memberships = await opts.payments.organizations.getMyMemberships();
25
+ if (!memberships || memberships.length === 0) {
26
+ throw new Error('Card setup is only available to members of an organization. Create or join one in the dashboard, then re-run this command.');
27
+ }
28
+ if (memberships.length === 1) {
29
+ const m = memberships[0];
30
+ return { orgId: m.orgId, orgName: m.orgName ?? '' };
31
+ }
32
+ const isTTY = opts.isTTY ?? process.stdin.isTTY;
33
+ if (!isTTY) {
34
+ throw new Error(`Multiple organizations found (${memberships.length}). Pass --org <id> to specify which organization to use.`);
35
+ }
36
+ opts.log('You are a member of multiple organizations:');
37
+ memberships.forEach((m, idx) => {
38
+ const role = m.role ? ` — ${m.role}` : '';
39
+ opts.log(` ${idx + 1}. ${m.orgName ?? m.orgId}${role} (${m.orgId})`);
40
+ });
41
+ const pick = await promptIndex(memberships.length, '> Select an organization [1]: ');
42
+ const chosen = memberships[pick - 1];
43
+ return { orgId: chosen.orgId, orgName: chosen.orgName ?? '' };
44
+ }
45
+ async function promptIndex(max, prompt) {
46
+ return new Promise((resolve, reject) => {
47
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
48
+ rl.question(prompt, (answer) => {
49
+ rl.close();
50
+ const trimmed = answer.trim();
51
+ // Default to 1 when the user just hits Enter — matches the "[1]" in
52
+ // the prompt and gives interactive users a single-keystroke happy
53
+ // path when the first org is the one they want.
54
+ if (trimmed === '')
55
+ return resolve(1);
56
+ const n = Number(trimmed);
57
+ if (!Number.isInteger(n) || n < 1 || n > max) {
58
+ reject(new Error(`Please enter a number between 1 and ${max}.`));
59
+ return;
60
+ }
61
+ resolve(n);
62
+ });
63
+ });
64
+ }
65
+ //# sourceMappingURL=orgs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/utils/orgs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ export interface WidgetRedirectFlowOptions {
2
+ /** Frontend base URL — e.g. `Environments[env].frontend`. */
3
+ frontendUrl: string;
4
+ /**
5
+ * Relative embed path the CLI wants to open, e.g.
6
+ * `/embed/cards/setup` or `/embed/cards/enroll`.
7
+ */
8
+ embedPath: string;
9
+ /**
10
+ * Called once the local callback server is listening, with the bound
11
+ * `returnUrl`. The caller mints a widget session against that URL and
12
+ * returns the resulting `sessionToken`. Splitting it this way lets
13
+ * the backend validate `returnUrl` at session-creation time (the
14
+ * spec'd contract) — minting before the port is known would leave
15
+ * the backend's pre-flight allow-list check with no URL to verify.
16
+ */
17
+ mintSession: (args: {
18
+ returnUrl: string;
19
+ }) => Promise<{
20
+ sessionToken: string;
21
+ }>;
22
+ /** Extra query params to forward to the embed page (e.g. `provider=stripe`, `paymentMethodId=pm_x`). */
23
+ extraSearchParams?: Record<string, string>;
24
+ /** If true, prints the URL instead of opening the browser. */
25
+ noBrowser?: boolean;
26
+ /** Caller-supplied logger / printer. The login command uses oclif's formatter; this is the same shape. */
27
+ log: (msg: string) => void;
28
+ /** Suggested success-page wording — keeps the wording aligned with whichever command is calling. */
29
+ successPageTitle?: string;
30
+ /** Suggested timeout-error wording — keeps it specific to the calling command instead of "Card setup ..." for everything. */
31
+ timeoutMessage?: string;
32
+ }
33
+ export interface WidgetRedirectFlowResult {
34
+ /** Echoed `state` value — the helper has already verified it matches. */
35
+ state: string;
36
+ /** All query params the embed page redirected with (paymentMethodId, delegationId, …). */
37
+ query: Record<string, string>;
38
+ }
39
+ /**
40
+ * Shared redirect-mode handshake for any CLI command that hands the user
41
+ * off to an `/embed/*` page and waits for a localhost callback.
42
+ *
43
+ * Flow:
44
+ * 1. Bind a one-shot HTTP server on `127.0.0.1:0` (the OS picks a free port).
45
+ * 2. Compute `returnUrl = http://127.0.0.1:<port>/callback` and hand it
46
+ * to `opts.mintSession`. The caller mints a widget session bound to
47
+ * that exact returnUrl (which the backend can validate against the
48
+ * session-specific allow-list at creation time). We use the literal
49
+ * `127.0.0.1` rather than `localhost` because the server binds to
50
+ * `127.0.0.1` and Node 17+ resolves `localhost` to `::1` first on
51
+ * modern hosts — the browser would stall on the IPv6 attempt before
52
+ * falling back to IPv4.
53
+ * 3. Open the browser at `{frontend}/embed/<path>?sessionToken=…&returnUrl=…&state=<rand>`.
54
+ * 4. Resolve when the embed page redirects to `/callback?…&state=<echo>`.
55
+ * `state` is compared in constant time.
56
+ *
57
+ * Rejects on bind failure, mint failure, 5-minute timeout, or
58
+ * state-mismatched callback (the bad request gets a styled error page
59
+ * and the server stays alive — the legitimate callback can still land).
60
+ */
61
+ export declare function runWidgetRedirectFlow(opts: WidgetRedirectFlowOptions): Promise<WidgetRedirectFlowResult>;
62
+ /**
63
+ * POST to the `/widgets/session/self` endpoint with the caller's NVM
64
+ * API key. Lives here (not in the SDK) because v1 only wires this up
65
+ * for the CLI redirect flow — when the SDK exposes
66
+ * `payments.widgets.createSelfSession()` we can swap this for the SDK
67
+ * call without touching command callers.
68
+ */
69
+ export interface SelfMintSessionResponse {
70
+ sessionToken: string;
71
+ userId: string;
72
+ userWallet: string;
73
+ apiKeyHash: string;
74
+ expiresAt: string;
75
+ isReturnUrlAllowed?: boolean | null;
76
+ }
77
+ export declare function mintSelfWidgetSession(args: {
78
+ backendUrl: string;
79
+ nvmApiKey: string;
80
+ orgId: string;
81
+ returnUrl?: string;
82
+ }): Promise<SelfMintSessionResponse>;
83
+ //# sourceMappingURL=widget-redirect-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-redirect-flow.d.ts","sourceRoot":"","sources":["../../src/utils/widget-redirect-flow.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,WAAW,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC/E,wGAAwG;IACxG,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,8DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0GAA0G;IAC1G,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1B,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,6HAA6H;IAC7H,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,wBAAwB,CAAC,CAqHnC;AAgED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CACpC;AAiBD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAyDnC"}
@@ -0,0 +1,259 @@
1
+ import { createServer } from 'http';
2
+ import { randomBytes, timingSafeEqual } from 'crypto';
3
+ import { openBrowser } from './browser.js';
4
+ const SELF_MINT_FETCH_TIMEOUT_MS = 15_000;
5
+ /**
6
+ * Default timeout for a redirect-mode CLI flow. Mirrors the existing
7
+ * `nvm login` callback timeout — 5 minutes is enough for the user to
8
+ * tab into the browser, complete a card enrolment + delegation, and
9
+ * land back at the CLI.
10
+ */
11
+ const REDIRECT_TIMEOUT_MS = 5 * 60 * 1000;
12
+ /**
13
+ * Constant-time string equality for the CSRF `state` nonce. The state we
14
+ * issue is a 32-char hex string (`randomBytes(16).toString('hex')`), so
15
+ * we ALWAYS allocate fixed 16-byte buffers from `hex` regardless of the
16
+ * caller-supplied value's encoding. Computing buffer length from string
17
+ * length would diverge on non-ASCII input (`a.length` is UTF-16 code
18
+ * units; `Buffer.from(a, 'utf8').length` is byte count), and a crafted
19
+ * non-hex `received` could otherwise throw inside `timingSafeEqual`.
20
+ *
21
+ * Inputs must already have been verified to be 32 hex chars by the
22
+ * caller (or we reject up front).
23
+ */
24
+ function safeEqualHexState(received, expected) {
25
+ if (!/^[0-9a-f]{32}$/i.test(received) || !/^[0-9a-f]{32}$/i.test(expected))
26
+ return false;
27
+ return timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
28
+ }
29
+ /**
30
+ * Shared redirect-mode handshake for any CLI command that hands the user
31
+ * off to an `/embed/*` page and waits for a localhost callback.
32
+ *
33
+ * Flow:
34
+ * 1. Bind a one-shot HTTP server on `127.0.0.1:0` (the OS picks a free port).
35
+ * 2. Compute `returnUrl = http://127.0.0.1:<port>/callback` and hand it
36
+ * to `opts.mintSession`. The caller mints a widget session bound to
37
+ * that exact returnUrl (which the backend can validate against the
38
+ * session-specific allow-list at creation time). We use the literal
39
+ * `127.0.0.1` rather than `localhost` because the server binds to
40
+ * `127.0.0.1` and Node 17+ resolves `localhost` to `::1` first on
41
+ * modern hosts — the browser would stall on the IPv6 attempt before
42
+ * falling back to IPv4.
43
+ * 3. Open the browser at `{frontend}/embed/<path>?sessionToken=…&returnUrl=…&state=<rand>`.
44
+ * 4. Resolve when the embed page redirects to `/callback?…&state=<echo>`.
45
+ * `state` is compared in constant time.
46
+ *
47
+ * Rejects on bind failure, mint failure, 5-minute timeout, or
48
+ * state-mismatched callback (the bad request gets a styled error page
49
+ * and the server stays alive — the legitimate callback can still land).
50
+ */
51
+ export async function runWidgetRedirectFlow(opts) {
52
+ const state = randomBytes(16).toString('hex');
53
+ return new Promise((resolve, reject) => {
54
+ let resolved = false;
55
+ let timeout;
56
+ const finalize = (err, value) => {
57
+ if (resolved)
58
+ return;
59
+ resolved = true;
60
+ if (timeout)
61
+ clearTimeout(timeout);
62
+ server.close();
63
+ if (err)
64
+ reject(err);
65
+ else if (value)
66
+ resolve(value);
67
+ };
68
+ const server = createServer((req, res) => {
69
+ const url = new URL(req.url || '/', 'http://localhost');
70
+ if (url.pathname !== '/callback') {
71
+ res.writeHead(404).end('Not found');
72
+ return;
73
+ }
74
+ const receivedState = url.searchParams.get('state');
75
+ if (!receivedState || !safeEqualHexState(receivedState, state)) {
76
+ // INTENTIONALLY do NOT close the server here. The state nonce
77
+ // is 128 bits of randomness, so brute-forcing one bad callback
78
+ // per request is infeasible. Closing on first 400 would let an
79
+ // attacker (or a misconfigured redirect) DoS the legitimate
80
+ // browser callback that's about to arrive. The 5-minute timer
81
+ // is the upper bound on how long we'll wait either way.
82
+ res.writeHead(400, { 'Content-Type': 'text/html' }).end(errorHtml('Callback rejected', 'State mismatch. This callback did not match the request that started the flow — close this tab and re-run the command.'));
83
+ return;
84
+ }
85
+ // Convert URLSearchParams → plain object, dropping the bookkeeping
86
+ // `state` field (the caller doesn't need to re-validate it).
87
+ const query = {};
88
+ for (const [key, value] of url.searchParams.entries()) {
89
+ if (key === 'state')
90
+ continue;
91
+ query[key] = value;
92
+ }
93
+ res.writeHead(200, { 'Content-Type': 'text/html' }).end(successHtml(opts.successPageTitle ?? 'All done', 'You can close this tab and return to your terminal.'));
94
+ finalize(undefined, { state, query });
95
+ });
96
+ timeout = setTimeout(() => {
97
+ finalize(new Error(opts.timeoutMessage ?? 'Browser flow timed out after 5 minutes. Please try again.'));
98
+ }, REDIRECT_TIMEOUT_MS);
99
+ server.on('error', (err) => {
100
+ finalize(new Error(`Failed to start local callback server: ${err.message}`));
101
+ });
102
+ server.listen(0, '127.0.0.1', () => {
103
+ const addr = server.address();
104
+ if (!addr || typeof addr === 'string') {
105
+ finalize(new Error('Failed to obtain local callback port'));
106
+ return;
107
+ }
108
+ // Symmetric with the server bind above (`127.0.0.1` only). Using
109
+ // the `localhost` alias here would cause an IPv6 stall on modern
110
+ // Linux/macOS where Node's `dns.lookup` prefers `::1`. The backend
111
+ // returnUrl allow-list accepts both forms.
112
+ const returnUrl = `http://127.0.0.1:${addr.port}/callback`;
113
+ // Mint the session now that returnUrl is known. The backend's
114
+ // allow-list check at session creation can validate the URL the
115
+ // browser will actually be redirected to.
116
+ void (async () => {
117
+ let sessionToken;
118
+ try {
119
+ const minted = await opts.mintSession({ returnUrl });
120
+ sessionToken = minted.sessionToken;
121
+ }
122
+ catch (mintErr) {
123
+ finalize(mintErr instanceof Error ? mintErr : new Error(String(mintErr)));
124
+ return;
125
+ }
126
+ const browserUrl = buildEmbedUrl({
127
+ frontendUrl: opts.frontendUrl,
128
+ embedPath: opts.embedPath,
129
+ sessionToken,
130
+ returnUrl,
131
+ state,
132
+ extra: opts.extraSearchParams,
133
+ });
134
+ if (opts.noBrowser) {
135
+ opts.log('Open this URL in your browser to continue:');
136
+ opts.log('');
137
+ opts.log(browserUrl);
138
+ opts.log('');
139
+ opts.log('Waiting for completion...');
140
+ }
141
+ else {
142
+ opts.log('Opening browser...');
143
+ openBrowser(browserUrl).catch(() => {
144
+ opts.log('Could not open the browser automatically. Open this URL manually:');
145
+ opts.log('');
146
+ opts.log(browserUrl);
147
+ });
148
+ opts.log('Waiting for completion...');
149
+ }
150
+ })();
151
+ });
152
+ });
153
+ }
154
+ function buildEmbedUrl(opts) {
155
+ const url = new URL(opts.embedPath, opts.frontendUrl);
156
+ url.searchParams.set('sessionToken', opts.sessionToken);
157
+ url.searchParams.set('returnUrl', opts.returnUrl);
158
+ url.searchParams.set('state', opts.state);
159
+ if (opts.extra) {
160
+ for (const [k, v] of Object.entries(opts.extra)) {
161
+ url.searchParams.set(k, v);
162
+ }
163
+ }
164
+ return url.toString();
165
+ }
166
+ function successHtml(title, message) {
167
+ // Plain HTML, no third-party assets, no scripts. The page is shown
168
+ // once and the user closes the tab; keep it self-contained so a
169
+ // captive-portal-style network doesn't break the success state.
170
+ return buildHtmlPage(title, message, '#f8f9fa', '#0f5132', '#d1e7dd');
171
+ }
172
+ function errorHtml(title, message) {
173
+ // Same shell as the success page (so the layout looks consistent if
174
+ // the user sees both back-to-back), but tinted red so a state mismatch
175
+ // or other 4xx response is visually distinct from "we're done".
176
+ return buildHtmlPage(title, message, '#f8f9fa', '#842029', '#f8d7da');
177
+ }
178
+ function buildHtmlPage(title, message, pageBg, fgColor, panelBg) {
179
+ return `<!doctype html>
180
+ <html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head>
181
+ <body style="font-family: system-ui, sans-serif; display:flex; align-items:center; justify-content:center; min-height:100vh; margin:0; background:${pageBg};">
182
+ <div style="text-align:center; max-width: 480px; padding: 24px; background:${panelBg}; color:${fgColor}; border-radius:8px;">
183
+ <h2 style="margin: 0 0 12px;">${escapeHtml(title)}</h2>
184
+ <p style="margin: 0;">${escapeHtml(message)}</p>
185
+ </div>
186
+ </body></html>`;
187
+ }
188
+ function escapeHtml(value) {
189
+ return value
190
+ .replace(/&/g, '&amp;')
191
+ .replace(/</g, '&lt;')
192
+ .replace(/>/g, '&gt;')
193
+ .replace(/"/g, '&quot;')
194
+ .replace(/'/g, '&#39;');
195
+ }
196
+ /**
197
+ * Hosts a request to `mintSelfWidgetSession` may be sent over plaintext
198
+ * without warning. Loopback addresses are always safe; every other host
199
+ * must use `https:` because we're about to send the user's NVM API key
200
+ * in the `Authorization` header. The `custom` environment variable
201
+ * (`NVM_BACKEND_URL`) is the only realistic way a user could accidentally
202
+ * point this at a plaintext non-localhost URL.
203
+ */
204
+ const LOOPBACK_HOSTNAMES = new Set([
205
+ 'localhost',
206
+ '127.0.0.1',
207
+ '::1',
208
+ '[::1]',
209
+ ]);
210
+ export async function mintSelfWidgetSession(args) {
211
+ const url = new URL('/api/v1/widgets/session/self', args.backendUrl);
212
+ // Refuse to send the API key over plaintext to a non-loopback host.
213
+ // All four built-in environments are `https://`, so this only bites
214
+ // when a user has set `NVM_BACKEND_URL` to a custom plaintext
215
+ // endpoint — exactly the case where they'd otherwise leak the key
216
+ // without realising.
217
+ if (url.protocol === 'http:' &&
218
+ !LOOPBACK_HOSTNAMES.has(url.hostname.toLowerCase())) {
219
+ throw new Error(`Refusing to send the NVM API key over plaintext to ${url.host}. Set NVM_BACKEND_URL to an https:// endpoint, or use a loopback host (localhost / 127.0.0.1 / [::1]).`);
220
+ }
221
+ // 15s ceiling for the request — without it, an unreachable backend
222
+ // (DNS failure, TLS handshake hang, network partition) leaves the CLI
223
+ // frozen with no output. `AbortSignal.timeout` (Node 17.3+) is the
224
+ // idiomatic replacement for a hand-rolled `AbortController` + setTimeout.
225
+ let response;
226
+ try {
227
+ response = await fetch(url, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ Authorization: `Bearer ${args.nvmApiKey}`,
232
+ },
233
+ body: JSON.stringify({
234
+ orgId: args.orgId,
235
+ ...(args.returnUrl ? { returnUrl: args.returnUrl } : {}),
236
+ }),
237
+ signal: AbortSignal.timeout(SELF_MINT_FETCH_TIMEOUT_MS),
238
+ });
239
+ }
240
+ catch (cause) {
241
+ if (cause instanceof Error && (cause.name === 'TimeoutError' || cause.name === 'AbortError')) {
242
+ throw new Error(`Self-mint widget session request timed out after ${SELF_MINT_FETCH_TIMEOUT_MS / 1000}s — check that ${args.backendUrl} is reachable.`);
243
+ }
244
+ throw cause;
245
+ }
246
+ if (!response.ok) {
247
+ const detail = await response
248
+ .json()
249
+ .catch(() => ({ message: response.statusText }));
250
+ const message = detail.message ??
251
+ `Self-mint widget session failed (HTTP ${response.status})`;
252
+ const err = new Error(message);
253
+ err.status = response.status;
254
+ err.apiCode = detail.code;
255
+ throw err;
256
+ }
257
+ return (await response.json());
258
+ }
259
+ //# sourceMappingURL=widget-redirect-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-redirect-flow.js","sourceRoot":"","sources":["../../src/utils/widget-redirect-flow.ts"],"names":[],"mappings":"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"}