toolhome 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/README.zh.md +281 -0
- package/dist/server/app.d.ts +16 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +166 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/cli/main.d.ts +3 -0
- package/dist/server/cli/main.d.ts.map +1 -0
- package/dist/server/cli/main.js +587 -0
- package/dist/server/cli/main.js.map +1 -0
- package/dist/server/cli-plane/allow-list.d.ts +18 -0
- package/dist/server/cli-plane/allow-list.d.ts.map +1 -0
- package/dist/server/cli-plane/allow-list.js +28 -0
- package/dist/server/cli-plane/allow-list.js.map +1 -0
- package/dist/server/cli-plane/cli-service.d.ts +41 -0
- package/dist/server/cli-plane/cli-service.d.ts.map +1 -0
- package/dist/server/cli-plane/cli-service.js +282 -0
- package/dist/server/cli-plane/cli-service.js.map +1 -0
- package/dist/server/cli-plane/frames.d.ts +26 -0
- package/dist/server/cli-plane/frames.d.ts.map +1 -0
- package/dist/server/cli-plane/frames.js +12 -0
- package/dist/server/cli-plane/frames.js.map +1 -0
- package/dist/server/cli-plane/models.d.ts +174 -0
- package/dist/server/cli-plane/models.d.ts.map +1 -0
- package/dist/server/cli-plane/models.js +169 -0
- package/dist/server/cli-plane/models.js.map +1 -0
- package/dist/server/cli-plane/routes.d.ts +32 -0
- package/dist/server/cli-plane/routes.d.ts.map +1 -0
- package/dist/server/cli-plane/routes.js +94 -0
- package/dist/server/cli-plane/routes.js.map +1 -0
- package/dist/server/cli-plane/runner.d.ts +41 -0
- package/dist/server/cli-plane/runner.d.ts.map +1 -0
- package/dist/server/cli-plane/runner.js +163 -0
- package/dist/server/cli-plane/runner.js.map +1 -0
- package/dist/server/cli-plane/status.d.ts +12 -0
- package/dist/server/cli-plane/status.d.ts.map +1 -0
- package/dist/server/cli-plane/status.js +24 -0
- package/dist/server/cli-plane/status.js.map +1 -0
- package/dist/server/config/harness-import.d.ts +44 -0
- package/dist/server/config/harness-import.d.ts.map +1 -0
- package/dist/server/config/harness-import.js +166 -0
- package/dist/server/config/harness-import.js.map +1 -0
- package/dist/server/config.d.ts +20 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +80 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/control/client.d.ts +24 -0
- package/dist/server/control/client.d.ts.map +1 -0
- package/dist/server/control/client.js +115 -0
- package/dist/server/control/client.js.map +1 -0
- package/dist/server/control/control-api.d.ts +39 -0
- package/dist/server/control/control-api.d.ts.map +1 -0
- package/dist/server/control/control-api.js +291 -0
- package/dist/server/control/control-api.js.map +1 -0
- package/dist/server/control/control-service.d.ts +713 -0
- package/dist/server/control/control-service.d.ts.map +1 -0
- package/dist/server/control/control-service.js +693 -0
- package/dist/server/control/control-service.js.map +1 -0
- package/dist/server/control/openapi.d.ts +2 -0
- package/dist/server/control/openapi.d.ts.map +1 -0
- package/dist/server/control/openapi.js +370 -0
- package/dist/server/control/openapi.js.map +1 -0
- package/dist/server/data-plane/data-plane.d.ts +20 -0
- package/dist/server/data-plane/data-plane.d.ts.map +1 -0
- package/dist/server/data-plane/data-plane.js +335 -0
- package/dist/server/data-plane/data-plane.js.map +1 -0
- package/dist/server/data-plane/gateway-server.d.ts +13 -0
- package/dist/server/data-plane/gateway-server.d.ts.map +1 -0
- package/dist/server/data-plane/gateway-server.js +796 -0
- package/dist/server/data-plane/gateway-server.js.map +1 -0
- package/dist/server/data-plane/legacy-session.d.ts +18 -0
- package/dist/server/data-plane/legacy-session.d.ts.map +1 -0
- package/dist/server/data-plane/legacy-session.js +137 -0
- package/dist/server/data-plane/legacy-session.js.map +1 -0
- package/dist/server/data-plane/projection.d.ts +18 -0
- package/dist/server/data-plane/projection.d.ts.map +1 -0
- package/dist/server/data-plane/projection.js +44 -0
- package/dist/server/data-plane/projection.js.map +1 -0
- package/dist/server/data-plane/registry.d.ts +31 -0
- package/dist/server/data-plane/registry.d.ts.map +1 -0
- package/dist/server/data-plane/registry.js +129 -0
- package/dist/server/data-plane/registry.js.map +1 -0
- package/dist/server/data-plane/task-extension.d.ts +7 -0
- package/dist/server/data-plane/task-extension.d.ts.map +1 -0
- package/dist/server/data-plane/task-extension.js +33 -0
- package/dist/server/data-plane/task-extension.js.map +1 -0
- package/dist/server/data-plane/virtualization.d.ts +35 -0
- package/dist/server/data-plane/virtualization.d.ts.map +1 -0
- package/dist/server/data-plane/virtualization.js +228 -0
- package/dist/server/data-plane/virtualization.js.map +1 -0
- package/dist/server/domain/errors.d.ts +9 -0
- package/dist/server/domain/errors.d.ts.map +1 -0
- package/dist/server/domain/errors.js +19 -0
- package/dist/server/domain/errors.js.map +1 -0
- package/dist/server/domain/models.d.ts +720 -0
- package/dist/server/domain/models.d.ts.map +1 -0
- package/dist/server/domain/models.js +327 -0
- package/dist/server/domain/models.js.map +1 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.d.ts.map +1 -0
- package/dist/server/main.js +37 -0
- package/dist/server/main.js.map +1 -0
- package/dist/server/manage/management-mcp.d.ts +27 -0
- package/dist/server/manage/management-mcp.d.ts.map +1 -0
- package/dist/server/manage/management-mcp.js +431 -0
- package/dist/server/manage/management-mcp.js.map +1 -0
- package/dist/server/market/catalog.d.ts +111 -0
- package/dist/server/market/catalog.d.ts.map +1 -0
- package/dist/server/market/catalog.js +784 -0
- package/dist/server/market/catalog.js.map +1 -0
- package/dist/server/market/installers.d.ts +64 -0
- package/dist/server/market/installers.d.ts.map +1 -0
- package/dist/server/market/installers.js +181 -0
- package/dist/server/market/installers.js.map +1 -0
- package/dist/server/market/market-service.d.ts +146 -0
- package/dist/server/market/market-service.d.ts.map +1 -0
- package/dist/server/market/market-service.js +930 -0
- package/dist/server/market/market-service.js.map +1 -0
- package/dist/server/observability/call-recorder.d.ts +22 -0
- package/dist/server/observability/call-recorder.d.ts.map +1 -0
- package/dist/server/observability/call-recorder.js +99 -0
- package/dist/server/observability/call-recorder.js.map +1 -0
- package/dist/server/observability/logger.d.ts +16 -0
- package/dist/server/observability/logger.d.ts.map +1 -0
- package/dist/server/observability/logger.js +25 -0
- package/dist/server/observability/logger.js.map +1 -0
- package/dist/server/security/api-keys.d.ts +15 -0
- package/dist/server/security/api-keys.d.ts.map +1 -0
- package/dist/server/security/api-keys.js +35 -0
- package/dist/server/security/api-keys.js.map +1 -0
- package/dist/server/security/auth-service.d.ts +26 -0
- package/dist/server/security/auth-service.d.ts.map +1 -0
- package/dist/server/security/auth-service.js +76 -0
- package/dist/server/security/auth-service.js.map +1 -0
- package/dist/server/security/control-session.d.ts +9 -0
- package/dist/server/security/control-session.d.ts.map +1 -0
- package/dist/server/security/control-session.js +51 -0
- package/dist/server/security/control-session.js.map +1 -0
- package/dist/server/security/credential-materializer.d.ts +20 -0
- package/dist/server/security/credential-materializer.d.ts.map +1 -0
- package/dist/server/security/credential-materializer.js +82 -0
- package/dist/server/security/credential-materializer.js.map +1 -0
- package/dist/server/security/cursor-codec.d.ts +13 -0
- package/dist/server/security/cursor-codec.d.ts.map +1 -0
- package/dist/server/security/cursor-codec.js +43 -0
- package/dist/server/security/cursor-codec.js.map +1 -0
- package/dist/server/security/oauth-routes.d.ts +21 -0
- package/dist/server/security/oauth-routes.d.ts.map +1 -0
- package/dist/server/security/oauth-routes.js +100 -0
- package/dist/server/security/oauth-routes.js.map +1 -0
- package/dist/server/security/oauth-server.d.ts +16 -0
- package/dist/server/security/oauth-server.d.ts.map +1 -0
- package/dist/server/security/oauth-server.js +544 -0
- package/dist/server/security/oauth-server.js.map +1 -0
- package/dist/server/security/secret-box.d.ts +7 -0
- package/dist/server/security/secret-box.d.ts.map +1 -0
- package/dist/server/security/secret-box.js +27 -0
- package/dist/server/security/secret-box.js.map +1 -0
- package/dist/server/security/secure-action.d.ts +21 -0
- package/dist/server/security/secure-action.d.ts.map +1 -0
- package/dist/server/security/secure-action.js +69 -0
- package/dist/server/security/secure-action.js.map +1 -0
- package/dist/server/storage/sqlite-store.d.ts +70 -0
- package/dist/server/storage/sqlite-store.d.ts.map +1 -0
- package/dist/server/storage/sqlite-store.js +1522 -0
- package/dist/server/storage/sqlite-store.js.map +1 -0
- package/dist/server/storage/store.d.ts +82 -0
- package/dist/server/storage/store.d.ts.map +1 -0
- package/dist/server/storage/store.js +2 -0
- package/dist/server/storage/store.js.map +1 -0
- package/dist/server/upstream/adapter.d.ts +52 -0
- package/dist/server/upstream/adapter.d.ts.map +1 -0
- package/dist/server/upstream/adapter.js +807 -0
- package/dist/server/upstream/adapter.js.map +1 -0
- package/dist/server/upstream/credential-resolver.d.ts +14 -0
- package/dist/server/upstream/credential-resolver.d.ts.map +1 -0
- package/dist/server/upstream/credential-resolver.js +56 -0
- package/dist/server/upstream/credential-resolver.js.map +1 -0
- package/dist/server/upstream/extension-transport.d.ts +17 -0
- package/dist/server/upstream/extension-transport.d.ts.map +1 -0
- package/dist/server/upstream/extension-transport.js +207 -0
- package/dist/server/upstream/extension-transport.js.map +1 -0
- package/dist/server/upstream/manager.d.ts +23 -0
- package/dist/server/upstream/manager.d.ts.map +1 -0
- package/dist/server/upstream/manager.js +324 -0
- package/dist/server/upstream/manager.js.map +1 -0
- package/dist/server/upstream/oauth-provider.d.ts +38 -0
- package/dist/server/upstream/oauth-provider.d.ts.map +1 -0
- package/dist/server/upstream/oauth-provider.js +225 -0
- package/dist/server/upstream/oauth-provider.js.map +1 -0
- package/dist/server/upstream/oauth-refresh.d.ts +65 -0
- package/dist/server/upstream/oauth-refresh.d.ts.map +1 -0
- package/dist/server/upstream/oauth-refresh.js +240 -0
- package/dist/server/upstream/oauth-refresh.js.map +1 -0
- package/dist/server/upstream/oauth-routes.d.ts +17 -0
- package/dist/server/upstream/oauth-routes.d.ts.map +1 -0
- package/dist/server/upstream/oauth-routes.js +87 -0
- package/dist/server/upstream/oauth-routes.js.map +1 -0
- package/dist/server/upstream/oauth-service.d.ts +143 -0
- package/dist/server/upstream/oauth-service.d.ts.map +1 -0
- package/dist/server/upstream/oauth-service.js +217 -0
- package/dist/server/upstream/oauth-service.js.map +1 -0
- package/dist/server/upstream/stable-json.d.ts +3 -0
- package/dist/server/upstream/stable-json.d.ts.map +1 -0
- package/dist/server/upstream/stable-json.js +17 -0
- package/dist/server/upstream/stable-json.js.map +1 -0
- package/package.json +69 -0
- package/skills/toolhome/SKILL.md +238 -0
- package/skills/toolhome/references/cli-reference.md +242 -0
- package/skills/toolhome/references/deployment.md +113 -0
- package/skills/toolhome/references/market-guide.md +128 -0
- package/skills/toolhome/references/oauth-guide.md +96 -0
- package/skills/toolhome/references/troubleshooting.md +133 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CredentialPayload, CredentialRecord, ServerRecord } from '../domain/models.js';
|
|
2
|
+
import type { Logger } from '../observability/logger.js';
|
|
3
|
+
import type { Store } from '../storage/store.js';
|
|
4
|
+
import type { UpstreamOAuthService } from './oauth-service.js';
|
|
5
|
+
/**
|
|
6
|
+
* Seconds before `expiresAt` at which an OAuth credential becomes eligible
|
|
7
|
+
* for automatic refresh, so a near-expiry token is renewed before it is
|
|
8
|
+
* actually rejected by the upstream server.
|
|
9
|
+
*/
|
|
10
|
+
export declare const OAUTH_REFRESH_GRACE_SECONDS = 60;
|
|
11
|
+
export interface OAuthRefreshCandidate {
|
|
12
|
+
credentialId: string;
|
|
13
|
+
server: ServerRecord;
|
|
14
|
+
}
|
|
15
|
+
export interface OAuthRefreshSelectionInput {
|
|
16
|
+
credentials: CredentialRecord[];
|
|
17
|
+
payloads: ReadonlyMap<string, CredentialPayload>;
|
|
18
|
+
servers: ServerRecord[];
|
|
19
|
+
/** Epoch milliseconds to evaluate expiry against. */
|
|
20
|
+
now: number;
|
|
21
|
+
graceSeconds: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Picks OAuth credentials that are due for a non-interactive refresh: attached
|
|
25
|
+
* to exactly one enabled remote MCP server, carrying both an access token and
|
|
26
|
+
* a refresh token, with an `expiresAt` at or before `now + graceSeconds`.
|
|
27
|
+
* Credentials without a refresh token or without an `expiresAt` are never
|
|
28
|
+
* schedulable and are excluded.
|
|
29
|
+
*/
|
|
30
|
+
export declare function selectDueOAuthCredentials(input: OAuthRefreshSelectionInput): OAuthRefreshCandidate[];
|
|
31
|
+
/**
|
|
32
|
+
* Picks OAuth credentials that are already expired but carry no refresh token,
|
|
33
|
+
* attached to exactly one enabled remote MCP server. The sweep cannot refresh
|
|
34
|
+
* these; they are reported as auto-refresh failures so the event log explains
|
|
35
|
+
* why manual authorization is still required.
|
|
36
|
+
*/
|
|
37
|
+
export declare function selectExpiredUnrefreshableOAuthCredentials(input: OAuthRefreshSelectionInput): OAuthRefreshCandidate[];
|
|
38
|
+
export interface OAuthRefreshSweepResult {
|
|
39
|
+
/** Credentials selected as due for refresh this sweep. */
|
|
40
|
+
due: number;
|
|
41
|
+
refreshed: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
/** Credentials skipped because their payload changed mid-sweep. */
|
|
44
|
+
skipped: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Periodically sweeps stored OAuth credentials and refreshes expired or
|
|
48
|
+
* expiring access tokens through the non-interactive refresh-token grant,
|
|
49
|
+
* without any human action. Healthy credentials are left untouched; failures
|
|
50
|
+
* are recorded as events and never retried in a tight loop.
|
|
51
|
+
*/
|
|
52
|
+
export declare class OAuthRefreshSweeper {
|
|
53
|
+
#private;
|
|
54
|
+
constructor(store: Store, oauth: UpstreamOAuthService, logger: Logger, graceSeconds?: number);
|
|
55
|
+
/** Starts the interval timer plus one sweep shortly after startup. */
|
|
56
|
+
start(intervalSeconds: number): void;
|
|
57
|
+
/** Stops the timers and waits for an in-flight sweep so shutdown is clean. */
|
|
58
|
+
stop(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Runs one sweep immediately. Overlapping sweeps are deduped: a call while
|
|
61
|
+
* a sweep is in flight awaits that same sweep instead of starting a second.
|
|
62
|
+
*/
|
|
63
|
+
sweep(now?: number): Promise<OAuthRefreshSweepResult>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=oauth-refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-refresh.d.ts","sourceRoot":"","sources":["../../../src/upstream/oauth-refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACjD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,0BAA0B,GAChC,qBAAqB,EAAE,CAezB;AAED;;;;;GAKG;AACH,wBAAgB,0CAA0C,CACxD,KAAK,EAAE,0BAA0B,GAChC,qBAAqB,EAAE,CAazB;AAyBD,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,qBAAa,mBAAmB;;IAU9B,YACE,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,oBAAoB,EAC3B,MAAM,EAAE,MAAM,EACd,YAAY,SAA8B,EAM3C;IAED,sEAAsE;IACtE,KAAK,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAMnC;IAED,8EAA8E;IACxE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1B;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,SAAa,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAQxD;CAuGF"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { errorMessage } from '../domain/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Seconds before `expiresAt` at which an OAuth credential becomes eligible
|
|
4
|
+
* for automatic refresh, so a near-expiry token is renewed before it is
|
|
5
|
+
* actually rejected by the upstream server.
|
|
6
|
+
*/
|
|
7
|
+
export const OAUTH_REFRESH_GRACE_SECONDS = 60;
|
|
8
|
+
/**
|
|
9
|
+
* Picks OAuth credentials that are due for a non-interactive refresh: attached
|
|
10
|
+
* to exactly one enabled remote MCP server, carrying both an access token and
|
|
11
|
+
* a refresh token, with an `expiresAt` at or before `now + graceSeconds`.
|
|
12
|
+
* Credentials without a refresh token or without an `expiresAt` are never
|
|
13
|
+
* schedulable and are excluded.
|
|
14
|
+
*/
|
|
15
|
+
export function selectDueOAuthCredentials(input) {
|
|
16
|
+
const deadline = input.now + input.graceSeconds * 1000;
|
|
17
|
+
const attachments = attachmentIndex(input.servers);
|
|
18
|
+
const due = [];
|
|
19
|
+
for (const credential of input.credentials) {
|
|
20
|
+
const payload = input.payloads.get(credential.id);
|
|
21
|
+
if (!payload || payload.type !== 'oauth')
|
|
22
|
+
continue;
|
|
23
|
+
if (!payload.accessToken || !payload.refreshToken)
|
|
24
|
+
continue;
|
|
25
|
+
if (payload.expiresAt === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
if (Date.parse(payload.expiresAt) > deadline)
|
|
28
|
+
continue;
|
|
29
|
+
const server = soleEnabledRemoteServer(attachments, credential.id);
|
|
30
|
+
if (!server)
|
|
31
|
+
continue;
|
|
32
|
+
due.push({ credentialId: credential.id, server });
|
|
33
|
+
}
|
|
34
|
+
return due;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Picks OAuth credentials that are already expired but carry no refresh token,
|
|
38
|
+
* attached to exactly one enabled remote MCP server. The sweep cannot refresh
|
|
39
|
+
* these; they are reported as auto-refresh failures so the event log explains
|
|
40
|
+
* why manual authorization is still required.
|
|
41
|
+
*/
|
|
42
|
+
export function selectExpiredUnrefreshableOAuthCredentials(input) {
|
|
43
|
+
const attachments = attachmentIndex(input.servers);
|
|
44
|
+
const stalled = [];
|
|
45
|
+
for (const credential of input.credentials) {
|
|
46
|
+
const payload = input.payloads.get(credential.id);
|
|
47
|
+
if (!payload || payload.type !== 'oauth')
|
|
48
|
+
continue;
|
|
49
|
+
if (!payload.accessToken || payload.refreshToken !== undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (payload.expiresAt === undefined || Date.parse(payload.expiresAt) > input.now)
|
|
52
|
+
continue;
|
|
53
|
+
const server = soleEnabledRemoteServer(attachments, credential.id);
|
|
54
|
+
if (!server)
|
|
55
|
+
continue;
|
|
56
|
+
stalled.push({ credentialId: credential.id, server });
|
|
57
|
+
}
|
|
58
|
+
return stalled;
|
|
59
|
+
}
|
|
60
|
+
function attachmentIndex(servers) {
|
|
61
|
+
const attachments = new Map();
|
|
62
|
+
for (const server of servers) {
|
|
63
|
+
if (server.credentialId === null)
|
|
64
|
+
continue;
|
|
65
|
+
const list = attachments.get(server.credentialId);
|
|
66
|
+
if (list)
|
|
67
|
+
list.push(server);
|
|
68
|
+
else
|
|
69
|
+
attachments.set(server.credentialId, [server]);
|
|
70
|
+
}
|
|
71
|
+
return attachments;
|
|
72
|
+
}
|
|
73
|
+
function soleEnabledRemoteServer(attachments, credentialId) {
|
|
74
|
+
const attached = attachments.get(credentialId);
|
|
75
|
+
if (!attached || attached.length !== 1)
|
|
76
|
+
return null;
|
|
77
|
+
const server = attached[0];
|
|
78
|
+
if (!server.enabled)
|
|
79
|
+
return null;
|
|
80
|
+
if (server.kind !== 'remote' || server.transport.type !== 'streamable-http')
|
|
81
|
+
return null;
|
|
82
|
+
return server;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Periodically sweeps stored OAuth credentials and refreshes expired or
|
|
86
|
+
* expiring access tokens through the non-interactive refresh-token grant,
|
|
87
|
+
* without any human action. Healthy credentials are left untouched; failures
|
|
88
|
+
* are recorded as events and never retried in a tight loop.
|
|
89
|
+
*/
|
|
90
|
+
export class OAuthRefreshSweeper {
|
|
91
|
+
#store;
|
|
92
|
+
#oauth;
|
|
93
|
+
#logger;
|
|
94
|
+
#graceSeconds;
|
|
95
|
+
#running = null;
|
|
96
|
+
#timer;
|
|
97
|
+
#startup;
|
|
98
|
+
#stopped = false;
|
|
99
|
+
constructor(store, oauth, logger, graceSeconds = OAUTH_REFRESH_GRACE_SECONDS) {
|
|
100
|
+
this.#store = store;
|
|
101
|
+
this.#oauth = oauth;
|
|
102
|
+
this.#logger = logger;
|
|
103
|
+
this.#graceSeconds = graceSeconds;
|
|
104
|
+
}
|
|
105
|
+
/** Starts the interval timer plus one sweep shortly after startup. */
|
|
106
|
+
start(intervalSeconds) {
|
|
107
|
+
if (this.#timer)
|
|
108
|
+
return;
|
|
109
|
+
this.#timer = setInterval(() => void this.#tick(), intervalSeconds * 1_000);
|
|
110
|
+
this.#timer.unref?.();
|
|
111
|
+
this.#startup = setTimeout(() => void this.#tick(), 0);
|
|
112
|
+
this.#startup.unref?.();
|
|
113
|
+
}
|
|
114
|
+
/** Stops the timers and waits for an in-flight sweep so shutdown is clean. */
|
|
115
|
+
async stop() {
|
|
116
|
+
this.#stopped = true;
|
|
117
|
+
if (this.#timer)
|
|
118
|
+
clearInterval(this.#timer);
|
|
119
|
+
if (this.#startup)
|
|
120
|
+
clearTimeout(this.#startup);
|
|
121
|
+
this.#timer = undefined;
|
|
122
|
+
this.#startup = undefined;
|
|
123
|
+
await this.#running;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Runs one sweep immediately. Overlapping sweeps are deduped: a call while
|
|
127
|
+
* a sweep is in flight awaits that same sweep instead of starting a second.
|
|
128
|
+
*/
|
|
129
|
+
sweep(now = Date.now()) {
|
|
130
|
+
if (this.#stopped)
|
|
131
|
+
return Promise.resolve({ due: 0, refreshed: 0, failed: 0, skipped: 0 });
|
|
132
|
+
if (this.#running)
|
|
133
|
+
return this.#running;
|
|
134
|
+
const operation = this.#sweepNow(now).finally(() => {
|
|
135
|
+
this.#running = null;
|
|
136
|
+
});
|
|
137
|
+
this.#running = operation;
|
|
138
|
+
return operation;
|
|
139
|
+
}
|
|
140
|
+
async #tick() {
|
|
141
|
+
try {
|
|
142
|
+
await this.sweep();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.#logger.warn('OAuth credential auto-refresh sweep failed', {
|
|
146
|
+
error: errorMessage(error),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async #sweepNow(now) {
|
|
151
|
+
const credentials = this.#store.listCredentials();
|
|
152
|
+
const payloads = new Map();
|
|
153
|
+
for (const credential of credentials) {
|
|
154
|
+
const payload = this.#store.getCredentialPayload(credential.id);
|
|
155
|
+
if (payload)
|
|
156
|
+
payloads.set(credential.id, payload);
|
|
157
|
+
}
|
|
158
|
+
const servers = this.#store.listServers();
|
|
159
|
+
const selection = {
|
|
160
|
+
credentials,
|
|
161
|
+
payloads,
|
|
162
|
+
servers,
|
|
163
|
+
now,
|
|
164
|
+
graceSeconds: this.#graceSeconds,
|
|
165
|
+
};
|
|
166
|
+
const due = selectDueOAuthCredentials(selection);
|
|
167
|
+
const stalled = selectExpiredUnrefreshableOAuthCredentials(selection);
|
|
168
|
+
let refreshed = 0;
|
|
169
|
+
let failed = 0;
|
|
170
|
+
let skipped = 0;
|
|
171
|
+
for (const candidate of stalled) {
|
|
172
|
+
failed += 1;
|
|
173
|
+
this.#store.appendEvent({
|
|
174
|
+
level: 'warn',
|
|
175
|
+
type: 'credential.auto_refresh_failed',
|
|
176
|
+
serverId: candidate.server.id,
|
|
177
|
+
message: `OAuth credential for ${candidate.server.slug} expired without a refresh token; manual authorization required`,
|
|
178
|
+
detail: { credentialId: candidate.credentialId, reason: 'missing-refresh-token' },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
for (const candidate of due) {
|
|
182
|
+
const selected = payloads.get(candidate.credentialId);
|
|
183
|
+
const outcome = await this.#refreshCandidate(candidate, selected);
|
|
184
|
+
if (outcome === 'refreshed')
|
|
185
|
+
refreshed += 1;
|
|
186
|
+
else if (outcome === 'failed')
|
|
187
|
+
failed += 1;
|
|
188
|
+
else
|
|
189
|
+
skipped += 1;
|
|
190
|
+
}
|
|
191
|
+
return { due: due.length, refreshed, failed, skipped };
|
|
192
|
+
}
|
|
193
|
+
async #refreshCandidate(candidate, selectedPayload) {
|
|
194
|
+
const before = this.#store.getCredentialPayload(candidate.credentialId);
|
|
195
|
+
if (before?.type !== 'oauth' ||
|
|
196
|
+
(selectedPayload?.type === 'oauth' && before.accessToken !== selectedPayload.accessToken)) {
|
|
197
|
+
// A concurrent refresh (e.g. a 401-driven one) already replaced the
|
|
198
|
+
// token since selection; do not refresh the same credential twice.
|
|
199
|
+
return 'skipped';
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const result = await this.#oauth.begin(candidate.credentialId, {});
|
|
203
|
+
if (result.status === 'authorized') {
|
|
204
|
+
this.#store.appendEvent({
|
|
205
|
+
level: 'info',
|
|
206
|
+
type: 'credential.auto_refreshed',
|
|
207
|
+
serverId: candidate.server.id,
|
|
208
|
+
message: `Auto-refreshed OAuth credential for ${candidate.server.slug}`,
|
|
209
|
+
detail: { credentialId: candidate.credentialId },
|
|
210
|
+
});
|
|
211
|
+
return 'refreshed';
|
|
212
|
+
}
|
|
213
|
+
// begin() fell through to the interactive flow because the refresh grant
|
|
214
|
+
// was rejected. Restore the pre-sweep payload so the credential keeps
|
|
215
|
+
// its expired status; the manual authorize flow remains the fallback.
|
|
216
|
+
if (before.type === 'oauth') {
|
|
217
|
+
this.#store.updateCredential(candidate.credentialId, { payload: before });
|
|
218
|
+
}
|
|
219
|
+
this.#store.appendEvent({
|
|
220
|
+
level: 'warn',
|
|
221
|
+
type: 'credential.auto_refresh_failed',
|
|
222
|
+
serverId: candidate.server.id,
|
|
223
|
+
message: `Auto-refresh of OAuth credential for ${candidate.server.slug} failed; manual authorization required`,
|
|
224
|
+
detail: { credentialId: candidate.credentialId, reason: 'refresh-rejected' },
|
|
225
|
+
});
|
|
226
|
+
return 'failed';
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.#store.appendEvent({
|
|
230
|
+
level: 'warn',
|
|
231
|
+
type: 'credential.auto_refresh_failed',
|
|
232
|
+
serverId: candidate.server.id,
|
|
233
|
+
message: `Auto-refresh of OAuth credential for ${candidate.server.slug} failed`,
|
|
234
|
+
detail: { credentialId: candidate.credentialId, error: errorMessage(error) },
|
|
235
|
+
});
|
|
236
|
+
return 'failed';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=oauth-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-refresh.js","sourceRoot":"","sources":["../../../src/upstream/oauth-refresh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Logger } from '../observability/logger.js';
|
|
2
|
+
import type { UpstreamOAuthService } from './oauth-service.js';
|
|
3
|
+
interface HonoLike {
|
|
4
|
+
get(path: string, handler: (context: HonoContext) => Response | Promise<Response>): void;
|
|
5
|
+
}
|
|
6
|
+
interface HonoContext {
|
|
7
|
+
req: {
|
|
8
|
+
param(name: string): string;
|
|
9
|
+
query(name: string): string | undefined;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function mountUpstreamOAuthRoutes(app: HonoLike, options: {
|
|
13
|
+
oauth: UpstreamOAuthService;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
}): void;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=oauth-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-routes.d.ts","sourceRoot":"","sources":["../../../src/upstream/oauth-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,UAAU,QAAQ;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;CAC1F;AAED,UAAU,WAAW;IACnB,GAAG,EAAE;QACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAC5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACzC,CAAC;CACH;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE;IAAE,KAAK,EAAE,oBAAoB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvD,IAAI,CA+DN"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function mountUpstreamOAuthRoutes(app, options) {
|
|
2
|
+
app.get('/oauth/upstream/client/:credentialId', (context) => {
|
|
3
|
+
try {
|
|
4
|
+
return new Response(JSON.stringify(options.oauth.metadata(context.req.param('credentialId'))), {
|
|
5
|
+
status: 200,
|
|
6
|
+
headers: {
|
|
7
|
+
'content-type': 'application/json',
|
|
8
|
+
'cache-control': 'public, max-age=300',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return new Response(JSON.stringify({ error: 'client_metadata_not_found' }), {
|
|
14
|
+
status: 404,
|
|
15
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
app.get('/oauth/upstream/callback/:credentialId', async (context) => {
|
|
20
|
+
const credentialId = context.req.param('credentialId');
|
|
21
|
+
if (context.req.query('error')) {
|
|
22
|
+
return htmlPage('Authorization was not completed', 'Return to ToolHome and start the authorization flow again.', 400);
|
|
23
|
+
}
|
|
24
|
+
const code = context.req.query('code');
|
|
25
|
+
const state = context.req.query('state');
|
|
26
|
+
if (!code || !state) {
|
|
27
|
+
return htmlPage('Invalid authorization callback', 'The callback is missing required parameters. Return to ToolHome and try again.', 400);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const issuer = context.req.query('iss');
|
|
31
|
+
const result = await options.oauth.callback(credentialId, {
|
|
32
|
+
code,
|
|
33
|
+
state,
|
|
34
|
+
...(issuer === undefined ? {} : { iss: issuer }),
|
|
35
|
+
});
|
|
36
|
+
return htmlPage('Authorization complete', `${result.server.name} is connected. You can close this window.`, 200);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
options.logger.warn('Upstream OAuth callback failed', {
|
|
40
|
+
credentialId,
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
});
|
|
43
|
+
return htmlPage('Authorization failed', 'The authorization response could not be verified. Return to ToolHome and try again.', 400);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function htmlPage(title, message, status) {
|
|
48
|
+
return new Response(`<!doctype html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="utf-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
53
|
+
<title>${escapeHtml(title)} · ToolHome</title>
|
|
54
|
+
<style>
|
|
55
|
+
:root { color-scheme: light dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
56
|
+
* { box-sizing: border-box; }
|
|
57
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f5f5f2; color: #171714; }
|
|
58
|
+
main { width: min(520px, calc(100vw - 40px)); padding: 40px; border: 1px solid #d9d9d2; border-radius: 18px; background: #fff; }
|
|
59
|
+
p:first-child { margin: 0 0 24px; color: #6e6e67; font: 600 12px/1 ui-monospace, SFMono-Regular, monospace; letter-spacing: .12em; text-transform: uppercase; }
|
|
60
|
+
h1 { margin: 0 0 12px; font-size: 28px; letter-spacing: -.03em; }
|
|
61
|
+
p:last-child { margin: 0; color: #62625c; line-height: 1.6; }
|
|
62
|
+
@media (prefers-color-scheme: dark) {
|
|
63
|
+
body { background: #11110f; color: #f3f3ee; }
|
|
64
|
+
main { background: #191917; border-color: #34342f; }
|
|
65
|
+
p:first-child, p:last-child { color: #aaa99f; }
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
</head>
|
|
69
|
+
<body><main><p>ToolHome</p><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></main></body>
|
|
70
|
+
</html>`, {
|
|
71
|
+
status,
|
|
72
|
+
headers: {
|
|
73
|
+
'content-type': 'text/html; charset=utf-8',
|
|
74
|
+
'cache-control': 'no-store',
|
|
75
|
+
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function escapeHtml(value) {
|
|
80
|
+
return value
|
|
81
|
+
.replaceAll('&', '&')
|
|
82
|
+
.replaceAll('<', '<')
|
|
83
|
+
.replaceAll('>', '>')
|
|
84
|
+
.replaceAll('"', '"')
|
|
85
|
+
.replaceAll("'", ''');
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=oauth-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-routes.js","sourceRoot":"","sources":["../../../src/upstream/oauth-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ServerRecord } from '../domain/models.js';
|
|
2
|
+
import type { Logger } from '../observability/logger.js';
|
|
3
|
+
import type { Store } from '../storage/store.js';
|
|
4
|
+
import type { UpstreamManager } from './manager.js';
|
|
5
|
+
export declare class UpstreamOAuthService {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(store: Store, publicUrl: URL, upstreams: UpstreamManager, logger: Logger, urlClientId?: boolean);
|
|
8
|
+
metadata(credentialId: string): {
|
|
9
|
+
redirect_uris: string[];
|
|
10
|
+
token_endpoint_auth_method?: string | undefined;
|
|
11
|
+
grant_types?: string[] | undefined;
|
|
12
|
+
response_types?: string[] | undefined;
|
|
13
|
+
application_type?: string | undefined;
|
|
14
|
+
client_name?: string | undefined;
|
|
15
|
+
client_uri?: string | undefined;
|
|
16
|
+
logo_uri?: string | undefined;
|
|
17
|
+
scope?: string | undefined;
|
|
18
|
+
contacts?: string[] | undefined;
|
|
19
|
+
tos_uri?: string | undefined;
|
|
20
|
+
policy_uri?: string | undefined;
|
|
21
|
+
jwks_uri?: string | undefined;
|
|
22
|
+
jwks?: any;
|
|
23
|
+
software_id?: string | undefined;
|
|
24
|
+
software_version?: string | undefined;
|
|
25
|
+
software_statement?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
begin(credentialId: string, value?: unknown): Promise<{
|
|
28
|
+
status: string;
|
|
29
|
+
credential: {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
type: "api-key" | "bearer" | "env" | "headers" | "oauth";
|
|
33
|
+
status: "expired" | "invalid" | "pending" | "ready";
|
|
34
|
+
expiresAt: string | null;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
} | null;
|
|
38
|
+
serverId: string;
|
|
39
|
+
connection: {
|
|
40
|
+
refreshed: boolean;
|
|
41
|
+
reason: string;
|
|
42
|
+
} | {
|
|
43
|
+
reason?: undefined;
|
|
44
|
+
refreshed: boolean;
|
|
45
|
+
};
|
|
46
|
+
authorizationUrl?: undefined;
|
|
47
|
+
callbackUrl?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
connection?: undefined;
|
|
50
|
+
status: string;
|
|
51
|
+
credential: {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
type: "api-key" | "bearer" | "env" | "headers" | "oauth";
|
|
55
|
+
status: "expired" | "invalid" | "pending" | "ready";
|
|
56
|
+
expiresAt: string | null;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
} | null;
|
|
60
|
+
serverId: string;
|
|
61
|
+
authorizationUrl: string;
|
|
62
|
+
callbackUrl: string;
|
|
63
|
+
}>;
|
|
64
|
+
callback(credentialId: string, input: {
|
|
65
|
+
code: string;
|
|
66
|
+
state: string;
|
|
67
|
+
iss?: string;
|
|
68
|
+
}): Promise<{
|
|
69
|
+
status: string;
|
|
70
|
+
server: {
|
|
71
|
+
id: string;
|
|
72
|
+
slug: string;
|
|
73
|
+
name: string;
|
|
74
|
+
kind: "home" | "remote";
|
|
75
|
+
transport: {
|
|
76
|
+
type: "streamable-http";
|
|
77
|
+
url: string;
|
|
78
|
+
protocolMode: "auto" | "legacy" | "modern";
|
|
79
|
+
allowSseFallback: boolean;
|
|
80
|
+
headers: Record<string, string>;
|
|
81
|
+
} | {
|
|
82
|
+
type: "stdio";
|
|
83
|
+
command: string;
|
|
84
|
+
args: string[];
|
|
85
|
+
cwd?: string | undefined;
|
|
86
|
+
env: Record<string, string>;
|
|
87
|
+
protocolMode: "auto" | "legacy" | "modern";
|
|
88
|
+
};
|
|
89
|
+
credentialId: string | null;
|
|
90
|
+
enabled: boolean;
|
|
91
|
+
settings: {
|
|
92
|
+
connectTimeoutMs: number;
|
|
93
|
+
requestTimeoutMs: number;
|
|
94
|
+
maxTotalTimeoutMs: number;
|
|
95
|
+
maxConcurrency: number;
|
|
96
|
+
restart: "always" | "never" | "on-failure";
|
|
97
|
+
urlClientId?: boolean | undefined;
|
|
98
|
+
};
|
|
99
|
+
createdAt: string;
|
|
100
|
+
updatedAt: string;
|
|
101
|
+
} & {
|
|
102
|
+
transport: Extract<ServerRecord['transport'], {
|
|
103
|
+
type: 'streamable-http';
|
|
104
|
+
}>;
|
|
105
|
+
};
|
|
106
|
+
credential: {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
type: "api-key" | "bearer" | "env" | "headers" | "oauth";
|
|
110
|
+
status: "expired" | "invalid" | "pending" | "ready";
|
|
111
|
+
expiresAt: string | null;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
updatedAt: string;
|
|
114
|
+
} | null;
|
|
115
|
+
connection: {
|
|
116
|
+
refreshed: boolean;
|
|
117
|
+
reason: string;
|
|
118
|
+
} | {
|
|
119
|
+
reason?: undefined;
|
|
120
|
+
refreshed: boolean;
|
|
121
|
+
};
|
|
122
|
+
}>;
|
|
123
|
+
revoke(credentialId: string): Promise<{
|
|
124
|
+
revoked: boolean;
|
|
125
|
+
remoteRevocationSupported: boolean;
|
|
126
|
+
remoteRevoked: boolean;
|
|
127
|
+
attempts: {
|
|
128
|
+
tokenType: 'refresh_token' | 'access_token';
|
|
129
|
+
ok: boolean;
|
|
130
|
+
status?: number;
|
|
131
|
+
}[];
|
|
132
|
+
credential: {
|
|
133
|
+
id: string;
|
|
134
|
+
name: string;
|
|
135
|
+
type: "api-key" | "bearer" | "env" | "headers" | "oauth";
|
|
136
|
+
status: "expired" | "invalid" | "pending" | "ready";
|
|
137
|
+
expiresAt: string | null;
|
|
138
|
+
createdAt: string;
|
|
139
|
+
updatedAt: string;
|
|
140
|
+
} | null;
|
|
141
|
+
}>;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=oauth-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-service.d.ts","sourceRoot":"","sources":["../../../src/upstream/oauth-service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAQpD,qBAAa,oBAAoB;;IAO/B,YACE,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,GAAG,EACd,SAAS,EAAE,eAAe,EAC1B,MAAM,EAAE,MAAM,EACd,WAAW,UAAO,EAOnB;IAQD,QAAQ,CAAC,YAAY,EAAE,MAAM;;;;;;;;;;;;;;;;;;MAE5B;IAEK,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,OAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmDpD;IAEK,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA6H5E,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE;gBAAE,IAAI,EAAE,iBAAiB,CAAA;aAAE,CAAC;;;;;;;;;;;;;;;;;;OA9F3E;IAEK,MAAM,CAAC,YAAY,EAAE,MAAM;;;;;uBAYlB,eAAe,GAAG,cAAc;gBACvC,OAAO;qBACF,MAAM;;;;;;;;;;;OAqClB;CA+FF"}
|