@runneth/cli 0.0.0-sha.3142666bf4dc.production

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/oauth.js ADDED
@@ -0,0 +1,592 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import http from "node:http";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { resolveRunnethCliHome } from "./paths.js";
8
+ const CALLBACK_PATH = "/oauth/callback";
9
+ const DEFAULT_CLIENT_NAME = "Runneth MCP";
10
+ const DEFAULT_SCOPE = "openid profile email offline_access";
11
+ const DEFAULT_TIMEOUT_MS = 300_000;
12
+ const LOOPBACK_HOST = "localhost";
13
+ const OAUTH_CREDENTIAL_PATH_SEGMENT_LENGTH = 120;
14
+ const TOKEN_REFRESH_BUFFER_MS = 60_000;
15
+ const isRecord = (value) => {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ };
18
+ const isErrnoCode = (error, code) => {
19
+ return (typeof error === "object" &&
20
+ error !== null &&
21
+ "code" in error &&
22
+ error.code === code);
23
+ };
24
+ const parseStringField = (record, key) => {
25
+ const value = record[key];
26
+ if (typeof value !== "string" || value.trim().length === 0) {
27
+ throw new Error(`${key} must be a non-empty string`);
28
+ }
29
+ return value;
30
+ };
31
+ const parseOptionalStringField = (record, key) => {
32
+ const value = record[key];
33
+ if (value === undefined) {
34
+ return undefined;
35
+ }
36
+ if (typeof value !== "string" || value.trim().length === 0) {
37
+ throw new Error(`${key} must be a non-empty string`);
38
+ }
39
+ return value;
40
+ };
41
+ const parseOptionalNumberField = (record, key) => {
42
+ const value = record[key];
43
+ if (value === undefined) {
44
+ return undefined;
45
+ }
46
+ if (typeof value !== "number" || !Number.isFinite(value)) {
47
+ throw new Error(`${key} must be a finite number`);
48
+ }
49
+ return value;
50
+ };
51
+ const parseStringArrayField = (record, key) => {
52
+ const value = record[key];
53
+ if (!Array.isArray(value) || value.length === 0) {
54
+ throw new Error(`${key} must be a non-empty string array`);
55
+ }
56
+ for (const item of value) {
57
+ if (typeof item !== "string" || item.trim().length === 0) {
58
+ throw new Error(`${key} must contain non-empty strings`);
59
+ }
60
+ }
61
+ return value;
62
+ };
63
+ const parseTokenEndpointAuthMethod = (value) => {
64
+ switch (value) {
65
+ case "client_secret_basic":
66
+ case "client_secret_post":
67
+ case "none": {
68
+ return value;
69
+ }
70
+ default: {
71
+ throw new Error(`Unsupported token_endpoint_auth_method: ${value}`);
72
+ }
73
+ }
74
+ };
75
+ const toBase64Url = (buffer) => {
76
+ return buffer.toString("base64url");
77
+ };
78
+ const createPkcePair = () => {
79
+ const verifier = toBase64Url(randomBytes(32));
80
+ const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
81
+ return { challenge, verifier };
82
+ };
83
+ const normalizeOAuthResourceUrl = (value) => {
84
+ const url = new URL(value);
85
+ url.hash = "";
86
+ url.search = "";
87
+ if (url.pathname !== "/") {
88
+ url.pathname = url.pathname.replace(/\/+$/u, "");
89
+ }
90
+ return url.toString();
91
+ };
92
+ const encodeOAuthCredentialPathSegments = (resourceUrl) => {
93
+ const encodedResourceUrl = Buffer.from(resourceUrl, "utf8").toString("base64url");
94
+ const segments = [];
95
+ for (let index = 0; index < encodedResourceUrl.length; index += OAUTH_CREDENTIAL_PATH_SEGMENT_LENGTH) {
96
+ segments.push(encodedResourceUrl.slice(index, index + OAUTH_CREDENTIAL_PATH_SEGMENT_LENGTH));
97
+ }
98
+ const fileName = segments.pop();
99
+ if (fileName === undefined) {
100
+ throw new Error("OAuth resource URL did not produce a credential path");
101
+ }
102
+ return [...segments, `${fileName}.json`];
103
+ };
104
+ const metadataPathForUrl = (value, metadataName) => {
105
+ const url = new URL(value);
106
+ const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/u, "");
107
+ return `${url.origin}/.well-known/${metadataName}${pathname}`;
108
+ };
109
+ const fetchJsonObject = async (url, init, label) => {
110
+ const response = await fetch(url, init);
111
+ if (!response.ok) {
112
+ const body = await response.text();
113
+ throw new Error(`${label} failed with HTTP ${String(response.status)}: ${body}`);
114
+ }
115
+ const parsed = (await response.json());
116
+ if (!isRecord(parsed)) {
117
+ throw new Error(`${label} returned a non-object JSON response`);
118
+ }
119
+ return parsed;
120
+ };
121
+ const discoverProtectedResource = async (resourceUrl) => {
122
+ const metadataUrl = metadataPathForUrl(resourceUrl, "oauth-protected-resource");
123
+ const metadata = await fetchJsonObject(metadataUrl, {
124
+ headers: {
125
+ Accept: "application/json",
126
+ },
127
+ }, "OAuth protected resource metadata request");
128
+ return {
129
+ authorizationServers: parseStringArrayField(metadata, "authorization_servers"),
130
+ resource: parseStringField(metadata, "resource"),
131
+ };
132
+ };
133
+ const discoverAuthorizationServer = async (issuerUrl) => {
134
+ const metadataUrl = metadataPathForUrl(issuerUrl, "oauth-authorization-server");
135
+ const metadata = await fetchJsonObject(metadataUrl, {
136
+ headers: {
137
+ Accept: "application/json",
138
+ },
139
+ }, "OAuth authorization server metadata request");
140
+ return {
141
+ authorizationEndpoint: parseStringField(metadata, "authorization_endpoint"),
142
+ issuer: parseStringField(metadata, "issuer"),
143
+ registrationEndpoint: parseStringField(metadata, "registration_endpoint"),
144
+ tokenEndpoint: parseStringField(metadata, "token_endpoint"),
145
+ };
146
+ };
147
+ export const resolveOAuthCredentialPath = (input) => {
148
+ const homePath = input.homePath ?? resolveRunnethCliHome();
149
+ const resourceUrl = normalizeOAuthResourceUrl(input.resourceUrl);
150
+ return path.join(homePath, "oauth", ...encodeOAuthCredentialPathSegments(resourceUrl));
151
+ };
152
+ const writeOAuthCredential = async (input) => {
153
+ await mkdir(path.dirname(input.credentialPath), {
154
+ mode: 0o700,
155
+ recursive: true,
156
+ });
157
+ await writeFile(input.credentialPath, `${JSON.stringify(input.credential, null, 2)}\n`, {
158
+ encoding: "utf8",
159
+ mode: 0o600,
160
+ });
161
+ if (process.platform !== "win32") {
162
+ await chmod(input.credentialPath, 0o600);
163
+ }
164
+ };
165
+ const parseStoredCredential = (value) => {
166
+ if (!isRecord(value)) {
167
+ throw new Error("Stored OAuth credential must be a JSON object");
168
+ }
169
+ if (value.version !== 1) {
170
+ throw new Error("Stored OAuth credential has an unsupported version");
171
+ }
172
+ const authorizationServerValue = value.authorizationServer;
173
+ const clientValue = value.client;
174
+ const tokenValue = value.token;
175
+ if (!isRecord(authorizationServerValue)) {
176
+ throw new Error("Stored OAuth credential authorizationServer must be an object");
177
+ }
178
+ if (!isRecord(clientValue)) {
179
+ throw new Error("Stored OAuth credential client must be an object");
180
+ }
181
+ if (!isRecord(tokenValue)) {
182
+ throw new Error("Stored OAuth credential token must be an object");
183
+ }
184
+ return {
185
+ authorizationServer: {
186
+ authorizationEndpoint: parseStringField(authorizationServerValue, "authorizationEndpoint"),
187
+ issuer: parseStringField(authorizationServerValue, "issuer"),
188
+ registrationEndpoint: parseStringField(authorizationServerValue, "registrationEndpoint"),
189
+ tokenEndpoint: parseStringField(authorizationServerValue, "tokenEndpoint"),
190
+ },
191
+ client: {
192
+ clientId: parseStringField(clientValue, "clientId"),
193
+ clientName: parseStringField(clientValue, "clientName"),
194
+ clientSecret: parseOptionalStringField(clientValue, "clientSecret"),
195
+ redirectUri: parseStringField(clientValue, "redirectUri"),
196
+ tokenEndpointAuthMethod: parseTokenEndpointAuthMethod(parseStringField(clientValue, "tokenEndpointAuthMethod")),
197
+ },
198
+ createdAt: parseStringField(value, "createdAt"),
199
+ requestedResourceUrl: parseStringField(value, "requestedResourceUrl"),
200
+ resource: parseStringField(value, "resource"),
201
+ scope: parseStringField(value, "scope"),
202
+ token: {
203
+ accessToken: parseStringField(tokenValue, "accessToken"),
204
+ expiresAt: parseOptionalStringField(tokenValue, "expiresAt"),
205
+ idToken: parseOptionalStringField(tokenValue, "idToken"),
206
+ refreshToken: parseOptionalStringField(tokenValue, "refreshToken"),
207
+ scope: parseOptionalStringField(tokenValue, "scope"),
208
+ tokenType: parseStringField(tokenValue, "tokenType"),
209
+ },
210
+ updatedAt: parseStringField(value, "updatedAt"),
211
+ version: 1,
212
+ };
213
+ };
214
+ export const readOAuthCredential = async (input) => {
215
+ const credentialPath = resolveOAuthCredentialPath(input);
216
+ try {
217
+ const raw = await readFile(credentialPath, "utf8");
218
+ return parseStoredCredential(JSON.parse(raw));
219
+ }
220
+ catch (error) {
221
+ if (isErrnoCode(error, "ENOENT")) {
222
+ return null;
223
+ }
224
+ throw error;
225
+ }
226
+ };
227
+ export const readOAuthCredentialStatus = async (input) => {
228
+ const credentialPath = resolveOAuthCredentialPath(input);
229
+ const requestedResourceUrl = normalizeOAuthResourceUrl(input.resourceUrl);
230
+ const credential = await readOAuthCredential(input);
231
+ if (credential === null) {
232
+ return {
233
+ authenticated: false,
234
+ credentialPath,
235
+ requestedResourceUrl,
236
+ };
237
+ }
238
+ return {
239
+ authenticated: true,
240
+ clientId: credential.client.clientId,
241
+ credentialPath,
242
+ expiresAt: credential.token.expiresAt,
243
+ requestedResourceUrl,
244
+ resource: credential.resource,
245
+ scope: credential.token.scope ?? credential.scope,
246
+ tokenType: credential.token.tokenType,
247
+ };
248
+ };
249
+ export const logoutOAuthCredential = async (input) => {
250
+ const credentialPath = resolveOAuthCredentialPath(input);
251
+ await rm(credentialPath, { force: true });
252
+ return {
253
+ authenticated: false,
254
+ credentialPath,
255
+ requestedResourceUrl: normalizeOAuthResourceUrl(input.resourceUrl),
256
+ };
257
+ };
258
+ const startLoopbackCallbackServer = async () => {
259
+ let settleAuthorization;
260
+ let rejectAuthorization;
261
+ const waitForAuthorization = new Promise((resolve, reject) => {
262
+ settleAuthorization = resolve;
263
+ rejectAuthorization = reject;
264
+ });
265
+ const server = http.createServer((req, res) => {
266
+ const host = req.headers.host;
267
+ if (host === undefined || req.url === undefined) {
268
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
269
+ res.end("Invalid OAuth callback request.\n");
270
+ rejectAuthorization?.(new Error("Invalid OAuth callback request"));
271
+ return;
272
+ }
273
+ const callbackUrl = new URL(req.url, `http://${host}`);
274
+ if (callbackUrl.pathname !== CALLBACK_PATH) {
275
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
276
+ res.end("OAuth callback path not found.\n");
277
+ return;
278
+ }
279
+ const error = callbackUrl.searchParams.get("error");
280
+ if (error !== null) {
281
+ const description = callbackUrl.searchParams.get("error_description");
282
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
283
+ res.end("OAuth authorization failed.\n");
284
+ rejectAuthorization?.(new Error(description === null
285
+ ? `OAuth authorization failed: ${error}`
286
+ : description));
287
+ return;
288
+ }
289
+ const code = callbackUrl.searchParams.get("code");
290
+ const state = callbackUrl.searchParams.get("state");
291
+ if (code === null || state === null) {
292
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
293
+ res.end("OAuth callback is missing code or state.\n");
294
+ rejectAuthorization?.(new Error("OAuth callback is missing code or state"));
295
+ return;
296
+ }
297
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
298
+ res.end("OAuth authorization complete. You can return to runneth-cli.\n");
299
+ settleAuthorization?.({
300
+ code,
301
+ issuer: callbackUrl.searchParams.get("iss") ?? undefined,
302
+ state,
303
+ });
304
+ });
305
+ await new Promise((resolve, reject) => {
306
+ server.once("error", reject);
307
+ server.listen(0, LOOPBACK_HOST, () => {
308
+ server.off("error", reject);
309
+ resolve();
310
+ });
311
+ });
312
+ const address = server.address();
313
+ if (address === null || typeof address === "string") {
314
+ throw new Error("OAuth callback server did not bind to a TCP port");
315
+ }
316
+ return {
317
+ close: async () => {
318
+ await closeServer(server);
319
+ },
320
+ redirectUri: `http://${LOOPBACK_HOST}:${String(address.port)}${CALLBACK_PATH}`,
321
+ waitForAuthorization,
322
+ };
323
+ };
324
+ const closeServer = async (server) => {
325
+ await new Promise((resolve, reject) => {
326
+ server.close((error) => {
327
+ if (error) {
328
+ reject(error);
329
+ return;
330
+ }
331
+ resolve();
332
+ });
333
+ });
334
+ };
335
+ const registerOAuthClient = async (input) => {
336
+ const response = await fetchJsonObject(input.metadata.registrationEndpoint, {
337
+ body: JSON.stringify({
338
+ client_name: input.clientName,
339
+ grant_types: ["authorization_code", "refresh_token"],
340
+ redirect_uris: [input.redirectUri],
341
+ response_types: ["code"],
342
+ scope: input.scope,
343
+ token_endpoint_auth_method: "none",
344
+ type: "native",
345
+ }),
346
+ headers: {
347
+ Accept: "application/json",
348
+ "Content-Type": "application/json",
349
+ },
350
+ method: "POST",
351
+ }, "OAuth dynamic client registration");
352
+ return {
353
+ clientId: parseStringField(response, "client_id"),
354
+ clientName: input.clientName,
355
+ clientSecret: parseOptionalStringField(response, "client_secret"),
356
+ redirectUri: input.redirectUri,
357
+ tokenEndpointAuthMethod: parseTokenEndpointAuthMethod(parseStringField(response, "token_endpoint_auth_method")),
358
+ };
359
+ };
360
+ const openSystemBrowser = async (url) => {
361
+ const command = process.platform === "darwin"
362
+ ? "open"
363
+ : process.platform === "win32"
364
+ ? "cmd"
365
+ : "xdg-open";
366
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
367
+ await new Promise((resolve, reject) => {
368
+ const child = spawn(command, args, {
369
+ detached: true,
370
+ stdio: "ignore",
371
+ });
372
+ child.once("error", reject);
373
+ child.once("spawn", () => {
374
+ child.unref();
375
+ resolve();
376
+ });
377
+ });
378
+ };
379
+ const buildAuthorizationUrl = (input) => {
380
+ const authorizationUrl = new URL(input.metadata.authorizationEndpoint);
381
+ authorizationUrl.searchParams.set("client_id", input.client.clientId);
382
+ authorizationUrl.searchParams.set("code_challenge", input.pkceChallenge);
383
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
384
+ authorizationUrl.searchParams.set("redirect_uri", input.client.redirectUri);
385
+ authorizationUrl.searchParams.set("response_type", "code");
386
+ authorizationUrl.searchParams.set("scope", input.scope);
387
+ authorizationUrl.searchParams.set("prompt", "consent");
388
+ authorizationUrl.searchParams.set("state", input.state);
389
+ return authorizationUrl.toString();
390
+ };
391
+ const waitForAuthorizationCallback = async (waitForAuthorization, timeoutMs) => {
392
+ let timeout;
393
+ const timeoutPromise = new Promise((_, reject) => {
394
+ timeout = setTimeout(() => {
395
+ reject(new Error(`Timed out waiting for OAuth authorization after ${String(timeoutMs)}ms`));
396
+ }, timeoutMs);
397
+ });
398
+ try {
399
+ return await Promise.race([waitForAuthorization, timeoutPromise]);
400
+ }
401
+ finally {
402
+ if (timeout !== undefined) {
403
+ clearTimeout(timeout);
404
+ }
405
+ }
406
+ };
407
+ const parseTokenResponse = (response, nowMs) => {
408
+ const rawToken = {
409
+ access_token: parseStringField(response, "access_token"),
410
+ expires_at: parseOptionalNumberField(response, "expires_at"),
411
+ expires_in: parseOptionalNumberField(response, "expires_in"),
412
+ id_token: parseOptionalStringField(response, "id_token"),
413
+ refresh_token: parseOptionalStringField(response, "refresh_token"),
414
+ scope: parseOptionalStringField(response, "scope"),
415
+ token_type: parseStringField(response, "token_type"),
416
+ };
417
+ const expiresAt = rawToken.expires_at === undefined
418
+ ? rawToken.expires_in === undefined
419
+ ? undefined
420
+ : new Date(nowMs + rawToken.expires_in * 1000).toISOString()
421
+ : new Date(rawToken.expires_at * 1000).toISOString();
422
+ return {
423
+ accessToken: rawToken.access_token,
424
+ expiresAt,
425
+ idToken: rawToken.id_token,
426
+ refreshToken: rawToken.refresh_token,
427
+ scope: rawToken.scope,
428
+ tokenType: rawToken.token_type,
429
+ };
430
+ };
431
+ const requestOAuthToken = async (input) => {
432
+ const body = new URLSearchParams({
433
+ client_id: input.client.clientId,
434
+ code: input.code,
435
+ code_verifier: input.codeVerifier,
436
+ grant_type: "authorization_code",
437
+ redirect_uri: input.redirectUri,
438
+ resource: input.resource,
439
+ });
440
+ if (input.client.clientSecret !== undefined) {
441
+ body.set("client_secret", input.client.clientSecret);
442
+ }
443
+ const response = await fetchJsonObject(input.metadata.tokenEndpoint, {
444
+ body,
445
+ headers: {
446
+ Accept: "application/json",
447
+ "Content-Type": "application/x-www-form-urlencoded",
448
+ },
449
+ method: "POST",
450
+ }, "OAuth token request");
451
+ return parseTokenResponse(response, Date.now());
452
+ };
453
+ const refreshOAuthToken = async (input) => {
454
+ if (input.credential.token.refreshToken === undefined) {
455
+ throw new Error("Stored OAuth credential is expired and has no refresh token");
456
+ }
457
+ const body = new URLSearchParams({
458
+ client_id: input.credential.client.clientId,
459
+ grant_type: "refresh_token",
460
+ refresh_token: input.credential.token.refreshToken,
461
+ resource: input.credential.resource,
462
+ scope: input.credential.scope,
463
+ });
464
+ if (input.credential.client.clientSecret !== undefined) {
465
+ body.set("client_secret", input.credential.client.clientSecret);
466
+ }
467
+ const response = await fetchJsonObject(input.credential.authorizationServer.tokenEndpoint, {
468
+ body,
469
+ headers: {
470
+ Accept: "application/json",
471
+ "Content-Type": "application/x-www-form-urlencoded",
472
+ },
473
+ method: "POST",
474
+ }, "OAuth refresh token request");
475
+ const refreshed = parseTokenResponse(response, Date.now());
476
+ return {
477
+ ...refreshed,
478
+ refreshToken: refreshed.refreshToken ?? input.credential.token.refreshToken,
479
+ };
480
+ };
481
+ const tokenIsFresh = (token) => {
482
+ if (token.expiresAt === undefined) {
483
+ return true;
484
+ }
485
+ return (new Date(token.expiresAt).getTime() - Date.now() > TOKEN_REFRESH_BUFFER_MS);
486
+ };
487
+ export const loginWithOAuth = async (options) => {
488
+ const requestedResourceUrl = normalizeOAuthResourceUrl(options.resourceUrl);
489
+ const scope = options.scope?.trim() || DEFAULT_SCOPE;
490
+ const clientName = options.clientName?.trim() || DEFAULT_CLIENT_NAME;
491
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
492
+ const callbackServer = await startLoopbackCallbackServer();
493
+ const credentialPath = resolveOAuthCredentialPath({
494
+ homePath: options.homePath,
495
+ resourceUrl: requestedResourceUrl,
496
+ });
497
+ try {
498
+ const protectedResource = await discoverProtectedResource(requestedResourceUrl);
499
+ const authorizationServer = await discoverAuthorizationServer(protectedResource.authorizationServers[0]);
500
+ const client = await registerOAuthClient({
501
+ clientName,
502
+ metadata: authorizationServer,
503
+ redirectUri: callbackServer.redirectUri,
504
+ scope,
505
+ });
506
+ const state = toBase64Url(randomBytes(32));
507
+ const pkce = createPkcePair();
508
+ const authorizationUrl = buildAuthorizationUrl({
509
+ client,
510
+ metadata: authorizationServer,
511
+ pkceChallenge: pkce.challenge,
512
+ scope,
513
+ state,
514
+ });
515
+ await options.authorizationUrlHandler?.(authorizationUrl);
516
+ if (options.openBrowser ?? true) {
517
+ await openSystemBrowser(authorizationUrl);
518
+ }
519
+ const authorization = await waitForAuthorizationCallback(callbackServer.waitForAuthorization, timeoutMs);
520
+ if (authorization.state !== state) {
521
+ throw new Error("OAuth callback state did not match the authorization request");
522
+ }
523
+ if (authorization.issuer !== undefined &&
524
+ authorization.issuer !== authorizationServer.issuer) {
525
+ throw new Error("OAuth callback issuer did not match the authorization server metadata");
526
+ }
527
+ const token = await requestOAuthToken({
528
+ client,
529
+ code: authorization.code,
530
+ codeVerifier: pkce.verifier,
531
+ metadata: authorizationServer,
532
+ redirectUri: callbackServer.redirectUri,
533
+ resource: protectedResource.resource,
534
+ });
535
+ const now = new Date().toISOString();
536
+ const credential = {
537
+ authorizationServer,
538
+ client,
539
+ createdAt: now,
540
+ requestedResourceUrl,
541
+ resource: protectedResource.resource,
542
+ scope,
543
+ token,
544
+ updatedAt: now,
545
+ version: 1,
546
+ };
547
+ await writeOAuthCredential({ credential, credentialPath });
548
+ return {
549
+ accessToken: token.accessToken,
550
+ credential,
551
+ credentialPath,
552
+ expiresAt: token.expiresAt,
553
+ tokenType: token.tokenType,
554
+ };
555
+ }
556
+ finally {
557
+ await callbackServer.close();
558
+ }
559
+ };
560
+ export const getOAuthAccessToken = async (input) => {
561
+ const credentialPath = resolveOAuthCredentialPath(input);
562
+ const credential = await readOAuthCredential(input);
563
+ if (credential === null) {
564
+ throw new Error("No OAuth credential is stored for this resource");
565
+ }
566
+ if (tokenIsFresh(credential.token)) {
567
+ return {
568
+ accessToken: credential.token.accessToken,
569
+ credential,
570
+ credentialPath,
571
+ expiresAt: credential.token.expiresAt,
572
+ tokenType: credential.token.tokenType,
573
+ };
574
+ }
575
+ const refreshedToken = await refreshOAuthToken({ credential });
576
+ const refreshedCredential = {
577
+ ...credential,
578
+ token: refreshedToken,
579
+ updatedAt: new Date().toISOString(),
580
+ };
581
+ await writeOAuthCredential({
582
+ credential: refreshedCredential,
583
+ credentialPath,
584
+ });
585
+ return {
586
+ accessToken: refreshedToken.accessToken,
587
+ credential: refreshedCredential,
588
+ credentialPath,
589
+ expiresAt: refreshedToken.expiresAt,
590
+ tokenType: refreshedToken.tokenType,
591
+ };
592
+ };
@@ -0,0 +1,2 @@
1
+ export declare const resolveRunnethCliHome: () => string;
2
+ export declare const resolveSocketPath: (homePath?: string) => string;
package/dist/paths.js ADDED
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ export const resolveRunnethCliHome = () => {
6
+ const configured = process.env.RUNNETH_CLI_HOME?.trim();
7
+ return path.resolve(configured && configured.length > 0
8
+ ? configured
9
+ : path.join(os.homedir(), ".runneth-cli"));
10
+ };
11
+ export const resolveSocketPath = (homePath = resolveRunnethCliHome()) => {
12
+ const configured = process.env.RUNNETH_CLI_SOCKET?.trim();
13
+ if (configured && configured.length > 0) {
14
+ return configured;
15
+ }
16
+ const digest = createHash("sha256")
17
+ .update(path.resolve(homePath))
18
+ .digest("hex")
19
+ .slice(0, 16);
20
+ if (process.platform === "win32") {
21
+ return `\\\\.\\pipe\\runneth-cli-${digest}`;
22
+ }
23
+ const uid = typeof process.getuid === "function" ? String(process.getuid()) : "user";
24
+ return path.join(os.tmpdir(), `runneth-cli-${uid}-${digest}.sock`);
25
+ };
@@ -0,0 +1,15 @@
1
+ export type RunnethSkillAgent = "claude" | "codex";
2
+ export type RunnethSkillInstallTarget = Readonly<{
3
+ agent: RunnethSkillAgent;
4
+ path: string;
5
+ }>;
6
+ export type RunnethSkillInstallOptions = Readonly<{
7
+ agents?: readonly RunnethSkillAgent[];
8
+ }>;
9
+ export type RunnethSkillInstallResult = Readonly<{
10
+ skillName: string;
11
+ sourcePath: string;
12
+ targets: readonly RunnethSkillInstallTarget[];
13
+ }>;
14
+ export declare const resolveBundledRunnethSkillPath: () => string;
15
+ export declare const installRunnethSkills: (options?: RunnethSkillInstallOptions) => Promise<RunnethSkillInstallResult>;