@doubledigit/cli 0.12.0 → 0.13.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.
package/README.md CHANGED
@@ -126,6 +126,28 @@ npx @doubledigit/cli@latest actions component-hub register-component --json-file
126
126
 
127
127
  Component Hub uses the canonical `component-hub` action command. `init` and `add` are handled locally by the CLI because they create or modify files on the caller's machine. Other actions, such as search and registry lookup, call the configured Double Digit app over HTTP. The old `remotion-hub` action name remains a legacy alias.
128
128
 
129
+ ### Authentication
130
+
131
+ Public catalog actions can still run without credentials. Project-scoped actions and private deployments require either an interactive login or an API key:
132
+
133
+ ```bash
134
+ npx @doubledigit/cli@latest login --url https://your-double-digit-app.example
135
+ npx @doubledigit/cli@latest whoami --url https://your-double-digit-app.example
136
+ npx @doubledigit/cli@latest org list --url https://your-double-digit-app.example
137
+ npx @doubledigit/cli@latest org use acme --url https://your-double-digit-app.example
138
+ npx @doubledigit/cli@latest actions component-hub list-projects --org acme --url https://your-double-digit-app.example
139
+ ```
140
+
141
+ `dd login` stores a per-host bearer credential under the user config directory (`~/.config/doubledigit/credentials.json` on Linux/macOS, `%APPDATA%\doubledigit\credentials.json` on Windows) with private file permissions where supported. `dd logout` removes the stored host credential.
142
+
143
+ CI and agent automation should use an API key created from the app admin security dashboard at `/admin/security/api-keys`:
144
+
145
+ ```bash
146
+ DD_API_KEY=dd_... DD_ORG=acme npx @doubledigit/cli@latest actions component-hub list-projects --url https://your-double-digit-app.example
147
+ ```
148
+
149
+ Auth header resolution is `DD_API_KEY` or `DD_TOKEN` first, then the stored login token. Active organization resolution is `--org`, then `DD_ORG`, then the stored default selected by `dd org use`.
150
+
129
151
  Pass `--framework remotion|hyperframe` when the target framework is known. Remotion remains the default for compatibility and creates Remotion's Hello World starter so `remotion studio` opens with a visible composition. HyperFrames init requires Node.js 22 or newer and uses `npx hyperframes preview` as its dev command.
130
152
 
131
153
  Pass `--skip-framework-create` when adding Component Hub files to an existing project. `--skip-remotion-create` and `--hub-only` remain compatibility aliases.
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/commands/actions.ts"],"names":[],"mappings":"AAuCA,wBAAgB,uBAAuB,WAKtC;AAsED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE;WACf,MAAM;WAAS,MAAM;IAuBxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,WAGtD;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,WAGrD;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,EAAE,YAI9D;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,YAI7D;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuC3D"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/commands/actions.ts"],"names":[],"mappings":"AAwCA,wBAAgB,uBAAuB,WAKtC;AAuED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE;WACf,MAAM;WAAS,MAAM;IAuBxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,WAGtD;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,WAGrD;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,EAAE,YAI9D;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,YAI7D;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuC3D"}
@@ -16,6 +16,7 @@ const actionValueOptions = new Set([
16
16
  '--kind',
17
17
  '--limit',
18
18
  '--namespace',
19
+ '--org',
19
20
  '--preview-type',
20
21
  '--query',
21
22
  '--json-file',
@@ -41,6 +42,7 @@ Usage:
41
42
 
42
43
  Options:
43
44
  --url, --app-url <url> App URL (default: DD_APP_URL, APP_URL, BETTER_AUTH_URL, or ${getDefaultActionsAppUrl()})
45
+ --org <slug> Active organization slug (default: --org, DD_ORG, or stored login default)
44
46
  --json <json> JSON request body to send to the action
45
47
  --json-file <path> Read the JSON request body from a file
46
48
  --query <text> Set input.query
@@ -0,0 +1,15 @@
1
+ import { type AuthStorePathsOptions } from '../lib/auth-store.js';
2
+ type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
3
+ export interface AuthCommandOptions extends AuthStorePathsOptions {
4
+ env?: NodeJS.ProcessEnv;
5
+ fetchImpl?: FetchLike;
6
+ sleep?: (ms: number) => Promise<void>;
7
+ log?: (...parts: unknown[]) => void;
8
+ now?: () => number;
9
+ }
10
+ export declare function login(args: string[], options?: AuthCommandOptions): Promise<void>;
11
+ export declare function logout(args: string[], options?: AuthCommandOptions): Promise<void>;
12
+ export declare function whoami(args: string[], options?: AuthCommandOptions): Promise<void>;
13
+ export declare function org(args: string[], options?: AuthCommandOptions): Promise<void>;
14
+ export {};
15
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,qBAAqB,EAE3B,MAAM,sBAAsB,CAAC;AAK9B,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEhF,MAAM,WAAW,kBAAmB,SAAQ,qBAAqB;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAyZD,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwD3F;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAc5F;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkC5F;AAED,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+EzF"}
@@ -0,0 +1,467 @@
1
+ import path from 'node:path';
2
+ import { GENERATED_DEFAULT_ACTIONS_APP_URL } from '../generated/defaults.js';
3
+ import { normalizeAppUrl, resolveActionAppUrl, resolveDefaultActionAppUrl, } from '../lib/actions-client.js';
4
+ import { resolveAuthHeaders } from '../lib/auth-credential.js';
5
+ import { clearStoredCredential, readStoredCredential, writeStoredCredential, } from '../lib/auth-store.js';
6
+ import { DEFAULT_APP_URL, readEnvFile } from '../lib/onboarding.js';
7
+ import { resolveWorkspacePaths } from '../paths.js';
8
+ const CLIENT_ID = 'dd-cli';
9
+ const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
10
+ const HELP = `
11
+ dd auth — Authenticate the Double Digit CLI
12
+
13
+ Usage:
14
+ dd login [--url <url>]
15
+ dd logout [--url <url>]
16
+ dd whoami [--url <url>] [--json]
17
+ dd org list [--url <url>] [--json]
18
+ dd org use <slug> [--url <url>]
19
+
20
+ Options:
21
+ --url, --app-url <url> App URL (default: DD_APP_URL, APP_URL, BETTER_AUTH_URL, or ${getDefaultAuthAppUrl()})
22
+ --json Print machine-readable output where supported
23
+ `;
24
+ function getDefaultAuthAppUrl() {
25
+ return resolveDefaultActionAppUrl({
26
+ generatedUrl: GENERATED_DEFAULT_ACTIONS_APP_URL,
27
+ localDefaultUrl: DEFAULT_APP_URL,
28
+ });
29
+ }
30
+ function readAuthEnvFiles(paths) {
31
+ const env = {};
32
+ for (const envPath of [
33
+ path.join(paths.root, '.env'),
34
+ path.join(paths.mainAppDir, '.env'),
35
+ path.join(paths.root, '.env.local'),
36
+ path.join(paths.mainAppDir, '.env.local'),
37
+ ]) {
38
+ Object.assign(env, readEnvFile(envPath));
39
+ }
40
+ return env;
41
+ }
42
+ function tryResolveWorkspacePaths() {
43
+ try {
44
+ return resolveWorkspacePaths();
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ function parseAuthArgs(args) {
51
+ const positionals = [];
52
+ let appUrl;
53
+ let json = false;
54
+ let help = false;
55
+ for (let i = 0; i < args.length; i++) {
56
+ const arg = args[i];
57
+ if (arg === '--help' || arg === '-h') {
58
+ help = true;
59
+ continue;
60
+ }
61
+ if (arg === '--json') {
62
+ json = true;
63
+ continue;
64
+ }
65
+ if (!arg.startsWith('--')) {
66
+ positionals.push(arg);
67
+ continue;
68
+ }
69
+ const [flag, inlineValue] = arg.split(/=(.*)/s, 2);
70
+ if (flag !== '--url' && flag !== '--app-url') {
71
+ throw new Error(`Unknown option: ${flag}`);
72
+ }
73
+ if (inlineValue !== undefined) {
74
+ appUrl = inlineValue;
75
+ continue;
76
+ }
77
+ const value = args[i + 1];
78
+ if (!value || value.startsWith('--')) {
79
+ throw new Error(`${flag} requires a value`);
80
+ }
81
+ appUrl = value;
82
+ i++;
83
+ }
84
+ return { appUrl, json, help, positionals };
85
+ }
86
+ function resolveAuthAppUrl(parsed) {
87
+ const paths = tryResolveWorkspacePaths();
88
+ return normalizeAppUrl(resolveActionAppUrl({
89
+ explicitUrl: parsed.appUrl,
90
+ fileEnv: paths ? readAuthEnvFiles(paths) : {},
91
+ defaultUrl: getDefaultAuthAppUrl(),
92
+ }));
93
+ }
94
+ async function parseJsonResponse(response, url) {
95
+ const text = await response.text();
96
+ if (!text)
97
+ return null;
98
+ try {
99
+ return JSON.parse(text);
100
+ }
101
+ catch {
102
+ if (!response.ok) {
103
+ throw new Error(`Request to ${url} failed with HTTP ${response.status}`);
104
+ }
105
+ throw new Error(`Expected JSON response from ${url}`);
106
+ }
107
+ }
108
+ function objectPayload(payload) {
109
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
110
+ ? payload
111
+ : {};
112
+ }
113
+ function readString(payload, ...keys) {
114
+ for (const key of keys) {
115
+ const value = payload[key];
116
+ if (typeof value === 'string' && value.trim())
117
+ return value;
118
+ if (typeof value === 'number' && Number.isFinite(value))
119
+ return String(value);
120
+ }
121
+ return undefined;
122
+ }
123
+ function readNumber(payload, fallback, ...keys) {
124
+ for (const key of keys) {
125
+ const value = payload[key];
126
+ if (typeof value === 'number' && Number.isFinite(value))
127
+ return value;
128
+ }
129
+ return fallback;
130
+ }
131
+ function getErrorCode(payload) {
132
+ const object = objectPayload(payload);
133
+ return readString(object, 'error', 'code') ?? 'request_failed';
134
+ }
135
+ function getErrorMessage(payload, fallback) {
136
+ const object = objectPayload(payload);
137
+ return readString(object, 'error_description', 'message', 'error') ?? fallback;
138
+ }
139
+ async function fetchJson(fetchImpl, url, init) {
140
+ let response;
141
+ try {
142
+ response = await fetchImpl(url, init);
143
+ }
144
+ catch (error) {
145
+ const detail = error instanceof Error ? error.message : String(error);
146
+ throw new Error(`Unable to reach ${url}: ${detail}. Set DD_APP_URL or pass --url.`);
147
+ }
148
+ return { response, payload: await parseJsonResponse(response, url) };
149
+ }
150
+ function normalizeDeviceCode(payload) {
151
+ const object = objectPayload(payload);
152
+ const deviceCode = readString(object, 'device_code', 'deviceCode');
153
+ const userCode = readString(object, 'user_code', 'userCode');
154
+ const verificationUri = readString(object, 'verification_uri', 'verificationUri');
155
+ if (!deviceCode || !userCode || !verificationUri) {
156
+ throw new Error('Device authorization response did not include a device code, user code, and verification URL.');
157
+ }
158
+ return {
159
+ deviceCode,
160
+ userCode,
161
+ verificationUri,
162
+ verificationUriComplete: readString(object, 'verification_uri_complete', 'verificationUriComplete'),
163
+ expiresIn: readNumber(object, 600, 'expires_in', 'expiresIn'),
164
+ interval: readNumber(object, 5, 'interval'),
165
+ };
166
+ }
167
+ function normalizeTokenSuccess(payload) {
168
+ const object = objectPayload(payload);
169
+ const accessToken = readString(object, 'access_token', 'accessToken');
170
+ if (!accessToken) {
171
+ throw new Error('Device authorization token response did not include an access token.');
172
+ }
173
+ return {
174
+ accessToken,
175
+ expiresIn: readNumber(object, 0, 'expires_in', 'expiresIn') || undefined,
176
+ };
177
+ }
178
+ function normalizeSessionIdentity(payload) {
179
+ const object = objectPayload(payload);
180
+ const userPayload = objectPayload(object.user);
181
+ const sessionPayload = objectPayload(object.session);
182
+ const user = {};
183
+ const id = readString(userPayload, 'id');
184
+ const email = readString(userPayload, 'email');
185
+ if (id)
186
+ user.id = id;
187
+ if (email)
188
+ user.email = email;
189
+ const activeOrganizationId = readString(sessionPayload, 'activeOrganizationId');
190
+ return {
191
+ ...(Object.keys(user).length > 0 ? { user } : {}),
192
+ session: activeOrganizationId ? { activeOrganizationId } : undefined,
193
+ };
194
+ }
195
+ function normalizeOrganizations(payload) {
196
+ const source = Array.isArray(payload)
197
+ ? payload
198
+ : Array.isArray(objectPayload(payload).organizations)
199
+ ? objectPayload(payload).organizations
200
+ : [];
201
+ return source.map((entry) => {
202
+ const object = objectPayload(entry);
203
+ return {
204
+ id: readString(object, 'id'),
205
+ slug: readString(object, 'slug'),
206
+ name: readString(object, 'name'),
207
+ };
208
+ }).filter((organization) => organization.id || organization.slug || organization.name);
209
+ }
210
+ async function fetchSession(appUrl, headers, fetchImpl) {
211
+ const url = `${appUrl}/api/auth/get-session`;
212
+ const { response, payload } = await fetchJson(fetchImpl, url, {
213
+ method: 'GET',
214
+ headers,
215
+ });
216
+ if (!response.ok || !payload) {
217
+ throw new Error(getErrorMessage(payload, 'Not signed in.'));
218
+ }
219
+ const identity = normalizeSessionIdentity(payload);
220
+ if (!identity.user?.id && !identity.user?.email) {
221
+ throw new Error('Not signed in.');
222
+ }
223
+ return identity;
224
+ }
225
+ async function fetchOrganizations(appUrl, headers, fetchImpl) {
226
+ const url = `${appUrl}/api/auth/organization/list`;
227
+ const { response, payload } = await fetchJson(fetchImpl, url, {
228
+ method: 'GET',
229
+ headers,
230
+ });
231
+ if (!response.ok) {
232
+ throw new Error(getErrorMessage(payload, 'Unable to list organizations.'));
233
+ }
234
+ return normalizeOrganizations(payload);
235
+ }
236
+ async function resolveLoginDefaultOrg({ existingDefaultOrg, activeOrganizationId, appUrl, headers, fetchImpl, }) {
237
+ if (existingDefaultOrg) {
238
+ return existingDefaultOrg;
239
+ }
240
+ if (!activeOrganizationId) {
241
+ return undefined;
242
+ }
243
+ try {
244
+ const organizations = await fetchOrganizations(appUrl, headers, fetchImpl);
245
+ const match = organizations.find((organization) => organization.id === activeOrganizationId);
246
+ return match?.slug ?? match?.id ?? activeOrganizationId;
247
+ }
248
+ catch {
249
+ return activeOrganizationId;
250
+ }
251
+ }
252
+ async function requestDeviceCode(appUrl, fetchImpl) {
253
+ const url = `${appUrl}/api/auth/device/code`;
254
+ const { response, payload } = await fetchJson(fetchImpl, url, {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify({ client_id: CLIENT_ID }),
258
+ });
259
+ if (!response.ok) {
260
+ throw new Error(getErrorMessage(payload, 'Unable to start device authorization.'));
261
+ }
262
+ return normalizeDeviceCode(payload);
263
+ }
264
+ async function pollDeviceToken({ appUrl, deviceCode, expiresIn, interval, fetchImpl, sleep, now, }) {
265
+ const url = `${appUrl}/api/auth/device/token`;
266
+ const deadline = now() + expiresIn * 1000;
267
+ let intervalMs = Math.max(1, interval) * 1000;
268
+ while (now() < deadline) {
269
+ const { response, payload } = await fetchJson(fetchImpl, url, {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({
273
+ grant_type: DEVICE_GRANT_TYPE,
274
+ device_code: deviceCode,
275
+ client_id: CLIENT_ID,
276
+ }),
277
+ });
278
+ if (response.ok) {
279
+ return normalizeTokenSuccess(payload);
280
+ }
281
+ const code = getErrorCode(payload);
282
+ if (code === 'authorization_pending') {
283
+ await sleep(intervalMs);
284
+ continue;
285
+ }
286
+ if (code === 'slow_down') {
287
+ intervalMs += 5000;
288
+ await sleep(intervalMs);
289
+ continue;
290
+ }
291
+ if (code === 'expired_token') {
292
+ throw new Error('Device authorization expired. Run `dd login` again.');
293
+ }
294
+ if (code === 'access_denied') {
295
+ throw new Error('Device authorization was denied.');
296
+ }
297
+ throw new Error(getErrorMessage(payload, 'Device authorization failed.'));
298
+ }
299
+ throw new Error('Device authorization expired. Run `dd login` again.');
300
+ }
301
+ export async function login(args, options = {}) {
302
+ const parsed = parseAuthArgs(args);
303
+ const log = options.log ?? console.log;
304
+ if (parsed.help) {
305
+ log(HELP);
306
+ return;
307
+ }
308
+ if (parsed.positionals.length > 0) {
309
+ throw new Error(`Unexpected positional argument: ${parsed.positionals[0]}`);
310
+ }
311
+ const appUrl = resolveAuthAppUrl(parsed);
312
+ const fetchImpl = options.fetchImpl ?? fetch;
313
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
314
+ const now = options.now ?? Date.now;
315
+ const code = await requestDeviceCode(appUrl, fetchImpl);
316
+ log(`Visit: ${code.verificationUriComplete ?? code.verificationUri}`);
317
+ log(`Code: ${code.userCode}`);
318
+ log('Waiting for approval...');
319
+ const token = await pollDeviceToken({
320
+ appUrl,
321
+ deviceCode: code.deviceCode,
322
+ expiresIn: code.expiresIn,
323
+ interval: code.interval,
324
+ fetchImpl,
325
+ sleep,
326
+ now,
327
+ });
328
+ const existing = readStoredCredential({ appUrl, env: options.env, platform: options.platform, homedir: options.homedir });
329
+ const authHeaders = { Authorization: `Bearer ${token.accessToken}` };
330
+ const identity = await fetchSession(appUrl, authHeaders, fetchImpl);
331
+ const defaultOrg = await resolveLoginDefaultOrg({
332
+ existingDefaultOrg: existing?.defaultOrg,
333
+ activeOrganizationId: identity.session?.activeOrganizationId,
334
+ appUrl,
335
+ headers: authHeaders,
336
+ fetchImpl,
337
+ });
338
+ writeStoredCredential({
339
+ appUrl,
340
+ env: options.env,
341
+ platform: options.platform,
342
+ homedir: options.homedir,
343
+ credential: {
344
+ token: token.accessToken,
345
+ ...(defaultOrg ? { defaultOrg } : {}),
346
+ ...(identity.user ? { user: identity.user } : {}),
347
+ },
348
+ });
349
+ const label = identity.user?.email ?? identity.user?.id ?? 'current user';
350
+ log(`Signed in to ${appUrl} as ${label}.`);
351
+ }
352
+ export async function logout(args, options = {}) {
353
+ const parsed = parseAuthArgs(args);
354
+ const log = options.log ?? console.log;
355
+ if (parsed.help) {
356
+ log(HELP);
357
+ return;
358
+ }
359
+ if (parsed.positionals.length > 0) {
360
+ throw new Error(`Unexpected positional argument: ${parsed.positionals[0]}`);
361
+ }
362
+ const appUrl = resolveAuthAppUrl(parsed);
363
+ clearStoredCredential({ appUrl, env: options.env, platform: options.platform, homedir: options.homedir });
364
+ log(`Signed out of ${appUrl}.`);
365
+ }
366
+ export async function whoami(args, options = {}) {
367
+ const parsed = parseAuthArgs(args);
368
+ const log = options.log ?? console.log;
369
+ if (parsed.help) {
370
+ log(HELP);
371
+ return;
372
+ }
373
+ if (parsed.positionals.length > 0) {
374
+ throw new Error(`Unexpected positional argument: ${parsed.positionals[0]}`);
375
+ }
376
+ const appUrl = resolveAuthAppUrl(parsed);
377
+ const headers = resolveAuthHeaders({
378
+ appUrl,
379
+ env: options.env,
380
+ platform: options.platform,
381
+ homedir: options.homedir,
382
+ });
383
+ if (!headers.Authorization && !headers['x-api-key']) {
384
+ throw new Error('Not signed in. Run `dd login` or set DD_API_KEY.');
385
+ }
386
+ const identity = await fetchSession(appUrl, headers, options.fetchImpl ?? fetch);
387
+ if (parsed.json) {
388
+ log(JSON.stringify({ appUrl, ...identity }, null, 2));
389
+ return;
390
+ }
391
+ const stored = readStoredCredential({ appUrl, env: options.env, platform: options.platform, homedir: options.homedir });
392
+ log(`Signed in to ${appUrl}`);
393
+ log(`User: ${identity.user?.email ?? identity.user?.id ?? 'unknown'}`);
394
+ if (stored?.defaultOrg) {
395
+ log(`Default organization: ${stored.defaultOrg}`);
396
+ }
397
+ }
398
+ export async function org(args, options = {}) {
399
+ const parsed = parseAuthArgs(args);
400
+ const log = options.log ?? console.log;
401
+ const subcommand = parsed.positionals[0] ?? 'list';
402
+ if (parsed.help) {
403
+ log(HELP);
404
+ return;
405
+ }
406
+ const appUrl = resolveAuthAppUrl(parsed);
407
+ const headers = resolveAuthHeaders({
408
+ appUrl,
409
+ env: options.env,
410
+ platform: options.platform,
411
+ homedir: options.homedir,
412
+ });
413
+ if (!headers.Authorization && !headers['x-api-key']) {
414
+ throw new Error('Not signed in. Run `dd login` or set DD_API_KEY.');
415
+ }
416
+ if (subcommand === 'list') {
417
+ if (parsed.positionals.length > 1) {
418
+ throw new Error(`Unexpected positional argument: ${parsed.positionals[1]}`);
419
+ }
420
+ const organizations = await fetchOrganizations(appUrl, headers, options.fetchImpl ?? fetch);
421
+ if (parsed.json) {
422
+ log(JSON.stringify({ appUrl, organizations }, null, 2));
423
+ return;
424
+ }
425
+ if (organizations.length === 0) {
426
+ log('No organizations found.');
427
+ return;
428
+ }
429
+ for (const organization of organizations) {
430
+ const slug = organization.slug ? ` (${organization.slug})` : '';
431
+ const id = organization.id ? ` [${organization.id}]` : '';
432
+ log(`${organization.name ?? organization.slug ?? organization.id}${slug}${id}`);
433
+ }
434
+ return;
435
+ }
436
+ if (subcommand === 'use') {
437
+ const slug = parsed.positionals[1];
438
+ if (!slug) {
439
+ throw new Error('Usage: dd org use <slug>');
440
+ }
441
+ if (parsed.positionals.length > 2) {
442
+ throw new Error(`Unexpected positional argument: ${parsed.positionals[2]}`);
443
+ }
444
+ const stored = readStoredCredential({ appUrl, env: options.env, platform: options.platform, homedir: options.homedir });
445
+ if (!stored?.token) {
446
+ throw new Error('No stored login found. Run `dd login` before saving a default organization.');
447
+ }
448
+ const organizations = await fetchOrganizations(appUrl, headers, options.fetchImpl ?? fetch);
449
+ const match = organizations.find((organization) => organization.slug === slug || organization.id === slug);
450
+ if (!match) {
451
+ throw new Error(`Organization not found or not available to this user: ${slug}`);
452
+ }
453
+ writeStoredCredential({
454
+ appUrl,
455
+ env: options.env,
456
+ platform: options.platform,
457
+ homedir: options.homedir,
458
+ credential: {
459
+ ...stored,
460
+ defaultOrg: match.slug ?? match.id ?? slug,
461
+ },
462
+ });
463
+ log(`Default organization set to ${match.slug ?? match.id ?? slug}.`);
464
+ return;
465
+ }
466
+ throw new Error(`Unknown org subcommand: ${subcommand}`);
467
+ }
@@ -1,2 +1,2 @@
1
- export declare const GENERATED_DEFAULT_ACTIONS_APP_URL = "https://necto.pro";
1
+ export declare const GENERATED_DEFAULT_ACTIONS_APP_URL = "https://doubledigit.vercel.app";
2
2
  //# sourceMappingURL=defaults.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/generated/defaults.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iCAAiC,sBAAsB,CAAC"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/generated/defaults.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iCAAiC,mCAAmC,CAAC"}
@@ -1 +1 @@
1
- export const GENERATED_DEFAULT_ACTIONS_APP_URL = "https://necto.pro";
1
+ export const GENERATED_DEFAULT_ACTIONS_APP_URL = "https://doubledigit.vercel.app";
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * without importing the full extension-management dependency graph first.
7
7
  */
8
8
  declare const command: string, args: string[];
9
- declare const HELP = "\n@doubledigit/cli \u2014 Manage extensions and local setup\n\nCommands:\n doctor Check local prerequisites and project health\n onboard Prepare local development and start the app\n run Start the local app with automatic DB bootstrap\n dev Start DB + app without migrations or seed data\n db <subcommand> Database helpers (status, migrate, create)\n actions <app> [action] Discover and invoke micro-app actions\n create <name> Scaffold a new micro-app from the template\n init <project-name> Scaffold a new Double Digit project\n add|install <source> Install an extension from GitHub or a marketplace\n sync Regenerate micro-apps.ts from dd-apps.config.json\n enable <name> Enable a micro-app (updates config + runs sync)\n disable <name> Disable a micro-app (updates config + runs sync)\n uninstall|remove <name> Completely remove a micro-app\n list List all discovered micro-apps with enabled/disabled status\n info <name> Show detailed info about an installed extension\n outdated Check for outdated marketplace extensions\n reconcile Detect drift between lock file, marketplace, and local files\n marketplace <sub> Manage marketplace registrations (add/list/update/remove)\n browse [marketplace] Browse available extensions in registered marketplaces\n\nOptions:\n --help, -h Show this help message\n\nExamples:\n dd doctor\n dd onboard --yes\n dd onboard # setup + start the dev server\n dd onboard --no-run # setup only\n dd init my-project # scaffold and bootstrap a fresh project\n dd init my-project --run # scaffold + bootstrap + start\n dd init my-project --skip-install --no-git\n dd run\n dd dev\n dd db status\n npx @doubledigit/cli@latest actions component-hub init my-video --framework remotion --yes\n npx @doubledigit/cli@latest actions component-hub init my-video --framework remotion --skip-skills\n npx @doubledigit/cli@latest actions component-hub search-components --framework remotion --query \"animated chart\"\n dd create invoice-tracker\n dd add gh:owner/repo/extensions/micro-apps/habit-tracker\n dd add habit-tracker@community\n dd info habit-tracker\n dd reconcile\n dd marketplace add digitaldouble/dd-marketplace\n dd browse community\n DD_APPS=tasks,agent-v2 dd sync\n dd sync --profile necto-component-hub\n";
9
+ declare const HELP = "\n@doubledigit/cli \u2014 Manage extensions and local setup\n\nCommands:\n doctor Check local prerequisites and project health\n onboard Prepare local development and start the app\n run Start the local app with automatic DB bootstrap\n dev Start DB + app without migrations or seed data\n db <subcommand> Database helpers (status, migrate, create)\n login Sign in with browser-based device authorization\n logout Remove stored CLI credentials\n whoami Show the signed-in CLI user\n org <subcommand> List or select the default organization\n actions <app> [action] Discover and invoke micro-app actions\n create <name> Scaffold a new micro-app from the template\n init <project-name> Scaffold a new Double Digit project\n add|install <source> Install an extension from GitHub or a marketplace\n sync Regenerate micro-apps.ts from dd-apps.config.json\n enable <name> Enable a micro-app (updates config + runs sync)\n disable <name> Disable a micro-app (updates config + runs sync)\n uninstall|remove <name> Completely remove a micro-app\n list List all discovered micro-apps with enabled/disabled status\n info <name> Show detailed info about an installed extension\n outdated Check for outdated marketplace extensions\n reconcile Detect drift between lock file, marketplace, and local files\n marketplace <sub> Manage marketplace registrations (add/list/update/remove)\n browse [marketplace] Browse available extensions in registered marketplaces\n\nOptions:\n --help, -h Show this help message\n\nExamples:\n dd doctor\n dd onboard --yes\n dd onboard # setup + start the dev server\n dd onboard --no-run # setup only\n dd init my-project # scaffold and bootstrap a fresh project\n dd init my-project --run # scaffold + bootstrap + start\n dd init my-project --skip-install --no-git\n dd run\n dd dev\n dd login --url https://component-hub.example.com\n dd whoami\n dd org list\n dd org use my-org\n dd db status\n npx @doubledigit/cli@latest actions component-hub init my-video --framework remotion --yes\n npx @doubledigit/cli@latest actions component-hub init my-video --framework remotion --skip-skills\n npx @doubledigit/cli@latest actions component-hub search-components --framework remotion --query \"animated chart\"\n dd create invoice-tracker\n dd add gh:owner/repo/extensions/micro-apps/habit-tracker\n dd add habit-tracker@community\n dd info habit-tracker\n dd reconcile\n dd marketplace add digitaldouble/dd-marketplace\n dd browse community\n DD_APPS=tasks,agent-v2 dd sync\n dd sync --profile necto-component-hub\n";
10
10
  declare function requireArg(value: string | undefined, usage: string): string;
11
11
  declare function runAddCommand(rawArgs: string[]): Promise<void>;
12
12
  declare function main(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,QAAA,MAAW,OAAO,UAAK,IAAI,UAAgB,CAAC;AAE5C,QAAA,MAAM,IAAI,q+EAkDT,CAAC;AAEF,iBAAS,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,iBAAe,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB7D;AAED,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAsInC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,QAAA,MAAW,OAAO,UAAK,IAAI,UAAgB,CAAC;AAE5C,QAAA,MAAM,IAAI,o0FA0DT,CAAC;AAEF,iBAAS,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,iBAAe,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB7D;AAED,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA8JnC"}
package/dist/index.js CHANGED
@@ -16,6 +16,10 @@ Commands:
16
16
  run Start the local app with automatic DB bootstrap
17
17
  dev Start DB + app without migrations or seed data
18
18
  db <subcommand> Database helpers (status, migrate, create)
19
+ login Sign in with browser-based device authorization
20
+ logout Remove stored CLI credentials
21
+ whoami Show the signed-in CLI user
22
+ org <subcommand> List or select the default organization
19
23
  actions <app> [action] Discover and invoke micro-app actions
20
24
  create <name> Scaffold a new micro-app from the template
21
25
  init <project-name> Scaffold a new Double Digit project
@@ -44,6 +48,10 @@ Examples:
44
48
  dd init my-project --skip-install --no-git
45
49
  dd run
46
50
  dd dev
51
+ dd login --url https://component-hub.example.com
52
+ dd whoami
53
+ dd org list
54
+ dd org use my-org
47
55
  dd db status
48
56
  npx @doubledigit/cli@latest actions component-hub init my-video --framework remotion --yes
49
57
  npx @doubledigit/cli@latest actions component-hub init my-video --framework remotion --skip-skills
@@ -116,6 +124,26 @@ async function main() {
116
124
  await db(args);
117
125
  break;
118
126
  }
127
+ case 'login': {
128
+ const { login } = await import('./commands/auth.js');
129
+ await login(args);
130
+ break;
131
+ }
132
+ case 'logout': {
133
+ const { logout } = await import('./commands/auth.js');
134
+ await logout(args);
135
+ break;
136
+ }
137
+ case 'whoami': {
138
+ const { whoami } = await import('./commands/auth.js');
139
+ await whoami(args);
140
+ break;
141
+ }
142
+ case 'org': {
143
+ const { org } = await import('./commands/auth.js');
144
+ await org(args);
145
+ break;
146
+ }
119
147
  case 'actions':
120
148
  case 'action': {
121
149
  const { actions } = await import('./commands/actions.js');
@@ -1,8 +1,10 @@
1
+ import { type ResolveAuthHeadersOptions } from './auth-credential.js';
1
2
  export interface ParsedActionsArgs {
2
3
  microApp?: string;
3
4
  actionName?: string;
4
5
  extraPositionals: string[];
5
6
  appUrl?: string;
7
+ org?: string;
6
8
  input: Record<string, unknown>;
7
9
  help: boolean;
8
10
  }
@@ -10,6 +12,9 @@ export interface ActionRequest {
10
12
  url: string;
11
13
  init: RequestInit;
12
14
  }
15
+ export interface BuildActionRequestOptions extends Pick<ResolveAuthHeadersOptions, 'env' | 'platform' | 'homedir'> {
16
+ org?: string;
17
+ }
13
18
  export interface ResolveActionAppUrlOptions {
14
19
  explicitUrl?: string;
15
20
  env?: Record<string, string | undefined>;
@@ -26,6 +31,6 @@ export declare function resolveActionAppUrl({ explicitUrl, env, fileEnv, default
26
31
  export declare function parseActionsArgs(args: string[]): ParsedActionsArgs;
27
32
  export declare function normalizeAppUrl(appUrl: string): string;
28
33
  export declare function isLocalAppUrl(appUrl: string): boolean;
29
- export declare function buildActionRequest(parsed: Pick<ParsedActionsArgs, 'microApp' | 'actionName' | 'input'>, appUrl: string): ActionRequest;
34
+ export declare function buildActionRequest(parsed: Pick<ParsedActionsArgs, 'microApp' | 'actionName' | 'input' | 'org'>, appUrl: string, options?: BuildActionRequestOptions): ActionRequest;
30
35
  export declare function fetchActionRequest(request: ActionRequest): Promise<unknown>;
31
36
  //# sourceMappingURL=actions-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"actions-client.d.ts","sourceRoot":"","sources":["../../src/lib/actions-client.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,iCAAiC;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,0BAA0B,CAAC,EACzC,GAAiB,EACjB,YAAY,EACZ,eAAe,GAChB,EAAE,iCAAiC,UAKnC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,GAAiB,EACjB,OAAY,EACZ,UAAU,GACX,EAAE,0BAA0B,UAS5B;AA4ED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,iBAAiB,CA+DlE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,UAS7C;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,WAK3C;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,YAAY,GAAG,OAAO,CAAC,EACpE,MAAM,EAAE,MAAM,GACb,aAAa,CAoBf;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAiCjF"}
1
+ {"version":3,"file":"actions-client.d.ts","sourceRoot":"","sources":["../../src/lib/actions-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,yBAAyB,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;IAChH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,iCAAiC;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,0BAA0B,CAAC,EACzC,GAAiB,EACjB,YAAY,EACZ,eAAe,GAChB,EAAE,iCAAiC,UAKnC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,GAAiB,EACjB,OAAY,EACZ,UAAU,GACX,EAAE,0BAA0B,UAS5B;AA4ED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAmElE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,UAS7C;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,WAK3C;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,KAAK,CAAC,EAC5E,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,yBAA8B,GACtC,aAAa,CAiCf;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAoCjF"}
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { resolveAuthHeaders } from './auth-credential.js';
2
3
  function readUrlValue(value) {
3
4
  const trimmed = value?.trim();
4
5
  return trimmed || undefined;
@@ -90,6 +91,7 @@ export function parseActionsArgs(args) {
90
91
  const input = {};
91
92
  const positional = [];
92
93
  let appUrl;
94
+ let org;
93
95
  let help = false;
94
96
  for (let i = 0; i < args.length; i++) {
95
97
  const arg = args[i];
@@ -112,6 +114,9 @@ export function parseActionsArgs(args) {
112
114
  if (flag === '--url' || flag === '--app-url') {
113
115
  appUrl = readValue();
114
116
  }
117
+ else if (flag === '--org') {
118
+ org = readValue();
119
+ }
115
120
  else if (flag === '--json') {
116
121
  Object.assign(input, parseJsonValue(readValue(), '--json'));
117
122
  }
@@ -154,6 +159,7 @@ export function parseActionsArgs(args) {
154
159
  actionName: positional[1],
155
160
  extraPositionals: positional.slice(2),
156
161
  appUrl,
162
+ org,
157
163
  input,
158
164
  help,
159
165
  };
@@ -174,23 +180,36 @@ export function isLocalAppUrl(appUrl) {
174
180
  || parsed.hostname === '127.0.0.1'
175
181
  || parsed.hostname === '::1';
176
182
  }
177
- export function buildActionRequest(parsed, appUrl) {
183
+ export function buildActionRequest(parsed, appUrl, options = {}) {
178
184
  if (!parsed.microApp) {
179
185
  throw new Error('missing micro-app name');
180
186
  }
181
187
  const base = normalizeAppUrl(appUrl);
188
+ const authHeaders = resolveAuthHeaders({
189
+ appUrl: base,
190
+ org: parsed.org,
191
+ env: options.env,
192
+ platform: options.platform,
193
+ homedir: options.homedir,
194
+ });
182
195
  const actionPath = parsed.actionName
183
196
  ? `/${encodeURIComponent(parsed.actionName)}`
184
197
  : '';
198
+ const requestHeaders = parsed.actionName
199
+ ? { 'Content-Type': 'application/json', ...authHeaders }
200
+ : authHeaders;
185
201
  return {
186
202
  url: `${base}/api/${encodeURIComponent(parsed.microApp)}/actions${actionPath}`,
187
203
  init: parsed.actionName
188
204
  ? {
189
205
  method: 'POST',
190
- headers: { 'Content-Type': 'application/json' },
206
+ headers: requestHeaders,
191
207
  body: JSON.stringify(parsed.input),
192
208
  }
193
- : { method: 'GET' },
209
+ : {
210
+ method: 'GET',
211
+ ...(Object.keys(requestHeaders).length > 0 ? { headers: requestHeaders } : {}),
212
+ },
194
213
  };
195
214
  }
196
215
  export async function fetchActionRequest(request) {
@@ -220,6 +239,9 @@ export async function fetchActionRequest(request) {
220
239
  const error = payload && typeof payload === 'object' && 'error' in payload
221
240
  ? String(payload.error)
222
241
  : `Request failed with HTTP ${response.status}`;
242
+ if (response.status === 401) {
243
+ throw new Error(`${error}. Run \`dd login\` or set DD_API_KEY.`);
244
+ }
223
245
  throw new Error(error);
224
246
  }
225
247
  return payload;
@@ -0,0 +1,8 @@
1
+ import { type AuthStorePathsOptions } from './auth-store.js';
2
+ export interface ResolveAuthHeadersOptions extends AuthStorePathsOptions {
3
+ appUrl: string;
4
+ org?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ }
7
+ export declare function resolveAuthHeaders({ appUrl, org, env, ...storeOptions }: ResolveAuthHeadersOptions): Record<string, string>;
8
+ //# sourceMappingURL=auth-credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-credential.d.ts","sourceRoot":"","sources":["../../src/lib/auth-credential.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAEnF,MAAM,WAAW,yBAA0B,SAAQ,qBAAqB;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,GAAG,EACH,GAAiB,EACjB,GAAG,YAAY,EAChB,EAAE,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBpD"}
@@ -0,0 +1,25 @@
1
+ import { readStoredCredential } from './auth-store.js';
2
+ export function resolveAuthHeaders({ appUrl, org, env = process.env, ...storeOptions }) {
3
+ const headers = {};
4
+ const apiKey = readTrimmedValue(env.DD_API_KEY) ?? readTrimmedValue(env.DD_TOKEN);
5
+ const storedCredential = apiKey
6
+ ? undefined
7
+ : readStoredCredential({ appUrl, env, ...storeOptions });
8
+ if (apiKey) {
9
+ headers['x-api-key'] = apiKey;
10
+ }
11
+ else if (storedCredential?.token) {
12
+ headers.Authorization = `Bearer ${storedCredential.token}`;
13
+ }
14
+ const resolvedOrg = readTrimmedValue(org)
15
+ ?? readTrimmedValue(env.DD_ORG)
16
+ ?? readTrimmedValue(storedCredential?.defaultOrg);
17
+ if (resolvedOrg) {
18
+ headers['x-active-organization'] = resolvedOrg;
19
+ }
20
+ return headers;
21
+ }
22
+ function readTrimmedValue(value) {
23
+ const trimmed = value?.trim();
24
+ return trimmed || undefined;
25
+ }
@@ -0,0 +1,33 @@
1
+ export interface StoredAuthUser {
2
+ id?: string;
3
+ email?: string;
4
+ }
5
+ export interface StoredHostCredential {
6
+ token: string;
7
+ defaultOrg?: string;
8
+ user?: StoredAuthUser;
9
+ }
10
+ export interface AuthStorePathsOptions {
11
+ env?: NodeJS.ProcessEnv;
12
+ platform?: NodeJS.Platform;
13
+ homedir?: () => string;
14
+ }
15
+ export interface ReadStoredCredentialOptions extends AuthStorePathsOptions {
16
+ appUrl: string;
17
+ }
18
+ export interface WriteStoredCredentialOptions extends AuthStorePathsOptions {
19
+ appUrl: string;
20
+ credential: StoredHostCredential;
21
+ }
22
+ export interface ClearStoredCredentialOptions extends AuthStorePathsOptions {
23
+ appUrl: string;
24
+ }
25
+ export declare function getAuthStorePaths({ env, platform, homedir, }?: AuthStorePathsOptions): {
26
+ configDir: string;
27
+ credentialsPath: string;
28
+ };
29
+ export declare function getAuthHostKey(appUrl: string): string;
30
+ export declare function readStoredCredential({ appUrl, ...options }: ReadStoredCredentialOptions): StoredHostCredential | undefined;
31
+ export declare function writeStoredCredential({ appUrl, credential, ...options }: WriteStoredCredentialOptions): void;
32
+ export declare function clearStoredCredential({ appUrl, ...options }: ClearStoredCredentialOptions): void;
33
+ //# sourceMappingURL=auth-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../src/lib/auth-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAMD,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA4B,SAAQ,qBAAqB;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,4BAA6B,SAAQ,qBAAqB;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,oBAAoB,CAAC;CAClC;AAED,MAAM,WAAW,4BAA6B,SAAQ,qBAAqB;IACzE,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD,wBAAgB,iBAAiB,CAAC,EAChC,GAAiB,EACjB,QAA2B,EAC3B,OAAoB,GACrB,GAAE,qBAA0B;;;EAS5B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,UAE5C;AAwDD,wBAAgB,oBAAoB,CAAC,EACnC,MAAM,EACN,GAAG,OAAO,EACX,EAAE,2BAA2B,GAAG,oBAAoB,GAAG,SAAS,CAGhE;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,UAAU,EACV,GAAG,OAAO,EACX,EAAE,4BAA4B,GAAG,IAAI,CAIrC;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,GAAG,OAAO,EACX,EAAE,4BAA4B,GAAG,IAAI,CAWrC"}
@@ -0,0 +1,89 @@
1
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ const CONFIG_DIR_NAME = 'doubledigit';
5
+ const CREDENTIALS_FILE_NAME = 'credentials.json';
6
+ function readTrimmedValue(value) {
7
+ const trimmed = value?.trim();
8
+ return trimmed || undefined;
9
+ }
10
+ export function getAuthStorePaths({ env = process.env, platform = process.platform, homedir = os.homedir, } = {}) {
11
+ const configRoot = platform === 'win32'
12
+ ? readTrimmedValue(env.APPDATA) ?? path.join(homedir(), 'AppData', 'Roaming')
13
+ : readTrimmedValue(env.XDG_CONFIG_HOME) ?? path.join(homedir(), '.config');
14
+ const configDir = path.join(configRoot, CONFIG_DIR_NAME);
15
+ return {
16
+ configDir,
17
+ credentialsPath: path.join(configDir, CREDENTIALS_FILE_NAME),
18
+ };
19
+ }
20
+ export function getAuthHostKey(appUrl) {
21
+ return new URL(normalizeAuthStoreAppUrl(appUrl)).origin.toLowerCase();
22
+ }
23
+ function normalizeAuthStoreAppUrl(appUrl) {
24
+ const trimmed = appUrl.trim().replace(/\/+$/, '');
25
+ if (/^https?:\/\//i.test(trimmed)) {
26
+ return trimmed;
27
+ }
28
+ if (/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])(?::|\/|$)/i.test(trimmed)) {
29
+ return `http://${trimmed}`;
30
+ }
31
+ return `https://${trimmed}`;
32
+ }
33
+ function readAuthStoreFile(options = {}) {
34
+ const { credentialsPath } = getAuthStorePaths(options);
35
+ try {
36
+ const raw = readFileSync(credentialsPath, 'utf8');
37
+ const parsed = JSON.parse(raw);
38
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
39
+ return { hosts: {} };
40
+ }
41
+ const hosts = parsed.hosts;
42
+ if (!hosts || typeof hosts !== 'object' || Array.isArray(hosts)) {
43
+ return { hosts: {} };
44
+ }
45
+ return { hosts: hosts };
46
+ }
47
+ catch (error) {
48
+ if (error.code === 'ENOENT') {
49
+ return { hosts: {} };
50
+ }
51
+ const detail = error instanceof Error ? error.message : String(error);
52
+ throw new Error(`Unable to read CLI credentials store: ${detail}`);
53
+ }
54
+ }
55
+ function writeAuthStoreFile(file, options = {}) {
56
+ const { configDir, credentialsPath } = getAuthStorePaths(options);
57
+ mkdirSync(configDir, { recursive: true });
58
+ writeFileSync(credentialsPath, `${JSON.stringify(file, null, 2)}\n`, {
59
+ encoding: 'utf8',
60
+ mode: 0o600,
61
+ });
62
+ if ((options.platform ?? process.platform) !== 'win32') {
63
+ try {
64
+ chmodSync(credentialsPath, 0o600);
65
+ }
66
+ catch {
67
+ // Best-effort on filesystems that do not support POSIX permissions.
68
+ }
69
+ }
70
+ }
71
+ export function readStoredCredential({ appUrl, ...options }) {
72
+ const file = readAuthStoreFile(options);
73
+ return file.hosts[getAuthHostKey(appUrl)];
74
+ }
75
+ export function writeStoredCredential({ appUrl, credential, ...options }) {
76
+ const file = readAuthStoreFile(options);
77
+ file.hosts[getAuthHostKey(appUrl)] = credential;
78
+ writeAuthStoreFile(file, options);
79
+ }
80
+ export function clearStoredCredential({ appUrl, ...options }) {
81
+ const file = readAuthStoreFile(options);
82
+ delete file.hosts[getAuthHostKey(appUrl)];
83
+ if (Object.keys(file.hosts).length === 0) {
84
+ const { credentialsPath } = getAuthStorePaths(options);
85
+ rmSync(credentialsPath, { force: true });
86
+ return;
87
+ }
88
+ writeAuthStoreFile(file, options);
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doubledigit/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "description": "CLI for Double Digit local setup and extension management.",
6
6
  "license": "MIT",