@framedash/cli 0.1.2 → 0.1.4
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 +42 -7
- package/dist/commands/alerts.d.ts.map +1 -1
- package/dist/commands/alerts.js +2 -1
- package/dist/commands/alerts.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +25 -2
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +142 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +79 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/map-capture.d.ts +2 -1
- package/dist/commands/map-capture.d.ts.map +1 -1
- package/dist/commands/map-capture.js +38 -9
- package/dist/commands/map-capture.js.map +1 -1
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +26 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +12 -0
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/run-profile-test.d.ts.map +1 -1
- package/dist/commands/run-profile-test.js +143 -26
- package/dist/commands/run-profile-test.js.map +1 -1
- package/dist/commands/threshold-profiles.d.ts +2 -0
- package/dist/commands/threshold-profiles.d.ts.map +1 -0
- package/dist/commands/threshold-profiles.js +24 -0
- package/dist/commands/threshold-profiles.js.map +1 -0
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +42 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +44 -14
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/create-client.d.ts +25 -7
- package/dist/lib/create-client.d.ts.map +1 -1
- package/dist/lib/create-client.js +160 -33
- package/dist/lib/create-client.js.map +1 -1
- package/dist/lib/formatters.js +109 -1
- package/dist/lib/formatters.js.map +1 -1
- package/dist/lib/oauth/loopback-server.d.ts +27 -0
- package/dist/lib/oauth/loopback-server.d.ts.map +1 -0
- package/dist/lib/oauth/loopback-server.js +164 -0
- package/dist/lib/oauth/loopback-server.js.map +1 -0
- package/dist/lib/oauth/manager.d.ts +36 -0
- package/dist/lib/oauth/manager.d.ts.map +1 -0
- package/dist/lib/oauth/manager.js +130 -0
- package/dist/lib/oauth/manager.js.map +1 -0
- package/dist/lib/oauth/pkce.d.ts +15 -0
- package/dist/lib/oauth/pkce.d.ts.map +1 -0
- package/dist/lib/oauth/pkce.js +24 -0
- package/dist/lib/oauth/pkce.js.map +1 -0
- package/dist/lib/oauth/token-endpoint.d.ts +39 -0
- package/dist/lib/oauth/token-endpoint.d.ts.map +1 -0
- package/dist/lib/oauth/token-endpoint.js +117 -0
- package/dist/lib/oauth/token-endpoint.js.map +1 -0
- package/dist/lib/oauth/token-store.d.ts +34 -0
- package/dist/lib/oauth/token-store.d.ts.map +1 -0
- package/dist/lib/oauth/token-store.js +164 -0
- package/dist/lib/oauth/token-store.js.map +1 -0
- package/dist/lib/run-command.js +2 -2
- package/dist/lib/run-command.js.map +1 -1
- package/dist/lib/run-profile-test-lib.d.ts +39 -0
- package/dist/lib/run-profile-test-lib.d.ts.map +1 -1
- package/dist/lib/run-profile-test-lib.js +47 -0
- package/dist/lib/run-profile-test-lib.js.map +1 -1
- package/dist/lib/uploader.d.ts +14 -1
- package/dist/lib/uploader.d.ts.map +1 -1
- package/dist/lib/uploader.js +26 -12
- package/dist/lib/uploader.js.map +1 -1
- package/package.json +3 -3
package/dist/lib/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { ApiClient } from "@framedash/api-client";
|
|
2
|
+
import type { CliCredential } from "./config.js";
|
|
3
|
+
import { OAuthTokenManager } from "./oauth/manager.js";
|
|
4
|
+
export type CreateClientOptions = {
|
|
5
|
+
throwOnError?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Test-only: drop cached managers so each test starts from its own entry. */
|
|
8
|
+
export declare function resetOAuthManagerCacheForTests(): void;
|
|
2
9
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* ingest poll), where a transient 429/5xx must be thrown and retried, not exit.
|
|
10
|
+
* Process-shared token manager for an origin. Also used by non-ApiClient
|
|
11
|
+
* consumers (the map-capture uploader) so EVERY Bearer credential in the
|
|
12
|
+
* process rides on the same rotation state.
|
|
7
13
|
*/
|
|
8
|
-
export declare function
|
|
9
|
-
|
|
10
|
-
}):
|
|
14
|
+
export declare function getSharedOAuthManager(baseUrl: string, credential: Extract<CliCredential, {
|
|
15
|
+
kind: "oauth";
|
|
16
|
+
}>): OAuthTokenManager;
|
|
17
|
+
/**
|
|
18
|
+
* Create the CLI's API client from the resolved credential. By default a
|
|
19
|
+
* request error prints a message and exits the process (the right behavior
|
|
20
|
+
* for one-shot commands). Pass `throwOnError` for a client used inside a
|
|
21
|
+
* retry loop (e.g. the run-profile-test ingest poll), where a transient
|
|
22
|
+
* 429/5xx must be thrown and retried, not exit.
|
|
23
|
+
*
|
|
24
|
+
* API keys go on X-API-Key as before. A stored OAuth login gets a wrapper
|
|
25
|
+
* that refreshes near expiry, retries exactly once after a 401, and persists
|
|
26
|
+
* rotated refresh tokens.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createClient(baseUrl: string, credential: CliCredential, projectId: string, options?: CreateClientOptions): ApiClient;
|
|
11
29
|
//# sourceMappingURL=create-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-client.d.ts","sourceRoot":"","sources":["../../src/lib/create-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"create-client.d.ts","sourceRoot":"","sources":["../../src/lib/create-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAY,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAA2B,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,MAAM,MAAM,mBAAmB,GAAG;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAY7D,8EAA8E;AAC9E,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,GACnD,iBAAiB,CAOnB;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC3B,SAAS,CAiBX"}
|
|
@@ -1,46 +1,173 @@
|
|
|
1
|
-
import { ApiClient } from "@framedash/api-client";
|
|
1
|
+
import { ApiClient, ApiError } from "@framedash/api-client";
|
|
2
2
|
import { error } from "./logger.js";
|
|
3
|
+
import { OAuthLoginRequiredError, OAuthTokenManager } from "./oauth/manager.js";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* ONE token manager per origin per process. Refresh tokens are single-use
|
|
6
|
+
* (rotation), so two independent managers over the same stored login would
|
|
7
|
+
* race: the second to refresh presents an already-consumed token, which the
|
|
8
|
+
* server treats as theft and answers by revoking the whole grant. Commands
|
|
9
|
+
* that build several clients (e.g. run-profile-test's exit-on-error client
|
|
10
|
+
* plus its throwing poll client) must therefore share rotation state.
|
|
8
11
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const managerCache = new Map();
|
|
13
|
+
/** Test-only: drop cached managers so each test starts from its own entry. */
|
|
14
|
+
export function resetOAuthManagerCacheForTests() {
|
|
15
|
+
managerCache.clear();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Process-shared token manager for an origin. Also used by non-ApiClient
|
|
19
|
+
* consumers (the map-capture uploader) so EVERY Bearer credential in the
|
|
20
|
+
* process rides on the same rotation state.
|
|
21
|
+
*/
|
|
22
|
+
export function getSharedOAuthManager(baseUrl, credential) {
|
|
23
|
+
let manager = managerCache.get(credential.origin);
|
|
24
|
+
if (!manager) {
|
|
25
|
+
manager = new OAuthTokenManager(baseUrl, credential.origin, credential.entry);
|
|
26
|
+
managerCache.set(credential.origin, manager);
|
|
27
|
+
}
|
|
28
|
+
return manager;
|
|
29
|
+
}
|
|
30
|
+
/** Print an ApiError (with 429 rate-limit detail) and exit -- the default for one-shot commands. */
|
|
31
|
+
function printAndExit(err) {
|
|
32
|
+
if (err.status === 429) {
|
|
33
|
+
const retryAfter = err.retryAfter;
|
|
34
|
+
if (retryAfter !== undefined) {
|
|
35
|
+
error(`Rate limit exceeded (429). Retry after ${retryAfter}s.`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const reset = err.headers.get("X-RateLimit-Reset");
|
|
39
|
+
const resetNum = reset ? Number(reset) : Number.NaN;
|
|
40
|
+
const resetStr = !Number.isNaN(resetNum) && resetNum > 0
|
|
41
|
+
? new Date(resetNum * 1000).toLocaleTimeString()
|
|
42
|
+
: "unknown";
|
|
43
|
+
error(`Rate limit exceeded (429). Resets at ${resetStr}.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
error(err.message);
|
|
48
|
+
}
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create the CLI's API client from the resolved credential. By default a
|
|
53
|
+
* request error prints a message and exits the process (the right behavior
|
|
54
|
+
* for one-shot commands). Pass `throwOnError` for a client used inside a
|
|
55
|
+
* retry loop (e.g. the run-profile-test ingest poll), where a transient
|
|
56
|
+
* 429/5xx must be thrown and retried, not exit.
|
|
57
|
+
*
|
|
58
|
+
* API keys go on X-API-Key as before. A stored OAuth login gets a wrapper
|
|
59
|
+
* that refreshes near expiry, retries exactly once after a 401, and persists
|
|
60
|
+
* rotated refresh tokens.
|
|
61
|
+
*/
|
|
62
|
+
export function createClient(baseUrl, credential, projectId, options) {
|
|
63
|
+
const onError = options?.throwOnError
|
|
64
|
+
? (err) => {
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
: printAndExit;
|
|
68
|
+
if (credential.kind === "api-key") {
|
|
69
|
+
return new ApiClient({ baseUrl, apiKey: credential.apiKey, projectId, onError });
|
|
70
|
+
}
|
|
71
|
+
return new OAuthApiClient(baseUrl, projectId, getSharedOAuthManager(baseUrl, credential), options);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* ApiClient facade for the OAuth path. Extends ApiClient so the rest of the
|
|
75
|
+
* CLI (typed against ApiClient) needs no changes, but every verb delegates
|
|
76
|
+
* to a short-lived inner client carrying the CURRENT access token from the
|
|
77
|
+
* token manager; the superclass instance itself never sends a request.
|
|
78
|
+
*
|
|
79
|
+
* 401 handling: refresh once, retry once. A second failure surfaces like any
|
|
80
|
+
* other API error. A dead grant (refresh -> invalid_grant) always prints the
|
|
81
|
+
* re-login instruction and exits, even in throwOnError mode -- retry loops
|
|
82
|
+
* (which swallow thrown errors) must not spin on an unrecoverable credential.
|
|
83
|
+
*/
|
|
84
|
+
class OAuthApiClient extends ApiClient {
|
|
85
|
+
oauthBaseUrl;
|
|
86
|
+
oauthProjectId;
|
|
87
|
+
manager;
|
|
88
|
+
options;
|
|
89
|
+
constructor(oauthBaseUrl, oauthProjectId, manager, options) {
|
|
90
|
+
super({
|
|
91
|
+
baseUrl: oauthBaseUrl,
|
|
92
|
+
projectId: oauthProjectId,
|
|
93
|
+
// Never sent: all verbs are overridden to use the inner client.
|
|
94
|
+
accessToken: "oauth-managed-placeholder",
|
|
95
|
+
onError: (err) => {
|
|
96
|
+
throw err;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
this.oauthBaseUrl = oauthBaseUrl;
|
|
100
|
+
this.oauthProjectId = oauthProjectId;
|
|
101
|
+
this.manager = manager;
|
|
102
|
+
this.options = options;
|
|
103
|
+
}
|
|
104
|
+
async get(path) {
|
|
105
|
+
return this.execute((client) => client.get(path));
|
|
106
|
+
}
|
|
107
|
+
async post(path, body) {
|
|
108
|
+
return this.execute((client) => client.post(path, body));
|
|
109
|
+
}
|
|
110
|
+
async patch(path, body) {
|
|
111
|
+
return this.execute((client) => client.patch(path, body));
|
|
112
|
+
}
|
|
113
|
+
async delete(path) {
|
|
114
|
+
return this.execute((client) => client.delete(path));
|
|
115
|
+
}
|
|
116
|
+
withProject(projectId) {
|
|
117
|
+
return new OAuthApiClient(this.oauthBaseUrl, projectId, this.manager, this.options);
|
|
118
|
+
}
|
|
119
|
+
buildInner(accessToken) {
|
|
11
120
|
return new ApiClient({
|
|
12
|
-
baseUrl,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
onError(err) {
|
|
121
|
+
baseUrl: this.oauthBaseUrl,
|
|
122
|
+
projectId: this.oauthProjectId,
|
|
123
|
+
accessToken,
|
|
124
|
+
onError: (err) => {
|
|
16
125
|
throw err;
|
|
17
126
|
},
|
|
18
127
|
});
|
|
19
128
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (err
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
129
|
+
async token(forceRefresh) {
|
|
130
|
+
try {
|
|
131
|
+
return forceRefresh ? await this.manager.forceRefresh() : await this.manager.getAccessToken();
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof OAuthLoginRequiredError) {
|
|
135
|
+
error(err.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async execute(fn) {
|
|
142
|
+
const accessToken = await this.token(false);
|
|
143
|
+
try {
|
|
144
|
+
return await fn(this.buildInner(accessToken));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (isApiError(err) && err.status === 401) {
|
|
148
|
+
// The server rejected a token we believed valid (revoked access
|
|
149
|
+
// token, clock skew): refresh and retry exactly once.
|
|
150
|
+
const freshToken = await this.token(true);
|
|
151
|
+
try {
|
|
152
|
+
return await fn(this.buildInner(freshToken));
|
|
29
153
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const resetNum = reset ? Number(reset) : Number.NaN;
|
|
33
|
-
const resetStr = !Number.isNaN(resetNum) && resetNum > 0
|
|
34
|
-
? new Date(resetNum * 1000).toLocaleTimeString()
|
|
35
|
-
: "unknown";
|
|
36
|
-
error(`Rate limit exceeded (429). Resets at ${resetStr}.`);
|
|
154
|
+
catch (retryErr) {
|
|
155
|
+
return this.reportError(retryErr);
|
|
37
156
|
}
|
|
38
157
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
158
|
+
return this.reportError(err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
reportError(err) {
|
|
162
|
+
if (isApiError(err) && !this.options?.throwOnError) {
|
|
163
|
+
printAndExit(err);
|
|
164
|
+
}
|
|
165
|
+
// throwOnError mode, or a non-API failure (network error): propagate,
|
|
166
|
+
// matching the api-key client (index.ts prints and exits).
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function isApiError(err) {
|
|
171
|
+
return err instanceof ApiError;
|
|
45
172
|
}
|
|
46
173
|
//# sourceMappingURL=create-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-client.js","sourceRoot":"","sources":["../../src/lib/create-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"create-client.js","sourceRoot":"","sources":["../../src/lib/create-client.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/formatters.js
CHANGED
|
@@ -10,13 +10,121 @@ export function formatOutput(data, format) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
function formatTable(data) {
|
|
13
|
+
// The /api/v1/query endpoint returns { rows: [...], rowCount: N }. Tabulate
|
|
14
|
+
// the rows themselves rather than dumping the whole array into one JSON cell.
|
|
15
|
+
if (isQueryResult(data)) {
|
|
16
|
+
return formatTable(data.rows);
|
|
17
|
+
}
|
|
18
|
+
// A single object whose values contain nested arrays/objects (e.g. the
|
|
19
|
+
// dashboard's { kpis, dailyActiveUsers, topEvents }) renders one titled
|
|
20
|
+
// section per key rather than a single blob-cell row. Flat arrays-of-objects
|
|
21
|
+
// (maps list, alerts list, builds) fall through to the plain table renderer
|
|
22
|
+
// unchanged.
|
|
23
|
+
if (isSectionedPayload(data)) {
|
|
24
|
+
return formatSections(data);
|
|
25
|
+
}
|
|
13
26
|
const rows = toRows(data);
|
|
14
27
|
if (rows.length === 0)
|
|
15
28
|
return "(no data)";
|
|
16
29
|
const firstRow = rows[0];
|
|
17
30
|
if (!firstRow)
|
|
18
31
|
return "(no data)";
|
|
19
|
-
|
|
32
|
+
return renderRows(rows, Object.keys(firstRow));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True for the /api/v1/query result envelope { rows: [...], rowCount: number }.
|
|
36
|
+
* Only that endpoint returns this shape, so table format can safely unwrap it to
|
|
37
|
+
* tabulate the individual records instead of rendering one giant JSON cell.
|
|
38
|
+
*/
|
|
39
|
+
function isQueryResult(data) {
|
|
40
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
41
|
+
return false;
|
|
42
|
+
const obj = data;
|
|
43
|
+
return Array.isArray(obj.rows) && typeof obj.rowCount === "number";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* True for an envelope-style object whose every field is a nested array/object
|
|
47
|
+
* (e.g. the dashboard's { kpis, dailyActiveUsers, topEvents } or status's
|
|
48
|
+
* { project, kpis }). Records that mix scalar fields with a nested one -- such as
|
|
49
|
+
* an alert row's scalars plus channelIds: [] -- stay on the plain one-row table
|
|
50
|
+
* path, so `alerts create/update --format table` output does not regress.
|
|
51
|
+
*/
|
|
52
|
+
function isSectionedPayload(data) {
|
|
53
|
+
if (Array.isArray(data))
|
|
54
|
+
return false;
|
|
55
|
+
if (typeof data !== "object" || data === null)
|
|
56
|
+
return false;
|
|
57
|
+
const values = Object.values(data);
|
|
58
|
+
if (values.length === 0)
|
|
59
|
+
return false;
|
|
60
|
+
return values.every((v) => v !== null && typeof v === "object");
|
|
61
|
+
}
|
|
62
|
+
/** Render each top-level key of a sectioned payload as a titled sub-table. */
|
|
63
|
+
function formatSections(obj) {
|
|
64
|
+
const sections = Object.entries(obj).map(([key, value]) => {
|
|
65
|
+
let table;
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
table = formatArraySection(value);
|
|
68
|
+
}
|
|
69
|
+
else if (value !== null && typeof value === "object") {
|
|
70
|
+
table = formatObjectSection(value);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
table = cellString(value);
|
|
74
|
+
}
|
|
75
|
+
return `## ${key}\n${table}`;
|
|
76
|
+
});
|
|
77
|
+
return sections.join("\n\n");
|
|
78
|
+
}
|
|
79
|
+
/** An array-of-objects section: normal table, nested objects flattened to dot-paths. */
|
|
80
|
+
function formatArraySection(arr) {
|
|
81
|
+
if (arr.length === 0)
|
|
82
|
+
return "(no data)";
|
|
83
|
+
const rows = arr.map((item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
84
|
+
? flattenOneLevel(item)
|
|
85
|
+
: { value: item });
|
|
86
|
+
return renderRows(rows, unionKeys(rows));
|
|
87
|
+
}
|
|
88
|
+
/** An object section: two-column key/value rows. */
|
|
89
|
+
function formatObjectSection(obj) {
|
|
90
|
+
const rows = Object.entries(obj).map(([key, value]) => ({ key, value }));
|
|
91
|
+
if (rows.length === 0)
|
|
92
|
+
return "(no data)";
|
|
93
|
+
return renderRows(rows, ["key", "value"]);
|
|
94
|
+
}
|
|
95
|
+
/** Flatten one level of nested objects into dot-path keys; deeper nesting stays JSON. */
|
|
96
|
+
function flattenOneLevel(row) {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [key, value] of Object.entries(row)) {
|
|
99
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
100
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
101
|
+
out[`${key}.${subKey}`] = subValue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
out[key] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/** Union of row keys in first-seen order. */
|
|
111
|
+
function unionKeys(rows) {
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const keys = [];
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
for (const key of Object.keys(row)) {
|
|
116
|
+
if (!seen.has(key)) {
|
|
117
|
+
seen.add(key);
|
|
118
|
+
keys.push(key);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return keys;
|
|
123
|
+
}
|
|
124
|
+
/** Render aligned columns for the given rows/keys using cellString for each cell. */
|
|
125
|
+
function renderRows(rows, keys) {
|
|
126
|
+
if (rows.length === 0 || keys.length === 0)
|
|
127
|
+
return "(no data)";
|
|
20
128
|
const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => cellString(r[k]).length)));
|
|
21
129
|
const header = keys.map((k, i) => k.padEnd(widths[i] ?? k.length)).join(" ");
|
|
22
130
|
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.js","sourceRoot":"","sources":["../../src/lib/formatters.ts"],"names":[],"mappings":"AAEA,0DAA0D;AAC1D,MAAM,UAAU,YAAY,CAAC,IAAa,EAAE,MAAoB;IAC/D,QAAQ,MAAM,EAAE,CAAC;QAChB,KAAK,MAAM;YACV,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACtC,KAAK,OAAO;YACX,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;QAC1B,KAAK,KAAK;YACT,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;AACF,CAAC;AAED,SAAS,WAAW,CAAC,IAAa;IACjC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC;IAE1C,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,CAAC,QAAQ;QAAE,OAAO,WAAW,CAAC;IAElC,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"formatters.js","sourceRoot":"","sources":["../../src/lib/formatters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authorization server bounced back an OAuth error (e.g. the user hit
|
|
3
|
+
* Deny -> access_denied). Distinguished from local failures so the CLI can
|
|
4
|
+
* present it as a normal outcome rather than a bug.
|
|
5
|
+
*/
|
|
6
|
+
export declare class OAuthCallbackError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
constructor(code: string, description: string | null);
|
|
9
|
+
}
|
|
10
|
+
export type LoopbackServer = {
|
|
11
|
+
/** The ephemeral port actually bound on 127.0.0.1. */
|
|
12
|
+
port: number;
|
|
13
|
+
/** The exact redirect URI to register in the authorize request. */
|
|
14
|
+
redirectUri: string;
|
|
15
|
+
/** Resolve with the authorization code, or reject on error/timeout. */
|
|
16
|
+
waitForCallback: (timeoutMs: number) => Promise<{
|
|
17
|
+
code: string;
|
|
18
|
+
}>;
|
|
19
|
+
close: () => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Start the loopback callback receiver. `expectedState` is the CSRF state
|
|
23
|
+
* minted for this login attempt; only a /callback request carrying exactly
|
|
24
|
+
* that state can complete the flow.
|
|
25
|
+
*/
|
|
26
|
+
export declare function startLoopbackServer(expectedState: string): Promise<LoopbackServer>;
|
|
27
|
+
//# sourceMappingURL=loopback-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback-server.d.ts","sourceRoot":"","sources":["../../../src/lib/oauth/loopback-server.ts"],"names":[],"mappings":"AAiCA;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;aAE3B,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,WAAW,EAAE,MAAM,GAAG,IAAI;CAK3B;AAWD,MAAM,MAAM,cAAc,GAAG;IAC5B,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA+HlF"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
// RFC 8252 section 7.3 loopback redirect receiver for `framedash login`.
|
|
3
|
+
//
|
|
4
|
+
// SECURITY invariants:
|
|
5
|
+
// - Binds STRICTLY to 127.0.0.1 (never 0.0.0.0 / ::) on an ephemeral port,
|
|
6
|
+
// so nothing off-host can reach the callback.
|
|
7
|
+
// - The `state` parameter is validated FIRST, for success AND error
|
|
8
|
+
// callbacks alike, BEFORE the caller ever sees a code. A mismatched or
|
|
9
|
+
// missing state does NOT settle the login: the request gets a generic
|
|
10
|
+
// 400 and the flow keeps waiting for the legitimate callback -- a forged
|
|
11
|
+
// local request can neither abort nor complete a pending sign-in, and
|
|
12
|
+
// any accompanying code is discarded (no token exchange happens).
|
|
13
|
+
// - Responses to the browser are static HTML with no token/code material,
|
|
14
|
+
// and nothing from the callback URL is reflected into the page.
|
|
15
|
+
const HTML_HEADERS = {
|
|
16
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
17
|
+
"Cache-Control": "no-store",
|
|
18
|
+
"Referrer-Policy": "no-referrer",
|
|
19
|
+
};
|
|
20
|
+
function page(title, body) {
|
|
21
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem;"><h1 style="font-size: 1.2rem;">${title}</h1><p>${body}</p></body></html>`;
|
|
22
|
+
}
|
|
23
|
+
const SUCCESS_PAGE = page("Login complete", "You can close this tab and return to the terminal.");
|
|
24
|
+
const FAILURE_PAGE = page("Login failed", "The sign-in did not complete. Close this tab and check the terminal for details.");
|
|
25
|
+
const DONE_PAGE = page("Login already completed", "You can close this tab.");
|
|
26
|
+
/**
|
|
27
|
+
* The authorization server bounced back an OAuth error (e.g. the user hit
|
|
28
|
+
* Deny -> access_denied). Distinguished from local failures so the CLI can
|
|
29
|
+
* present it as a normal outcome rather than a bug.
|
|
30
|
+
*/
|
|
31
|
+
export class OAuthCallbackError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
constructor(code, description) {
|
|
34
|
+
super(`Authorization was not granted (${code})${description ? `: ${description}` : ""}`);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.name = "OAuthCallbackError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Keep attacker-influenced callback values printable before they reach the
|
|
41
|
+
* terminal: strip everything outside a conservative ASCII subset (kills
|
|
42
|
+
* control characters / ANSI escapes) and cap the length.
|
|
43
|
+
*/
|
|
44
|
+
function sanitizeForTerminal(value) {
|
|
45
|
+
return value.replace(/[^a-zA-Z0-9 _.,:;/'()-]/g, "").slice(0, 200);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Start the loopback callback receiver. `expectedState` is the CSRF state
|
|
49
|
+
* minted for this login attempt; only a /callback request carrying exactly
|
|
50
|
+
* that state can complete the flow.
|
|
51
|
+
*/
|
|
52
|
+
export function startLoopbackServer(expectedState) {
|
|
53
|
+
let settled = false;
|
|
54
|
+
let resolveCallback = () => { };
|
|
55
|
+
let rejectCallback = () => { };
|
|
56
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
57
|
+
resolveCallback = resolve;
|
|
58
|
+
rejectCallback = reject;
|
|
59
|
+
});
|
|
60
|
+
// A rejection that nobody has awaited yet (e.g. state mismatch arriving
|
|
61
|
+
// before waitForCallback is called) must not crash the process.
|
|
62
|
+
callbackPromise.catch(() => { });
|
|
63
|
+
const settle = (outcome) => {
|
|
64
|
+
if (settled)
|
|
65
|
+
return;
|
|
66
|
+
settled = true;
|
|
67
|
+
if (outcome instanceof Error) {
|
|
68
|
+
rejectCallback(outcome);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
resolveCallback(outcome);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
76
|
+
if (req.method !== "GET" || url.pathname !== "/callback") {
|
|
77
|
+
res.writeHead(404, HTML_HEADERS);
|
|
78
|
+
res.end(page("Not found", "Nothing to see here."));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (settled) {
|
|
82
|
+
res.writeHead(200, HTML_HEADERS);
|
|
83
|
+
res.end(DONE_PAGE);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// STRICT state validation FIRST, for success AND error callbacks alike
|
|
87
|
+
// (RFC 6749 section 4.1.2.1 requires `state` in error redirects too).
|
|
88
|
+
// A mismatched/missing state is treated as an unexpected request and
|
|
89
|
+
// deliberately does NOT settle: neither a forged error= callback nor a
|
|
90
|
+
// forged code may abort or complete someone's pending login, and the
|
|
91
|
+
// accompanying code (if any) is never handed to the caller, so no
|
|
92
|
+
// token exchange can happen for a forged/replayed callback.
|
|
93
|
+
const state = url.searchParams.get("state");
|
|
94
|
+
if (state !== expectedState) {
|
|
95
|
+
res.writeHead(400, HTML_HEADERS);
|
|
96
|
+
res.end(page("Unexpected request", "This request did not match a pending sign-in."));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const errorParam = url.searchParams.get("error");
|
|
100
|
+
if (errorParam !== null) {
|
|
101
|
+
res.writeHead(200, HTML_HEADERS);
|
|
102
|
+
res.end(FAILURE_PAGE);
|
|
103
|
+
const description = url.searchParams.get("error_description");
|
|
104
|
+
settle(new OAuthCallbackError(sanitizeForTerminal(errorParam) || "unknown_error", description ? sanitizeForTerminal(description) : null));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const code = url.searchParams.get("code");
|
|
108
|
+
if (!code) {
|
|
109
|
+
res.writeHead(400, HTML_HEADERS);
|
|
110
|
+
res.end(FAILURE_PAGE);
|
|
111
|
+
settle(new Error("Login callback carried no authorization code."));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
res.writeHead(200, HTML_HEADERS);
|
|
115
|
+
res.end(SUCCESS_PAGE);
|
|
116
|
+
settle({ code });
|
|
117
|
+
});
|
|
118
|
+
// Refuse to linger: once the single callback settles the login, keep-alive
|
|
119
|
+
// sockets must not hold the process open.
|
|
120
|
+
server.keepAliveTimeout = 1000;
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
server.once("error", reject);
|
|
123
|
+
// STRICT loopback bind: 127.0.0.1 only (never 0.0.0.0/::), ephemeral port.
|
|
124
|
+
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
|
|
125
|
+
const address = server.address();
|
|
126
|
+
if (address === null || typeof address === "string") {
|
|
127
|
+
reject(new Error("Loopback server failed to report a bound port"));
|
|
128
|
+
server.close();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const port = address.port;
|
|
132
|
+
resolve({
|
|
133
|
+
port,
|
|
134
|
+
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
135
|
+
waitForCallback: (timeoutMs) => {
|
|
136
|
+
return new Promise((resolveWait, rejectWait) => {
|
|
137
|
+
const timer = setTimeout(() => {
|
|
138
|
+
settle(new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the browser login callback.`));
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
timer.unref();
|
|
141
|
+
callbackPromise
|
|
142
|
+
.then((value) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
resolveWait(value);
|
|
145
|
+
})
|
|
146
|
+
.catch((err) => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
rejectWait(err);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
close: () => new Promise((resolveClose) => {
|
|
153
|
+
server.close(() => resolveClose());
|
|
154
|
+
// Node >= 18.2 only; on older 18.x the short keepAliveTimeout
|
|
155
|
+
// set above still lets close() finish promptly.
|
|
156
|
+
if (typeof server.closeAllConnections === "function") {
|
|
157
|
+
server.closeAllConnections();
|
|
158
|
+
}
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=loopback-server.js.map
|