@cjhyy/code-shell-server 0.9.26 → 0.9.27

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.
@@ -0,0 +1,16 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { LinkAuthorization } from "@cjhyy/code-shell-link";
3
+ interface Binding {
4
+ check(): Promise<void>;
5
+ complete(callbackUrl: string): Promise<LinkAuthorization>;
6
+ cancel(): Promise<unknown>;
7
+ }
8
+ /** An external browser carries only OAuth state. The original Host keeps its owner and PKCE. */
9
+ export declare function createLinkBrowserHandoff(now?: () => number): {
10
+ close(): void;
11
+ create(job: LinkAuthorization, binding: Binding, origin: string): {
12
+ launchUrl: string;
13
+ };
14
+ handle(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
15
+ };
16
+ export {};
@@ -0,0 +1,150 @@
1
+ import { randomBytes } from "node:crypto";
2
+ /** An external browser carries only OAuth state. The original Host keeps its owner and PKCE. */
3
+ export function createLinkBrowserHandoff(now = Date.now) {
4
+ const tickets = new Map();
5
+ const states = new Map();
6
+ let closed = false;
7
+ const forget = (attempt) => {
8
+ tickets.delete(attempt.ticket);
9
+ states.delete(attempt.state);
10
+ };
11
+ const prune = () => {
12
+ for (const attempt of states.values())
13
+ if (attempt.expiresAt <= now())
14
+ forget(attempt);
15
+ };
16
+ const headers = { "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" };
17
+ const result = (res) => {
18
+ res.writeHead(303, { ...headers, Location: "/link/authorization-result" });
19
+ res.end();
20
+ };
21
+ return {
22
+ close() {
23
+ closed = true;
24
+ tickets.clear();
25
+ states.clear();
26
+ },
27
+ create(job, binding, origin) {
28
+ prune();
29
+ const redirect = job.step?.kind === "redirect" ? job.step : job.redirect;
30
+ if (closed || job.state !== "pending" || !redirect || states.size >= 128)
31
+ throw new Error("Browser authorization is unavailable");
32
+ const authorization = new URL(redirect.authorizationUrl);
33
+ const callback = new URL(authorization.searchParams.get("redirect_uri") ?? "");
34
+ const state = authorization.searchParams.get("state");
35
+ const expiresAt = Math.min(Date.parse(redirect.expiresAt), now() + 600_000);
36
+ if (!state ||
37
+ !/^[A-Za-z0-9_-]{32,128}$/.test(state) ||
38
+ authorization.username ||
39
+ authorization.password ||
40
+ authorization.hash ||
41
+ authorization.pathname !== "/oauth/authorize" ||
42
+ (authorization.protocol !== "https:" &&
43
+ !(authorization.protocol === "http:" &&
44
+ ["localhost", "127.0.0.1", "[::1]"].includes(authorization.hostname))) ||
45
+ authorization.searchParams.getAll("state").length !== 1 ||
46
+ authorization.searchParams.getAll("redirect_uri").length !== 1 ||
47
+ authorization.searchParams.get("redirect_uri") !== callback.href ||
48
+ callback.origin !== origin ||
49
+ callback.pathname !== "/link/callback" ||
50
+ callback.search ||
51
+ callback.hash ||
52
+ callback.username ||
53
+ callback.password ||
54
+ !Number.isFinite(expiresAt) ||
55
+ expiresAt <= now() ||
56
+ states.has(state))
57
+ throw new Error("Invalid browser authorization");
58
+ const ticket = randomBytes(32).toString("base64url");
59
+ const attempt = {
60
+ ticket,
61
+ state,
62
+ authorizationUrl: authorization.href,
63
+ callback,
64
+ expiresAt,
65
+ launched: false,
66
+ completing: false,
67
+ binding,
68
+ };
69
+ tickets.set(ticket, attempt);
70
+ states.set(state, attempt);
71
+ return { launchUrl: `${callback.origin}/link/authorize?ticket=${ticket}` };
72
+ },
73
+ async handle(req, res) {
74
+ const url = new URL(req.url ?? "/", "http://localhost");
75
+ if (!["/link/authorize", "/link/callback", "/link/authorization-result"].includes(url.pathname))
76
+ return false;
77
+ if (req.method !== "GET")
78
+ return false;
79
+ prune();
80
+ if (url.pathname === "/link/authorization-result") {
81
+ res.writeHead(200, {
82
+ ...headers,
83
+ "Content-Type": "text/html; charset=utf-8",
84
+ "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'",
85
+ "X-Content-Type-Options": "nosniff",
86
+ });
87
+ res.end('<!doctype html><html lang="zh"><meta charset="utf-8"><title>Link 授权</title><main><h1>Link 授权</h1><p>授权已处理。请返回原工作台查看连接结果;可以关闭此页面。</p></main></html>');
88
+ return true;
89
+ }
90
+ if (url.pathname === "/link/authorize") {
91
+ const ticket = url.searchParams.get("ticket");
92
+ const attempt = ticket && tickets.get(ticket);
93
+ if (!attempt || closed || url.searchParams.size !== 1) {
94
+ res.writeHead(410, headers).end("Authorization expired. Return to the workbench.");
95
+ return true;
96
+ }
97
+ tickets.delete(attempt.ticket);
98
+ try {
99
+ await attempt.binding.check();
100
+ if (closed || states.get(attempt.state) !== attempt || attempt.expiresAt <= now())
101
+ throw new Error("Authorization expired");
102
+ attempt.launched = true;
103
+ res.writeHead(303, { ...headers, Location: attempt.authorizationUrl });
104
+ res.end();
105
+ }
106
+ catch {
107
+ forget(attempt);
108
+ result(res);
109
+ }
110
+ return true;
111
+ }
112
+ const state = url.searchParams.get("state");
113
+ const attempt = state && states.get(state);
114
+ // Unregistered callbacks belong to the existing same-browser flow.
115
+ if (!attempt)
116
+ return false;
117
+ if (closed ||
118
+ !attempt.launched ||
119
+ attempt.completing ||
120
+ url.searchParams.getAll("state").length !== 1 ||
121
+ url.searchParams.has("code") === url.searchParams.has("error") ||
122
+ url.searchParams.getAll("code").length > 1 ||
123
+ url.searchParams.getAll("error").length > 1) {
124
+ res.writeHead(400, headers).end("Invalid authorization callback.");
125
+ return true;
126
+ }
127
+ attempt.completing = true;
128
+ try {
129
+ await attempt.binding.check();
130
+ if (closed || states.get(attempt.state) !== attempt || attempt.expiresAt <= now())
131
+ throw new Error("Authorization expired");
132
+ if (url.searchParams.has("error"))
133
+ await attempt.binding.cancel();
134
+ else {
135
+ const callback = new URL(attempt.callback);
136
+ callback.search = url.search;
137
+ await attempt.binding.complete(callback.href);
138
+ }
139
+ }
140
+ catch {
141
+ // A token exchange is never replayed. The original owner's polling reconciles the result.
142
+ }
143
+ finally {
144
+ forget(attempt);
145
+ }
146
+ result(res);
147
+ return true;
148
+ },
149
+ };
150
+ }
@@ -1,7 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { type LinkServiceOptions } from "./service.js";
3
- import type { LinkConnectionInput, LinkOperationContext, TokenConnectionInput } from "./types.js";
3
+ import type { LinkAuthorizationResponse, LinkConnectionInput, LinkOperationContext, TokenConnectionInput } from "./types.js";
4
4
  export interface LinkHttpOptions extends LinkServiceOptions {
5
+ /** Project gateways own the public callback; private runtimes must not advertise it themselves. */
6
+ browserHandoff?: boolean;
5
7
  ownerId: (request: IncomingMessage) => Promise<string | undefined>;
6
8
  isAuthorized: (request: IncomingMessage) => Promise<boolean>;
7
9
  }
@@ -23,6 +25,9 @@ export declare function createLinkHttp(options: LinkHttpOptions): {
23
25
  rename: (context: LinkOperationContext, id: string, label: string, expectedRevision: string) => Promise<import("@cjhyy/code-shell-link").MaskedLinkConnection>;
24
26
  disconnect: (context: LinkOperationContext, id: string, expectedRevision: string) => Promise<void>;
25
27
  startDeviceAuth: (context: LinkOperationContext, input: LinkConnectionInput) => Promise<import("@cjhyy/code-shell-link").LinkAuthorization>;
28
+ startAuthorization: (context: LinkOperationContext, input: LinkConnectionInput, rawAuthModeId: unknown) => Promise<import("@cjhyy/code-shell-link").LinkAuthorization>;
29
+ respondAuthorization: (context: LinkOperationContext, id: string, response: LinkAuthorizationResponse) => Promise<import("@cjhyy/code-shell-link").LinkAuthorization>;
30
+ refreshRemoteCatalog: (context: LinkOperationContext) => Promise<void>;
26
31
  startRemoteAuth: (context: LinkOperationContext, input: LinkConnectionInput) => Promise<import("@cjhyy/code-shell-link").LinkAuthorization>;
27
32
  completeRemoteAuth: (context: LinkOperationContext, id: string, callbackUrl: string) => Promise<import("@cjhyy/code-shell-link").LinkAuthorization>;
28
33
  retryRemoteCleanup: () => Promise<void>;
@@ -31,7 +36,8 @@ export declare function createLinkHttp(options: LinkHttpOptions): {
31
36
  cancelOwner: (ownerId: string) => void;
32
37
  close: () => void;
33
38
  };
34
- close: () => void;
39
+ close: () => Promise<void>;
35
40
  cancelOwner: (ownerId: string) => void;
41
+ handlePublic: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
36
42
  handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
37
43
  };
@@ -1,3 +1,4 @@
1
+ import { createLinkBrowserHandoff } from "./browser-handoff.js";
1
2
  import { createLinkService, LinkServiceError, publicLinkError, } from "./service.js";
2
3
  const ROOT = "/api/v1/links";
3
4
  const MAX_BODY_BYTES = 32 * 1024;
@@ -46,6 +47,7 @@ function fields(input, allowed) {
46
47
  /** Host authenticates and validates Origin first; writes independently recheck the same owner. */
47
48
  export function createLinkHttp(options) {
48
49
  const service = createLinkService(options);
50
+ const browser = createLinkBrowserHandoff(options.now);
49
51
  async function context(request, abandoned) {
50
52
  const ownerId = await options.ownerId(request);
51
53
  if (!ownerId || !(await options.isAuthorized(request)))
@@ -59,8 +61,12 @@ export function createLinkHttp(options) {
59
61
  }
60
62
  return {
61
63
  service,
62
- close: service.close,
64
+ close: async () => {
65
+ browser.close();
66
+ await service.close();
67
+ },
63
68
  cancelOwner: service.cancelOwner,
69
+ handlePublic: browser.handle,
64
70
  async handle(request, response) {
65
71
  const url = new URL(request.url ?? "/", "http://localhost");
66
72
  if (url.pathname !== ROOT && !url.pathname.startsWith(ROOT + "/"))
@@ -75,14 +81,67 @@ export function createLinkHttp(options) {
75
81
  const owner = await context(request, () => abandoned || request.aborted);
76
82
  const method = request.method;
77
83
  let result;
78
- if (url.pathname === ROOT && method === "GET")
79
- result = service.snapshot();
84
+ if (url.pathname === ROOT && method === "GET") {
85
+ await service.refreshRemoteCatalog(owner);
86
+ const snapshot = service.snapshot();
87
+ const configured = options.remoteLink?.();
88
+ result = {
89
+ ...snapshot,
90
+ capabilities: {
91
+ ...snapshot.capabilities,
92
+ ...(options.browserHandoff !== false &&
93
+ configured &&
94
+ new URL(configured.redirectUri).pathname === "/link/callback"
95
+ ? { browserHandoff: 1 }
96
+ : {}),
97
+ },
98
+ };
99
+ }
80
100
  else {
81
101
  const cli = /^\/api\/v1\/links\/providers\/([^/]+)\/cli$/.exec(url.pathname);
82
102
  const connection = /^\/api\/v1\/links\/connections\/([^/]+)$/.exec(url.pathname);
83
103
  const remoteComplete = /^\/api\/v1\/links\/authorizations\/([^/]+)\/complete$/.exec(url.pathname);
104
+ const handoff = /^\/api\/v1\/links\/authorizations\/([^/]+)\/browser$/.exec(url.pathname);
84
105
  const authorization = /^\/api\/v1\/links\/authorizations\/([^/]+)$/.exec(url.pathname);
85
- if (remoteComplete && method === "POST") {
106
+ const respond = /^\/api\/v1\/links\/authorizations\/([^/]+)\/responses$/.exec(url.pathname);
107
+ if (url.pathname === ROOT + "/authorizations" && method === "POST") {
108
+ const input = await body(request);
109
+ fields(input, [
110
+ "providerId",
111
+ "methodId",
112
+ "label",
113
+ "connectionId",
114
+ "expectedRevision",
115
+ "authModeId",
116
+ ]);
117
+ result = await service.startAuthorization(owner, input, input.authModeId);
118
+ }
119
+ else if (handoff && method === "POST") {
120
+ fields(await body(request), []);
121
+ const id = decodeURIComponent(handoff[1]);
122
+ const check = async () => {
123
+ const job = await service.authorization(owner, id);
124
+ if (job.state !== "pending")
125
+ throw new LinkServiceError(409, "conflict");
126
+ return job;
127
+ };
128
+ const configured = options.remoteLink?.();
129
+ if (!configured)
130
+ throw new LinkServiceError(503, "unavailable");
131
+ result = browser.create(await check(), {
132
+ check: async () => {
133
+ await check();
134
+ },
135
+ complete: (callbackUrl) => service.completeRemoteAuth(owner, id, callbackUrl),
136
+ cancel: () => service.cancelAuthorization(owner, id),
137
+ }, new URL(configured.redirectUri).origin);
138
+ }
139
+ else if (respond && method === "POST") {
140
+ const input = await body(request);
141
+ fields(input, ["stepId", "operation", "input"]);
142
+ result = await service.respondAuthorization(owner, decodeURIComponent(respond[1]), input);
143
+ }
144
+ else if (remoteComplete && method === "POST") {
86
145
  const input = await body(request);
87
146
  fields(input, ["callbackUrl"]);
88
147
  result = await service.completeRemoteAuth(owner, decodeURIComponent(remoteComplete[1]), input.callbackUrl);
@@ -0,0 +1,94 @@
1
+ import { type RemoteLinkConfiguration } from "@cjhyy/code-shell-core";
2
+ import { type RemoteLinkProviderId } from "@cjhyy/code-shell-core";
3
+ /** The Host's compiled executors own authority, never remotely supplied presentation metadata. */
4
+ export declare const REMOTE_LINK_ADAPTERS: ({
5
+ methodId: string;
6
+ authModeId: string;
7
+ scopes: string[];
8
+ id: "github";
9
+ name: "GitHub";
10
+ group: "repositories";
11
+ actions: readonly ["list_repositories", "get_readme", "get_file", "list_issues", "get_issue", "list_pull_requests", "get_pull_request", "create_issue", "get_repository", "get_starred", "set_starred"];
12
+ } | {
13
+ methodId: string;
14
+ authModeId: string;
15
+ scopes: string[];
16
+ id: "gitlab";
17
+ name: "GitLab";
18
+ group: "projects";
19
+ actions: readonly ["list_projects", "list_issues"];
20
+ } | {
21
+ methodId: string;
22
+ authModeId: string;
23
+ scopes: string[];
24
+ id: "sentry";
25
+ name: "Sentry";
26
+ group: "organizations";
27
+ actions: readonly ["list_organizations", "list_projects"];
28
+ } | {
29
+ methodId: string;
30
+ authModeId: string;
31
+ scopes: string[];
32
+ id: "vercel";
33
+ name: "Vercel";
34
+ group: "projects";
35
+ actions: readonly ["list_projects", "list_deployments"];
36
+ } | {
37
+ methodId: string;
38
+ authModeId: string;
39
+ scopes: string[];
40
+ id: "slack";
41
+ name: "Slack";
42
+ group: "channels";
43
+ actions: readonly ["list_channels", "get_channel_history"];
44
+ } | {
45
+ methodId: string;
46
+ authModeId: string;
47
+ scopes: string[];
48
+ id: "notion";
49
+ name: "Notion";
50
+ group: "pages";
51
+ actions: readonly ["search", "get_page"];
52
+ } | {
53
+ methodId: string;
54
+ authModeId: string;
55
+ scopes: string[];
56
+ id: "linear";
57
+ name: "Linear";
58
+ group: "teams";
59
+ actions: readonly ["list_issues", "list_teams"];
60
+ } | {
61
+ methodId: string;
62
+ authModeId: string;
63
+ scopes: string[];
64
+ id: "todoist";
65
+ name: "Todoist";
66
+ group: "projects";
67
+ actions: readonly ["list_projects", "list_tasks"];
68
+ } | {
69
+ methodId: string;
70
+ authModeId: string;
71
+ scopes: string[];
72
+ id: "airtable";
73
+ name: "Airtable";
74
+ group: "bases";
75
+ actions: readonly ["list_bases", "list_tables"];
76
+ } | {
77
+ methodId: string;
78
+ authModeId: string;
79
+ scopes: string[];
80
+ id: "figma";
81
+ name: "Figma";
82
+ group: "files";
83
+ actions: readonly ["get_file", "get_comments"];
84
+ })[];
85
+ export type RemoteProviderId = RemoteLinkProviderId;
86
+ export interface RemoteProviderCapabilities {
87
+ id: RemoteProviderId;
88
+ actions: string[];
89
+ scopes: string[];
90
+ }
91
+ export declare const LEGACY_REMOTE_PROVIDERS: RemoteProviderCapabilities[];
92
+ export declare function readRemoteProviderCatalog(configuration: RemoteLinkConfiguration): Promise<RemoteProviderId[]>;
93
+ /** A legacy v1 service has no catalog. Its one already-reviewed adapter remains compatible. */
94
+ export declare function readRemoteProviderCapabilities(configuration: RemoteLinkConfiguration): Promise<RemoteProviderCapabilities[]>;
@@ -0,0 +1,90 @@
1
+ import { beginRemoteLinkAuthorization } from "@cjhyy/code-shell-core";
2
+ import { REMOTE_LINK_PROVIDER_ADAPTERS, LEGACY_GITHUB_ACTIONS, } from "@cjhyy/code-shell-core";
3
+ /** The Host's compiled executors own authority, never remotely supplied presentation metadata. */
4
+ export const REMOTE_LINK_ADAPTERS = REMOTE_LINK_PROVIDER_ADAPTERS.map((adapter) => ({
5
+ ...adapter,
6
+ methodId: "remote-link",
7
+ authModeId: "remote-link",
8
+ scopes: adapter.actions.map((action) => `${adapter.id}:${action}`),
9
+ }));
10
+ export const LEGACY_REMOTE_PROVIDERS = [
11
+ {
12
+ id: "github",
13
+ actions: [...LEGACY_GITHUB_ACTIONS],
14
+ scopes: LEGACY_GITHUB_ACTIONS.map((action) => `github:${action}`),
15
+ },
16
+ ];
17
+ export async function readRemoteProviderCatalog(configuration) {
18
+ return (await readRemoteProviderCapabilities(configuration)).map((provider) => provider.id);
19
+ }
20
+ /** A legacy v1 service has no catalog. Its one already-reviewed adapter remains compatible. */
21
+ export async function readRemoteProviderCapabilities(configuration) {
22
+ const issuer = beginRemoteLinkAuthorization(configuration).configuration.issuer;
23
+ const response = await fetch(new URL("/api/v1/links/providers", issuer), {
24
+ redirect: "error",
25
+ signal: AbortSignal.timeout(5_000),
26
+ headers: { Accept: "application/json" },
27
+ });
28
+ if ([404, 501].includes(response.status)) {
29
+ await response.body?.cancel();
30
+ return structuredClone(LEGACY_REMOTE_PROVIDERS);
31
+ }
32
+ if (!response.ok || !response.body) {
33
+ await response.body?.cancel();
34
+ throw new Error("Link catalog unavailable");
35
+ }
36
+ const reader = response.body.getReader();
37
+ const chunks = [];
38
+ let size = 0;
39
+ try {
40
+ for (;;) {
41
+ const { done, value } = await reader.read();
42
+ if (done)
43
+ break;
44
+ size += value.byteLength;
45
+ if (size > 32_768) {
46
+ await reader.cancel();
47
+ return [];
48
+ }
49
+ chunks.push(value);
50
+ }
51
+ }
52
+ finally {
53
+ reader.releaseLock();
54
+ }
55
+ let catalog;
56
+ try {
57
+ catalog = JSON.parse(Buffer.concat(chunks).toString("utf8"));
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ if (catalog?.version !== 1 || !Array.isArray(catalog.providers) || catalog.providers.length > 32)
63
+ return [];
64
+ return REMOTE_LINK_ADAPTERS.flatMap((adapter) => {
65
+ const matches = catalog.providers.filter((item) => item?.id === adapter.id);
66
+ if (matches.length !== 1)
67
+ return [];
68
+ const provider = matches[0];
69
+ const actions = provider.actions;
70
+ const scopes = provider.scopes;
71
+ if (!Array.isArray(actions) ||
72
+ !actions.length ||
73
+ actions.length > adapter.actions.length ||
74
+ new Set(actions).size !== actions.length ||
75
+ actions.some((action) => !adapter.actions.includes(action)) ||
76
+ !Array.isArray(scopes) ||
77
+ scopes.length !== actions.length ||
78
+ new Set(scopes).size !== scopes.length ||
79
+ actions.some((action) => !scopes.includes(`${adapter.id}:${action}`)))
80
+ return [];
81
+ if (!Array.isArray(provider.methods) ||
82
+ !provider.methods.some((method) => method?.id === adapter.methodId &&
83
+ method.authKind === "oauth" &&
84
+ method.defaultAuthModeId === "browser" &&
85
+ Array.isArray(method.authModes) &&
86
+ method.authModes.some((mode) => mode?.id === "browser" && mode.kind === "redirect")))
87
+ return [];
88
+ return [{ id: adapter.id, actions: [...actions], scopes: [...scopes] }];
89
+ });
90
+ }
@@ -1,6 +1,7 @@
1
1
  import { CredentialStore, type RemoteLinkConfiguration, connectCliLink, getCliLinkStatus, validateLocalLinkToken, type LocalLinkValidationResult } from "@cjhyy/code-shell-core";
2
+ import { readRemoteProviderCatalog } from "./remote-catalog.js";
2
3
  import { LinkDeviceOAuthBroker, type LocalBrowserAuthToken } from "./device-oauth.js";
3
- import type { LinkAuthorization, LinkConnectionInput, LinkErrorCode, LinkOperationContext, LinkSnapshot, MaskedLinkConnection, TokenConnectionInput } from "./types.js";
4
+ import type { LinkAuthorization, LinkAuthorizationResponse, LinkConnectionInput, LinkErrorCode, LinkOperationContext, LinkSnapshot, MaskedLinkConnection, TokenConnectionInput } from "./types.js";
4
5
  export declare class LinkServiceError extends Error {
5
6
  readonly status: number;
6
7
  readonly code: LinkErrorCode;
@@ -20,6 +21,9 @@ export interface LinkServiceOptions {
20
21
  cliStatus?: typeof getCliLinkStatus;
21
22
  bindCli?: typeof connectCliLink;
22
23
  createDeviceBroker?: () => LinkDeviceOAuthBroker;
24
+ readRemoteCatalog?: typeof readRemoteProviderCatalog;
25
+ /** Native Host only; remote browser callers may bind existing Host sessions but cannot start login. */
26
+ allowCliLogin?: boolean;
23
27
  }
24
28
  /** Trusted host adapter input; deliberately absent from the HTTP API. */
25
29
  export type ValidatedLinkAuthentication = {
@@ -48,6 +52,9 @@ export declare function createLinkService(options?: LinkServiceOptions): {
48
52
  rename: (context: LinkOperationContext, id: string, label: string, expectedRevision: string) => Promise<MaskedLinkConnection>;
49
53
  disconnect: (context: LinkOperationContext, id: string, expectedRevision: string) => Promise<void>;
50
54
  startDeviceAuth: (context: LinkOperationContext, input: LinkConnectionInput) => Promise<LinkAuthorization>;
55
+ startAuthorization: (context: LinkOperationContext, input: LinkConnectionInput, rawAuthModeId: unknown) => Promise<LinkAuthorization>;
56
+ respondAuthorization: (context: LinkOperationContext, id: string, response: LinkAuthorizationResponse) => Promise<LinkAuthorization>;
57
+ refreshRemoteCatalog: (context: LinkOperationContext) => Promise<void>;
51
58
  startRemoteAuth: (context: LinkOperationContext, input: LinkConnectionInput) => Promise<LinkAuthorization>;
52
59
  completeRemoteAuth: (context: LinkOperationContext, id: string, callbackUrl: string) => Promise<LinkAuthorization>;
53
60
  retryRemoteCleanup: () => Promise<void>;