runline 0.28.1 → 0.30.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/dist/auth/errors.d.ts +22 -0
- package/dist/auth/errors.js +25 -0
- package/dist/auth/oauth2.d.ts +21 -0
- package/dist/auth/oauth2.js +154 -0
- package/dist/auth/token.d.ts +11 -0
- package/dist/auth/token.js +231 -0
- package/dist/auth/types.d.ts +93 -0
- package/dist/auth/types.js +1 -0
- package/dist/commands/auth.js +26 -13
- package/dist/commands/connection.js +3 -6
- package/dist/commands/exec.js +7 -2
- package/dist/commands/init.js +9 -2
- package/dist/config/loader.d.ts +7 -13
- package/dist/config/loader.js +58 -88
- package/dist/config/store.d.ts +17 -0
- package/dist/config/store.js +85 -0
- package/dist/connections/file.d.ts +13 -0
- package/dist/connections/file.js +53 -0
- package/dist/connections/memory.d.ts +15 -0
- package/dist/connections/memory.js +70 -0
- package/dist/connections/types.d.ts +47 -0
- package/dist/connections/types.js +10 -0
- package/dist/core/engine.d.ts +5 -2
- package/dist/core/engine.js +48 -55
- package/dist/core/oauth.d.ts +20 -15
- package/dist/core/oauth.js +195 -98
- package/dist/credentials/http.d.ts +19 -0
- package/dist/credentials/http.js +109 -0
- package/dist/credentials/policy.d.ts +5 -0
- package/dist/credentials/policy.js +92 -0
- package/dist/credentials/registry.d.ts +31 -0
- package/dist/credentials/registry.js +171 -0
- package/dist/credentials/transport.d.ts +38 -0
- package/dist/credentials/transport.js +283 -0
- package/dist/credentials/types.d.ts +80 -0
- package/dist/credentials/types.js +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/plugin/types.d.ts +30 -17
- package/dist/plugins/_shared/credentialAdapter.js +88 -0
- package/dist/plugins/_shared/googleAuth.js +89 -103
- package/dist/plugins/_shared/googleCredentials.js +160 -0
- package/dist/plugins/_shared/microsoftAuth.js +81 -74
- package/dist/plugins/_shared/microsoftCredentials.js +90 -0
- package/dist/plugins/_shared/tokenRefresh.js +39 -0
- package/dist/plugins/bitwarden/src/index.js +13 -31
- package/dist/plugins/gmail/src/index.js +8 -44
- package/dist/plugins/googleAppsScript/src/index.js +3 -25
- package/dist/plugins/googleCalendar/src/index.js +3 -41
- package/dist/plugins/googleContacts/src/index.js +3 -36
- package/dist/plugins/googleDocs/src/index.js +5 -0
- package/dist/plugins/googleDocs/src/shared.js +2 -30
- package/dist/plugins/googleDrive/src/index.js +52 -98
- package/dist/plugins/googleSheets/src/index.js +3 -40
- package/dist/plugins/googleSlides/src/index.js +5 -39
- package/dist/plugins/googleTasks/src/index.js +3 -30
- package/dist/plugins/halopsa/src/index.js +16 -31
- package/dist/plugins/linear/src/comments.js +2 -2
- package/dist/plugins/linear/src/index.js +1 -1
- package/dist/plugins/linear/src/issues.js +1 -17
- package/dist/plugins/microsoftCalendar/src/index.js +2 -1
- package/dist/plugins/microsoftFiles/src/index.js +10 -24
- package/dist/plugins/microsoftMail/src/index.js +2 -1
- package/dist/plugins/openai/src/index.js +13 -8
- package/dist/plugins/paypal/src/index.js +14 -29
- package/dist/plugins/plaud/src/index.js +210 -0
- package/dist/plugins/plaud/src/shared.js +221 -0
- package/dist/plugins/salesforce/src/shared.js +17 -32
- package/dist/sdk.d.ts +13 -5
- package/dist/sdk.js +35 -29
- package/dist/utils/observer.d.ts +2 -0
- package/dist/utils/observer.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { AuthError } from "../auth/errors.js";
|
|
4
|
+
import { acquireOAuth2ClientToken, acquireOAuth2JwtToken, refreshOAuth2Token, } from "../auth/oauth2.js";
|
|
5
|
+
import { sendResource } from "./http.js";
|
|
6
|
+
import { headerName, resourceUrl } from "./policy.js";
|
|
7
|
+
import { OAuthGrantSchema, validateCredential, } from "./registry.js";
|
|
8
|
+
function boundSnapshot(binding, method, snapshot) {
|
|
9
|
+
if (snapshot.name !== binding.identity.name ||
|
|
10
|
+
snapshot.plugin !== binding.identity.plugin)
|
|
11
|
+
throw new AuthError("binding_changed");
|
|
12
|
+
validateCredential(method, snapshot.config);
|
|
13
|
+
return structuredClone(snapshot.config);
|
|
14
|
+
}
|
|
15
|
+
function grantFrom(method, config) {
|
|
16
|
+
const auth = method.authentication;
|
|
17
|
+
if (auth.kind !== "oauth2")
|
|
18
|
+
throw new AuthError("invalid_definition");
|
|
19
|
+
const grant = config[auth.grantField];
|
|
20
|
+
if (grant === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (!Check(OAuthGrantSchema, grant))
|
|
23
|
+
throw new AuthError("invalid_credentials");
|
|
24
|
+
const result = grant;
|
|
25
|
+
if (result.tokens.tokenType !== undefined &&
|
|
26
|
+
result.tokens.tokenType.toLowerCase() !== "bearer")
|
|
27
|
+
throw new AuthError("unsupported_operation");
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
function pinBinding(selection) {
|
|
31
|
+
return {
|
|
32
|
+
...selection,
|
|
33
|
+
identity: { ...selection.identity },
|
|
34
|
+
application: structuredClone(selection.application),
|
|
35
|
+
jwtIdentity: structuredClone(selection.jwtIdentity),
|
|
36
|
+
connection: {
|
|
37
|
+
read: selection.connection.read.bind(selection.connection),
|
|
38
|
+
update: selection.connection.update.bind(selection.connection),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function fresh(grant) {
|
|
43
|
+
return (!!grant?.tokens.accessToken &&
|
|
44
|
+
(grant.tokens.expiresAt === undefined ||
|
|
45
|
+
grant.tokens.expiresAt > Date.now() + 60_000));
|
|
46
|
+
}
|
|
47
|
+
function secret(value) {
|
|
48
|
+
if (typeof value !== "string" ||
|
|
49
|
+
!value ||
|
|
50
|
+
[...value].some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) > 126))
|
|
51
|
+
throw new AuthError("invalid_credentials");
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Trusted-host primitive, not a workspace sandbox. Callers authorize a binding on
|
|
56
|
+
* every invocation. Handles own lifecycle fencing; no second lock or token cache.
|
|
57
|
+
*/
|
|
58
|
+
export class CredentialTransport {
|
|
59
|
+
registry;
|
|
60
|
+
options;
|
|
61
|
+
constructor(registry, options) {
|
|
62
|
+
this.registry = registry;
|
|
63
|
+
this.options = {
|
|
64
|
+
fetch: options.fetch,
|
|
65
|
+
timeoutMs: options.timeoutMs ?? 20_000,
|
|
66
|
+
maxResponseBytes: options.maxResponseBytes ?? 8 * 1024 * 1024,
|
|
67
|
+
maxRequestBytes: options.maxRequestBytes ?? 1024 * 1024,
|
|
68
|
+
};
|
|
69
|
+
if (typeof this.options.fetch !== "function" ||
|
|
70
|
+
!Number.isInteger(this.options.timeoutMs) ||
|
|
71
|
+
this.options.timeoutMs <= 0 ||
|
|
72
|
+
this.options.timeoutMs > 120_000 ||
|
|
73
|
+
!Number.isSafeInteger(this.options.maxResponseBytes) ||
|
|
74
|
+
this.options.maxResponseBytes <= 0 ||
|
|
75
|
+
this.options.maxResponseBytes > 64 * 1024 * 1024 ||
|
|
76
|
+
!Number.isSafeInteger(this.options.maxRequestBytes) ||
|
|
77
|
+
this.options.maxRequestBytes <= 0 ||
|
|
78
|
+
this.options.maxRequestBytes > 64 * 1024 * 1024)
|
|
79
|
+
throw new AuthError("invalid_definition");
|
|
80
|
+
}
|
|
81
|
+
async read(binding, method) {
|
|
82
|
+
let snapshot;
|
|
83
|
+
try {
|
|
84
|
+
snapshot = await binding.connection.read();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new AuthError("credential_store_failed");
|
|
88
|
+
}
|
|
89
|
+
return boundSnapshot(binding, method, snapshot);
|
|
90
|
+
}
|
|
91
|
+
async renew(binding, method, observedRevision, rejected = false) {
|
|
92
|
+
const auth = method.authentication;
|
|
93
|
+
if (auth.kind !== "oauth2")
|
|
94
|
+
throw new AuthError("invalid_definition");
|
|
95
|
+
let callbackError;
|
|
96
|
+
let committed;
|
|
97
|
+
try {
|
|
98
|
+
committed = await binding.connection.update(async (current) => {
|
|
99
|
+
try {
|
|
100
|
+
validateCredential(method, current);
|
|
101
|
+
const previous = grantFrom(method, current);
|
|
102
|
+
if ((fresh(previous) && !rejected) ||
|
|
103
|
+
(previous &&
|
|
104
|
+
previous.revision !== observedRevision &&
|
|
105
|
+
previous.tokens.accessToken &&
|
|
106
|
+
(previous.tokens.expiresAt === undefined ||
|
|
107
|
+
previous.tokens.expiresAt > Date.now())))
|
|
108
|
+
return;
|
|
109
|
+
const options = {
|
|
110
|
+
fetch: this.options.fetch,
|
|
111
|
+
timeoutMs: this.options.timeoutMs,
|
|
112
|
+
};
|
|
113
|
+
const jwtIdentity = binding.jwtIdentity;
|
|
114
|
+
if (auth.renewal === "jwtBearer" && !jwtIdentity)
|
|
115
|
+
throw new AuthError("invalid_credentials");
|
|
116
|
+
const tokens = auth.renewal === "refresh"
|
|
117
|
+
? await refreshOAuth2Token(auth.definition, {
|
|
118
|
+
application: binding.application,
|
|
119
|
+
tokens: previous?.tokens ?? {},
|
|
120
|
+
scopes: auth.scopes,
|
|
121
|
+
}, options)
|
|
122
|
+
: auth.renewal === "jwtBearer" && jwtIdentity
|
|
123
|
+
? await acquireOAuth2JwtToken(auth.definition, {
|
|
124
|
+
identity: jwtIdentity,
|
|
125
|
+
scopes: auth.scopes ?? [],
|
|
126
|
+
}, options)
|
|
127
|
+
: await acquireOAuth2ClientToken(auth.definition, {
|
|
128
|
+
application: binding.application ?? { clientId: "" },
|
|
129
|
+
scopes: auth.scopes,
|
|
130
|
+
}, options);
|
|
131
|
+
const patch = {
|
|
132
|
+
[auth.grantField]: { tokens, revision: randomUUID() },
|
|
133
|
+
};
|
|
134
|
+
const next = { ...current, ...patch };
|
|
135
|
+
validateCredential(method, next);
|
|
136
|
+
grantFrom(method, next);
|
|
137
|
+
secret(tokens.accessToken);
|
|
138
|
+
return patch;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
callbackError = error;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error === callbackError && error instanceof AuthError)
|
|
148
|
+
throw error;
|
|
149
|
+
throw new AuthError("credential_store_failed");
|
|
150
|
+
}
|
|
151
|
+
const grant = grantFrom(method, boundSnapshot(binding, method, committed));
|
|
152
|
+
if (!grant)
|
|
153
|
+
throw new AuthError("invalid_credentials");
|
|
154
|
+
return grant;
|
|
155
|
+
}
|
|
156
|
+
async request(selection, input) {
|
|
157
|
+
// Pin authority, routing, headers, registration, and replayable bytes before IO.
|
|
158
|
+
const binding = pinBinding(selection);
|
|
159
|
+
const method = this.registry.select(binding.type, binding.method);
|
|
160
|
+
const target = Object.hasOwn(method.targets, input.target)
|
|
161
|
+
? method.targets[input.target]
|
|
162
|
+
: undefined;
|
|
163
|
+
if (!target)
|
|
164
|
+
throw new AuthError("request_not_allowed");
|
|
165
|
+
const url = resourceUrl(target, input.path);
|
|
166
|
+
const verb = input.method ?? "GET";
|
|
167
|
+
if (!target.methods.includes(verb) ||
|
|
168
|
+
(input.retry !== undefined && input.retry !== "never"))
|
|
169
|
+
throw new AuthError("request_not_allowed");
|
|
170
|
+
for (const name of url.searchParams.keys()) {
|
|
171
|
+
if ([
|
|
172
|
+
"access_token",
|
|
173
|
+
"refresh_token",
|
|
174
|
+
"client_secret",
|
|
175
|
+
"api_key",
|
|
176
|
+
"authorization",
|
|
177
|
+
].includes(name.toLowerCase()))
|
|
178
|
+
throw new AuthError("request_not_allowed");
|
|
179
|
+
}
|
|
180
|
+
const auth = method.authentication;
|
|
181
|
+
const authHeader = auth.kind === "apiKey" ? headerName(auth.header) : "authorization";
|
|
182
|
+
let headers;
|
|
183
|
+
let body;
|
|
184
|
+
try {
|
|
185
|
+
headers = new Headers();
|
|
186
|
+
const allowed = new Set([
|
|
187
|
+
"accept",
|
|
188
|
+
"content-type",
|
|
189
|
+
...(target.allowedHeaders ?? []).map((name) => name.toLowerCase()),
|
|
190
|
+
]);
|
|
191
|
+
for (const [name, value] of Object.entries(input.headers ?? {})) {
|
|
192
|
+
const normalized = headerName(name);
|
|
193
|
+
if (!allowed.has(normalized) ||
|
|
194
|
+
normalized === authHeader ||
|
|
195
|
+
normalized === "authorization" ||
|
|
196
|
+
normalized === target.idempotency?.header.toLowerCase() ||
|
|
197
|
+
typeof value !== "string")
|
|
198
|
+
throw new Error();
|
|
199
|
+
headers.set(normalized, value);
|
|
200
|
+
}
|
|
201
|
+
if (input.body !== undefined) {
|
|
202
|
+
if (verb === "GET" ||
|
|
203
|
+
verb === "HEAD" ||
|
|
204
|
+
(typeof input.body !== "string" &&
|
|
205
|
+
!(input.body instanceof Uint8Array)))
|
|
206
|
+
throw new Error();
|
|
207
|
+
const size = typeof input.body === "string"
|
|
208
|
+
? Buffer.byteLength(input.body)
|
|
209
|
+
: input.body.byteLength;
|
|
210
|
+
if (size > this.options.maxRequestBytes)
|
|
211
|
+
throw new Error();
|
|
212
|
+
body = Buffer.from(input.body);
|
|
213
|
+
}
|
|
214
|
+
if (input.idempotencyKey !== undefined) {
|
|
215
|
+
if (!target.idempotency?.methods.includes(verb))
|
|
216
|
+
throw new Error();
|
|
217
|
+
headers.set(target.idempotency.header, secret(input.idempotencyKey));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new AuthError("request_not_allowed");
|
|
222
|
+
}
|
|
223
|
+
const replay = input.retry !== "never" &&
|
|
224
|
+
(verb === "GET" || verb === "HEAD" || input.idempotencyKey !== undefined);
|
|
225
|
+
let { token, grant } = await this.authorize(binding, method);
|
|
226
|
+
const send = async (value) => {
|
|
227
|
+
const signed = new Headers(headers);
|
|
228
|
+
signed.set(authHeader, auth.kind === "apiKey" ? value : `Bearer ${value}`);
|
|
229
|
+
return sendResource(url.toString(), verb, signed, body, {
|
|
230
|
+
...this.options,
|
|
231
|
+
resumableUpload: target.resumableUpload,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const first = await send(token);
|
|
235
|
+
if (auth.kind !== "oauth2" ||
|
|
236
|
+
!grant ||
|
|
237
|
+
!replay ||
|
|
238
|
+
first.status !== (auth.rejectionStatus ?? 401))
|
|
239
|
+
return first;
|
|
240
|
+
// Exactly one replay, only after durable renewal (or a newer committed revision).
|
|
241
|
+
grant = await this.renew(binding, method, grant.revision, true);
|
|
242
|
+
return send(secret(grant.tokens.accessToken));
|
|
243
|
+
}
|
|
244
|
+
/** Only a declared read-only probe runs. Results never contain provider text or secrets. */
|
|
245
|
+
async probe(binding) {
|
|
246
|
+
const probe = this.registry.select(binding.type, binding.method).probe;
|
|
247
|
+
if (!probe)
|
|
248
|
+
return { outcome: "unverified" };
|
|
249
|
+
try {
|
|
250
|
+
const response = await this.request(binding, probe);
|
|
251
|
+
return {
|
|
252
|
+
outcome: probe.acceptedStatuses.includes(response.status)
|
|
253
|
+
? "accepted"
|
|
254
|
+
: [401, 403].includes(response.status)
|
|
255
|
+
? "rejected"
|
|
256
|
+
: "unverified",
|
|
257
|
+
status: response.status,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
outcome: error instanceof AuthError && error.code === "reconnect_required"
|
|
263
|
+
? "rejected"
|
|
264
|
+
: "unverified",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Trusted-host compatibility primitive. Never expose raw tokens through a broker. */
|
|
269
|
+
async accessToken(selection) {
|
|
270
|
+
const binding = pinBinding(selection);
|
|
271
|
+
return (await this.authorize(binding, this.registry.select(binding.type, binding.method))).token;
|
|
272
|
+
}
|
|
273
|
+
async authorize(binding, method) {
|
|
274
|
+
const config = await this.read(binding, method);
|
|
275
|
+
const auth = method.authentication;
|
|
276
|
+
if (auth.kind !== "oauth2")
|
|
277
|
+
return { token: secret(config[auth.field]) };
|
|
278
|
+
let grant = grantFrom(method, config);
|
|
279
|
+
if (!fresh(grant))
|
|
280
|
+
grant = await this.renew(binding, method, grant?.revision);
|
|
281
|
+
return { token: secret(grant?.tokens.accessToken), grant };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import type { OAuth2Definition, OAuthApplication, OAuthJwtIdentity, OAuthTokens } from "../auth/types.js";
|
|
3
|
+
import type { ConnectionHandle } from "../connections/types.js";
|
|
4
|
+
export type HttpMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
5
|
+
/** Host-approved resource boundary. No wildcards, templates, or agent-selected origins. */
|
|
6
|
+
export interface CredentialTarget {
|
|
7
|
+
/** HTTPS URL ending in /. Requests stay beneath this exact origin and path. */
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
methods: HttpMethod[];
|
|
10
|
+
/** Additional caller-set headers beyond Accept and Content-Type. Auth is reserved. */
|
|
11
|
+
allowedHeaders?: string[];
|
|
12
|
+
/** Google-style PUT upload acknowledgements: allow 308 only without Location. */
|
|
13
|
+
resumableUpload?: boolean;
|
|
14
|
+
/** Declare only when the provider guarantees deduplication for these methods. */
|
|
15
|
+
idempotency?: {
|
|
16
|
+
header: string;
|
|
17
|
+
methods: HttpMethod[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export type CredentialAuthentication = {
|
|
21
|
+
kind: "apiKey";
|
|
22
|
+
field: string;
|
|
23
|
+
header: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "bearer";
|
|
26
|
+
field: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "oauth2";
|
|
29
|
+
/** Top-level config field containing a revisioned OAuthGrant. */
|
|
30
|
+
grantField: string;
|
|
31
|
+
definition: OAuth2Definition;
|
|
32
|
+
/** Explicit renewal strategy, never inferred from available secrets. */
|
|
33
|
+
renewal: "refresh" | "clientCredentials" | "jwtBearer";
|
|
34
|
+
scopes?: string[];
|
|
35
|
+
/** Defaults to 401. A 403 must be explicitly documented by the provider. */
|
|
36
|
+
rejectionStatus?: 401 | 403;
|
|
37
|
+
};
|
|
38
|
+
export interface CredentialProbe {
|
|
39
|
+
target: string;
|
|
40
|
+
/** Fixed relative path, including any fixed query. No expressions or callbacks. */
|
|
41
|
+
path: string;
|
|
42
|
+
method: "GET" | "HEAD";
|
|
43
|
+
/** Only these 2xx responses count as accepted; this does not prove identity. */
|
|
44
|
+
acceptedStatuses: number[];
|
|
45
|
+
}
|
|
46
|
+
export interface CredentialMethod {
|
|
47
|
+
/** Strict object schema for this method's credential config, not action input. */
|
|
48
|
+
schema: TSchema;
|
|
49
|
+
authentication: CredentialAuthentication;
|
|
50
|
+
targets: Record<string, CredentialTarget>;
|
|
51
|
+
probe?: CredentialProbe;
|
|
52
|
+
}
|
|
53
|
+
/** Registered by trusted hosts; method names describe provider-specific choices. */
|
|
54
|
+
export interface CredentialType {
|
|
55
|
+
id: string;
|
|
56
|
+
methods: Record<string, CredentialMethod>;
|
|
57
|
+
}
|
|
58
|
+
/** Host-authorized selection. Never derive this binding from action input. */
|
|
59
|
+
export interface CredentialBinding {
|
|
60
|
+
type: string;
|
|
61
|
+
method: string;
|
|
62
|
+
connection: ConnectionHandle;
|
|
63
|
+
/** Expected identity of every read and committed update. */
|
|
64
|
+
identity: {
|
|
65
|
+
name: string;
|
|
66
|
+
plugin: string;
|
|
67
|
+
};
|
|
68
|
+
application?: OAuthApplication;
|
|
69
|
+
jwtIdentity?: OAuthJwtIdentity;
|
|
70
|
+
}
|
|
71
|
+
/** Revision changes on every issuance, even when the provider repeats the token value. */
|
|
72
|
+
export interface OAuthGrant {
|
|
73
|
+
/** Setup may seed only a refresh token; issuance always supplies an access token. */
|
|
74
|
+
tokens: Partial<OAuthTokens>;
|
|
75
|
+
revision: string;
|
|
76
|
+
}
|
|
77
|
+
export type CredentialProbeResult = {
|
|
78
|
+
outcome: "accepted" | "rejected" | "unverified";
|
|
79
|
+
status?: number;
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
export { AuthError, type AuthErrorCode } from "./auth/errors.js";
|
|
2
|
+
export { acquireOAuth2ClientToken, acquireOAuth2JwtToken, buildOAuth2AuthorizationUrl, exchangeOAuth2Code, refreshOAuth2Token, } from "./auth/oauth2.js";
|
|
3
|
+
export { decodeOAuthTokens, requestOAuth2Token } from "./auth/token.js";
|
|
4
|
+
export type { OAuth2Definition, OAuth2TokenEndpoint, OAuthApplication, OAuthAuthorizationOptions, OAuthCodeOptions, OAuthEvent, OAuthJwtIdentity, OAuthOperation, OAuthRuntimeOptions, } from "./auth/types.js";
|
|
1
5
|
export { addConnection, findConfigDir, getConnection, loadConfig, removeConnection, saveConfig, updateConnectionConfig, } from "./config/loader.js";
|
|
2
6
|
export type { RunlineConfig } from "./config/types.js";
|
|
3
7
|
export { DEFAULT_CONFIG } from "./config/types.js";
|
|
8
|
+
export { FileConnectionProvider } from "./connections/file.js";
|
|
9
|
+
export { MemoryConnectionProvider } from "./connections/memory.js";
|
|
10
|
+
export type { ConnectionHandle, ConnectionPatch, ConnectionProvider, ConnectionRequest, ConnectionUpdate, ConnectionUpdater, } from "./connections/types.js";
|
|
4
11
|
export type { ActionInvocation, EngineHooks, EngineOptions, ExecuteResult, } from "./core/engine.js";
|
|
5
12
|
export { ExecutionEngine } from "./core/engine.js";
|
|
6
13
|
export type { BuildAuthUrlOptions, ExchangeCodeOptions, OAuthTokens, PKCEPair, RunOAuthOptions, } from "./core/oauth.js";
|
|
7
14
|
export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
|
|
15
|
+
export { downloadResource } from "./credentials/http.js";
|
|
16
|
+
export { CredentialRegistry, OAuthGrantSchema, OAuthTokensSchema, validateCredential, } from "./credentials/registry.js";
|
|
17
|
+
export type { AuthenticatedRequest, CredentialTransportOptions, } from "./credentials/transport.js";
|
|
18
|
+
export { CredentialTransport } from "./credentials/transport.js";
|
|
19
|
+
export type { CredentialAuthentication, CredentialBinding, CredentialMethod, CredentialProbe, CredentialProbeResult, CredentialTarget, CredentialType, HttpMethod, OAuthGrant, } from "./credentials/types.js";
|
|
8
20
|
export type { ActionDefinition, PluginFunction, RunlinePluginAPI, } from "./plugin/api.js";
|
|
9
21
|
export { createPluginAPI, isPluginFunction, resolvePluginExport, } from "./plugin/api.js";
|
|
10
22
|
export type { InstalledPlugin, PluginSource } from "./plugin/installer.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export { AuthError } from "./auth/errors.js";
|
|
2
|
+
export { acquireOAuth2ClientToken, acquireOAuth2JwtToken, buildOAuth2AuthorizationUrl, exchangeOAuth2Code, refreshOAuth2Token, } from "./auth/oauth2.js";
|
|
3
|
+
export { decodeOAuthTokens, requestOAuth2Token } from "./auth/token.js";
|
|
1
4
|
export { addConnection, findConfigDir, getConnection, loadConfig, removeConnection, saveConfig, updateConnectionConfig, } from "./config/loader.js";
|
|
2
5
|
export { DEFAULT_CONFIG } from "./config/types.js";
|
|
6
|
+
export { FileConnectionProvider } from "./connections/file.js";
|
|
7
|
+
export { MemoryConnectionProvider } from "./connections/memory.js";
|
|
3
8
|
export { ExecutionEngine } from "./core/engine.js";
|
|
4
9
|
export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
|
|
10
|
+
export { downloadResource } from "./credentials/http.js";
|
|
11
|
+
export { CredentialRegistry, OAuthGrantSchema, OAuthTokensSchema, validateCredential, } from "./credentials/registry.js";
|
|
12
|
+
export { CredentialTransport } from "./credentials/transport.js";
|
|
5
13
|
export { createPluginAPI, isPluginFunction, resolvePluginExport, } from "./plugin/api.js";
|
|
6
14
|
export { installPlugin, listInstalled, parsePluginSource, removePlugin, } from "./plugin/installer.js";
|
|
7
15
|
export { discoverPlugins, loadAllPlugins, loadPluginFromPath, loadPluginsFromConfig, } from "./plugin/loader.js";
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
|
+
import type { OAuth2Definition } from "../auth/types.js";
|
|
3
|
+
import type { ConnectionUpdate } from "../connections/types.js";
|
|
2
4
|
export interface InputField {
|
|
3
5
|
type: "string" | "number" | "boolean" | "object" | "array";
|
|
4
6
|
description?: string;
|
|
@@ -64,18 +66,15 @@ export interface ActionContext {
|
|
|
64
66
|
error(msg: string): void;
|
|
65
67
|
};
|
|
66
68
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
+
* Persist a shallow config patch through the host's connection provider.
|
|
70
|
+
* For token refresh, pass an async updater: it receives freshly read config
|
|
71
|
+
* under update ownership, so the provider coordinates the whole refresh.
|
|
72
|
+
* Return undefined to reuse a token refreshed by another caller.
|
|
69
73
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* guarded by a file lock, so concurrent `runline exec`
|
|
73
|
-
* processes refreshing the same connection won't race.
|
|
74
|
-
*
|
|
75
|
-
* In-memory `ctx.connection.config` is also mutated so the
|
|
76
|
-
* rest of the current action sees the new values.
|
|
74
|
+
* This action's snapshot changes only after a successful store update.
|
|
75
|
+
* A patch alone serializes the write, not any preceding network request.
|
|
77
76
|
*/
|
|
78
|
-
updateConnection(
|
|
77
|
+
updateConnection(change: ConnectionUpdate): Promise<void>;
|
|
79
78
|
}
|
|
80
79
|
/**
|
|
81
80
|
* OAuth2 authorization-code configuration declared by a plugin.
|
|
@@ -84,19 +83,32 @@ export interface ActionContext {
|
|
|
84
83
|
* of `clientId`, `clientSecret`, `refreshToken`, `accessToken`,
|
|
85
84
|
* and `accessTokenExpiresAt` into the plugin's connection.
|
|
86
85
|
*/
|
|
87
|
-
export
|
|
88
|
-
/**
|
|
86
|
+
export type OAuthConfig = OAuthSetupOptions & ({
|
|
87
|
+
/** One provider definition supplies every endpoint and protocol policy. */
|
|
88
|
+
protocol: OAuth2Definition;
|
|
89
|
+
authUrl?: never;
|
|
90
|
+
tokenUrl?: never;
|
|
91
|
+
authParams?: never;
|
|
92
|
+
} | {
|
|
93
|
+
/** Flat declarations use body-secret authentication at the adapter boundary. */
|
|
94
|
+
protocol?: never;
|
|
89
95
|
authUrl: string;
|
|
90
|
-
/** Token endpoint, e.g. https://oauth2.googleapis.com/token */
|
|
91
96
|
tokenUrl: string;
|
|
97
|
+
authParams?: Record<string, string>;
|
|
98
|
+
});
|
|
99
|
+
interface OAuthSetupOptions {
|
|
92
100
|
/** Scopes to request on the consent screen. */
|
|
93
101
|
scopes: string[];
|
|
94
102
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
103
|
+
* Provider-fixed loopback callback (`http://localhost:<port>/<path>`),
|
|
104
|
+
* replacing Runline's default `http://127.0.0.1:47823/callback`. Use only
|
|
105
|
+
* when the provider validates redirects against a URI it published.
|
|
98
106
|
*/
|
|
99
|
-
|
|
107
|
+
redirectUri?: string;
|
|
108
|
+
/** Public client: PKCE protects the exchange and no client secret is collected or sent. */
|
|
109
|
+
publicClient?: boolean;
|
|
110
|
+
/** Provider-published client identifier used when the user supplies none. */
|
|
111
|
+
defaultClientId?: string;
|
|
100
112
|
/**
|
|
101
113
|
* Printed by `runline auth <plugin>` before credentials are
|
|
102
114
|
* requested. Each array entry is a line. The token
|
|
@@ -119,3 +131,4 @@ export interface PluginDef {
|
|
|
119
131
|
/** @internal */
|
|
120
132
|
initHooks?: Array<(config: Record<string, unknown>) => void>;
|
|
121
133
|
}
|
|
134
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AuthError, CredentialRegistry, CredentialTransport, } from "runline";
|
|
3
|
+
/** Flat CLI/env storage is a projection, not a second token cache or refresh engine. */
|
|
4
|
+
export function credentialRuntime(ctx, definition, method, authority) {
|
|
5
|
+
const identity = { name: ctx.connection.name, plugin: ctx.connection.plugin };
|
|
6
|
+
const fingerprint = (config) => createHash("sha256")
|
|
7
|
+
.update(JSON.stringify([definition.id, method, authority(config)]))
|
|
8
|
+
.digest("hex");
|
|
9
|
+
const expected = fingerprint(ctx.connection.config);
|
|
10
|
+
const project = (current) => {
|
|
11
|
+
if (fingerprint(current) !== expected)
|
|
12
|
+
throw new AuthError("binding_changed");
|
|
13
|
+
const compatible = current.authTokenBinding === undefined ||
|
|
14
|
+
current.authTokenBinding === expected;
|
|
15
|
+
const tokens = {
|
|
16
|
+
...(compatible && current.accessToken !== undefined
|
|
17
|
+
? { accessToken: current.accessToken }
|
|
18
|
+
: {}),
|
|
19
|
+
...(current.refreshToken !== undefined
|
|
20
|
+
? { refreshToken: current.refreshToken }
|
|
21
|
+
: {}),
|
|
22
|
+
...(compatible && current.accessTokenExpiresAt !== undefined
|
|
23
|
+
? { expiresAt: current.accessTokenExpiresAt }
|
|
24
|
+
: {}),
|
|
25
|
+
...(compatible && current.authTokenScope !== undefined
|
|
26
|
+
? { scope: current.authTokenScope }
|
|
27
|
+
: {}),
|
|
28
|
+
...(compatible && current.authTokenType !== undefined
|
|
29
|
+
? { tokenType: current.authTokenType }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
if (!Object.keys(tokens).length)
|
|
33
|
+
return {};
|
|
34
|
+
const revision = typeof current.authTokenRevision === "string" && compatible
|
|
35
|
+
? current.authTokenRevision
|
|
36
|
+
: createHash("sha256").update(JSON.stringify(tokens)).digest("hex");
|
|
37
|
+
return { grant: { tokens, revision } };
|
|
38
|
+
};
|
|
39
|
+
const snapshot = () => ({
|
|
40
|
+
...identity,
|
|
41
|
+
config: project(ctx.connection.config),
|
|
42
|
+
});
|
|
43
|
+
const binding = {
|
|
44
|
+
type: definition.id,
|
|
45
|
+
method,
|
|
46
|
+
identity,
|
|
47
|
+
application: {
|
|
48
|
+
clientId: ctx.connection.config.clientId,
|
|
49
|
+
clientSecret: ctx.connection.config.clientSecret,
|
|
50
|
+
},
|
|
51
|
+
connection: {
|
|
52
|
+
async read() {
|
|
53
|
+
return snapshot();
|
|
54
|
+
},
|
|
55
|
+
async update(change) {
|
|
56
|
+
await ctx.updateConnection(async (current) => {
|
|
57
|
+
const projected = project(current);
|
|
58
|
+
const patch = typeof change === "function" ? await change(projected) : change;
|
|
59
|
+
if (!patch)
|
|
60
|
+
return;
|
|
61
|
+
const grant = patch.grant;
|
|
62
|
+
if (!grant?.tokens.accessToken)
|
|
63
|
+
throw new AuthError("invalid_credentials");
|
|
64
|
+
return {
|
|
65
|
+
accessToken: grant.tokens.accessToken,
|
|
66
|
+
accessTokenExpiresAt: grant.tokens.expiresAt,
|
|
67
|
+
refreshToken: grant.tokens.refreshToken ?? current.refreshToken,
|
|
68
|
+
authTokenScope: grant.tokens.scope,
|
|
69
|
+
authTokenType: grant.tokens.tokenType,
|
|
70
|
+
authTokenRevision: grant.revision,
|
|
71
|
+
authTokenBinding: expected,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
return snapshot();
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const registry = new CredentialRegistry();
|
|
79
|
+
registry.register(definition);
|
|
80
|
+
// Builtins run in the caller's trusted runtime. Vex must supply its privileged
|
|
81
|
+
// egress transport via the broker rather than execute these plugins server-side.
|
|
82
|
+
const transport = new CredentialTransport(registry, {
|
|
83
|
+
fetch: globalThis.fetch,
|
|
84
|
+
maxRequestBytes: 64 * 1024 * 1024,
|
|
85
|
+
maxResponseBytes: 64 * 1024 * 1024,
|
|
86
|
+
});
|
|
87
|
+
return { binding, transport };
|
|
88
|
+
}
|