@permute/sdk 0.6.0 → 0.8.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.
@@ -0,0 +1,64 @@
1
+ import { PermuteClient } from '../client.js';
2
+ import { type CredentialStore, type LoginCredentials } from './store.js';
3
+ export { SystemCredentialStore, type CredentialStore, type LoginCredentials } from './store.js';
4
+ export declare const DEFAULT_ISSUER = "https://auth.permute.ai";
5
+ export declare const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
6
+ export interface OAuthOptions {
7
+ issuer?: string;
8
+ apiUrl?: string;
9
+ store?: CredentialStore;
10
+ fetch?: typeof fetch;
11
+ }
12
+ export declare class OAuthLogin {
13
+ readonly issuer: string;
14
+ readonly apiUrl: string;
15
+ private readonly store;
16
+ private readonly fetchImpl;
17
+ constructor(options?: OAuthOptions);
18
+ private endpoint;
19
+ private post;
20
+ private tokens;
21
+ private persist;
22
+ beginDevice(): Promise<{
23
+ deviceCode: string;
24
+ userCode: string;
25
+ url: string;
26
+ expiresIn: number;
27
+ expiresAt: number;
28
+ interval: number;
29
+ }>;
30
+ finishDevice(request: Awaited<ReturnType<OAuthLogin['beginDevice']>>, signal?: AbortSignal): Promise<{
31
+ issuer: string;
32
+ apiUrl: string;
33
+ workspaceId: string | undefined;
34
+ organizationId: string;
35
+ sessionId: string;
36
+ access: string;
37
+ refresh: string;
38
+ expiresAt: number;
39
+ }>;
40
+ beginBrowser(redirectURI: string): {
41
+ url: string;
42
+ verifier: string;
43
+ state: string;
44
+ redirectURI: string;
45
+ };
46
+ finishBrowser(request: ReturnType<OAuthLogin['beginBrowser']>, callback: URL): Promise<{
47
+ issuer: string;
48
+ apiUrl: string;
49
+ workspaceId: string | undefined;
50
+ organizationId: string;
51
+ sessionId: string;
52
+ access: string;
53
+ refresh: string;
54
+ expiresAt: number;
55
+ }>;
56
+ credentials(): Promise<LoginCredentials>;
57
+ client(): Promise<PermuteClient>;
58
+ logout(): Promise<void>;
59
+ private revoke;
60
+ }
61
+ export declare class OAuthLoginError extends Error {
62
+ readonly code: string;
63
+ constructor(code: string);
64
+ }
@@ -0,0 +1,221 @@
1
+ import { z } from 'zod';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { setTimeout } from 'node:timers/promises';
4
+ import { PermuteClient } from '../client.js';
5
+ import { SystemCredentialStore } from './store.js';
6
+ export { SystemCredentialStore } from './store.js';
7
+ export const DEFAULT_ISSUER = 'https://auth.permute.ai';
8
+ const CLIENT_ID = 'permute-cli';
9
+ export const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
10
+ const TokensSchema = z.object({
11
+ access_token: z.string().min(1),
12
+ refresh_token: z.string().min(1),
13
+ expires_in: z.number().int().positive(),
14
+ workspace_id: z.string().min(1).optional(),
15
+ organization_id: z.string().min(1),
16
+ session_id: z.string().min(1),
17
+ });
18
+ const DeviceSchema = z.object({
19
+ device_code: z.string().min(1),
20
+ user_code: z.string().min(1),
21
+ verification_uri_complete: z.string().url(),
22
+ expires_in: z.number().int().positive(),
23
+ interval: z.number().int().min(5),
24
+ });
25
+ export class OAuthLogin {
26
+ issuer;
27
+ apiUrl;
28
+ store;
29
+ fetchImpl;
30
+ constructor(options = {}) {
31
+ this.issuer = this.endpoint(options.issuer ?? DEFAULT_ISSUER);
32
+ this.apiUrl = this.endpoint(options.apiUrl ?? 'https://api.permute.ai');
33
+ this.store = options.store ?? new SystemCredentialStore(this.issuer);
34
+ this.fetchImpl = options.fetch ?? fetch;
35
+ }
36
+ endpoint(value) {
37
+ const url = new URL(value);
38
+ if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(url.hostname))) ||
39
+ url.username ||
40
+ url.password ||
41
+ url.search ||
42
+ url.hash ||
43
+ url.pathname !== '/')
44
+ throw new Error('Use an HTTPS origin (or loopback for local development).');
45
+ return url.origin;
46
+ }
47
+ async post(path, input) {
48
+ const response = await this.fetchImpl(`${this.issuer}${path}`, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
51
+ body: new URLSearchParams(input),
52
+ signal: AbortSignal.timeout(15_000),
53
+ redirect: 'error',
54
+ });
55
+ const result = await response.json();
56
+ if (!response.ok) {
57
+ const error = z.object({ error: z.string().max(100) }).safeParse(result);
58
+ throw new OAuthLoginError(error.success ? error.data.error : 'login_failed');
59
+ }
60
+ return result;
61
+ }
62
+ tokens(value) {
63
+ const parsed = TokensSchema.safeParse(value);
64
+ if (!parsed.success)
65
+ throw new Error('Invalid credential response');
66
+ return parsed.data;
67
+ }
68
+ async persist(tokens) {
69
+ const credentials = {
70
+ issuer: this.issuer,
71
+ apiUrl: this.apiUrl,
72
+ workspaceId: tokens.workspace_id,
73
+ organizationId: tokens.organization_id,
74
+ sessionId: tokens.session_id,
75
+ access: tokens.access_token,
76
+ refresh: tokens.refresh_token,
77
+ expiresAt: Date.now() + tokens.expires_in * 1000,
78
+ };
79
+ try {
80
+ const previous = await this.store.read();
81
+ if (previous && previous.sessionId !== tokens.session_id)
82
+ await this.revoke(previous.refresh);
83
+ await this.store.write(credentials);
84
+ }
85
+ catch (error) {
86
+ // Failed replacement or secure storage must not orphan the new credential.
87
+ await this.revoke(tokens.refresh_token);
88
+ throw error;
89
+ }
90
+ return credentials;
91
+ }
92
+ async beginDevice() {
93
+ const parsed = DeviceSchema.safeParse(await this.post('/cli/device/authorization', { client_id: CLIENT_ID, scope: 'workspace' }));
94
+ if (!parsed.success)
95
+ throw new Error('Invalid device login response');
96
+ const result = parsed.data;
97
+ return {
98
+ deviceCode: result.device_code,
99
+ userCode: result.user_code,
100
+ url: result.verification_uri_complete,
101
+ expiresIn: result.expires_in,
102
+ expiresAt: Date.now() + result.expires_in * 1000,
103
+ interval: result.interval,
104
+ };
105
+ }
106
+ async finishDevice(request, signal) {
107
+ const deadline = request.expiresAt;
108
+ let interval = request.interval;
109
+ while (Date.now() < deadline) {
110
+ await setTimeout(interval * 1000, undefined, { signal });
111
+ try {
112
+ return await this.store.exclusive(async () => {
113
+ const tokens = this.tokens(await this.post('/token', {
114
+ client_id: CLIENT_ID,
115
+ grant_type: DEVICE_GRANT,
116
+ device_code: request.deviceCode,
117
+ }));
118
+ return this.persist(tokens);
119
+ });
120
+ }
121
+ catch (error) {
122
+ if (!(error instanceof OAuthLoginError))
123
+ throw error;
124
+ if (error.code === 'authorization_pending')
125
+ continue;
126
+ if (error.code === 'slow_down') {
127
+ interval += 5;
128
+ continue;
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+ throw new OAuthLoginError('expired_token');
134
+ }
135
+ beginBrowser(redirectURI) {
136
+ const verifier = randomBytes(32).toString('base64url');
137
+ const state = randomBytes(32).toString('base64url');
138
+ const url = new URL(`${this.issuer}/authorize`);
139
+ url.search = new URLSearchParams({
140
+ client_id: CLIENT_ID,
141
+ scope: 'workspace',
142
+ response_type: 'code',
143
+ redirect_uri: redirectURI,
144
+ state,
145
+ code_challenge: createHash('sha256').update(verifier).digest('base64url'),
146
+ code_challenge_method: 'S256',
147
+ }).toString();
148
+ return { url: url.toString(), verifier, state, redirectURI };
149
+ }
150
+ async finishBrowser(request, callback) {
151
+ if (callback.origin + callback.pathname !== request.redirectURI ||
152
+ callback.searchParams.get('state') !== request.state)
153
+ throw new Error('Login callback did not match this request');
154
+ if (callback.searchParams.has('error'))
155
+ throw new OAuthLoginError('access_denied');
156
+ const code = callback.searchParams.get('code');
157
+ if (!code)
158
+ throw new Error('Login callback has no authorization code');
159
+ return this.store.exclusive(async () => {
160
+ const tokens = this.tokens(await this.post('/token', {
161
+ client_id: CLIENT_ID,
162
+ grant_type: 'authorization_code',
163
+ redirect_uri: request.redirectURI,
164
+ code_verifier: request.verifier,
165
+ code,
166
+ }));
167
+ return this.persist(tokens);
168
+ });
169
+ }
170
+ async credentials() {
171
+ return this.store.exclusive(async () => {
172
+ const current = await this.store.read();
173
+ if (!current || current.apiUrl !== this.apiUrl)
174
+ throw new Error('No login for this server. Run permute auth login.');
175
+ if (current.expiresAt > Date.now() + 60_000)
176
+ return current;
177
+ const tokens = this.tokens(await this.post('/token', {
178
+ client_id: CLIENT_ID,
179
+ grant_type: 'refresh_token',
180
+ refresh_token: current.refresh,
181
+ }));
182
+ return this.persist(tokens);
183
+ });
184
+ }
185
+ async client() {
186
+ const credentials = await this.credentials();
187
+ return new PermuteClient({
188
+ baseUrl: this.apiUrl,
189
+ fetch: this.fetchImpl,
190
+ workspaceId: credentials.workspaceId,
191
+ accessToken: async () => (await this.credentials()).access,
192
+ });
193
+ }
194
+ async logout() {
195
+ await this.store.exclusive(async () => {
196
+ const current = await this.store.read();
197
+ if (!current)
198
+ return;
199
+ // Keep the local record on failure so revocation can be retried.
200
+ await this.revoke(current.refresh);
201
+ await this.store.remove();
202
+ });
203
+ }
204
+ async revoke(refresh) {
205
+ const response = await this.fetchImpl(`${this.issuer}/revoke`, {
206
+ method: 'POST',
207
+ body: new URLSearchParams({ client_id: CLIENT_ID, token: refresh }),
208
+ signal: AbortSignal.timeout(15_000),
209
+ redirect: 'error',
210
+ });
211
+ if (!response.ok)
212
+ throw new Error('Could not revoke login. Retry logout when connected.');
213
+ }
214
+ }
215
+ export class OAuthLoginError extends Error {
216
+ code;
217
+ constructor(code) {
218
+ super(`Login failed (${code}). Run permute auth login to try again.`);
219
+ this.code = code;
220
+ }
221
+ }
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ declare const LoginCredentialsSchema: z.ZodObject<{
3
+ issuer: z.ZodString;
4
+ apiUrl: z.ZodString;
5
+ workspaceId: z.ZodOptional<z.ZodString>;
6
+ organizationId: z.ZodOptional<z.ZodString>;
7
+ sessionId: z.ZodString;
8
+ access: z.ZodString;
9
+ refresh: z.ZodString;
10
+ expiresAt: z.ZodNumber;
11
+ }, z.core.$strip>;
12
+ export type LoginCredentials = z.infer<typeof LoginCredentialsSchema>;
13
+ export interface CredentialStore {
14
+ read(): Promise<LoginCredentials | null>;
15
+ write(value: LoginCredentials): Promise<void>;
16
+ remove(): Promise<void>;
17
+ exclusive<T>(run: () => Promise<T>): Promise<T>;
18
+ }
19
+ /** Tokens never fall back to a plaintext file. The disk lock contains no credentials. */
20
+ export declare class SystemCredentialStore implements CredentialStore {
21
+ private readonly issuer;
22
+ private readonly account;
23
+ constructor(issuer: string);
24
+ private entry;
25
+ read(): Promise<{
26
+ issuer: string;
27
+ apiUrl: string;
28
+ workspaceId?: string | undefined;
29
+ organizationId?: string | undefined;
30
+ sessionId: string;
31
+ access: string;
32
+ refresh: string;
33
+ expiresAt: number;
34
+ } | null>;
35
+ write(value: LoginCredentials): Promise<void>;
36
+ remove(): Promise<void>;
37
+ exclusive<T>(run: () => Promise<T>): Promise<T>;
38
+ }
39
+ export {};
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ import { createHash } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { mkdir, rm } from 'node:fs/promises';
6
+ import { setTimeout } from 'node:timers/promises';
7
+ const LoginCredentialsSchema = z.object({
8
+ issuer: z.string().url(),
9
+ apiUrl: z.string().url(),
10
+ workspaceId: z.string().min(1).optional(),
11
+ organizationId: z.string().min(1).optional(),
12
+ sessionId: z.string().min(1),
13
+ access: z.string().min(1),
14
+ refresh: z.string().min(1),
15
+ expiresAt: z.number().finite(),
16
+ });
17
+ /** Tokens never fall back to a plaintext file. The disk lock contains no credentials. */
18
+ export class SystemCredentialStore {
19
+ issuer;
20
+ account;
21
+ constructor(issuer) {
22
+ this.issuer = issuer;
23
+ this.account = createHash('sha256').update(issuer).digest('hex');
24
+ }
25
+ async entry() {
26
+ const { AsyncEntry } = await import('@napi-rs/keyring');
27
+ return new AsyncEntry('permute-cli', this.account, { linux: { store: 'secret-service' } });
28
+ }
29
+ async read() {
30
+ const value = await (await this.entry()).getPassword();
31
+ if (!value)
32
+ return null;
33
+ const parsed = LoginCredentialsSchema.safeParse(JSON.parse(value));
34
+ if (!parsed.success || parsed.data.issuer !== this.issuer)
35
+ throw new Error('Stored login is invalid. Run permute auth login again.');
36
+ return parsed.data;
37
+ }
38
+ async write(value) {
39
+ await (await this.entry()).setPassword(JSON.stringify(value));
40
+ }
41
+ async remove() {
42
+ const entry = await this.entry();
43
+ if (await entry.getPassword())
44
+ await entry.deletePassword();
45
+ }
46
+ async exclusive(run) {
47
+ const directory = join(homedir(), '.config', 'permute', 'auth');
48
+ await mkdir(directory, { recursive: true, mode: 0o700 });
49
+ const lock = join(directory, `${this.account}.lock`);
50
+ const deadline = Date.now() + 30_000;
51
+ while (true) {
52
+ const acquired = await mkdir(lock, { mode: 0o700 }).then(() => true, (error) => {
53
+ if (error.code !== 'EEXIST')
54
+ throw error;
55
+ return false;
56
+ });
57
+ if (acquired)
58
+ break;
59
+ if (Date.now() >= deadline)
60
+ throw new Error(`Another process is updating this login. Retry after it finishes. If a process crashed, stop other Permute CLI processes before removing ${lock}.`);
61
+ await setTimeout(100);
62
+ }
63
+ try {
64
+ return await run();
65
+ }
66
+ finally {
67
+ await rm(lock, { recursive: true, force: true });
68
+ }
69
+ }
70
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './auth/oauth.js';
package/dist/auth.js ADDED
@@ -0,0 +1 @@
1
+ export * from './auth/oauth.js';
@@ -0,0 +1,10 @@
1
+ import { PermuteClient } from './client.js';
2
+ import type { PublicApiOperationId } from './generated/runtime-contract.js';
3
+ /** Executes the SDK itself behind a JSON RPC boundary; it does not implement API operations. */
4
+ export declare class PermuteSdkBridge {
5
+ private readonly client;
6
+ constructor(client: PermuteClient);
7
+ supports(method: string, acceptsOperation?: (operationId: PublicApiOperationId) => boolean): boolean;
8
+ invoke(method: string, args: unknown[]): Promise<unknown>;
9
+ private resolve;
10
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,62 @@
1
+ import { PermuteDataset, PermuteFileDataset } from './client.js';
2
+ import { SdkMethodFactory } from './method.js';
3
+ const safeName = (name) => /^[A-Za-z][A-Za-z0-9_]*$/.test(name) && !['constructor', 'prototype', '__proto__'].includes(name);
4
+ function decode(value) {
5
+ if (!value || typeof value !== 'object')
6
+ return value;
7
+ if (Array.isArray(value))
8
+ return value.map(decode);
9
+ const item = value;
10
+ if (Object.keys(item).length === 1 && Array.isArray(item.__permuteSdkBytes)) {
11
+ if (!item.__permuteSdkBytes.every((byte) => typeof byte === 'number' && Number.isInteger(byte) && byte >= 0 && byte <= 255))
12
+ throw new Error('Invalid SDK byte data.');
13
+ return new Uint8Array(item.__permuteSdkBytes);
14
+ }
15
+ return Object.fromEntries(Object.entries(item).map(([key, child]) => [key, decode(child)]));
16
+ }
17
+ /** Executes the SDK itself behind a JSON RPC boundary; it does not implement API operations. */
18
+ export class PermuteSdkBridge {
19
+ client;
20
+ constructor(client) {
21
+ this.client = client;
22
+ }
23
+ supports(method, acceptsOperation) {
24
+ const selected = this.resolve(method);
25
+ if (!selected)
26
+ return false;
27
+ const operations = SdkMethodFactory.operationsFor(selected.fn);
28
+ return !!operations?.length && (!acceptsOperation || operations.every(acceptsOperation));
29
+ }
30
+ async invoke(method, args) {
31
+ const selected = this.resolve(method);
32
+ if (!selected)
33
+ throw new Error(`Unsupported SDK method: ${method}`);
34
+ const value = await Reflect.apply(selected.fn, selected.owner, args.map(decode));
35
+ // Resource helpers contain callbacks. Their public data is the portable result of an RPC.
36
+ const data = value instanceof PermuteDataset || value instanceof PermuteFileDataset ? value.data : value;
37
+ return JSON.parse(JSON.stringify(data ?? null));
38
+ }
39
+ resolve(method) {
40
+ const names = method.split('.');
41
+ if (names.some((name) => !safeName(name)) || names.length > 2)
42
+ return null;
43
+ const client = this.client;
44
+ if (names.length === 1) {
45
+ const name = names[0];
46
+ const fn = client[name];
47
+ return typeof fn === 'function' && SdkMethodFactory.operationsFor(fn)
48
+ ? { owner: this.client, fn: fn }
49
+ : null;
50
+ }
51
+ const namespace = client[names[0]];
52
+ if (!Object.hasOwn(client, names[0]) ||
53
+ !namespace ||
54
+ typeof namespace !== 'object' ||
55
+ !Object.hasOwn(namespace, names[1]))
56
+ return null;
57
+ const fn = namespace[names[1]];
58
+ return typeof fn === 'function' && SdkMethodFactory.operationsFor(fn)
59
+ ? { owner: namespace, fn: fn }
60
+ : null;
61
+ }
62
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runCli(args?: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from 'node:http';
3
+ import { spawn } from 'node:child_process';
4
+ import { parseArgs } from 'node:util';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { realpathSync } from 'node:fs';
7
+ import { OAuthLogin } from './auth.js';
8
+ function openBrowser(url) {
9
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'linux' ? 'xdg-open' : undefined;
10
+ if (!command)
11
+ return;
12
+ const child = spawn(command, [url], { stdio: 'ignore' });
13
+ child.on('error', () => {
14
+ /* The printed link also works when no local browser is available. */
15
+ });
16
+ child.unref();
17
+ }
18
+ async function browserLogin(login, open) {
19
+ let destination;
20
+ let resolveCallback;
21
+ let rejectCallback;
22
+ const callback = new Promise((resolve, reject) => {
23
+ resolveCallback = resolve;
24
+ rejectCallback = reject;
25
+ });
26
+ const server = createServer((request, response) => {
27
+ if (!destination ||
28
+ request.method !== 'GET' ||
29
+ request.headers.host !== new URL(destination.redirectURI).host ||
30
+ !request.url?.startsWith('/callback?')) {
31
+ response.writeHead(400).end('This callback does not match the login request.');
32
+ return;
33
+ }
34
+ const url = new URL(request.url, destination.redirectURI);
35
+ if (url.searchParams.get('state') !== destination.state) {
36
+ response.writeHead(400).end('This callback does not match the login request.');
37
+ return;
38
+ }
39
+ response
40
+ .writeHead(200, {
41
+ 'Content-Type': 'text/plain',
42
+ 'Cache-Control': 'no-store',
43
+ 'Content-Security-Policy': "default-src 'none'",
44
+ })
45
+ .end('Return to your terminal to finish connecting. You can close this tab.');
46
+ resolveCallback(url);
47
+ });
48
+ await new Promise((resolve, reject) => {
49
+ server.once('error', reject);
50
+ server.listen(0, '127.0.0.1', resolve);
51
+ });
52
+ const address = server.address();
53
+ if (!address || typeof address === 'string')
54
+ throw new Error('Could not open a loopback callback');
55
+ destination = login.beginBrowser(`http://127.0.0.1:${address.port}/callback`);
56
+ const timeout = setTimeout(() => rejectCallback(new Error('Login timed out. Start login again.')), 600_000);
57
+ const cancel = () => rejectCallback(new Error('Login canceled.'));
58
+ process.once('SIGINT', cancel);
59
+ try {
60
+ console.log(`Open this link to connect Permute CLI:\n${destination.url}`);
61
+ if (open)
62
+ openBrowser(destination.url);
63
+ return await login.finishBrowser(destination, await callback);
64
+ }
65
+ finally {
66
+ clearTimeout(timeout);
67
+ process.off('SIGINT', cancel);
68
+ server.closeAllConnections();
69
+ server.close();
70
+ }
71
+ }
72
+ export async function runCli(args = process.argv.slice(2)) {
73
+ const { values, positionals } = parseArgs({
74
+ args,
75
+ allowPositionals: true,
76
+ options: {
77
+ issuer: { type: 'string' },
78
+ 'api-url': { type: 'string' },
79
+ 'device-code': { type: 'boolean' },
80
+ 'no-browser': { type: 'boolean' },
81
+ help: { type: 'boolean' },
82
+ },
83
+ });
84
+ const [area, command] = positionals;
85
+ if (values.help || area !== 'auth' || !['login', 'logout', 'status'].includes(command ?? '')) {
86
+ console.log('permute auth login [--device-code] [--no-browser]\npermute auth status\npermute auth logout\n\nUse --issuer and --api-url together for a development environment.');
87
+ if (!values.help)
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ if (!!values.issuer !== !!values['api-url'])
92
+ throw new Error('Provide both --issuer and --api-url for another environment.');
93
+ const login = new OAuthLogin({ issuer: values.issuer, apiUrl: values['api-url'] });
94
+ if (command === 'logout') {
95
+ await login.logout();
96
+ console.log('CLI login revoked and removed from this device.');
97
+ return;
98
+ }
99
+ if (command === 'status') {
100
+ const credentials = await login.credentials();
101
+ console.log(`Connected to ${credentials.apiUrl}\nOrganization: ${credentials.organizationId ?? 'Not specified'}\nWorkspace: ${credentials.workspaceId ?? 'All accessible workspaces in this organization'}`);
102
+ return;
103
+ }
104
+ const controller = new AbortController();
105
+ const cancel = () => controller.abort();
106
+ process.once('SIGINT', cancel);
107
+ try {
108
+ if (values['device-code']) {
109
+ const request = await login.beginDevice();
110
+ console.log(`Open this link on any device:\n${request.url}\n\nConfirm this code: ${request.userCode}\nWaiting for your approval…`);
111
+ if (!values['no-browser'])
112
+ openBrowser(request.url);
113
+ await login.finishDevice(request, controller.signal);
114
+ }
115
+ else
116
+ await browserLogin(login, !values['no-browser']);
117
+ console.log('Connected. Credentials are saved in your operating system’s credential store.');
118
+ }
119
+ finally {
120
+ process.off('SIGINT', cancel);
121
+ }
122
+ }
123
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href)
124
+ runCli().catch(() => {
125
+ console.error('Could not complete authentication. Ensure your OS credential store is available, then retry. Use --device-code --no-browser to approve from another device.');
126
+ process.exitCode = 1;
127
+ });