@oka-core/reason 0.2.22 → 0.2.23
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/dist/auth.d.ts +21 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +83 -14
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -14,11 +14,30 @@ export declare function extractJwtExp(token: string): number | null;
|
|
|
14
14
|
export declare function loadEnvFile(dir: string): void;
|
|
15
15
|
/**
|
|
16
16
|
* Refresh the access token using a stored refresh_token.
|
|
17
|
+
*
|
|
17
18
|
* Calls `POST ${authServer}/api/token?grant_type=refresh_token`.
|
|
18
|
-
* On success
|
|
19
|
-
*
|
|
19
|
+
* On success: updates ~/.oka/credentials.json (rotating the
|
|
20
|
+
* refresh_token if the server returned a new one) and returns the
|
|
21
|
+
* new access token.
|
|
22
|
+
* On hard failure (`invalid_grant` / `invalid_request` — the
|
|
23
|
+
* refresh token is dead): DELETES the stored refresh_token so the
|
|
24
|
+
* next call surfaces "no credentials" cleanly instead of looping on
|
|
25
|
+
* the same dead value. Returns null.
|
|
26
|
+
* On soft failure (network error, 5xx, timeout): leaves credentials
|
|
27
|
+
* untouched so a later attempt can retry. Returns null.
|
|
28
|
+
*
|
|
29
|
+
* Stderr-logged at every branch so the operator running the MCP
|
|
30
|
+
* host (or `wrangler tail` / `journalctl`) sees WHY a refresh failed
|
|
31
|
+
* instead of just "Authentication failed" two layers up the stack.
|
|
20
32
|
*/
|
|
21
33
|
export declare function refreshAccessToken(authServerUrl?: string): Promise<string | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Wipe the stored refresh_token / access_token (preserves `api_key`
|
|
36
|
+
* if present) so the next auth-resolution call surfaces "no
|
|
37
|
+
* credentials" instead of looping on a server-rejected refresh
|
|
38
|
+
* token. Called from `refreshAccessToken` on hard failures.
|
|
39
|
+
*/
|
|
40
|
+
export declare function clearStoredRefreshToken(): void;
|
|
22
41
|
/**
|
|
23
42
|
* Attempt token refresh with deduplication.
|
|
24
43
|
* Multiple concurrent callers share the same in-flight request.
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAgB,qBAAqB,IAAI,iBAAiB,GAAG,IAAI,CAOhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAa/D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW1D;AAID,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CA0B7C;AAOD
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAgB,qBAAqB,IAAI,iBAAiB,GAAG,IAAI,CAOhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAa/D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW1D;AAID,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CA0B7C;AAOD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoFxB;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAY9C;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOnD;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAuBtC;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,CAAC,CAwBhE;AASD,qBAAa,eAAe;IAC1B,OAAO,CAAC,YAAY,CAA8C;IAElE;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;IAsCtC;;OAEG;IACH,OAAO,CAAC,eAAe;YAeT,iBAAiB;IAQ/B,OAAO,CAAC,UAAU;IAOlB,OAAO,IAAI,IAAI;CAGhB;AAuGD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,aAAa,CAAC,EAAE,MAAM,EACtB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CAsCtB"}
|
package/dist/auth.js
CHANGED
|
@@ -83,35 +83,104 @@ export function loadEnvFile(dir) {
|
|
|
83
83
|
let refreshInFlight = null;
|
|
84
84
|
/**
|
|
85
85
|
* Refresh the access token using a stored refresh_token.
|
|
86
|
+
*
|
|
86
87
|
* Calls `POST ${authServer}/api/token?grant_type=refresh_token`.
|
|
87
|
-
* On success
|
|
88
|
-
*
|
|
88
|
+
* On success: updates ~/.oka/credentials.json (rotating the
|
|
89
|
+
* refresh_token if the server returned a new one) and returns the
|
|
90
|
+
* new access token.
|
|
91
|
+
* On hard failure (`invalid_grant` / `invalid_request` — the
|
|
92
|
+
* refresh token is dead): DELETES the stored refresh_token so the
|
|
93
|
+
* next call surfaces "no credentials" cleanly instead of looping on
|
|
94
|
+
* the same dead value. Returns null.
|
|
95
|
+
* On soft failure (network error, 5xx, timeout): leaves credentials
|
|
96
|
+
* untouched so a later attempt can retry. Returns null.
|
|
97
|
+
*
|
|
98
|
+
* Stderr-logged at every branch so the operator running the MCP
|
|
99
|
+
* host (or `wrangler tail` / `journalctl`) sees WHY a refresh failed
|
|
100
|
+
* instead of just "Authentication failed" two layers up the stack.
|
|
89
101
|
*/
|
|
90
102
|
export async function refreshAccessToken(authServerUrl) {
|
|
91
103
|
const stored = loadStoredCredentials();
|
|
92
|
-
if (!stored?.refresh_token)
|
|
104
|
+
if (!stored?.refresh_token) {
|
|
105
|
+
console.warn("[oka-auth] refresh skipped: no refresh_token stored — run `mcp__oka__login`");
|
|
93
106
|
return null;
|
|
107
|
+
}
|
|
94
108
|
const server = authServerUrl ?? DEFAULT_AUTH_SERVER;
|
|
109
|
+
let res;
|
|
95
110
|
try {
|
|
96
|
-
|
|
111
|
+
res = await fetch(`${server}/api/token?grant_type=refresh_token`, {
|
|
97
112
|
method: "POST",
|
|
98
113
|
headers: { "Content-Type": "application/json" },
|
|
99
114
|
body: JSON.stringify({ refresh_token: stored.refresh_token }),
|
|
100
115
|
signal: AbortSignal.timeout(10_000),
|
|
101
116
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// Network error / abort / DNS failure — keep credentials, retry later.
|
|
120
|
+
console.warn(`[oka-auth] refresh network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
// 4xx with a known OAuth error code means the refresh_token is
|
|
125
|
+
// permanently dead (rotated by another client, revoked server-
|
|
126
|
+
// side, or never valid in the first place — e.g. the 12-char
|
|
127
|
+
// junk that DeviceAuthPage used to write when no real refresh
|
|
128
|
+
// token was in scope). Wipe the dead token so subsequent calls
|
|
129
|
+
// surface a clean "no credentials" instead of looping on the
|
|
130
|
+
// same bad value forever and producing the same opaque
|
|
131
|
+
// "Authentication failed" the user reported.
|
|
132
|
+
//
|
|
133
|
+
// `res.json` may not even exist on a mocked / stubbed Response,
|
|
134
|
+
// so we wrap the entire read so a test fixture that omits it
|
|
135
|
+
// still falls through to "unknown error" cleanly.
|
|
136
|
+
const body = await (async () => {
|
|
137
|
+
try {
|
|
138
|
+
return (await res.json());
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
const fatal = res.status >= 400 &&
|
|
145
|
+
res.status < 500 &&
|
|
146
|
+
(body.error === "invalid_grant" || body.error === "invalid_request");
|
|
147
|
+
if (fatal) {
|
|
148
|
+
console.warn(`[oka-auth] refresh rejected (${body.error ?? res.status}: ${body.error_description ?? "no description"}) — clearing stored refresh_token`);
|
|
149
|
+
clearStoredRefreshToken();
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.warn(`[oka-auth] refresh failed: HTTP ${res.status} ${body.error ?? ""} ${body.error_description ?? ""}`.trim());
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const data = (await res.json());
|
|
157
|
+
// storeCredentials will extract the real JWT exp automatically
|
|
158
|
+
storeCredentials({
|
|
159
|
+
access_token: data.access_token,
|
|
160
|
+
refresh_token: data.refresh_token ?? stored.refresh_token,
|
|
161
|
+
expires_at: Math.floor(Date.now() / 1000) + data.expires_in, // overridden by JWT exp in storeCredentials
|
|
162
|
+
});
|
|
163
|
+
return data.access_token;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Wipe the stored refresh_token / access_token (preserves `api_key`
|
|
167
|
+
* if present) so the next auth-resolution call surfaces "no
|
|
168
|
+
* credentials" instead of looping on a server-rejected refresh
|
|
169
|
+
* token. Called from `refreshAccessToken` on hard failures.
|
|
170
|
+
*/
|
|
171
|
+
export function clearStoredRefreshToken() {
|
|
172
|
+
const stored = loadStoredCredentials();
|
|
173
|
+
if (!stored)
|
|
174
|
+
return;
|
|
175
|
+
const { api_key } = stored;
|
|
176
|
+
const next = api_key ? { api_key } : {};
|
|
177
|
+
try {
|
|
178
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(next, null, 2), {
|
|
179
|
+
mode: 0o600,
|
|
110
180
|
});
|
|
111
|
-
return data.access_token;
|
|
112
181
|
}
|
|
113
182
|
catch {
|
|
114
|
-
|
|
183
|
+
/* swallow — we're already in an error path */
|
|
115
184
|
}
|
|
116
185
|
}
|
|
117
186
|
/**
|