runline 0.29.0 → 0.30.1

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.
Files changed (69) hide show
  1. package/dist/auth/errors.d.ts +22 -0
  2. package/dist/auth/errors.js +25 -0
  3. package/dist/auth/oauth2.d.ts +21 -0
  4. package/dist/auth/oauth2.js +154 -0
  5. package/dist/auth/token.d.ts +11 -0
  6. package/dist/auth/token.js +231 -0
  7. package/dist/auth/types.d.ts +93 -0
  8. package/dist/auth/types.js +1 -0
  9. package/dist/commands/auth.js +26 -13
  10. package/dist/commands/connection.js +3 -6
  11. package/dist/commands/exec.js +7 -2
  12. package/dist/commands/init.js +9 -2
  13. package/dist/config/loader.d.ts +7 -13
  14. package/dist/config/loader.js +58 -88
  15. package/dist/config/store.d.ts +17 -0
  16. package/dist/config/store.js +85 -0
  17. package/dist/connections/file.d.ts +13 -0
  18. package/dist/connections/file.js +53 -0
  19. package/dist/connections/memory.d.ts +15 -0
  20. package/dist/connections/memory.js +70 -0
  21. package/dist/connections/types.d.ts +47 -0
  22. package/dist/connections/types.js +10 -0
  23. package/dist/core/engine.d.ts +5 -2
  24. package/dist/core/engine.js +48 -55
  25. package/dist/core/oauth.d.ts +20 -15
  26. package/dist/core/oauth.js +195 -98
  27. package/dist/credentials/http.d.ts +19 -0
  28. package/dist/credentials/http.js +109 -0
  29. package/dist/credentials/policy.d.ts +5 -0
  30. package/dist/credentials/policy.js +92 -0
  31. package/dist/credentials/registry.d.ts +31 -0
  32. package/dist/credentials/registry.js +171 -0
  33. package/dist/credentials/transport.d.ts +38 -0
  34. package/dist/credentials/transport.js +283 -0
  35. package/dist/credentials/types.d.ts +80 -0
  36. package/dist/credentials/types.js +1 -0
  37. package/dist/index.d.ts +13 -1
  38. package/dist/index.js +9 -1
  39. package/dist/plugin/types.d.ts +30 -17
  40. package/dist/plugins/_shared/credentialAdapter.js +88 -0
  41. package/dist/plugins/_shared/googleAuth.js +89 -103
  42. package/dist/plugins/_shared/googleCredentials.js +160 -0
  43. package/dist/plugins/_shared/microsoftAuth.js +81 -74
  44. package/dist/plugins/_shared/microsoftCredentials.js +90 -0
  45. package/dist/plugins/_shared/tokenRefresh.js +39 -0
  46. package/dist/plugins/bitwarden/src/index.js +13 -31
  47. package/dist/plugins/gmail/src/index.js +8 -44
  48. package/dist/plugins/googleAppsScript/src/index.js +3 -25
  49. package/dist/plugins/googleCalendar/src/index.js +3 -41
  50. package/dist/plugins/googleContacts/src/index.js +3 -36
  51. package/dist/plugins/googleDocs/src/index.js +5 -0
  52. package/dist/plugins/googleDocs/src/shared.js +2 -30
  53. package/dist/plugins/googleDrive/src/index.js +52 -98
  54. package/dist/plugins/googleSheets/src/index.js +3 -40
  55. package/dist/plugins/googleSlides/src/index.js +5 -39
  56. package/dist/plugins/googleTasks/src/index.js +3 -30
  57. package/dist/plugins/halopsa/src/index.js +16 -31
  58. package/dist/plugins/microsoftCalendar/src/index.js +2 -1
  59. package/dist/plugins/microsoftFiles/src/index.js +10 -24
  60. package/dist/plugins/microsoftMail/src/index.js +2 -1
  61. package/dist/plugins/paypal/src/index.js +14 -29
  62. package/dist/plugins/plaud/src/index.js +210 -0
  63. package/dist/plugins/plaud/src/shared.js +221 -0
  64. package/dist/plugins/salesforce/src/shared.js +17 -32
  65. package/dist/sdk.d.ts +13 -5
  66. package/dist/sdk.js +35 -29
  67. package/dist/utils/observer.d.ts +2 -0
  68. package/dist/utils/observer.js +11 -0
  69. package/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ declare const messages: {
2
+ readonly invalid_definition: "Invalid authentication definition";
3
+ readonly invalid_credentials: "Missing or invalid authentication credentials";
4
+ readonly request_not_allowed: "Authenticated request is outside the approved policy";
5
+ readonly transport_failed: "Authenticated request failed; remote outcome may be unknown";
6
+ readonly response_too_large: "Authenticated response exceeds the configured limit";
7
+ readonly credential_store_failed: "Credential storage failed; provider outcome may be unknown";
8
+ readonly binding_changed: "Credential binding identity changed";
9
+ readonly unsupported_operation: "Authentication operation is not supported";
10
+ readonly invalid_response: "Invalid provider response";
11
+ readonly request_failed: "OAuth token request failed; provider outcome may be unknown";
12
+ readonly provider_rejected: "OAuth provider rejected the token request";
13
+ readonly reconnect_required: "OAuth grant is invalid; reconnect the account";
14
+ };
15
+ export type AuthErrorCode = keyof typeof messages;
16
+ /** Safe to serialize. No raw response, endpoint URL, credential, or nested cause. */
17
+ export declare class AuthError extends Error {
18
+ readonly code: AuthErrorCode;
19
+ readonly status?: number;
20
+ constructor(code: AuthErrorCode, status?: number);
21
+ }
22
+ export {};
@@ -0,0 +1,25 @@
1
+ const messages = {
2
+ invalid_definition: "Invalid authentication definition",
3
+ invalid_credentials: "Missing or invalid authentication credentials",
4
+ request_not_allowed: "Authenticated request is outside the approved policy",
5
+ transport_failed: "Authenticated request failed; remote outcome may be unknown",
6
+ response_too_large: "Authenticated response exceeds the configured limit",
7
+ credential_store_failed: "Credential storage failed; provider outcome may be unknown",
8
+ binding_changed: "Credential binding identity changed",
9
+ unsupported_operation: "Authentication operation is not supported",
10
+ invalid_response: "Invalid provider response",
11
+ request_failed: "OAuth token request failed; provider outcome may be unknown",
12
+ provider_rejected: "OAuth provider rejected the token request",
13
+ reconnect_required: "OAuth grant is invalid; reconnect the account",
14
+ };
15
+ /** Safe to serialize. No raw response, endpoint URL, credential, or nested cause. */
16
+ export class AuthError extends Error {
17
+ code;
18
+ status;
19
+ constructor(code, status) {
20
+ super(messages[code]);
21
+ this.name = "AuthError";
22
+ this.code = code;
23
+ this.status = status;
24
+ }
25
+ }
@@ -0,0 +1,21 @@
1
+ import type { OAuth2Definition, OAuthApplication, OAuthAuthorizationOptions, OAuthCodeOptions, OAuthJwtIdentity, OAuthRuntimeOptions, OAuthTokens } from "./types.js";
2
+ /** Shared by definition registration and both setup primitives; no truthy coercion. */
3
+ export declare function validateOAuth2ExchangePolicy(endpoint: OAuth2Definition["exchange"]): void;
4
+ /** Redirect registration and state custody belong to the host's setup lifecycle. */
5
+ export declare function buildOAuth2AuthorizationUrl(definition: OAuth2Definition, options: OAuthAuthorizationOptions): string;
6
+ export declare function exchangeOAuth2Code(definition: OAuth2Definition, input: OAuthCodeOptions, options?: OAuthRuntimeOptions): Promise<OAuthTokens>;
7
+ /** Call under host update ownership. An omitted replacement refresh token preserves the grant. */
8
+ export declare function refreshOAuth2Token(definition: OAuth2Definition, input: {
9
+ application?: OAuthApplication;
10
+ tokens: Partial<OAuthTokens>;
11
+ scopes?: string[];
12
+ }, options?: OAuthRuntimeOptions): Promise<OAuthTokens>;
13
+ /** Fixed RS256 assertion protocol; no caller-defined signer or executable credential hook. */
14
+ export declare function acquireOAuth2JwtToken(definition: OAuth2Definition, input: {
15
+ identity: OAuthJwtIdentity;
16
+ scopes: string[];
17
+ }, options?: OAuthRuntimeOptions): Promise<OAuthTokens>;
18
+ export declare function acquireOAuth2ClientToken(definition: OAuth2Definition, input: {
19
+ application: OAuthApplication;
20
+ scopes?: string[];
21
+ }, options?: OAuthRuntimeOptions): Promise<OAuthTokens>;
@@ -0,0 +1,154 @@
1
+ import { createPrivateKey, sign } from "node:crypto";
2
+ import { notifyObserver } from "../utils/observer.js";
3
+ import { AuthError } from "./errors.js";
4
+ import { oauthEndpoint, providerParameters, requestOAuth2Token, } from "./token.js";
5
+ function required(value) {
6
+ if (typeof value !== "string" || !value)
7
+ throw new AuthError("invalid_credentials");
8
+ return value;
9
+ }
10
+ /** Shared by definition registration and both setup primitives; no truthy coercion. */
11
+ export function validateOAuth2ExchangePolicy(endpoint) {
12
+ for (const key of ["sendState", "requirePkce"]) {
13
+ if (endpoint?.[key] !== undefined && typeof endpoint[key] !== "boolean")
14
+ throw new AuthError("invalid_definition");
15
+ }
16
+ }
17
+ /** Redirect registration and state custody belong to the host's setup lifecycle. */
18
+ export function buildOAuth2AuthorizationUrl(definition, options) {
19
+ validateOAuth2ExchangePolicy(definition.exchange);
20
+ if (!definition.authorization)
21
+ throw new AuthError("unsupported_operation");
22
+ if (definition.exchange?.requirePkce && !options.pkceChallenge)
23
+ throw new AuthError("invalid_credentials");
24
+ const url = oauthEndpoint(definition.authorization.url);
25
+ const parameters = providerParameters(definition.authorization.parameters);
26
+ for (const [key, value] of Object.entries(parameters))
27
+ url.searchParams.set(key, value);
28
+ url.searchParams.set("client_id", required(options.application.clientId));
29
+ url.searchParams.set("redirect_uri", required(options.redirectUri));
30
+ url.searchParams.set("response_type", "code");
31
+ url.searchParams.set("state", required(options.state));
32
+ if (options.scopes?.length)
33
+ url.searchParams.set("scope", options.scopes.join(" "));
34
+ if (options.pkceChallenge !== undefined) {
35
+ url.searchParams.set("code_challenge", required(options.pkceChallenge));
36
+ url.searchParams.set("code_challenge_method", "S256");
37
+ }
38
+ return url.toString();
39
+ }
40
+ async function operate(definition, operation, application, parameters, options) {
41
+ const event = {
42
+ definition: definition.id,
43
+ provider: definition.provider,
44
+ operation,
45
+ };
46
+ try {
47
+ const configured = definition[operation];
48
+ if (!configured)
49
+ throw new AuthError("unsupported_operation");
50
+ const standard = {
51
+ exchange: "authorization_code",
52
+ refresh: "refresh_token",
53
+ clientCredentials: "client_credentials",
54
+ jwtBearer: "urn:ietf:params:oauth:grant-type:jwt-bearer",
55
+ };
56
+ const tokens = await requestOAuth2Token(configured, { ...parameters, grant_type: standard[operation] }, application, options);
57
+ notifyObserver(options.onEvent, { ...event, outcome: "issued" });
58
+ return tokens;
59
+ }
60
+ catch (error) {
61
+ const safe = error instanceof AuthError ? error : new AuthError("invalid_definition");
62
+ notifyObserver(options.onEvent, {
63
+ ...event,
64
+ outcome: "failed",
65
+ code: safe.code,
66
+ });
67
+ throw safe;
68
+ }
69
+ }
70
+ export async function exchangeOAuth2Code(definition, input, options = {}) {
71
+ validateOAuth2ExchangePolicy(definition.exchange);
72
+ const fields = {
73
+ code: required(input.code),
74
+ redirect_uri: required(input.redirectUri),
75
+ };
76
+ if (definition.exchange?.requirePkce || input.codeVerifier !== undefined)
77
+ fields.code_verifier = required(input.codeVerifier ?? "");
78
+ if (definition.exchange?.sendState)
79
+ fields.state = required(input.state ?? "");
80
+ return operate(definition, "exchange", input.application, fields, options);
81
+ }
82
+ /** Call under host update ownership. An omitted replacement refresh token preserves the grant. */
83
+ export async function refreshOAuth2Token(definition, input, options = {}) {
84
+ const refreshToken = required(input.tokens.refreshToken ?? "");
85
+ const scope = input.tokens.scope;
86
+ const tokenType = input.tokens.tokenType;
87
+ const metadata = structuredClone(input.tokens.metadata);
88
+ const next = await operate(definition, "refresh", input.application, {
89
+ refresh_token: refreshToken,
90
+ ...(input.scopes?.length ? { scope: input.scopes.join(" ") } : {}),
91
+ }, options);
92
+ // Expiry belongs to the new access token and is never inherited from the old one.
93
+ return {
94
+ ...(scope === undefined ? {} : { scope }),
95
+ ...(tokenType === undefined ? {} : { tokenType }),
96
+ refreshToken,
97
+ ...next,
98
+ ...(metadata || next.metadata
99
+ ? { metadata: { ...metadata, ...next.metadata } }
100
+ : {}),
101
+ };
102
+ }
103
+ /** Fixed RS256 assertion protocol; no caller-defined signer or executable credential hook. */
104
+ export async function acquireOAuth2JwtToken(definition, input, options = {}) {
105
+ const endpoint = definition.jwtBearer;
106
+ if (!endpoint)
107
+ throw new AuthError("unsupported_operation");
108
+ const audience = oauthEndpoint(endpoint.url).toString();
109
+ if (endpoint.clientAuthentication !== "none" ||
110
+ endpoint.grantType !== undefined)
111
+ throw new AuthError("invalid_definition");
112
+ let assertion;
113
+ let now;
114
+ try {
115
+ now = (options.now ?? Date.now)();
116
+ if (!Number.isSafeInteger(now) ||
117
+ now < 0 ||
118
+ now > Number.MAX_SAFE_INTEGER - 3_600_000)
119
+ throw new Error();
120
+ if (!Array.isArray(input.scopes) ||
121
+ !input.scopes.length ||
122
+ input.scopes.some((scope) => typeof scope !== "string" || !scope || /\s/.test(scope)))
123
+ throw new Error();
124
+ const identity = input.identity;
125
+ required(identity.issuer);
126
+ if (identity.subject !== undefined)
127
+ required(identity.subject);
128
+ if (typeof identity.privateKey !== "string" ||
129
+ identity.privateKey.length > 65_536)
130
+ throw new Error();
131
+ const key = createPrivateKey(identity.privateKey);
132
+ if (key.asymmetricKeyType !== "rsa" ||
133
+ (key.asymmetricKeyDetails?.modulusLength ?? 0) < 2048)
134
+ throw new Error();
135
+ const iat = Math.floor(now / 1000);
136
+ const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
137
+ const unsigned = `${encode({ alg: "RS256", typ: "JWT" })}.${encode({
138
+ iss: identity.issuer,
139
+ aud: audience,
140
+ scope: input.scopes.join(" "),
141
+ iat,
142
+ exp: iat + 3600,
143
+ ...(identity.subject === undefined ? {} : { sub: identity.subject }),
144
+ })}`;
145
+ assertion = `${unsigned}.${sign("RSA-SHA256", Buffer.from(unsigned), key).toString("base64url")}`;
146
+ }
147
+ catch {
148
+ throw new AuthError("invalid_credentials");
149
+ }
150
+ return operate(definition, "jwtBearer", undefined, { assertion }, { ...options, now: () => now });
151
+ }
152
+ export async function acquireOAuth2ClientToken(definition, input, options = {}) {
153
+ return operate(definition, "clientCredentials", input.application, input.scopes?.length ? { scope: input.scopes.join(" ") } : {}, options);
154
+ }
@@ -0,0 +1,11 @@
1
+ import type { OAuth2TokenEndpoint, OAuthApplication, OAuthRuntimeOptions, OAuthTokens } from "./types.js";
2
+ export declare function providerParameters(parameters?: Record<string, string>): Record<string, string>;
3
+ export declare function oauthEndpoint(value: string): URL;
4
+ /** Only normalized grant fields survive decoding; arbitrary response metadata is discarded. */
5
+ export declare function decodeOAuthTokens(input: unknown, mapping?: OAuth2TokenEndpoint["response"], now?: number): OAuthTokens;
6
+ /**
7
+ * Trusted protocol primitive, not a general authenticated proxy. Hosts approve
8
+ * endpoint definitions and enforce network policy through their transport.
9
+ * No redirects or automatic retries: either can leak or spend a rotating token.
10
+ */
11
+ export declare function requestOAuth2Token(endpoint: OAuth2TokenEndpoint, parameters: Record<string, string>, application?: OAuthApplication, options?: Omit<OAuthRuntimeOptions, "onEvent">): Promise<OAuthTokens>;
@@ -0,0 +1,231 @@
1
+ import { AuthError } from "./errors.js";
2
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
3
+ const protectedParameters = new Set([
4
+ "client_id",
5
+ "client_secret",
6
+ "grant_type",
7
+ "code",
8
+ "redirect_uri",
9
+ "refresh_token",
10
+ "code_verifier",
11
+ "state",
12
+ "response_type",
13
+ "code_challenge",
14
+ "code_challenge_method",
15
+ "scope",
16
+ "assertion",
17
+ ]);
18
+ export function providerParameters(parameters = {}) {
19
+ for (const [key, value] of Object.entries(parameters)) {
20
+ if (protectedParameters.has(key) || typeof value !== "string")
21
+ throw new AuthError("invalid_definition");
22
+ }
23
+ return { ...parameters };
24
+ }
25
+ export function oauthEndpoint(value) {
26
+ try {
27
+ const url = new URL(value);
28
+ if (url.protocol !== "https:" || url.username || url.password || url.hash)
29
+ throw new Error();
30
+ for (const key of url.searchParams.keys()) {
31
+ if (protectedParameters.has(key))
32
+ throw new Error();
33
+ }
34
+ return url;
35
+ }
36
+ catch {
37
+ throw new AuthError("invalid_definition");
38
+ }
39
+ }
40
+ function record(value) {
41
+ return value !== null && typeof value === "object" && !Array.isArray(value);
42
+ }
43
+ function field(data, key) {
44
+ return Object.hasOwn(data, key) ? data[key] : undefined;
45
+ }
46
+ function optionalString(value) {
47
+ if (value === undefined)
48
+ return undefined;
49
+ if (typeof value !== "string" || !value)
50
+ throw new AuthError("invalid_response");
51
+ return value;
52
+ }
53
+ /** Only normalized grant fields survive decoding; arbitrary response metadata is discarded. */
54
+ export function decodeOAuthTokens(input, mapping = {}, now = Date.now()) {
55
+ if (!record(input) || field(input, "error") !== undefined)
56
+ throw new AuthError("invalid_response");
57
+ let data = input;
58
+ for (const key of mapping.path ?? []) {
59
+ if (!record(data))
60
+ throw new AuthError("invalid_response");
61
+ data = field(data, key);
62
+ }
63
+ if (!record(data) || field(data, "error") !== undefined)
64
+ throw new AuthError("invalid_response");
65
+ const accessToken = optionalString(field(data, mapping.accessToken ?? "access_token"));
66
+ if (!accessToken)
67
+ throw new AuthError("invalid_response");
68
+ const refreshToken = optionalString(field(data, mapping.refreshToken ?? "refresh_token"));
69
+ const scope = optionalString(field(data, mapping.scope ?? "scope"));
70
+ const tokenType = optionalString(field(data, mapping.tokenType ?? "token_type"));
71
+ const expiresIn = field(data, mapping.expiresIn ?? "expires_in");
72
+ let expiresAt;
73
+ if (expiresIn !== undefined) {
74
+ if (typeof expiresIn !== "number" ||
75
+ !Number.isFinite(expiresIn) ||
76
+ expiresIn <= 0) {
77
+ throw new AuthError("invalid_response");
78
+ }
79
+ expiresAt = now + expiresIn * 1000;
80
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= now)
81
+ throw new AuthError("invalid_response");
82
+ }
83
+ const metadata = [];
84
+ for (const [name, key] of Object.entries(mapping.metadata ?? {})) {
85
+ const value = optionalString(field(data, key));
86
+ if (value !== undefined)
87
+ metadata.push([name, value]);
88
+ }
89
+ return {
90
+ accessToken,
91
+ ...(metadata.length ? { metadata: Object.fromEntries(metadata) } : {}),
92
+ ...(refreshToken === undefined ? {} : { refreshToken }),
93
+ ...(expiresAt === undefined ? {} : { expiresAt }),
94
+ ...(scope === undefined ? {} : { scope }),
95
+ ...(tokenType === undefined ? {} : { tokenType }),
96
+ };
97
+ }
98
+ async function responseJson(response) {
99
+ if (!response.body)
100
+ throw new AuthError("invalid_response");
101
+ const reader = response.body.getReader();
102
+ const decoder = new TextDecoder();
103
+ let size = 0;
104
+ let text = "";
105
+ try {
106
+ while (true) {
107
+ const { done, value } = await reader.read();
108
+ if (done)
109
+ break;
110
+ size += value.byteLength;
111
+ if (size > MAX_RESPONSE_BYTES) {
112
+ void reader.cancel().catch(() => { });
113
+ throw new AuthError("invalid_response");
114
+ }
115
+ text += decoder.decode(value, { stream: true });
116
+ }
117
+ return JSON.parse(text + decoder.decode());
118
+ }
119
+ catch {
120
+ throw new AuthError("invalid_response");
121
+ }
122
+ finally {
123
+ reader.releaseLock();
124
+ }
125
+ }
126
+ function formValue(value) {
127
+ return new URLSearchParams({ v: value }).toString().slice(2);
128
+ }
129
+ /**
130
+ * Trusted protocol primitive, not a general authenticated proxy. Hosts approve
131
+ * endpoint definitions and enforce network policy through their transport.
132
+ * No redirects or automatic retries: either can leak or spend a rotating token.
133
+ */
134
+ export async function requestOAuth2Token(endpoint, parameters, application, options = {}) {
135
+ const url = oauthEndpoint(endpoint.url);
136
+ const mapping = structuredClone(endpoint.response);
137
+ const timeoutMs = options.timeoutMs ?? 20_000;
138
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 120_000) {
139
+ throw new AuthError("invalid_definition");
140
+ }
141
+ const fields = { ...providerParameters(endpoint.parameters), ...parameters };
142
+ if (Object.values(fields).some((value) => typeof value !== "string"))
143
+ throw new AuthError("invalid_definition");
144
+ if (endpoint.grantType === null)
145
+ delete fields.grant_type;
146
+ else if (endpoint.grantType !== undefined) {
147
+ if (typeof endpoint.grantType !== "string" || !endpoint.grantType)
148
+ throw new AuthError("invalid_definition");
149
+ fields.grant_type = endpoint.grantType;
150
+ }
151
+ const headers = { Accept: "application/json" };
152
+ if (endpoint.clientAuthentication !== "none") {
153
+ if (!application ||
154
+ typeof application.clientId !== "string" ||
155
+ !application.clientId) {
156
+ throw new AuthError("invalid_credentials");
157
+ }
158
+ if ("client_id" in fields || "client_secret" in fields)
159
+ throw new AuthError("invalid_definition");
160
+ if (endpoint.clientAuthentication === "client_id")
161
+ fields.client_id = application.clientId;
162
+ else if (endpoint.clientAuthentication === "client_id_basic") {
163
+ if (application.clientSecret !== undefined)
164
+ throw new AuthError("invalid_credentials");
165
+ headers.Authorization = `Basic ${Buffer.from(`${formValue(application.clientId)}:`).toString("base64")}`;
166
+ }
167
+ else {
168
+ if (typeof application.clientSecret !== "string" ||
169
+ !application.clientSecret) {
170
+ throw new AuthError("invalid_credentials");
171
+ }
172
+ if (endpoint.clientAuthentication === "client_secret_basic") {
173
+ headers.Authorization = `Basic ${Buffer.from(`${formValue(application.clientId)}:${formValue(application.clientSecret)}`).toString("base64")}`;
174
+ }
175
+ else if (endpoint.clientAuthentication === "client_secret_post") {
176
+ fields.client_id = application.clientId;
177
+ fields.client_secret = application.clientSecret;
178
+ }
179
+ else
180
+ throw new AuthError("invalid_definition");
181
+ }
182
+ }
183
+ if (endpoint.encoding !== undefined &&
184
+ endpoint.encoding !== "form" &&
185
+ endpoint.encoding !== "json") {
186
+ throw new AuthError("invalid_definition");
187
+ }
188
+ const json = endpoint.encoding === "json";
189
+ headers["Content-Type"] = json
190
+ ? "application/json"
191
+ : "application/x-www-form-urlencoded";
192
+ // Expiry starts before the request; network latency never extends token lifetime.
193
+ let started;
194
+ try {
195
+ started = (options.now ?? Date.now)();
196
+ }
197
+ catch {
198
+ throw new AuthError("invalid_definition");
199
+ }
200
+ if (!Number.isSafeInteger(started) || started < 0)
201
+ throw new AuthError("invalid_definition");
202
+ let response;
203
+ try {
204
+ response = await (options.fetch ?? globalThis.fetch)(url.toString(), {
205
+ method: "POST",
206
+ headers,
207
+ body: json
208
+ ? JSON.stringify(fields)
209
+ : new URLSearchParams(fields).toString(),
210
+ redirect: "error",
211
+ signal: AbortSignal.timeout(timeoutMs),
212
+ });
213
+ }
214
+ catch {
215
+ throw new AuthError("request_failed");
216
+ }
217
+ let data;
218
+ try {
219
+ data = await responseJson(response);
220
+ }
221
+ catch {
222
+ if (!response.ok)
223
+ throw new AuthError("provider_rejected", response.status);
224
+ throw new AuthError("invalid_response");
225
+ }
226
+ if (!response.ok || (record(data) && field(data, "error") !== undefined)) {
227
+ const revoked = record(data) && field(data, "error") === "invalid_grant";
228
+ throw new AuthError(revoked ? "reconnect_required" : "provider_rejected", response.status);
229
+ }
230
+ return decodeOAuthTokens(data, mapping, started);
231
+ }
@@ -0,0 +1,93 @@
1
+ /** Provider protocol metadata. Hosts approve definitions independently of workspace code. */
2
+ export interface OAuth2Definition {
3
+ id: string;
4
+ provider: string;
5
+ authorization?: {
6
+ url: string;
7
+ parameters?: Record<string, string>;
8
+ };
9
+ exchange?: OAuth2TokenEndpoint & {
10
+ /** Some providers require the host-validated state again at code exchange. */
11
+ sendState?: boolean;
12
+ requirePkce?: boolean;
13
+ };
14
+ refresh?: OAuth2TokenEndpoint;
15
+ clientCredentials?: OAuth2TokenEndpoint;
16
+ jwtBearer?: OAuth2TokenEndpoint;
17
+ }
18
+ /** Each operation can use a different endpoint, encoding, and client authentication. */
19
+ export interface OAuth2TokenEndpoint {
20
+ url: string;
21
+ /** `client_id_basic` is a public client: Basic `client_id:` with an empty secret. */
22
+ clientAuthentication: "none" | "client_id" | "client_id_basic" | "client_secret_basic" | "client_secret_post";
23
+ encoding?: "form" | "json";
24
+ /** Omitted uses the operation's standard grant; null omits grant_type entirely. */
25
+ grantType?: string | null;
26
+ /** Provider-specific fields; cannot replace protocol-owned parameters. */
27
+ parameters?: Record<string, string>;
28
+ response?: {
29
+ /** Path to a token object inside a provider envelope. No expression evaluation. */
30
+ path?: string[];
31
+ accessToken?: string;
32
+ refreshToken?: string;
33
+ expiresIn?: string;
34
+ tokenType?: string;
35
+ scope?: string;
36
+ /** Explicitly retained provider fields, e.g. an account's API instance URL. */
37
+ metadata?: Record<string, string>;
38
+ };
39
+ }
40
+ /** Selected by the host, never embedded in a provider definition. */
41
+ export interface OAuthApplication {
42
+ clientId: string;
43
+ clientSecret?: string;
44
+ }
45
+ export interface OAuthTokens {
46
+ accessToken: string;
47
+ refreshToken?: string;
48
+ /** Milliseconds since epoch. Missing means unknown, not already expired. */
49
+ expiresAt?: number;
50
+ scope?: string;
51
+ tokenType?: string;
52
+ /** Only fields explicitly selected by the definition's response mapping. */
53
+ metadata?: Record<string, string>;
54
+ }
55
+ /** Host-selected signing identity. Never accepted from resource/action input. */
56
+ export interface OAuthJwtIdentity {
57
+ issuer: string;
58
+ privateKey: string;
59
+ subject?: string;
60
+ }
61
+ export type OAuthOperation = "exchange" | "refresh" | "clientCredentials" | "jwtBearer";
62
+ /** Protocol events report issuance, never durability. They contain no credential data. */
63
+ export interface OAuthEvent {
64
+ definition: string;
65
+ provider: string;
66
+ operation: OAuthOperation;
67
+ outcome: "issued" | "failed";
68
+ code?: import("./errors.js").AuthErrorCode;
69
+ }
70
+ export interface OAuthRuntimeOptions {
71
+ /** Host transport hook for egress/SSRF enforcement. Defaults to global fetch. */
72
+ fetch?: typeof globalThis.fetch;
73
+ now?: () => number;
74
+ /** Positive integer milliseconds, at most 120 seconds. Defaults to 20 seconds. */
75
+ timeoutMs?: number;
76
+ /** Observer failures cannot turn an issued rotating token into a failed exchange. */
77
+ onEvent?: (event: OAuthEvent) => void | Promise<void>;
78
+ }
79
+ export interface OAuthAuthorizationOptions {
80
+ application: OAuthApplication;
81
+ redirectUri: string;
82
+ state: string;
83
+ scopes?: string[];
84
+ pkceChallenge?: string;
85
+ }
86
+ export interface OAuthCodeOptions {
87
+ application: OAuthApplication;
88
+ code: string;
89
+ redirectUri: string;
90
+ codeVerifier?: string;
91
+ /** State already validated by the host callback lifecycle. */
92
+ state?: string;
93
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,7 +2,7 @@ import { stdin, stdout } from "node:process";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import chalk from "chalk";
4
4
  import { addConnection } from "../config/loader.js";
5
- import { OAUTH_CALLBACK_PORT, runOAuth } from "../core/oauth.js";
5
+ import { oauthCallback, runOAuth } from "../core/oauth.js";
6
6
  import { loadAllPlugins } from "../plugin/loader.js";
7
7
  import { registry } from "../plugin/registry.js";
8
8
  import { connectionFields } from "../plugin/schema.js";
@@ -18,25 +18,33 @@ export async function auth(plugin, options) {
18
18
  printError(`Plugin "${plugin}" does not declare OAuth config.`);
19
19
  process.exit(1);
20
20
  }
21
- // Client credentials: CLI flag > env > interactive prompt.
22
- // Env var names follow the plugin's own convention when declared
23
- // on its connection schema; fall back to generic names otherwise.
21
+ // Client credentials: CLI flag > env > plugin default > interactive prompt.
22
+ // Environment names come from the plugin's connection schema.
24
23
  const connectionSchema = connectionFields(def.connectionConfigSchema);
25
24
  const envIdVar = connectionSchema.clientId?.env;
26
25
  const envSecretVar = connectionSchema.clientSecret?.env;
27
- const resolvedClientId = options.clientId ?? (envIdVar ? process.env[envIdVar] : undefined);
28
- const resolvedClientSecret = options.clientSecret ??
29
- (envSecretVar ? process.env[envSecretVar] : undefined);
26
+ const publicClient = def.oauth.publicClient === true;
27
+ const resolvedClientId = options.clientId ??
28
+ (envIdVar ? process.env[envIdVar] : undefined) ??
29
+ def.oauth.defaultClientId;
30
+ const resolvedClientSecret = publicClient
31
+ ? undefined
32
+ : (options.clientSecret ??
33
+ (envSecretVar ? process.env[envSecretVar] : undefined));
34
+ if (publicClient && options.clientSecret !== undefined) {
35
+ printError(`Plugin "${plugin}" uses a public OAuth client; no secret.`);
36
+ process.exit(1);
37
+ }
30
38
  // If we're about to prompt and the plugin published setup help,
31
39
  // print it once so the user knows what to paste. Suppressed
32
40
  // under --json and --quiet.
33
- const willPrompt = !resolvedClientId || !resolvedClientSecret;
41
+ const willPrompt = !resolvedClientId || (!publicClient && !resolvedClientSecret);
34
42
  if (willPrompt &&
35
43
  def.oauth.setupHelp &&
36
44
  def.oauth.setupHelp.length > 0 &&
37
45
  !options.json &&
38
46
  !options.quiet) {
39
- const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}/callback`;
47
+ const redirectUri = oauthCallback(def.oauth).uri;
40
48
  console.log();
41
49
  console.log(chalk.bold(`Setting up ${plugin} OAuth`));
42
50
  console.log();
@@ -47,9 +55,14 @@ export async function auth(plugin, options) {
47
55
  console.log();
48
56
  }
49
57
  const clientId = resolvedClientId ?? (await prompt(`${plugin} OAuth client ID: `));
50
- const clientSecret = resolvedClientSecret ?? (await prompt(`${plugin} OAuth client secret: `));
51
- if (!clientId || !clientSecret) {
52
- printError("Both client ID and client secret are required.");
58
+ const clientSecret = publicClient
59
+ ? undefined
60
+ : (resolvedClientSecret ??
61
+ (await prompt(`${plugin} OAuth client secret: `)));
62
+ if (!clientId || (!publicClient && !clientSecret)) {
63
+ printError(publicClient
64
+ ? "A client ID is required."
65
+ : "Both client ID and client secret are required.");
53
66
  process.exit(1);
54
67
  }
55
68
  const connectionName = options.name ?? plugin;
@@ -66,7 +79,7 @@ export async function auth(plugin, options) {
66
79
  }
67
80
  },
68
81
  });
69
- addConnection(connectionName, plugin, {
82
+ await addConnection(connectionName, plugin, {
70
83
  clientId,
71
84
  clientSecret,
72
85
  refreshToken: tokens.refreshToken,
@@ -11,7 +11,7 @@ export async function connectionAdd(name, options) {
11
11
  }
12
12
  configValues[kv.slice(0, eq)] = kv.slice(eq + 1);
13
13
  }
14
- addConnection(name, options.plugin, configValues);
14
+ await addConnection(name, options.plugin, configValues);
15
15
  if (options.json) {
16
16
  printJson({ ok: true, name, plugin: options.plugin });
17
17
  }
@@ -20,7 +20,7 @@ export async function connectionAdd(name, options) {
20
20
  }
21
21
  }
22
22
  export async function connectionRemove(name, options) {
23
- const removed = removeConnection(name);
23
+ const removed = await removeConnection(name);
24
24
  if (!removed) {
25
25
  printError(`Connection "${name}" not found`);
26
26
  process.exit(1);
@@ -39,10 +39,7 @@ export async function connectionList(options) {
39
39
  printJson(connections.map((c) => ({
40
40
  name: c.name,
41
41
  plugin: c.plugin,
42
- config: Object.fromEntries(Object.entries(c.config).map(([k, v]) => [
43
- k,
44
- typeof v === "string" && v.length > 8 ? `${v.slice(0, 4)}...` : v,
45
- ])),
42
+ config: Object.fromEntries(Object.keys(c.config).map((key) => [key, "[redacted]"])),
46
43
  })));
47
44
  return;
48
45
  }