@framedash/cli 0.1.1 → 0.1.3
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 +62 -5
- 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/builds.d.ts +2 -0
- package/dist/commands/builds.d.ts.map +1 -0
- package/dist/commands/builds.js +31 -0
- package/dist/commands/builds.js.map +1 -0
- package/dist/commands/content.d.ts.map +1 -1
- package/dist/commands/content.js +2 -2
- package/dist/commands/content.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +3 -3
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/funnel.d.ts.map +1 -1
- package/dist/commands/funnel.js +7 -8
- package/dist/commands/funnel.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 +42 -12
- package/dist/commands/map-capture.js.map +1 -1
- package/dist/commands/perf-diff.d.ts +2 -0
- package/dist/commands/perf-diff.d.ts.map +1 -0
- package/dist/commands/perf-diff.js +125 -0
- package/dist/commands/perf-diff.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/retention.d.ts.map +1 -1
- package/dist/commands/retention.js +3 -3
- package/dist/commands/retention.js.map +1 -1
- package/dist/commands/run-profile-test.d.ts +7 -0
- package/dist/commands/run-profile-test.d.ts.map +1 -0
- package/dist/commands/run-profile-test.js +503 -0
- package/dist/commands/run-profile-test.js.map +1 -0
- 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 +16 -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 +27 -1
- package/dist/lib/create-client.d.ts.map +1 -1
- package/dist/lib/create-client.js +167 -24
- package/dist/lib/create-client.js.map +1 -1
- package/dist/lib/formatters.js +93 -1
- package/dist/lib/formatters.js.map +1 -1
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +4 -0
- package/dist/lib/logger.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/perf-diff-eval.d.ts +58 -0
- package/dist/lib/perf-diff-eval.d.ts.map +1 -0
- package/dist/lib/perf-diff-eval.js +49 -0
- package/dist/lib/perf-diff-eval.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 +156 -0
- package/dist/lib/run-profile-test-lib.d.ts.map +1 -0
- package/dist/lib/run-profile-test-lib.js +171 -0
- package/dist/lib/run-profile-test-lib.js.map +1 -0
- 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
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback-server.js","sourceRoot":"","sources":["../../../src/lib/oauth/loopback-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type StoredTokenEntry } from "./token-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* The stored grant is dead (refresh returned invalid_grant: revoked, expired,
|
|
4
|
+
* or rotated out from under us). The stored entry has already been cleared;
|
|
5
|
+
* the only recovery is an interactive re-login, so callers must surface the
|
|
6
|
+
* message and stop -- retrying cannot succeed.
|
|
7
|
+
*/
|
|
8
|
+
export declare class OAuthLoginRequiredError extends Error {
|
|
9
|
+
constructor(origin: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class OAuthTokenManager {
|
|
12
|
+
private readonly baseUrl;
|
|
13
|
+
private readonly origin;
|
|
14
|
+
private entry;
|
|
15
|
+
private refreshPromise;
|
|
16
|
+
constructor(baseUrl: string, origin: string, entry: StoredTokenEntry);
|
|
17
|
+
/** Space-delimited granted scopes (safe to display). */
|
|
18
|
+
get scope(): string;
|
|
19
|
+
/** Access-token expiry, epoch ms (safe to display). */
|
|
20
|
+
get expiresAt(): number;
|
|
21
|
+
/**
|
|
22
|
+
* Current access token, refreshing first when it expires within 60s so an
|
|
23
|
+
* about-to-expire token is not sent only to bounce with a 401.
|
|
24
|
+
*/
|
|
25
|
+
getAccessToken(): Promise<string>;
|
|
26
|
+
/** Force a refresh (after a 401 despite a locally-unexpired token). */
|
|
27
|
+
forceRefresh(): Promise<string>;
|
|
28
|
+
/** Coalesce concurrent callers onto a single refresh request. */
|
|
29
|
+
private refresh;
|
|
30
|
+
/**
|
|
31
|
+
* Refresh with the CURRENT stored token, then persist the rotated pair
|
|
32
|
+
* (atomic write, last-writer-wins).
|
|
33
|
+
*/
|
|
34
|
+
private doRefresh;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/lib/oauth/manager.ts"],"names":[],"mappings":"AACA,OAAO,EAGN,KAAK,gBAAgB,EAErB,MAAM,kBAAkB,CAAC;AAgB1B;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,MAAM,EAAE,MAAM;CAO1B;AAED,qBAAa,iBAAiB;IAK5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALxB,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,cAAc,CAAgC;gBAGpC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EAC/B,KAAK,EAAE,gBAAgB;IAKxB,wDAAwD;IACxD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,uDAAuD;IACvD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAOvC,uEAAuE;IACjE,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAIrC,iEAAiE;IACjE,OAAO,CAAC,OAAO;IASf;;;OAGG;YACW,SAAS;CAsDvB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { OAuthTokenRequestError, refreshTokenGrant, toStoredEntry } from "./token-endpoint.js";
|
|
2
|
+
import { deleteStoredEntry, readStoredEntry, saveStoredEntry, } from "./token-store.js";
|
|
3
|
+
// In-process lifecycle for one origin's stored OAuth credentials: hands out
|
|
4
|
+
// the access token, refreshes proactively near expiry (and on demand after a
|
|
5
|
+
// 401), and persists rotated refresh tokens atomically via the token store.
|
|
6
|
+
//
|
|
7
|
+
// Concurrency: refreshes are coalesced IN-PROCESS (one request per manager,
|
|
8
|
+
// and create-client shares one manager per origin). Across processes the
|
|
9
|
+
// store is last-writer-wins with no locking; a concurrent same-origin
|
|
10
|
+
// refresh can race rotation, whose worst case is the server's reuse
|
|
11
|
+
// revocation surfacing as the ordinary invalid_grant re-login path below.
|
|
12
|
+
// See token-store.ts for the full concurrency-model note.
|
|
13
|
+
/** Refresh when the access token expires within this window. */
|
|
14
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
15
|
+
/**
|
|
16
|
+
* The stored grant is dead (refresh returned invalid_grant: revoked, expired,
|
|
17
|
+
* or rotated out from under us). The stored entry has already been cleared;
|
|
18
|
+
* the only recovery is an interactive re-login, so callers must surface the
|
|
19
|
+
* message and stop -- retrying cannot succeed.
|
|
20
|
+
*/
|
|
21
|
+
export class OAuthLoginRequiredError extends Error {
|
|
22
|
+
constructor(origin) {
|
|
23
|
+
super(`Your stored login for ${origin} has expired or been revoked. ` +
|
|
24
|
+
`Run 'framedash login' to sign in again (CI should use FRAMEDASH_API_KEY instead).`);
|
|
25
|
+
this.name = "OAuthLoginRequiredError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class OAuthTokenManager {
|
|
29
|
+
baseUrl;
|
|
30
|
+
origin;
|
|
31
|
+
entry;
|
|
32
|
+
refreshPromise = null;
|
|
33
|
+
constructor(baseUrl, origin, entry) {
|
|
34
|
+
this.baseUrl = baseUrl;
|
|
35
|
+
this.origin = origin;
|
|
36
|
+
this.entry = entry;
|
|
37
|
+
}
|
|
38
|
+
/** Space-delimited granted scopes (safe to display). */
|
|
39
|
+
get scope() {
|
|
40
|
+
return this.entry.scope;
|
|
41
|
+
}
|
|
42
|
+
/** Access-token expiry, epoch ms (safe to display). */
|
|
43
|
+
get expiresAt() {
|
|
44
|
+
return this.entry.expires_at;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Current access token, refreshing first when it expires within 60s so an
|
|
48
|
+
* about-to-expire token is not sent only to bounce with a 401.
|
|
49
|
+
*/
|
|
50
|
+
async getAccessToken() {
|
|
51
|
+
if (this.entry.expires_at - Date.now() > EXPIRY_SKEW_MS) {
|
|
52
|
+
return this.entry.access_token;
|
|
53
|
+
}
|
|
54
|
+
return this.refresh();
|
|
55
|
+
}
|
|
56
|
+
/** Force a refresh (after a 401 despite a locally-unexpired token). */
|
|
57
|
+
async forceRefresh() {
|
|
58
|
+
return this.refresh();
|
|
59
|
+
}
|
|
60
|
+
/** Coalesce concurrent callers onto a single refresh request. */
|
|
61
|
+
refresh() {
|
|
62
|
+
if (this.refreshPromise === null) {
|
|
63
|
+
this.refreshPromise = this.doRefresh().finally(() => {
|
|
64
|
+
this.refreshPromise = null;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return this.refreshPromise;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Refresh with the CURRENT stored token, then persist the rotated pair
|
|
71
|
+
* (atomic write, last-writer-wins).
|
|
72
|
+
*/
|
|
73
|
+
async doRefresh() {
|
|
74
|
+
// Re-read first: another process may have rotated since this manager
|
|
75
|
+
// loaded. Presenting an already-rotated token would trip the server's
|
|
76
|
+
// reuse detection, so prefer the stored (current) credentials.
|
|
77
|
+
const stored = readStoredEntry(this.origin);
|
|
78
|
+
if (!stored) {
|
|
79
|
+
// The entry is GONE: a logout (or an invalid_grant cleanup) completed
|
|
80
|
+
// while we held stale in-memory credentials -- demand a re-login
|
|
81
|
+
// rather than reviving a login the user terminated.
|
|
82
|
+
throw new OAuthLoginRequiredError(this.origin);
|
|
83
|
+
}
|
|
84
|
+
if (stored.access_token !== this.entry.access_token) {
|
|
85
|
+
this.entry = stored;
|
|
86
|
+
if (stored.expires_at - Date.now() > EXPIRY_SKEW_MS) {
|
|
87
|
+
// Another process's access token is still fresh: no refresh needed.
|
|
88
|
+
return stored.access_token;
|
|
89
|
+
}
|
|
90
|
+
// Fall through and refresh with the reloaded (current) refresh token.
|
|
91
|
+
}
|
|
92
|
+
let entry;
|
|
93
|
+
try {
|
|
94
|
+
entry = toStoredEntry(await refreshTokenGrant(this.baseUrl, this.entry.refresh_token));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err instanceof OAuthTokenRequestError && err.code === "invalid_grant") {
|
|
98
|
+
// Dead grant: revoked, expired, or a concurrent refresh in another
|
|
99
|
+
// process won the rotation race (reuse revocation). Clear the local
|
|
100
|
+
// entry and ask for a re-login -- the accepted worst case of the
|
|
101
|
+
// lock-free store.
|
|
102
|
+
await deleteStoredEntry(this.origin);
|
|
103
|
+
throw new OAuthLoginRequiredError(this.origin);
|
|
104
|
+
}
|
|
105
|
+
// Transient failure (network, 5xx, rate limit): keep the stored entry
|
|
106
|
+
// so a later attempt can retry.
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
// Persist BEFORE first use: rotation invalidated the old refresh token
|
|
110
|
+
// server-side, so losing the new one would strand this login.
|
|
111
|
+
try {
|
|
112
|
+
await saveStoredEntry(this.origin, entry);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// The rotation could not be persisted, but the OLD refresh token on
|
|
116
|
+
// disk is already consumed server-side: best-effort remove it so the
|
|
117
|
+
// next run gets a clean re-login instead of reuse revocation.
|
|
118
|
+
try {
|
|
119
|
+
await deleteStoredEntry(this.origin);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Best effort: the same broken disk likely fails this too.
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
this.entry = entry;
|
|
127
|
+
return entry.access_token;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/lib/oauth/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate an RFC 7636 code_verifier: 48 random bytes -> 64 base64url chars
|
|
3
|
+
* (within the required 43-128 range, alphabet [A-Za-z0-9-_] which is a subset
|
|
4
|
+
* of the allowed unreserved characters).
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateCodeVerifier(): string;
|
|
7
|
+
/** Compute the S256 code_challenge: base64url(sha256(verifier)), no padding. */
|
|
8
|
+
export declare function computeS256CodeChallenge(codeVerifier: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Generate the CSRF `state` parameter: 24 random bytes -> 32 base64url chars.
|
|
11
|
+
* Compared byte-for-byte against the loopback callback; any mismatch aborts
|
|
12
|
+
* the login without a token exchange.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateState(): string;
|
|
15
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../../src/lib/oauth/pkce.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
// PKCE (RFC 7636) + CSRF-state helpers for the `framedash login` flow.
|
|
3
|
+
// Pure node:crypto module (no I/O) so tests exercise it directly.
|
|
4
|
+
/**
|
|
5
|
+
* Generate an RFC 7636 code_verifier: 48 random bytes -> 64 base64url chars
|
|
6
|
+
* (within the required 43-128 range, alphabet [A-Za-z0-9-_] which is a subset
|
|
7
|
+
* of the allowed unreserved characters).
|
|
8
|
+
*/
|
|
9
|
+
export function generateCodeVerifier() {
|
|
10
|
+
return randomBytes(48).toString("base64url");
|
|
11
|
+
}
|
|
12
|
+
/** Compute the S256 code_challenge: base64url(sha256(verifier)), no padding. */
|
|
13
|
+
export function computeS256CodeChallenge(codeVerifier) {
|
|
14
|
+
return createHash("sha256").update(codeVerifier, "ascii").digest("base64url");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate the CSRF `state` parameter: 24 random bytes -> 32 base64url chars.
|
|
18
|
+
* Compared byte-for-byte against the loopback callback; any mismatch aborts
|
|
19
|
+
* the login without a token exchange.
|
|
20
|
+
*/
|
|
21
|
+
export function generateState() {
|
|
22
|
+
return randomBytes(24).toString("base64url");
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../../src/lib/oauth/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,uEAAuE;AACvE,kEAAkE;AAElE;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IACnC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC9C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,wBAAwB,CAAC,YAAoB;IAC5D,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC/E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC5B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StoredTokenEntry } from "./token-store.js";
|
|
2
|
+
/** Well-known client_id of the seeded first-party CLI client. */
|
|
3
|
+
export declare const CLI_OAUTH_CLIENT_ID = "fdc_framedash_cli";
|
|
4
|
+
/** Per-request timeout for token/revocation calls. */
|
|
5
|
+
export declare const TOKEN_REQUEST_TIMEOUT_MS = 30000;
|
|
6
|
+
/** Structured OAuth error (RFC 6749 section 5.2) from the token endpoint. */
|
|
7
|
+
export declare class OAuthTokenRequestError extends Error {
|
|
8
|
+
/** Machine-readable error code, e.g. "invalid_grant". */
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly status: number;
|
|
11
|
+
constructor(
|
|
12
|
+
/** Machine-readable error code, e.g. "invalid_grant". */
|
|
13
|
+
code: string, description: string, status: number);
|
|
14
|
+
}
|
|
15
|
+
export type TokenResponse = {
|
|
16
|
+
access_token: string;
|
|
17
|
+
refresh_token: string;
|
|
18
|
+
/** Access token lifetime in seconds. */
|
|
19
|
+
expires_in: number;
|
|
20
|
+
/** Space-delimited granted scopes. */
|
|
21
|
+
scope: string;
|
|
22
|
+
};
|
|
23
|
+
/** RFC 6749 section 4.1.3 authorization-code exchange with the PKCE verifier. */
|
|
24
|
+
export declare function exchangeAuthorizationCode(baseUrl: string, options: {
|
|
25
|
+
code: string;
|
|
26
|
+
codeVerifier: string;
|
|
27
|
+
redirectUri: string;
|
|
28
|
+
}): Promise<TokenResponse>;
|
|
29
|
+
/** RFC 6749 section 6 refresh grant (the server rotates the refresh token). */
|
|
30
|
+
export declare function refreshTokenGrant(baseUrl: string, refreshToken: string): Promise<TokenResponse>;
|
|
31
|
+
/**
|
|
32
|
+
* RFC 7009 revocation. Revoking the refresh token disconnects the whole
|
|
33
|
+
* grant server-side. Throws on transport errors; callers treat revocation
|
|
34
|
+
* as best effort (local credentials are removed regardless).
|
|
35
|
+
*/
|
|
36
|
+
export declare function revokeToken(baseUrl: string, token: string): Promise<void>;
|
|
37
|
+
/** Convert a token response into the persisted entry shape. */
|
|
38
|
+
export declare function toStoredEntry(response: TokenResponse, now?: number): StoredTokenEntry;
|
|
39
|
+
//# sourceMappingURL=token-endpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-endpoint.d.ts","sourceRoot":"","sources":["../../../src/lib/oauth/token-endpoint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAWzD,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,sBAAsB,CAAC;AAEvD,sDAAsD;AACtD,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,6EAA6E;AAC7E,qBAAa,sBAAuB,SAAQ,KAAK;IAE/C,yDAAyD;aACzC,IAAI,EAAE,MAAM;aAEZ,MAAM,EAAE,MAAM;;IAH9B,yDAAyD;IACzC,IAAI,EAAE,MAAM,EAC5B,WAAW,EAAE,MAAM,EACH,MAAM,EAAE,MAAM;CAK/B;AAED,MAAM,MAAM,aAAa,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AA6DF,iFAAiF;AACjF,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAClE,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,+EAA+E;AAC/E,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,aAAa,CAAC,CAMxB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/E;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,SAAa,GAAG,gBAAgB,CAOzF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { assertSafeBaseUrl } from "@framedash/api-client";
|
|
2
|
+
// HTTP client for the authorization server's token and revocation endpoints
|
|
3
|
+
// (RFC 6749 / RFC 7009). The CLI is the seeded first-party PUBLIC client
|
|
4
|
+
// (migration 0058): no client secret; possession of the PKCE verifier or the
|
|
5
|
+
// rotating refresh token is the proof.
|
|
6
|
+
//
|
|
7
|
+
// SECURITY: token values pass through here in memory only. Error paths must
|
|
8
|
+
// never include request parameters (which contain tokens) -- only the
|
|
9
|
+
// server's error code/description and the HTTP status.
|
|
10
|
+
/** Well-known client_id of the seeded first-party CLI client. */
|
|
11
|
+
export const CLI_OAUTH_CLIENT_ID = "fdc_framedash_cli";
|
|
12
|
+
/** Per-request timeout for token/revocation calls. */
|
|
13
|
+
export const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
|
|
14
|
+
/** Structured OAuth error (RFC 6749 section 5.2) from the token endpoint. */
|
|
15
|
+
export class OAuthTokenRequestError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
status;
|
|
18
|
+
constructor(
|
|
19
|
+
/** Machine-readable error code, e.g. "invalid_grant". */
|
|
20
|
+
code, description, status) {
|
|
21
|
+
super(`Token request failed (${code}): ${description}`);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.name = "OAuthTokenRequestError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function endpointUrl(baseUrl, path) {
|
|
28
|
+
// Origin-rooted: the AS serves its endpoints at the app origin regardless
|
|
29
|
+
// of any path on the configured base URL.
|
|
30
|
+
assertSafeBaseUrl(baseUrl);
|
|
31
|
+
return new URL(path, baseUrl).toString();
|
|
32
|
+
}
|
|
33
|
+
async function postForm(url, params) {
|
|
34
|
+
return fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
38
|
+
Accept: "application/json",
|
|
39
|
+
},
|
|
40
|
+
body: params.toString(),
|
|
41
|
+
// Never follow a redirect: it would re-send the form body (which
|
|
42
|
+
// contains token material) to the redirect target.
|
|
43
|
+
redirect: "manual",
|
|
44
|
+
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function requestToken(baseUrl, params) {
|
|
48
|
+
params.set("client_id", CLI_OAUTH_CLIENT_ID);
|
|
49
|
+
const response = await postForm(endpointUrl(baseUrl, "/api/oauth/token"), params);
|
|
50
|
+
let json;
|
|
51
|
+
try {
|
|
52
|
+
json = JSON.parse(await response.text());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(`Token endpoint returned a non-JSON response (HTTP ${response.status})`);
|
|
56
|
+
}
|
|
57
|
+
const obj = typeof json === "object" && json !== null ? json : {};
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const code = typeof obj.error === "string" ? obj.error : "unknown_error";
|
|
60
|
+
const description = typeof obj.error_description === "string" ? obj.error_description : `HTTP ${response.status}`;
|
|
61
|
+
throw new OAuthTokenRequestError(code, description, response.status);
|
|
62
|
+
}
|
|
63
|
+
if (typeof obj.access_token !== "string" ||
|
|
64
|
+
obj.access_token === "" ||
|
|
65
|
+
typeof obj.refresh_token !== "string" ||
|
|
66
|
+
obj.refresh_token === "" ||
|
|
67
|
+
typeof obj.expires_in !== "number" ||
|
|
68
|
+
!Number.isFinite(obj.expires_in)) {
|
|
69
|
+
throw new Error("Token endpoint returned an unexpected response shape");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
access_token: obj.access_token,
|
|
73
|
+
refresh_token: obj.refresh_token,
|
|
74
|
+
expires_in: obj.expires_in,
|
|
75
|
+
scope: typeof obj.scope === "string" ? obj.scope : "",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** RFC 6749 section 4.1.3 authorization-code exchange with the PKCE verifier. */
|
|
79
|
+
export async function exchangeAuthorizationCode(baseUrl, options) {
|
|
80
|
+
const params = new URLSearchParams({
|
|
81
|
+
grant_type: "authorization_code",
|
|
82
|
+
code: options.code,
|
|
83
|
+
redirect_uri: options.redirectUri,
|
|
84
|
+
code_verifier: options.codeVerifier,
|
|
85
|
+
});
|
|
86
|
+
return requestToken(baseUrl, params);
|
|
87
|
+
}
|
|
88
|
+
/** RFC 6749 section 6 refresh grant (the server rotates the refresh token). */
|
|
89
|
+
export async function refreshTokenGrant(baseUrl, refreshToken) {
|
|
90
|
+
const params = new URLSearchParams({
|
|
91
|
+
grant_type: "refresh_token",
|
|
92
|
+
refresh_token: refreshToken,
|
|
93
|
+
});
|
|
94
|
+
return requestToken(baseUrl, params);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* RFC 7009 revocation. Revoking the refresh token disconnects the whole
|
|
98
|
+
* grant server-side. Throws on transport errors; callers treat revocation
|
|
99
|
+
* as best effort (local credentials are removed regardless).
|
|
100
|
+
*/
|
|
101
|
+
export async function revokeToken(baseUrl, token) {
|
|
102
|
+
const params = new URLSearchParams({ token, client_id: CLI_OAUTH_CLIENT_ID });
|
|
103
|
+
const response = await postForm(endpointUrl(baseUrl, "/api/oauth/revoke"), params);
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`Revocation endpoint returned HTTP ${response.status}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Convert a token response into the persisted entry shape. */
|
|
109
|
+
export function toStoredEntry(response, now = Date.now()) {
|
|
110
|
+
return {
|
|
111
|
+
access_token: response.access_token,
|
|
112
|
+
refresh_token: response.refresh_token,
|
|
113
|
+
expires_at: now + response.expires_in * 1000,
|
|
114
|
+
scope: response.scope,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=token-endpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-endpoint.js","sourceRoot":"","sources":["../../../src/lib/oauth/token-endpoint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type StoredTokenEntry = {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token: string;
|
|
4
|
+
/** Absolute expiry of the access token, Unix epoch milliseconds. */
|
|
5
|
+
expires_at: number;
|
|
6
|
+
/** Space-delimited granted scopes (display + status output). */
|
|
7
|
+
scope: string;
|
|
8
|
+
};
|
|
9
|
+
export type TokenStore = Record<string, StoredTokenEntry>;
|
|
10
|
+
/** Resolve the credentials file path (honors XDG_CONFIG_HOME at call time). */
|
|
11
|
+
export declare function credentialsFilePath(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Read the whole token store. Missing, unreadable, or corrupt files -- and
|
|
14
|
+
* individual malformed entries -- degrade to "absent" rather than throwing:
|
|
15
|
+
* the caller then falls back to other credential sources or asks the user to
|
|
16
|
+
* log in again.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readTokenStore(): TokenStore;
|
|
19
|
+
/** Read the stored entry for one base-URL origin, if any. */
|
|
20
|
+
export declare function readStoredEntry(origin: string): StoredTokenEntry | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Insert or replace the entry for an origin (atomic write). Async signature
|
|
23
|
+
* kept for call-site stability even though the body is synchronous.
|
|
24
|
+
*/
|
|
25
|
+
export declare function saveStoredEntry(origin: string, entry: StoredTokenEntry): Promise<void>;
|
|
26
|
+
/** Remove the entry for an origin. Returns true if an entry was removed. */
|
|
27
|
+
export declare function deleteStoredEntry(origin: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Delete the whole credentials file (logout --all), including any orphaned
|
|
30
|
+
* credentials.json.*.tmp files a crashed write left behind -- those hold the
|
|
31
|
+
* same token material as the store itself.
|
|
32
|
+
*/
|
|
33
|
+
export declare function clearTokenStore(): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../src/lib/oauth/token-store.ts"],"names":[],"mappings":"AAmCA,MAAM,MAAM,gBAAgB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAE1D,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,MAAM,CAI5C;AAgBD;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAuB3C;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAK5E;AAqED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5F;AAED,4EAA4E;AAC5E,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQxE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrD"}
|