@doist/cli-core 0.7.1 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +66 -13
- package/dist/auth/errors.d.ts +7 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +2 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth/flow.d.ts +47 -0
- package/dist/auth/flow.d.ts.map +1 -0
- package/dist/auth/flow.js +320 -0
- package/dist/auth/flow.js.map +1 -0
- package/dist/auth/index.d.ts +9 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/pkce.d.ts +29 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +41 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/providers/pkce.d.ts +42 -0
- package/dist/auth/providers/pkce.d.ts.map +1 -0
- package/dist/auth/providers/pkce.js +114 -0
- package/dist/auth/providers/pkce.js.map +1 -0
- package/dist/auth/types.d.ts +81 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -21
- package/dist/commands/update.js.map +1 -1
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +6 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +18 -1
- package/dist/options.js.map +1 -1
- package/package.json +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.8.0](https://github.com/Doist/cli-core/compare/v0.7.1...v0.8.0) (2026-05-09)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **auth:** extract OAuth login runtime into ./auth subpath ([#12](https://github.com/Doist/cli-core/issues/12)) ([d402f02](https://github.com/Doist/cli-core/commit/d402f02d45237245259df753dbc8a97e0c7791e8))
|
|
6
|
+
|
|
1
7
|
## [0.7.1](https://github.com/Doist/cli-core/compare/v0.7.0...v0.7.1) (2026-05-09)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -12,19 +12,20 @@ npm install @doist/cli-core
|
|
|
12
12
|
|
|
13
13
|
## What's in it
|
|
14
14
|
|
|
15
|
-
| Module | Key exports
|
|
16
|
-
| -------------------- |
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
15
|
+
| Module | Key exports | Purpose |
|
|
16
|
+
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `auth` (subpath) | `registerAuthCommand`, `runOAuthFlow`, `startCallbackServer`, `createPkceProvider`, `createConfigTokenStore`, PKCE helpers, `AuthProvider` / `TokenStore` types | OAuth runtime plus the `registerAuthCommand` Commander registrar for `<cli> [auth] login`. Ships the standard public-client PKCE flow + single-user config-backed token store; the `AuthProvider` and `TokenStore` interfaces are the escape hatches for DCR, OS-keychain, multi-account, etc. `commander` (when using the registrar) and `open` (browser launch) are optional peer-deps. |
|
|
18
|
+
| `commands` (subpath) | `registerChangelogCommand`, `registerUpdateCommand` (+ semver helpers) | Commander wiring for cli-core's standard commands (e.g. `<cli> changelog`, `<cli> update`, `<cli> update switch`). **Requires** `commander` as an optional peer-dep. |
|
|
19
|
+
| `config` | `getConfigPath`, `readConfig`, `readConfigStrict`, `writeConfig`, `updateConfig`, `CoreConfig`, `UpdateChannel` | Read / write a per-CLI JSON config file with typed error codes; `CoreConfig` is the shape of fields cli-core itself owns (extend it for per-CLI fields). |
|
|
20
|
+
| `empty` | `printEmpty` | Print an empty-state message gated on `--json` / `--ndjson` so machine consumers never see human strings on stdout. |
|
|
21
|
+
| `errors` | `CliError` | Typed CLI error class with `code` and exit-code mapping. |
|
|
22
|
+
| `global-args` | `parseGlobalArgs`, `createGlobalArgsStore`, `createAccessibleGate`, `createSpinnerGate`, `getProgressJsonlPath`, `isProgressJsonlEnabled` | Parse well-known global flags (`--json`, `--ndjson`, `--quiet`, `--verbose`, `--accessible`, `--no-spinner`, `--progress-jsonl`) and derive predicates from them. |
|
|
23
|
+
| `json` | `formatJson`, `formatNdjson` | Stable JSON / newline-delimited JSON formatting for stdout. |
|
|
24
|
+
| `markdown` (subpath) | `preloadMarkdown`, `renderMarkdown`, `TerminalRendererOptions` | Lazy-init terminal markdown renderer. **Requires** `marked` and `marked-terminal-renderer` as peer-deps — install only if your CLI uses this subpath. |
|
|
25
|
+
| `options` | `ViewOptions` | Type contract for `{ json?, ndjson? }` per-command options that machine-output gates derive from. |
|
|
26
|
+
| `spinner` | `createSpinner` | Loading spinner factory wrapping `yocto-spinner` with disable gates. |
|
|
27
|
+
| `terminal` | `isCI`, `isStderrTTY`, `isStdinTTY`, `isStdoutTTY` | TTY / CI detection helpers. |
|
|
28
|
+
| `testing` (subpath) | `describeEmptyMachineOutput` | Vitest helpers reusable by consuming CLIs (e.g. parametrised empty-state suite covering `--json` / `--ndjson` / human modes). |
|
|
28
29
|
|
|
29
30
|
## Usage
|
|
30
31
|
|
|
@@ -121,6 +122,58 @@ registerUpdateCommand(program, {
|
|
|
121
122
|
|
|
122
123
|
The semver helpers (`parseVersion`, `compareVersions`, `isNewer`, `getInstallTag`, `fetchLatestVersion`, `getConfiguredUpdateChannel`) are also exported for ad-hoc use outside the registered command.
|
|
123
124
|
|
|
125
|
+
### Auth (optional subpath)
|
|
126
|
+
|
|
127
|
+
Wire `<cli> [auth] login` and the supporting OAuth runtime in one call. cli-core ships the standard public-client PKCE flow (`createPkceProvider`) and a single-user config-backed `TokenStore` (`createConfigTokenStore`). Bespoke flows (Dynamic Client Registration, device code, magic link, username/password) implement the `AuthProvider` interface directly — no cli-core release needed.
|
|
128
|
+
|
|
129
|
+
`logout`, `status`, and `token` are intentionally **not** in this surface yet. They're short and currently CLI-specific in shape; each CLI keeps its own implementations until a concrete migration proves them worth sharing. OS-keychain-backed storage and multi-account stores are likewise out of scope for this first release — implement the `TokenStore` interface directly until the shared shape stabilises.
|
|
130
|
+
|
|
131
|
+
Install peer-deps in the consuming CLI:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm install commander open # `open` is optional
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { createSpinner, getConfigPath } from '@doist/cli-core'
|
|
141
|
+
import {
|
|
142
|
+
createConfigTokenStore,
|
|
143
|
+
createPkceProvider,
|
|
144
|
+
registerAuthCommand,
|
|
145
|
+
} from '@doist/cli-core/auth'
|
|
146
|
+
|
|
147
|
+
type Account = { id: string; label?: string; email: string }
|
|
148
|
+
|
|
149
|
+
const configPath = getConfigPath('outline-cli')
|
|
150
|
+
const store = createConfigTokenStore<Account>({ configPath })
|
|
151
|
+
|
|
152
|
+
const provider = createPkceProvider<Account>({
|
|
153
|
+
authorizeUrl: ({ handshake }) => `${handshake.baseUrl as string}/oauth/authorize`,
|
|
154
|
+
tokenUrl: ({ handshake }) => `${handshake.baseUrl as string}/oauth/token`,
|
|
155
|
+
clientId: ({ flags }) => flags.clientId as string,
|
|
156
|
+
scopes: ['read', 'write'],
|
|
157
|
+
validate: async ({ token, handshake }) => probeUser(token, handshake.baseUrl as string),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const { withSpinner } = createSpinner()
|
|
161
|
+
registerAuthCommand<Account>(program, {
|
|
162
|
+
displayName: 'Outline',
|
|
163
|
+
provider,
|
|
164
|
+
store,
|
|
165
|
+
resolveScopes: ({ readOnly }) => (readOnly ? ['read'] : ['read', 'write']),
|
|
166
|
+
callbackPort: { preferred: 54969, fallbackCount: 5 },
|
|
167
|
+
renderSuccess: (ctx) => `<html>...</html>`,
|
|
168
|
+
renderError: (ctx) => `<html>...</html>`,
|
|
169
|
+
withSpinner,
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The success / error HTML is a render hook — every CLI brings its own template (no shared layout enforced). Errors are `CliError` (`AUTH_OAUTH_FAILED`, `AUTH_STATE_MISMATCH`, `AUTH_CALLBACK_TIMEOUT`, `AUTH_PORT_BIND_FAILED`, `AUTH_TOKEN_EXCHANGE_FAILED`, `AUTH_STORE_WRITE_FAILED`); the consumer's top-level handler formats and exits.
|
|
174
|
+
|
|
175
|
+
For a lower-level integration that doesn't want the registrar, `runOAuthFlow` and `startCallbackServer` are exposed directly.
|
|
176
|
+
|
|
124
177
|
## Development
|
|
125
178
|
|
|
126
179
|
```bash
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes thrown by `@doist/cli-core/auth`. Folded into the `CliErrorCode`
|
|
3
|
+
* aggregator in `../errors.ts` so consumers don't have to redeclare them in
|
|
4
|
+
* their own `TCode` union when catching.
|
|
5
|
+
*/
|
|
6
|
+
export type AuthErrorCode = 'AUTH_OAUTH_FAILED' | 'AUTH_CALLBACK_TIMEOUT' | 'AUTH_PORT_BIND_FAILED' | 'AUTH_TOKEN_EXCHANGE_FAILED' | 'AUTH_STORE_WRITE_FAILED';
|
|
7
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACnB,mBAAmB,GACnB,uBAAuB,GACvB,uBAAuB,GACvB,4BAA4B,GAC5B,yBAAyB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AuthAccount, AuthProvider, TokenStore } from './types.js';
|
|
2
|
+
export type RunOAuthFlowOptions<TAccount extends AuthAccount = AuthAccount> = {
|
|
3
|
+
provider: AuthProvider<TAccount>;
|
|
4
|
+
store: TokenStore<TAccount>;
|
|
5
|
+
/** Resolved scope list to request. */
|
|
6
|
+
scopes: string[];
|
|
7
|
+
/** Was `--read-only` set? Threaded through to the provider. */
|
|
8
|
+
readOnly: boolean;
|
|
9
|
+
/** Per-CLI flags from the command line (Commander option object). */
|
|
10
|
+
flags: Record<string, unknown>;
|
|
11
|
+
/** Preferred local callback port. */
|
|
12
|
+
preferredPort: number;
|
|
13
|
+
/** Walk up this many sequential ports if `preferredPort` is busy. Default 5. */
|
|
14
|
+
portFallbackCount?: number;
|
|
15
|
+
/** Callback path the OAuth provider redirects to. Default `'/callback'`. */
|
|
16
|
+
callbackPath?: string;
|
|
17
|
+
/** Bind address. Default `'127.0.0.1'`. */
|
|
18
|
+
callbackHost?: string;
|
|
19
|
+
/** HTML returned to the browser on success. */
|
|
20
|
+
renderSuccess: () => string;
|
|
21
|
+
/** HTML returned to the browser on failure. Receives the OAuth error description. */
|
|
22
|
+
renderError: (message: string) => string;
|
|
23
|
+
/** Override the browser opener (tests). When omitted, dynamically imports `open`. */
|
|
24
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
25
|
+
/** Print the authorize URL to stdout as a fallback when the browser can't open it. */
|
|
26
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
27
|
+
/** Callback timeout in ms. Default 3 minutes. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
/** Cancellation signal (Ctrl-C wiring). */
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
};
|
|
32
|
+
export type RunOAuthFlowResult<TAccount extends AuthAccount = AuthAccount> = {
|
|
33
|
+
token: string;
|
|
34
|
+
account: TAccount;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Drive the OAuth dance end-to-end and persist the resulting token.
|
|
38
|
+
*
|
|
39
|
+
* `prepare?` → bind callback server → `authorize` → open browser →
|
|
40
|
+
* wait for callback → `exchangeCode` → `validateToken` → `store.set`.
|
|
41
|
+
*
|
|
42
|
+
* The local HTTP callback server is an internal implementation detail; it
|
|
43
|
+
* is not a separately reusable module since OAuth login is its only
|
|
44
|
+
* consumer today.
|
|
45
|
+
*/
|
|
46
|
+
export declare function runOAuthFlow<TAccount extends AuthAccount>(options: RunOAuthFlowOptions<TAccount>): Promise<RunOAuthFlowResult<TAccount>>;
|
|
47
|
+
//# sourceMappingURL=flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../../src/auth/flow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEvE,MAAM,MAAM,mBAAmB,CAAC,QAAQ,SAAS,WAAW,GAAG,WAAW,IAAI;IAC1E,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;IAChC,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;IAC3B,sCAAsC;IACtC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,+DAA+D;IAC/D,QAAQ,EAAE,OAAO,CAAA;IACjB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,MAAM,CAAA;IAC3B,qFAAqF;IACrF,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;IACxC,qFAAqF;IACrF,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,sFAAsF;IACtF,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,kBAAkB,CAAC,QAAQ,SAAS,WAAW,GAAG,WAAW,IAAI;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,QAAQ,CAAA;CACpB,CAAA;AAOD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,QAAQ,SAAS,WAAW,EAC3D,OAAO,EAAE,mBAAmB,CAAC,QAAQ,CAAC,GACvC,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CA0GvC"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { CliError, getErrorMessage } from '../errors.js';
|
|
3
|
+
import { isStdoutTTY } from '../terminal.js';
|
|
4
|
+
import { generateState } from './pkce.js';
|
|
5
|
+
const DEFAULT_PORT_FALLBACK_COUNT = 5;
|
|
6
|
+
const DEFAULT_CALLBACK_TIMEOUT_MS = 3 * 60 * 1000;
|
|
7
|
+
const DEFAULT_CALLBACK_PATH = '/callback';
|
|
8
|
+
const DEFAULT_CALLBACK_HOST = '127.0.0.1';
|
|
9
|
+
/**
|
|
10
|
+
* Drive the OAuth dance end-to-end and persist the resulting token.
|
|
11
|
+
*
|
|
12
|
+
* `prepare?` → bind callback server → `authorize` → open browser →
|
|
13
|
+
* wait for callback → `exchangeCode` → `validateToken` → `store.set`.
|
|
14
|
+
*
|
|
15
|
+
* The local HTTP callback server is an internal implementation detail; it
|
|
16
|
+
* is not a separately reusable module since OAuth login is its only
|
|
17
|
+
* consumer today.
|
|
18
|
+
*/
|
|
19
|
+
export async function runOAuthFlow(options) {
|
|
20
|
+
assertValidPort(options.preferredPort, 'preferredPort');
|
|
21
|
+
const state = generateState();
|
|
22
|
+
let prepareHandshake = {};
|
|
23
|
+
const server = await startCallbackServer({
|
|
24
|
+
preferredPort: options.preferredPort,
|
|
25
|
+
portFallbackCount: options.portFallbackCount ?? DEFAULT_PORT_FALLBACK_COUNT,
|
|
26
|
+
path: options.callbackPath ?? DEFAULT_CALLBACK_PATH,
|
|
27
|
+
host: options.callbackHost ?? DEFAULT_CALLBACK_HOST,
|
|
28
|
+
expectedState: state,
|
|
29
|
+
renderSuccess: options.renderSuccess,
|
|
30
|
+
renderError: options.renderError,
|
|
31
|
+
});
|
|
32
|
+
let abortListener = null;
|
|
33
|
+
if (options.signal) {
|
|
34
|
+
abortListener = () => {
|
|
35
|
+
void server.stop();
|
|
36
|
+
};
|
|
37
|
+
options.signal.addEventListener('abort', abortListener);
|
|
38
|
+
}
|
|
39
|
+
const checkAborted = () => {
|
|
40
|
+
if (options.signal?.aborted) {
|
|
41
|
+
throw new CliError('AUTH_OAUTH_FAILED', 'Authorization aborted.');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
checkAborted();
|
|
46
|
+
if (options.provider.prepare) {
|
|
47
|
+
const prepared = await options.provider.prepare({
|
|
48
|
+
redirectUri: server.redirectUri,
|
|
49
|
+
flags: options.flags,
|
|
50
|
+
});
|
|
51
|
+
prepareHandshake = prepared.handshake;
|
|
52
|
+
checkAborted();
|
|
53
|
+
}
|
|
54
|
+
const authorize = await options.provider.authorize({
|
|
55
|
+
redirectUri: server.redirectUri,
|
|
56
|
+
state,
|
|
57
|
+
scopes: options.scopes,
|
|
58
|
+
readOnly: options.readOnly,
|
|
59
|
+
flags: options.flags,
|
|
60
|
+
handshake: prepareHandshake,
|
|
61
|
+
});
|
|
62
|
+
checkAborted();
|
|
63
|
+
await openOrFallback(authorize.authorizeUrl, options);
|
|
64
|
+
const callback = await server.waitForCallback(options.timeoutMs ?? DEFAULT_CALLBACK_TIMEOUT_MS);
|
|
65
|
+
checkAborted();
|
|
66
|
+
// Merge prepareHandshake into the downstream handshake so prepare-time
|
|
67
|
+
// state survives even when a custom provider's authorize() forgets to
|
|
68
|
+
// forward it. Then fold the runtime `flags` and `readOnly` into the
|
|
69
|
+
// handshake so providers' `exchangeCode` / `validateToken` get the
|
|
70
|
+
// same view that `authorize` had — the typed input fields don't
|
|
71
|
+
// carry them, and stuffing them here keeps consumer providers from
|
|
72
|
+
// having to re-thread them manually.
|
|
73
|
+
const downstreamHandshake = {
|
|
74
|
+
...prepareHandshake,
|
|
75
|
+
...authorize.handshake,
|
|
76
|
+
flags: options.flags,
|
|
77
|
+
readOnly: options.readOnly,
|
|
78
|
+
};
|
|
79
|
+
const exchange = await options.provider.exchangeCode({
|
|
80
|
+
code: callback.code,
|
|
81
|
+
state: callback.state,
|
|
82
|
+
redirectUri: server.redirectUri,
|
|
83
|
+
handshake: downstreamHandshake,
|
|
84
|
+
});
|
|
85
|
+
checkAborted();
|
|
86
|
+
const account = exchange.account ??
|
|
87
|
+
(await options.provider.validateToken({
|
|
88
|
+
token: exchange.accessToken,
|
|
89
|
+
handshake: downstreamHandshake,
|
|
90
|
+
}));
|
|
91
|
+
checkAborted();
|
|
92
|
+
try {
|
|
93
|
+
await options.store.set(account, exchange.accessToken);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof CliError)
|
|
97
|
+
throw error;
|
|
98
|
+
throw new CliError('AUTH_STORE_WRITE_FAILED', `Failed to persist token: ${getErrorMessage(error)}`);
|
|
99
|
+
}
|
|
100
|
+
return { token: exchange.accessToken, account };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (options.signal && abortListener) {
|
|
104
|
+
options.signal.removeEventListener('abort', abortListener);
|
|
105
|
+
}
|
|
106
|
+
await server.stop();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function startCallbackServer(options) {
|
|
110
|
+
let settle = null;
|
|
111
|
+
const outcomePromise = new Promise((resolve) => {
|
|
112
|
+
settle = resolve;
|
|
113
|
+
});
|
|
114
|
+
const server = createServer((req, res) => {
|
|
115
|
+
handleRequest(req, res, {
|
|
116
|
+
path: options.path,
|
|
117
|
+
expectedState: options.expectedState,
|
|
118
|
+
renderSuccess: options.renderSuccess,
|
|
119
|
+
renderError: options.renderError,
|
|
120
|
+
settle: (outcome) => settle?.(outcome),
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
const port = await listenWithFallback(server, options.host, options.preferredPort, options.portFallbackCount);
|
|
124
|
+
// Advertise as `localhost` when bound to the IPv4 loopback default —
|
|
125
|
+
// matches the redirect-URI allowlists OAuth apps typically register.
|
|
126
|
+
// IPv6 literals get bracket-wrapped per RFC 3986. Custom hostnames are
|
|
127
|
+
// advertised verbatim.
|
|
128
|
+
const redirectUri = `http://${formatHostForUrl(options.host)}:${port}${options.path}`;
|
|
129
|
+
let stopped = false;
|
|
130
|
+
return {
|
|
131
|
+
redirectUri,
|
|
132
|
+
async waitForCallback(timeoutMs) {
|
|
133
|
+
let timer;
|
|
134
|
+
const timeoutOutcome = new Promise((resolve) => {
|
|
135
|
+
timer = setTimeout(() => {
|
|
136
|
+
resolve({
|
|
137
|
+
ok: false,
|
|
138
|
+
error: new CliError('AUTH_CALLBACK_TIMEOUT', `Authorization timed out after ${Math.round(timeoutMs / 1000)}s.`, {
|
|
139
|
+
hints: ['Re-run the login command and complete the browser step.'],
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
}, timeoutMs);
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const outcome = await Promise.race([outcomePromise, timeoutOutcome]);
|
|
146
|
+
if (!outcome.ok)
|
|
147
|
+
throw outcome.error;
|
|
148
|
+
return outcome.result;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (timer)
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async stop() {
|
|
156
|
+
if (stopped)
|
|
157
|
+
return;
|
|
158
|
+
stopped = true;
|
|
159
|
+
// Settle the outcome so a still-pending `waitForCallback`
|
|
160
|
+
// (e.g. one cancelled via AbortSignal) doesn't hang forever.
|
|
161
|
+
settle?.({
|
|
162
|
+
ok: false,
|
|
163
|
+
error: new CliError('AUTH_OAUTH_FAILED', 'Callback server stopped before authorization completed.'),
|
|
164
|
+
});
|
|
165
|
+
// Browsers keep the success-page connection alive for several
|
|
166
|
+
// seconds after the redirect; closeAllConnections lets the CLI
|
|
167
|
+
// exit promptly instead of waiting for those sockets.
|
|
168
|
+
server.closeAllConnections();
|
|
169
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function handleRequest(req, res, ctx) {
|
|
174
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
175
|
+
if (url.pathname !== ctx.path) {
|
|
176
|
+
res.statusCode = 404;
|
|
177
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
178
|
+
res.end('Not found');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const error = url.searchParams.get('error');
|
|
182
|
+
if (error) {
|
|
183
|
+
const description = url.searchParams.get('error_description') ?? error;
|
|
184
|
+
respondHtml(res, 400, ctx.renderError(description));
|
|
185
|
+
ctx.settle({
|
|
186
|
+
ok: false,
|
|
187
|
+
error: new CliError('AUTH_OAUTH_FAILED', `Authorization failed: ${description}`, {
|
|
188
|
+
hints: ['Check the browser tab for details and try again.'],
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Bad-shape callbacks (missing code/state, state mismatch) render a 400
|
|
194
|
+
// page but do *not* settle the wait — a browser-extension prefetch or
|
|
195
|
+
// accidental reload shouldn't kill an in-flight OAuth flow. Only a
|
|
196
|
+
// provider-driven `?error=` is treated as final. The wait still
|
|
197
|
+
// settles on timeout / `stop()` if a valid callback never arrives.
|
|
198
|
+
const code = url.searchParams.get('code');
|
|
199
|
+
const state = url.searchParams.get('state');
|
|
200
|
+
if (!code || !state) {
|
|
201
|
+
respondHtml(res, 400, ctx.renderError('Authorization callback missing code or state.'));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (state !== ctx.expectedState) {
|
|
205
|
+
respondHtml(res, 400, ctx.renderError('Authorization state did not match. Possible CSRF attempt.'));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
respondHtml(res, 200, ctx.renderSuccess());
|
|
209
|
+
ctx.settle({ ok: true, result: { code, state } });
|
|
210
|
+
}
|
|
211
|
+
function respondHtml(res, status, html) {
|
|
212
|
+
res.statusCode = status;
|
|
213
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
214
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
215
|
+
res.end(html);
|
|
216
|
+
}
|
|
217
|
+
async function listenWithFallback(server, host, preferred, fallback) {
|
|
218
|
+
// Port 0 = OS-assigned ephemeral. The bind always succeeds and there's
|
|
219
|
+
// nothing meaningful to walk to from there.
|
|
220
|
+
if (preferred === 0) {
|
|
221
|
+
try {
|
|
222
|
+
await tryListen(server, host, 0);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw wrapBindError(error, host, 0);
|
|
226
|
+
}
|
|
227
|
+
const address = server.address();
|
|
228
|
+
if (!address || typeof address === 'string') {
|
|
229
|
+
throw new CliError('AUTH_PORT_BIND_FAILED', 'Could not resolve assigned port.');
|
|
230
|
+
}
|
|
231
|
+
return address.port;
|
|
232
|
+
}
|
|
233
|
+
let lastError = null;
|
|
234
|
+
for (let i = 0; i <= fallback; i++) {
|
|
235
|
+
const port = preferred + i;
|
|
236
|
+
// Stop walking past the valid port range; otherwise `server.listen`
|
|
237
|
+
// throws a raw `RangeError` outside the `CliError` envelope.
|
|
238
|
+
if (port > 65535)
|
|
239
|
+
break;
|
|
240
|
+
try {
|
|
241
|
+
await tryListen(server, host, port);
|
|
242
|
+
return port;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const err = error;
|
|
246
|
+
// Surface non-EADDRINUSE failures (EACCES on privileged ports,
|
|
247
|
+
// an unreachable host, …) via the typed error envelope rather
|
|
248
|
+
// than letting Node's raw error escape.
|
|
249
|
+
if (err.code !== 'EADDRINUSE')
|
|
250
|
+
throw wrapBindError(err, host, port);
|
|
251
|
+
lastError = err;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw new CliError('AUTH_PORT_BIND_FAILED', `Could not bind a local port in range ${preferred}..${preferred + fallback}.`, {
|
|
255
|
+
hints: [
|
|
256
|
+
'Free a port in that range or pass a different preferred port.',
|
|
257
|
+
lastError?.message ?? '',
|
|
258
|
+
].filter(Boolean),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function wrapBindError(error, host, port) {
|
|
262
|
+
return new CliError('AUTH_PORT_BIND_FAILED', `Could not bind callback server to ${host}:${port}: ${getErrorMessage(error)}`);
|
|
263
|
+
}
|
|
264
|
+
function tryListen(server, host, port) {
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
const onError = (err) => {
|
|
267
|
+
server.removeListener('listening', onListening);
|
|
268
|
+
reject(err);
|
|
269
|
+
};
|
|
270
|
+
const onListening = () => {
|
|
271
|
+
server.removeListener('error', onError);
|
|
272
|
+
resolve();
|
|
273
|
+
};
|
|
274
|
+
server.once('error', onError);
|
|
275
|
+
server.once('listening', onListening);
|
|
276
|
+
server.listen(port, host);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function formatHostForUrl(host) {
|
|
280
|
+
if (host === DEFAULT_CALLBACK_HOST)
|
|
281
|
+
return 'localhost';
|
|
282
|
+
if (host.includes(':'))
|
|
283
|
+
return `[${host}]`;
|
|
284
|
+
return host;
|
|
285
|
+
}
|
|
286
|
+
function assertValidPort(port, label) {
|
|
287
|
+
if (typeof port !== 'number' || !Number.isInteger(port) || port < 0 || port > 65535) {
|
|
288
|
+
throw new CliError('AUTH_PORT_BIND_FAILED', `Invalid ${label} '${String(port)}': expected an integer in [0..65535].`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function openOrFallback(url, options) {
|
|
292
|
+
const opener = options.openBrowser ?? (await loadDefaultOpener());
|
|
293
|
+
if (opener) {
|
|
294
|
+
try {
|
|
295
|
+
await opener(url);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Fall through to the URL print below.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// No opener available, or the opener threw. Surface the URL so the user
|
|
303
|
+
// can finish the flow manually.
|
|
304
|
+
if (options.onAuthorizeUrl)
|
|
305
|
+
options.onAuthorizeUrl(url);
|
|
306
|
+
else if (isStdoutTTY())
|
|
307
|
+
console.log(`Open this URL in your browser:\n ${url}`);
|
|
308
|
+
}
|
|
309
|
+
async function loadDefaultOpener() {
|
|
310
|
+
try {
|
|
311
|
+
const mod = (await import('open'));
|
|
312
|
+
return async (url) => {
|
|
313
|
+
await mod.default(url);
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../../src/auth/flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { AuthErrorCode } from './errors.js';
|
|
2
|
+
export { runOAuthFlow } from './flow.js';
|
|
3
|
+
export type { RunOAuthFlowOptions, RunOAuthFlowResult } from './flow.js';
|
|
4
|
+
export { DEFAULT_VERIFIER_ALPHABET, deriveChallenge, generateState, generateVerifier, } from './pkce.js';
|
|
5
|
+
export type { GenerateVerifierOptions } from './pkce.js';
|
|
6
|
+
export { createPkceProvider } from './providers/pkce.js';
|
|
7
|
+
export type { PkceLazyString, PkceProviderOptions } from './providers/pkce.js';
|
|
8
|
+
export type { AuthAccount, AuthorizeInput, AuthorizeResult, AuthProvider, ExchangeInput, ExchangeResult, PrepareInput, PrepareResult, TokenStore, ValidateInput, } from './types.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACxE,OAAO,EACH,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,gBAAgB,GACnB,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9E,YAAY,EACR,WAAW,EACX,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,GAChB,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,EACH,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,gBAAgB,GACnB,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default RFC 7636 unreserved character set: `A-Z a-z 0-9 - . _ ~`. 66 chars.
|
|
3
|
+
*
|
|
4
|
+
* Some providers (Todoist) ship a 64-char subset that drops `.~` to keep the
|
|
5
|
+
* verifier alphanumeric-with-dashes-and-underscores; pass it via
|
|
6
|
+
* `generateVerifier({ alphabet })` if you need to match a specific server's
|
|
7
|
+
* canonicalisation.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_VERIFIER_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
10
|
+
export type GenerateVerifierOptions = {
|
|
11
|
+
/** Verifier length. RFC 7636 §4.1 mandates 43–128. Default: 64. */
|
|
12
|
+
length?: number;
|
|
13
|
+
/** Override character set (must contain only RFC 7636 unreserved chars). Default: `DEFAULT_VERIFIER_ALPHABET`. */
|
|
14
|
+
alphabet?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Generate a PKCE `code_verifier`. Uses `crypto.randomInt` to map random bytes
|
|
18
|
+
* uniformly onto the alphabet — no modulo bias, no rejection sampling needed
|
|
19
|
+
* at the call site.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateVerifier(options?: GenerateVerifierOptions): string;
|
|
22
|
+
/** Derive the S256 `code_challenge` from a verifier: base64url(sha256(verifier)). */
|
|
23
|
+
export declare function deriveChallenge(verifier: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Generate a CSRF `state` token. 16 random bytes (128 bits) hex-encoded —
|
|
26
|
+
* comfortably above the 32-bit floor recommended by OAuth 2 §10.12.
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateState(): string;
|
|
29
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,uEACkC,CAAA;AAExE,MAAM,MAAM,uBAAuB,GAAG;IAClC,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kHAAkH;IAClH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,MAAM,CAa9E;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomInt } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Default RFC 7636 unreserved character set: `A-Z a-z 0-9 - . _ ~`. 66 chars.
|
|
4
|
+
*
|
|
5
|
+
* Some providers (Todoist) ship a 64-char subset that drops `.~` to keep the
|
|
6
|
+
* verifier alphanumeric-with-dashes-and-underscores; pass it via
|
|
7
|
+
* `generateVerifier({ alphabet })` if you need to match a specific server's
|
|
8
|
+
* canonicalisation.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_VERIFIER_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
11
|
+
/**
|
|
12
|
+
* Generate a PKCE `code_verifier`. Uses `crypto.randomInt` to map random bytes
|
|
13
|
+
* uniformly onto the alphabet — no modulo bias, no rejection sampling needed
|
|
14
|
+
* at the call site.
|
|
15
|
+
*/
|
|
16
|
+
export function generateVerifier(options = {}) {
|
|
17
|
+
const length = options.length ?? 64;
|
|
18
|
+
const alphabet = options.alphabet ?? DEFAULT_VERIFIER_ALPHABET;
|
|
19
|
+
if (length < 43 || length > 128) {
|
|
20
|
+
throw new RangeError(`PKCE verifier length must be 43..128, got ${length}`);
|
|
21
|
+
}
|
|
22
|
+
if (alphabet.length === 0)
|
|
23
|
+
throw new RangeError('PKCE verifier alphabet must be non-empty');
|
|
24
|
+
let out = '';
|
|
25
|
+
for (let i = 0; i < length; i++) {
|
|
26
|
+
out += alphabet[randomInt(0, alphabet.length)];
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/** Derive the S256 `code_challenge` from a verifier: base64url(sha256(verifier)). */
|
|
31
|
+
export function deriveChallenge(verifier) {
|
|
32
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generate a CSRF `state` token. 16 random bytes (128 bits) hex-encoded —
|
|
36
|
+
* comfortably above the 32-bit floor recommended by OAuth 2 §10.12.
|
|
37
|
+
*/
|
|
38
|
+
export function generateState() {
|
|
39
|
+
return randomBytes(16).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=pkce.js.map
|