@doist/outline-cli 1.5.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/commands/auth.d.ts +12 -0
  3. package/dist/commands/auth.js +110 -158
  4. package/dist/commands/auth.js.map +1 -1
  5. package/dist/commands/changelog.d.ts +1 -6
  6. package/dist/commands/changelog.js +8 -88
  7. package/dist/commands/changelog.js.map +1 -1
  8. package/dist/index.js +9 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/api.d.ts +5 -1
  11. package/dist/lib/api.js +9 -6
  12. package/dist/lib/api.js.map +1 -1
  13. package/dist/lib/auth-constants.d.ts +18 -0
  14. package/dist/lib/auth-constants.js +25 -0
  15. package/dist/lib/auth-constants.js.map +1 -0
  16. package/dist/lib/auth-pages.d.ts +2 -0
  17. package/dist/lib/auth-pages.js +101 -0
  18. package/dist/lib/auth-pages.js.map +1 -0
  19. package/dist/lib/auth-provider.d.ts +59 -0
  20. package/dist/lib/auth-provider.js +299 -0
  21. package/dist/lib/auth-provider.js.map +1 -0
  22. package/dist/lib/auth.d.ts +24 -7
  23. package/dist/lib/auth.js +53 -36
  24. package/dist/lib/auth.js.map +1 -1
  25. package/dist/lib/config.d.ts +23 -0
  26. package/dist/lib/config.js.map +1 -1
  27. package/dist/lib/errors.d.ts +3 -1
  28. package/dist/lib/errors.js +1 -0
  29. package/dist/lib/errors.js.map +1 -1
  30. package/dist/lib/global-args.d.ts +1 -0
  31. package/dist/lib/global-args.js +4 -0
  32. package/dist/lib/global-args.js.map +1 -1
  33. package/dist/lib/migrate-auth.d.ts +11 -0
  34. package/dist/lib/migrate-auth.js +92 -0
  35. package/dist/lib/migrate-auth.js.map +1 -0
  36. package/dist/lib/outline-account.d.ts +26 -0
  37. package/dist/lib/outline-account.js +12 -0
  38. package/dist/lib/outline-account.js.map +1 -0
  39. package/dist/lib/output.d.ts +6 -0
  40. package/dist/lib/output.js +16 -4
  41. package/dist/lib/output.js.map +1 -1
  42. package/dist/lib/skills/content.d.ts +1 -1
  43. package/dist/lib/skills/content.js +10 -3
  44. package/dist/lib/skills/content.js.map +1 -1
  45. package/dist/lib/user-records.d.ts +14 -0
  46. package/dist/lib/user-records.js +89 -0
  47. package/dist/lib/user-records.js.map +1 -0
  48. package/package.json +2 -2
  49. package/dist/lib/oauth-server.d.ts +0 -14
  50. package/dist/lib/oauth-server.js +0 -203
  51. package/dist/lib/oauth-server.js.map +0 -1
  52. package/dist/lib/oauth.d.ts +0 -17
  53. package/dist/lib/oauth.js +0 -42
  54. package/dist/lib/oauth.js.map +0 -1
  55. package/dist/lib/pkce.d.ts +0 -3
  56. package/dist/lib/pkce.js +0 -16
  57. package/dist/lib/pkce.js.map +0 -1
@@ -0,0 +1,25 @@
1
+ /** OS keyring `service` identifier for every outline-cli secret. */
2
+ export const SECURE_STORE_SERVICE = 'outline-cli';
3
+ /** Env var that short-circuits the token store with a manually-supplied token. */
4
+ export const TOKEN_ENV_VAR = 'OUTLINE_API_TOKEN';
5
+ /**
6
+ * Legacy single-user keyring slot. Outline never wrote a token to the
7
+ * keyring before this migration, so there is no real legacy slot to read —
8
+ * `migrateLegacyAuth` still expects the option, and its best-effort delete
9
+ * after migration is a harmless no-op on an empty entry.
10
+ */
11
+ export const LEGACY_KEYRING_ACCOUNT = 'api-token';
12
+ /**
13
+ * `updateConfig` payload that wipes every v1 auth key. Used by both the
14
+ * one-shot migration `cleanupLegacyConfig` and the runtime
15
+ * `dischargeLegacyState` so both paths stay in lockstep.
16
+ */
17
+ export const LEGACY_CLEAR_PAYLOAD = {
18
+ api_token: undefined,
19
+ base_url: undefined,
20
+ oauth_client_id: undefined,
21
+ auth_user_id: undefined,
22
+ auth_user_name: undefined,
23
+ auth_team_name: undefined,
24
+ };
25
+ //# sourceMappingURL=auth-constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-constants.js","sourceRoot":"","sources":["../../src/lib/auth-constants.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,aAAa,CAAA;AAEjD,kFAAkF;AAClF,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAAA;AAEhD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAA;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAoB;IACjD,SAAS,EAAE,SAAS;IACpB,QAAQ,EAAE,SAAS;IACnB,eAAe,EAAE,SAAS;IAC1B,YAAY,EAAE,SAAS;IACvB,cAAc,EAAE,SAAS;IACzB,cAAc,EAAE,SAAS;CAC5B,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare function renderSuccess(): string;
2
+ export declare function renderError(message: string): string;
@@ -0,0 +1,101 @@
1
+ function escapeHtml(text) {
2
+ return text
3
+ .replaceAll('&', '&')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;')
6
+ .replaceAll('"', '&quot;')
7
+ .replaceAll("'", '&#39;');
8
+ }
9
+ function renderPage(title, subtitle, body) {
10
+ return `<!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <title>${escapeHtml(title)} - Outline CLI</title>
16
+ <style>
17
+ :root {
18
+ --bg: #f5f7fb;
19
+ --surface: #ffffff;
20
+ --border: #e6e9f0;
21
+ --text: #1e2430;
22
+ --muted: #586178;
23
+ --ok: #15803d;
24
+ --ok-soft: #dcfce7;
25
+ --err: #b91c1c;
26
+ --err-soft: #fee2e2;
27
+ --radius: 14px;
28
+ }
29
+ * { box-sizing: border-box; }
30
+ body {
31
+ margin: 0;
32
+ min-height: 100vh;
33
+ display: grid;
34
+ place-items: center;
35
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
36
+ background: radial-gradient(1200px 500px at 50% -10%, #dbeafe 0%, var(--bg) 60%);
37
+ color: var(--text);
38
+ line-height: 1.5;
39
+ padding: 20px;
40
+ }
41
+ .card {
42
+ width: min(560px, 100%);
43
+ background: var(--surface);
44
+ border: 1px solid var(--border);
45
+ border-radius: var(--radius);
46
+ padding: 24px;
47
+ box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
48
+ }
49
+ h1 {
50
+ margin: 0 0 8px;
51
+ font-size: 24px;
52
+ letter-spacing: -0.01em;
53
+ }
54
+ .subtitle {
55
+ margin: 0 0 16px;
56
+ color: var(--muted);
57
+ font-size: 15px;
58
+ }
59
+ .message {
60
+ border-radius: 12px;
61
+ padding: 14px 16px;
62
+ font-size: 14px;
63
+ }
64
+ .success {
65
+ background: var(--ok-soft);
66
+ color: #14532d;
67
+ }
68
+ .error {
69
+ background: var(--err-soft);
70
+ color: #7f1d1d;
71
+ }
72
+ .hint {
73
+ margin-top: 14px;
74
+ color: var(--muted);
75
+ font-size: 13px;
76
+ }
77
+ code {
78
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
79
+ background: #eef2ff;
80
+ border-radius: 6px;
81
+ padding: 2px 6px;
82
+ }
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <main class="card">
87
+ <h1>${escapeHtml(title)}</h1>
88
+ <p class="subtitle">${escapeHtml(subtitle)}</p>
89
+ ${body}
90
+ <p class="hint">Return to your terminal and continue with <code>ol</code> commands.</p>
91
+ </main>
92
+ </body>
93
+ </html>`;
94
+ }
95
+ export function renderSuccess() {
96
+ return renderPage('Login complete', 'Outline CLI is now authenticated.', '<div class="message success">You can close this tab now.</div>');
97
+ }
98
+ export function renderError(message) {
99
+ return renderPage('Authentication failed', 'Outline CLI could not finish OAuth login.', `<div class="message error">${escapeHtml(message)}</div>`);
100
+ }
101
+ //# sourceMappingURL=auth-pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-pages.js","sourceRoot":"","sources":["../../src/lib/auth-pages.ts"],"names":[],"mappings":"AAAA,SAAS,UAAU,CAAC,IAAY;IAC5B,OAAO,IAAI;SACN,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AACjC,CAAC;AAED,SAAS,UAAU,CAAC,KAAa,EAAE,QAAgB,EAAE,IAAY;IAC7D,OAAO;;;;;WAKA,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAwElB,UAAU,CAAC,KAAK,CAAC;0BACD,UAAU,CAAC,QAAQ,CAAC;MACxC,IAAI;;;;QAIF,CAAA;AACR,CAAC;AAED,MAAM,UAAU,aAAa;IACzB,OAAO,UAAU,CACb,gBAAgB,EAChB,mCAAmC,EACnC,gEAAgE,CACnE,CAAA;AACL,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe;IACvC,OAAO,UAAU,CACb,uBAAuB,EACvB,2CAA2C,EAC3C,8BAA8B,UAAU,CAAC,OAAO,CAAC,QAAQ,CAC5D,CAAA;AACL,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { type AccountRef, type AuthProvider, type KeyringTokenStore } from '@doist/cli-core/auth';
2
+ import { type OutlineAccount } from './outline-account.js';
3
+ export type { OutlineAccount } from './outline-account.js';
4
+ export type AuthInfoResponse = {
5
+ user: {
6
+ id: string;
7
+ name: string;
8
+ email: string;
9
+ };
10
+ team: {
11
+ name: string;
12
+ subdomain: string;
13
+ };
14
+ };
15
+ export type OutlineTokenStore = KeyringTokenStore<OutlineAccount>;
16
+ export declare function createOutlineAuthProvider(): AuthProvider<OutlineAccount>;
17
+ /**
18
+ * Accepts the Outline user UUID or display name. Id matches are
19
+ * case-sensitive (UUIDs are canonical); label matches are
20
+ * case-insensitive so users can pass the name they see in `auth status`.
21
+ */
22
+ export declare function matchOutlineAccount(account: OutlineAccount, ref: AccountRef): boolean;
23
+ /**
24
+ * True when the v2 store is empty but a legacy v1 token snapshot is still
25
+ * the only thing keeping the CLI authenticated — typically because
26
+ * `migrateLegacyAuth` couldn't reach the Outline API to identify the
27
+ * account (`MigrateSkipReason: 'identify-failed'`) or the OS keyring is
28
+ * unreachable (`'legacy-keyring-unreachable'`, although outline has no
29
+ * v1 keyring slot to read from). Useful for downstream diagnostics.
30
+ */
31
+ export declare function isLegacyAuthActive(): Promise<boolean>;
32
+ /**
33
+ * `OUTLINE_API_TOKEN` short-circuits `active()` only when no explicit ref
34
+ * is supplied — cli-core's `KeyringTokenStore` doesn't know about the env
35
+ * var, and an explicit ref means the caller targets a specific stored
36
+ * account.
37
+ *
38
+ * `ensureMigrated()` runs **before** every mutating v2 op so the post-op
39
+ * legacy discharge can't race a still-pending migration into re-grabbing
40
+ * the legacy `api_token` we just consumed. `set()` / `clear()` then
41
+ * discharge legacy state **after** the v2 op succeeds. `set()` swallows
42
+ * the cleanup failure (v2 wins in `active()` regardless); `clear()`
43
+ * propagates it so a failed logout fails loudly instead of leaving the
44
+ * user authenticated via the legacy fallback.
45
+ */
46
+ export declare function createOutlineTokenStore(): OutlineTokenStore;
47
+ /**
48
+ * Where the currently-active token lives. Mirrors `active()`'s resolution
49
+ * order — env → v2 record → legacy plaintext — so the answer can never
50
+ * contradict the token the runtime is actually using.
51
+ *
52
+ * The precedence cascade is intentionally duplicated with `active()`:
53
+ * the only true dedupe options either (a) add an extra config read on
54
+ * every `apiRequest` call (regressing the request hot path) or (b)
55
+ * augment cli-core's `TokenStore` contract. The drift the duplication
56
+ * invites is guarded by the `getActiveTokenSource` regression test that
57
+ * asserts v2-record presence wins over a lingering v1 `api_token`.
58
+ */
59
+ export declare function getActiveTokenSource(): Promise<'env' | 'secure-store' | 'config-file'>;
@@ -0,0 +1,299 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { createKeyringTokenStore, deriveChallenge, generateVerifier, } from '@doist/cli-core/auth';
3
+ import { fetchWithRetry } from '../transport/fetch-with-retry.js';
4
+ import { apiRequest } from './api.js';
5
+ import { LEGACY_CLEAR_PAYLOAD, SECURE_STORE_SERVICE, TOKEN_ENV_VAR } from './auth-constants.js';
6
+ import { getBaseUrl, getOAuthClientId } from './auth.js';
7
+ import { getConfig, getConfigPath, updateConfig } from './config.js';
8
+ import { runMigrateLegacyAuth } from './migrate-auth.js';
9
+ import { makeOutlineAccount } from './outline-account.js';
10
+ import { createOutlineUserRecordStore, getDefaultUserRecord } from './user-records.js';
11
+ function asHandshake(value) {
12
+ return value;
13
+ }
14
+ function stringFlag(flags, key) {
15
+ const value = flags[key];
16
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
17
+ }
18
+ async function prompt(question) {
19
+ // Output to stderr so `--json` / `--ndjson` envelopes on stdout stay clean.
20
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
21
+ try {
22
+ return (await rl.question(question)).trim();
23
+ }
24
+ finally {
25
+ rl.close();
26
+ }
27
+ }
28
+ async function resolveBaseUrl(flags) {
29
+ const fromFlag = stringFlag(flags, 'baseUrl');
30
+ if (fromFlag)
31
+ return fromFlag.replace(/\/$/, '');
32
+ const fromEnv = process.env.OUTLINE_URL?.trim();
33
+ if (fromEnv)
34
+ return fromEnv.replace(/\/$/, '');
35
+ const configured = await getBaseUrl();
36
+ const answered = await prompt(`Base URL (default: ${configured}): `);
37
+ return (answered || configured).replace(/\/$/, '');
38
+ }
39
+ async function resolveClientId(flags) {
40
+ const fromFlag = stringFlag(flags, 'clientId');
41
+ if (fromFlag)
42
+ return fromFlag;
43
+ const existing = await getOAuthClientId();
44
+ if (existing)
45
+ return existing;
46
+ const answered = await prompt('OAuth Client ID: ');
47
+ if (!answered) {
48
+ throw new Error('OAuth client ID is required. Create a public OAuth app in Outline settings, then pass --client-id <id> or set OUTLINE_OAUTH_CLIENT_ID.');
49
+ }
50
+ return answered;
51
+ }
52
+ export function createOutlineAuthProvider() {
53
+ return {
54
+ async authorize({ redirectUri, state, flags }) {
55
+ const baseUrl = await resolveBaseUrl(flags);
56
+ const clientId = await resolveClientId(flags);
57
+ const codeVerifier = generateVerifier();
58
+ const codeChallenge = deriveChallenge(codeVerifier);
59
+ const url = new URL(`${baseUrl}/oauth/authorize`);
60
+ url.searchParams.set('client_id', clientId);
61
+ url.searchParams.set('response_type', 'code');
62
+ url.searchParams.set('code_challenge', codeChallenge);
63
+ url.searchParams.set('code_challenge_method', 'S256');
64
+ url.searchParams.set('redirect_uri', redirectUri);
65
+ url.searchParams.set('state', state);
66
+ const handshake = { baseUrl, clientId, codeVerifier };
67
+ return { authorizeUrl: url.toString(), handshake };
68
+ },
69
+ async exchangeCode({ code, redirectUri, handshake }) {
70
+ const hs = asHandshake(handshake);
71
+ if (!hs.codeVerifier) {
72
+ throw new Error('Missing PKCE code verifier from authorize step.');
73
+ }
74
+ const params = new URLSearchParams({
75
+ grant_type: 'authorization_code',
76
+ client_id: hs.clientId,
77
+ redirect_uri: redirectUri,
78
+ code_verifier: hs.codeVerifier,
79
+ code,
80
+ });
81
+ const res = await fetchWithRetry({
82
+ url: `${hs.baseUrl}/oauth/token`,
83
+ options: {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
86
+ body: params.toString(),
87
+ },
88
+ });
89
+ const json = (await res.json().catch(() => ({})));
90
+ if (!res.ok) {
91
+ const message = json.error_description || json.message || json.error || res.statusText;
92
+ throw new Error(`OAuth token exchange failed: ${message}`);
93
+ }
94
+ if (!json.access_token) {
95
+ throw new Error('OAuth token exchange did not return an access token.');
96
+ }
97
+ return { accessToken: json.access_token };
98
+ },
99
+ async validateToken({ token, handshake }) {
100
+ const hs = asHandshake(handshake);
101
+ const { data } = await apiRequest('auth.info', {}, {
102
+ token,
103
+ baseUrl: hs.baseUrl,
104
+ });
105
+ return makeOutlineAccount({
106
+ id: data.user.id,
107
+ label: data.user.name,
108
+ baseUrl: hs.baseUrl,
109
+ oauthClientId: hs.clientId,
110
+ teamName: data.team.name,
111
+ });
112
+ },
113
+ };
114
+ }
115
+ /**
116
+ * Accepts the Outline user UUID or display name. Id matches are
117
+ * case-sensitive (UUIDs are canonical); label matches are
118
+ * case-insensitive so users can pass the name they see in `auth status`.
119
+ */
120
+ export function matchOutlineAccount(account, ref) {
121
+ if (account.id === ref)
122
+ return true;
123
+ return account.label.toLowerCase() === ref.toLowerCase();
124
+ }
125
+ /** True when the v2 store is the authoritative source. */
126
+ function migrationIsConclusive(result) {
127
+ return (result.status === 'migrated' ||
128
+ result.status === 'already-migrated' ||
129
+ result.status === 'no-legacy-state');
130
+ }
131
+ /**
132
+ * Synthesise a snapshot from v1 plaintext state still on disk. Fallback for
133
+ * when migration can't complete (offline `identifyAccount`, WSL
134
+ * `legacy-keyring-unreachable`). Returns `null` when no legacy token is
135
+ * present. The synthesised account uses whatever identity fields the v1
136
+ * config has — for pre-#71 configs the id/label may be empty, but the
137
+ * runtime never renders those (status's `fetchLive` re-derives from the
138
+ * API), so empty placeholders are safe.
139
+ */
140
+ async function readLegacyTokenSnapshot() {
141
+ const config = await getConfig();
142
+ const token = config.api_token?.trim() || null;
143
+ if (!token)
144
+ return null;
145
+ return {
146
+ token,
147
+ account: makeOutlineAccount({
148
+ id: config.auth_user_id ?? '',
149
+ label: config.auth_user_name ?? '',
150
+ baseUrl: config.base_url,
151
+ oauthClientId: config.oauth_client_id,
152
+ teamName: config.auth_team_name,
153
+ }),
154
+ };
155
+ }
156
+ /**
157
+ * Discharge v1 plaintext state. Runs **after** a successful v2 write/clear
158
+ * — never before — so a v2-op failure doesn't strand the user with no
159
+ * recoverable credentials. Caller decides whether to swallow or propagate
160
+ * the `updateConfig` failure:
161
+ * - `set()` swallows (v2 record will win in `active()` regardless).
162
+ * - `clear()` propagates (v2 is empty, so a stale legacy token would
163
+ * shadow the logout via the fallback).
164
+ */
165
+ async function dischargeLegacyState() {
166
+ await updateConfig(LEGACY_CLEAR_PAYLOAD);
167
+ }
168
+ /**
169
+ * Memoised one-shot migration trigger. Resolves with `null` on rejection so
170
+ * the CLI never fails to start because of a migration error — the legacy
171
+ * snapshot fallback handles that case. Tests reset the memo with
172
+ * `vi.resetModules()` + a dynamic re-import.
173
+ */
174
+ let migrationPromise;
175
+ function ensureMigrated() {
176
+ if (!migrationPromise) {
177
+ migrationPromise = runMigrateLegacyAuth({ silent: true }).catch(() => null);
178
+ }
179
+ return migrationPromise;
180
+ }
181
+ /**
182
+ * True when the v2 store is empty but a legacy v1 token snapshot is still
183
+ * the only thing keeping the CLI authenticated — typically because
184
+ * `migrateLegacyAuth` couldn't reach the Outline API to identify the
185
+ * account (`MigrateSkipReason: 'identify-failed'`) or the OS keyring is
186
+ * unreachable (`'legacy-keyring-unreachable'`, although outline has no
187
+ * v1 keyring slot to read from). Useful for downstream diagnostics.
188
+ */
189
+ export async function isLegacyAuthActive() {
190
+ const result = await ensureMigrated();
191
+ if (result !== null && migrationIsConclusive(result))
192
+ return false;
193
+ const legacy = await readLegacyTokenSnapshot();
194
+ return legacy !== null;
195
+ }
196
+ /**
197
+ * `OUTLINE_API_TOKEN` short-circuits `active()` only when no explicit ref
198
+ * is supplied — cli-core's `KeyringTokenStore` doesn't know about the env
199
+ * var, and an explicit ref means the caller targets a specific stored
200
+ * account.
201
+ *
202
+ * `ensureMigrated()` runs **before** every mutating v2 op so the post-op
203
+ * legacy discharge can't race a still-pending migration into re-grabbing
204
+ * the legacy `api_token` we just consumed. `set()` / `clear()` then
205
+ * discharge legacy state **after** the v2 op succeeds. `set()` swallows
206
+ * the cleanup failure (v2 wins in `active()` regardless); `clear()`
207
+ * propagates it so a failed logout fails loudly instead of leaving the
208
+ * user authenticated via the legacy fallback.
209
+ */
210
+ export function createOutlineTokenStore() {
211
+ const inner = createKeyringTokenStore({
212
+ serviceName: SECURE_STORE_SERVICE,
213
+ userRecords: createOutlineUserRecordStore(),
214
+ recordsLocation: getConfigPath(),
215
+ matchAccount: matchOutlineAccount,
216
+ });
217
+ async function migrationIsInconclusive() {
218
+ const result = await ensureMigrated(); // memoised
219
+ return result === null || !migrationIsConclusive(result);
220
+ }
221
+ return {
222
+ async active(ref) {
223
+ if (ref === undefined) {
224
+ const envToken = process.env[TOKEN_ENV_VAR]?.trim();
225
+ if (envToken) {
226
+ return {
227
+ token: envToken,
228
+ account: makeOutlineAccount({
229
+ id: '',
230
+ label: '',
231
+ baseUrl: await getBaseUrl(),
232
+ }),
233
+ };
234
+ }
235
+ }
236
+ await ensureMigrated();
237
+ const fromStore = await inner.active(ref);
238
+ if (fromStore)
239
+ return fromStore;
240
+ const legacy = await readLegacyTokenSnapshot();
241
+ if (legacy && (ref === undefined || matchOutlineAccount(legacy.account, ref))) {
242
+ return legacy;
243
+ }
244
+ return null;
245
+ },
246
+ async set(account, token) {
247
+ await ensureMigrated();
248
+ await inner.set(account, token);
249
+ if (await migrationIsInconclusive()) {
250
+ // Best-effort: a lingering `api_token` is dormant because
251
+ // `active()` reads v2 first.
252
+ await dischargeLegacyState().catch(() => undefined);
253
+ }
254
+ },
255
+ async clear(ref) {
256
+ await ensureMigrated();
257
+ await inner.clear(ref);
258
+ if (await migrationIsInconclusive()) {
259
+ // Must succeed: v2 is now empty, so a surviving legacy
260
+ // token would shadow the logout via the fallback.
261
+ await dischargeLegacyState();
262
+ }
263
+ },
264
+ async list() {
265
+ await ensureMigrated();
266
+ return inner.list();
267
+ },
268
+ async setDefault(ref) {
269
+ await ensureMigrated();
270
+ return inner.setDefault(ref);
271
+ },
272
+ getLastStorageResult: () => inner.getLastStorageResult(),
273
+ getLastClearResult: () => inner.getLastClearResult(),
274
+ };
275
+ }
276
+ /**
277
+ * Where the currently-active token lives. Mirrors `active()`'s resolution
278
+ * order — env → v2 record → legacy plaintext — so the answer can never
279
+ * contradict the token the runtime is actually using.
280
+ *
281
+ * The precedence cascade is intentionally duplicated with `active()`:
282
+ * the only true dedupe options either (a) add an extra config read on
283
+ * every `apiRequest` call (regressing the request hot path) or (b)
284
+ * augment cli-core's `TokenStore` contract. The drift the duplication
285
+ * invites is guarded by the `getActiveTokenSource` regression test that
286
+ * asserts v2-record presence wins over a lingering v1 `api_token`.
287
+ */
288
+ export async function getActiveTokenSource() {
289
+ if (process.env[TOKEN_ENV_VAR]?.trim())
290
+ return 'env';
291
+ const config = await getConfig();
292
+ const record = getDefaultUserRecord(config);
293
+ if (record)
294
+ return record.fallbackToken ? 'config-file' : 'secure-store';
295
+ if (config.api_token?.trim())
296
+ return 'config-file';
297
+ return 'secure-store';
298
+ }
299
+ //# sourceMappingURL=auth-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-provider.js","sourceRoot":"","sources":["../../src/lib/auth-provider.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,28 @@
1
+ import { SecureStoreUnavailableError } from '@doist/cli-core/auth';
2
+ import { TOKEN_ENV_VAR } from './auth-constants.js';
3
+ import { getActiveTokenSource } from './auth-provider.js';
4
+ import { CliError } from './errors.js';
5
+ export { SecureStoreUnavailableError, getActiveTokenSource, TOKEN_ENV_VAR };
6
+ export declare class NoTokenError extends CliError {
7
+ constructor();
8
+ }
9
+ /**
10
+ * Read the active token. Hot path: when `OUTLINE_API_TOKEN` is set we
11
+ * return it directly without consulting the token store, since
12
+ * `apiRequest` already resolves the base URL separately — going through
13
+ * `store.active()` here would trigger a redundant `getBaseUrl()` lookup
14
+ * per request just to synthesise an account we don't need.
15
+ */
1
16
  export declare function getApiToken(): Promise<string>;
17
+ /**
18
+ * Base URL cascade: env var → default user record (v2) → legacy
19
+ * `base_url` config (v1) → built-in default. The record takes priority
20
+ * over the legacy slot so post-migration logins keep defaulting to the
21
+ * same Outline instance.
22
+ */
2
23
  export declare function getBaseUrl(): Promise<string>;
3
- export declare function getOAuthClientId(): Promise<string | undefined>;
4
- export declare function getTokenSource(): Promise<'env' | 'config' | null>;
5
- export declare function saveConfig(token: string, baseUrl?: string, oauthClientId?: string): Promise<void>;
6
24
  /**
7
- * Clear the auth-related keys without deleting the file. The config is now
8
- * shared with non-auth settings (notably `update_channel`); a blanket unlink
9
- * would silently reset the user's update-channel preference too.
25
+ * OAuth client id cascade: env var → default user record (v2) → legacy
26
+ * `oauth_client_id` config (v1) → undefined (caller prompts).
10
27
  */
11
- export declare function clearConfig(): Promise<void>;
28
+ export declare function getOAuthClientId(): Promise<string | undefined>;
package/dist/lib/auth.js CHANGED
@@ -1,57 +1,74 @@
1
- import { getConfig, setConfig, updateConfig } from './config.js';
2
- const DEFAULT_BASE_URL = 'https://app.getoutline.com';
1
+ import { SecureStoreUnavailableError } from '@doist/cli-core/auth';
2
+ import { TOKEN_ENV_VAR } from './auth-constants.js';
3
+ import { createOutlineTokenStore, getActiveTokenSource, } from './auth-provider.js';
4
+ import { getConfig } from './config.js';
5
+ import { CliError } from './errors.js';
6
+ import { DEFAULT_BASE_URL } from './outline-account.js';
7
+ import { getDefaultUserRecord } from './user-records.js';
8
+ export { SecureStoreUnavailableError, getActiveTokenSource, TOKEN_ENV_VAR };
9
+ export class NoTokenError extends CliError {
10
+ constructor() {
11
+ super('NO_TOKEN', `No API token found. Set ${TOKEN_ENV_VAR} env var or run: ol auth login`, [`Set ${TOKEN_ENV_VAR} or run: ol auth login`], 'info');
12
+ this.name = 'NoTokenError';
13
+ }
14
+ }
15
+ /**
16
+ * Module-level token-store singleton. Built lazily on first call; reused
17
+ * across every `apiRequest` so the request hot path doesn't reconstruct
18
+ * the keyring + user-record adapters per POST.
19
+ */
20
+ let storeSingleton;
21
+ function tokenStore() {
22
+ if (!storeSingleton)
23
+ storeSingleton = createOutlineTokenStore();
24
+ return storeSingleton;
25
+ }
26
+ /**
27
+ * Read the active token. Hot path: when `OUTLINE_API_TOKEN` is set we
28
+ * return it directly without consulting the token store, since
29
+ * `apiRequest` already resolves the base URL separately — going through
30
+ * `store.active()` here would trigger a redundant `getBaseUrl()` lookup
31
+ * per request just to synthesise an account we don't need.
32
+ */
3
33
  export async function getApiToken() {
4
- const envToken = process.env.OUTLINE_API_TOKEN;
34
+ const envToken = process.env[TOKEN_ENV_VAR]?.trim();
5
35
  if (envToken)
6
36
  return envToken;
7
- const config = await getConfig();
8
- if (config.api_token)
9
- return config.api_token;
10
- throw new Error('No API token found. Set OUTLINE_API_TOKEN env var or run: ol auth login');
37
+ const snapshot = await tokenStore().active();
38
+ if (!snapshot?.token)
39
+ throw new NoTokenError();
40
+ return snapshot.token;
11
41
  }
42
+ /**
43
+ * Base URL cascade: env var → default user record (v2) → legacy
44
+ * `base_url` config (v1) → built-in default. The record takes priority
45
+ * over the legacy slot so post-migration logins keep defaulting to the
46
+ * same Outline instance.
47
+ */
12
48
  export async function getBaseUrl() {
13
49
  const envUrl = process.env.OUTLINE_URL;
14
50
  if (envUrl)
15
51
  return envUrl.replace(/\/$/, '');
16
52
  const config = await getConfig();
53
+ const record = getDefaultUserRecord(config);
54
+ if (record?.account.baseUrl)
55
+ return record.account.baseUrl.replace(/\/$/, '');
17
56
  if (config.base_url)
18
57
  return config.base_url.replace(/\/$/, '');
19
58
  return DEFAULT_BASE_URL;
20
59
  }
60
+ /**
61
+ * OAuth client id cascade: env var → default user record (v2) → legacy
62
+ * `oauth_client_id` config (v1) → undefined (caller prompts).
63
+ */
21
64
  export async function getOAuthClientId() {
22
65
  const envClientId = process.env.OUTLINE_OAUTH_CLIENT_ID;
23
66
  if (envClientId)
24
67
  return envClientId;
25
68
  const config = await getConfig();
69
+ const record = getDefaultUserRecord(config);
70
+ if (record?.account.oauthClientId)
71
+ return record.account.oauthClientId;
26
72
  return config.oauth_client_id;
27
73
  }
28
- export async function getTokenSource() {
29
- if (process.env.OUTLINE_API_TOKEN)
30
- return 'env';
31
- const config = await getConfig();
32
- if (config.api_token)
33
- return 'config';
34
- return null;
35
- }
36
- export async function saveConfig(token, baseUrl, oauthClientId) {
37
- const updates = { api_token: token };
38
- if (baseUrl)
39
- updates.base_url = baseUrl.replace(/\/$/, '');
40
- if (oauthClientId)
41
- updates.oauth_client_id = oauthClientId;
42
- await updateConfig(updates);
43
- }
44
- /**
45
- * Clear the auth-related keys without deleting the file. The config is now
46
- * shared with non-auth settings (notably `update_channel`); a blanket unlink
47
- * would silently reset the user's update-channel preference too.
48
- */
49
- export async function clearConfig() {
50
- const existing = await getConfig();
51
- const { api_token, base_url, oauth_client_id, ...rest } = existing;
52
- void api_token;
53
- void base_url;
54
- void oauth_client_id;
55
- await setConfig(rest);
56
- }
57
74
  //# sourceMappingURL=auth.js.map