@flue/client 0.0.23 → 0.0.25
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/index.d.mts +12 -15
- package/dist/index.mjs +12 -57
- package/dist/proxies/index.d.mts +42 -20
- package/dist/proxies/index.mjs +191 -95
- package/dist/{types-37Tgp-H9.d.mts → types-DnTJOlQ7.d.mts} +14 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ProxyService, i as ProxyPresetResult, r as ProxyPolicy, t as PolicyRule } from "./types-DnTJOlQ7.mjs";
|
|
2
2
|
import * as v from "valibot";
|
|
3
3
|
|
|
4
4
|
//#region src/errors.d.ts
|
|
@@ -74,22 +74,22 @@ type FlueEvent = {
|
|
|
74
74
|
declare function transformEvent(raw: any): FlueEvent | null;
|
|
75
75
|
//#endregion
|
|
76
76
|
//#region src/types.d.ts
|
|
77
|
-
interface
|
|
77
|
+
interface FlueClientOptions {
|
|
78
78
|
/** OpenCode server URL (default: 'http://localhost:48765'). */
|
|
79
79
|
opencodeUrl?: string;
|
|
80
80
|
/** Working directory (the repo root). */
|
|
81
81
|
workdir: string;
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
/** Workflow arguments. */
|
|
85
|
-
args?: Record<string, unknown>;
|
|
86
|
-
/** Proxy instructions to append to every skill/prompt call. */
|
|
87
|
-
proxyInstructions?: string[];
|
|
82
|
+
/** Proxy configs — instructions are extracted and appended to every skill/prompt call. */
|
|
83
|
+
proxies?: ProxyService[];
|
|
88
84
|
/** Default model for skill/prompt invocations. */
|
|
89
85
|
model?: {
|
|
90
86
|
providerID: string;
|
|
91
87
|
modelID: string;
|
|
92
88
|
};
|
|
89
|
+
/** Fetch implementation for reaching the OpenCode server. */
|
|
90
|
+
fetch: (request: Request) => Promise<Response>;
|
|
91
|
+
/** Shell implementation for executing commands in the target environment. */
|
|
92
|
+
shell: (command: string, options?: ShellOptions) => Promise<ShellResult>;
|
|
93
93
|
}
|
|
94
94
|
interface SkillOptions<S extends v.GenericSchema | undefined = undefined> {
|
|
95
95
|
/** Key-value args serialized into the prompt. */
|
|
@@ -128,16 +128,13 @@ interface ShellResult {
|
|
|
128
128
|
}
|
|
129
129
|
//#endregion
|
|
130
130
|
//#region src/flue.d.ts
|
|
131
|
-
declare class
|
|
132
|
-
/** Working branch for commits. */
|
|
133
|
-
readonly branch: string;
|
|
134
|
-
/** Workflow arguments passed by the runner. */
|
|
135
|
-
readonly args: Record<string, unknown>;
|
|
131
|
+
declare class FlueClient {
|
|
136
132
|
private readonly workdir;
|
|
137
133
|
private readonly proxyInstructions;
|
|
138
134
|
private readonly model?;
|
|
139
135
|
private readonly client;
|
|
140
|
-
|
|
136
|
+
private readonly shellFn;
|
|
137
|
+
constructor(options: FlueClientOptions);
|
|
141
138
|
/** Run a named skill with a result schema. */
|
|
142
139
|
skill<S extends v.GenericSchema>(name: string, options: SkillOptions<S> & {
|
|
143
140
|
result: S;
|
|
@@ -156,4 +153,4 @@ declare class Flue {
|
|
|
156
153
|
close(): Promise<void>;
|
|
157
154
|
}
|
|
158
155
|
//#endregion
|
|
159
|
-
export {
|
|
156
|
+
export { FlueClient, type FlueClientOptions, type FlueEvent, type PolicyRule, type PromptOptions, type ProxyPolicy, type ProxyPresetResult, type ProxyService, type ShellOptions, type ShellResult, type SkillOptions, SkillOutputError, transformEvent };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
2
|
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
3
|
-
import { exec } from "node:child_process";
|
|
4
3
|
import * as v from "valibot";
|
|
5
4
|
|
|
6
5
|
//#region src/errors.ts
|
|
@@ -225,43 +224,6 @@ function buildSkillPrompt(name, args, schema, proxyInstructions) {
|
|
|
225
224
|
return parts.join("\n");
|
|
226
225
|
}
|
|
227
226
|
|
|
228
|
-
//#endregion
|
|
229
|
-
//#region src/shell.ts
|
|
230
|
-
async function runShell(command, options) {
|
|
231
|
-
console.log("[flue] shell: running", {
|
|
232
|
-
command,
|
|
233
|
-
cwd: options?.cwd,
|
|
234
|
-
env: options?.env ? Object.keys(options.env) : void 0,
|
|
235
|
-
stdin: options?.stdin ? `${options.stdin.length} chars` : void 0,
|
|
236
|
-
timeout: options?.timeout
|
|
237
|
-
});
|
|
238
|
-
return new Promise((resolve) => {
|
|
239
|
-
const child = exec(command, {
|
|
240
|
-
cwd: options?.cwd,
|
|
241
|
-
env: options?.env ?? process.env,
|
|
242
|
-
timeout: options?.timeout
|
|
243
|
-
}, (error, stdout, stderr) => {
|
|
244
|
-
const rawCode = error && typeof error.code === "number" ? error.code : 0;
|
|
245
|
-
const result = {
|
|
246
|
-
stdout: stdout ?? "",
|
|
247
|
-
stderr: stderr ?? "",
|
|
248
|
-
exitCode: error ? rawCode || 1 : 0
|
|
249
|
-
};
|
|
250
|
-
console.log("[flue] shell: completed", {
|
|
251
|
-
command,
|
|
252
|
-
exitCode: result.exitCode,
|
|
253
|
-
stdout: result.stdout.length > 200 ? `${result.stdout.slice(0, 200)}... (${result.stdout.length} chars)` : result.stdout,
|
|
254
|
-
stderr: result.stderr.length > 200 ? `${result.stderr.slice(0, 200)}... (${result.stderr.length} chars)` : result.stderr
|
|
255
|
-
});
|
|
256
|
-
resolve(result);
|
|
257
|
-
});
|
|
258
|
-
if (options?.stdin) {
|
|
259
|
-
child.stdin?.write(options.stdin);
|
|
260
|
-
child.stdin?.end();
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
227
|
//#endregion
|
|
266
228
|
//#region src/result.ts
|
|
267
229
|
/**
|
|
@@ -326,9 +288,9 @@ function extractLastResultBlock(text) {
|
|
|
326
288
|
//#endregion
|
|
327
289
|
//#region src/skill.ts
|
|
328
290
|
/** How often to poll and log progress (ms). */
|
|
329
|
-
const POLL_INTERVAL =
|
|
291
|
+
const POLL_INTERVAL = 15e3;
|
|
330
292
|
/** Max times we'll see 0 assistant messages before giving up. */
|
|
331
|
-
const MAX_EMPTY_POLLS =
|
|
293
|
+
const MAX_EMPTY_POLLS = 20;
|
|
332
294
|
/** Max time to poll before timing out (ms) - 45 minutes. */
|
|
333
295
|
const MAX_POLL_TIME = 2700 * 1e3;
|
|
334
296
|
/**
|
|
@@ -430,7 +392,7 @@ async function pollUntilIdle(client, sessionId, workdir, label, startTime) {
|
|
|
430
392
|
const parts = await fetchAllAssistantParts(client, sessionId, workdir);
|
|
431
393
|
if (parts.length === 0) {
|
|
432
394
|
emptyPolls++;
|
|
433
|
-
if (emptyPolls %
|
|
395
|
+
if (emptyPolls % 4 === 0) {
|
|
434
396
|
console.log(`[flue] ${label}: status result: ${JSON.stringify({
|
|
435
397
|
hasData: !!statusResult.data,
|
|
436
398
|
sessionIds: statusResult.data ? Object.keys(statusResult.data) : [],
|
|
@@ -455,7 +417,7 @@ async function pollUntilIdle(client, sessionId, workdir, label, startTime) {
|
|
|
455
417
|
}
|
|
456
418
|
return parts;
|
|
457
419
|
}
|
|
458
|
-
if (pollCount %
|
|
420
|
+
if (pollCount % 4 === 0) console.log(`[flue] ${label}: running (${elapsed}s)`);
|
|
459
421
|
}
|
|
460
422
|
}
|
|
461
423
|
/**
|
|
@@ -478,33 +440,26 @@ function sleep(ms) {
|
|
|
478
440
|
|
|
479
441
|
//#endregion
|
|
480
442
|
//#region src/flue.ts
|
|
481
|
-
var
|
|
482
|
-
/** Working branch for commits. */
|
|
483
|
-
branch;
|
|
484
|
-
/** Workflow arguments passed by the runner. */
|
|
485
|
-
args;
|
|
443
|
+
var FlueClient = class {
|
|
486
444
|
workdir;
|
|
487
445
|
proxyInstructions;
|
|
488
446
|
model;
|
|
489
447
|
client;
|
|
448
|
+
shellFn;
|
|
490
449
|
constructor(options) {
|
|
491
|
-
this.
|
|
492
|
-
this.args = options.args ?? {};
|
|
493
|
-
this.proxyInstructions = options.proxyInstructions ?? [];
|
|
450
|
+
this.proxyInstructions = options.proxies?.map((p) => p.instructions).filter((i) => !!i) ?? [];
|
|
494
451
|
this.workdir = options.workdir;
|
|
495
452
|
this.model = options.model;
|
|
453
|
+
this.shellFn = options.shell;
|
|
496
454
|
this.client = createOpencodeClient({
|
|
497
455
|
baseUrl: options.opencodeUrl ?? "http://localhost:48765",
|
|
498
|
-
directory: this.workdir
|
|
456
|
+
directory: this.workdir,
|
|
457
|
+
fetch: options.fetch
|
|
499
458
|
});
|
|
500
459
|
}
|
|
501
460
|
async skill(name, options) {
|
|
502
461
|
const mergedOptions = {
|
|
503
462
|
...options,
|
|
504
|
-
args: this.args || options?.args ? {
|
|
505
|
-
...this.args,
|
|
506
|
-
...options?.args
|
|
507
|
-
} : void 0,
|
|
508
463
|
model: options?.model ?? this.model
|
|
509
464
|
};
|
|
510
465
|
return runSkill(this.client, this.workdir, name, mergedOptions, this.proxyInstructions);
|
|
@@ -527,7 +482,7 @@ var Flue = class {
|
|
|
527
482
|
}
|
|
528
483
|
/** Execute a shell command with scoped environment variables. */
|
|
529
484
|
async shell(command, options) {
|
|
530
|
-
return
|
|
485
|
+
return this.shellFn(command, {
|
|
531
486
|
...options,
|
|
532
487
|
cwd: options?.cwd ?? this.workdir
|
|
533
488
|
});
|
|
@@ -537,4 +492,4 @@ var Flue = class {
|
|
|
537
492
|
};
|
|
538
493
|
|
|
539
494
|
//#endregion
|
|
540
|
-
export {
|
|
495
|
+
export { FlueClient, SkillOutputError, transformEvent };
|
package/dist/proxies/index.d.mts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ProxyService, i as ProxyPresetResult, n as ProxyFactory, r as ProxyPolicy, t as PolicyRule } from "../types-DnTJOlQ7.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/proxies/anthropic.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Anthropic model provider proxy preset.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Strips all non-allowlisted headers for security.
|
|
7
|
+
* Returns a ProxyFactory that, when called with `{ apiKey }`, produces a
|
|
8
|
+
* ProxyService proxying requests to api.anthropic.com with the API key
|
|
9
|
+
* injected. Strips all non-allowlisted headers for security.
|
|
10
10
|
*/
|
|
11
11
|
declare function anthropic(opts?: {
|
|
12
|
-
apiKey?: string;
|
|
13
12
|
policy?: string | ProxyPolicy;
|
|
14
|
-
}):
|
|
13
|
+
}): ProxyFactory<{
|
|
14
|
+
apiKey: string;
|
|
15
|
+
}>;
|
|
15
16
|
//#endregion
|
|
16
17
|
//#region src/proxies/github.d.ts
|
|
17
18
|
/**
|
|
18
19
|
* GitHub proxy preset.
|
|
19
20
|
*
|
|
20
|
-
* Returns
|
|
21
|
+
* Returns a ProxyFactory that, when called with `{ token }`, produces two
|
|
22
|
+
* ProxyService objects:
|
|
21
23
|
* - `github-api`: unix socket proxy for REST/GraphQL API (api.github.com),
|
|
22
24
|
* used by the `gh` CLI and `curl`.
|
|
23
25
|
* - `github-git`: TCP port proxy for git smart HTTP (github.com),
|
|
@@ -25,28 +27,48 @@ declare function anthropic(opts?: {
|
|
|
25
27
|
*
|
|
26
28
|
* Both share the same token and policy.
|
|
27
29
|
*/
|
|
28
|
-
declare function github(opts
|
|
29
|
-
token: string;
|
|
30
|
+
declare function github(opts?: {
|
|
30
31
|
policy?: string | ProxyPolicy;
|
|
31
|
-
}):
|
|
32
|
+
}): ProxyFactory<{
|
|
33
|
+
token: string;
|
|
34
|
+
}>;
|
|
32
35
|
/**
|
|
33
36
|
* Body validation helpers for GitHub API requests.
|
|
34
37
|
*
|
|
35
38
|
* These return validator functions suitable for `PolicyRule.body`.
|
|
36
39
|
*/
|
|
37
40
|
declare const githubBody: {
|
|
38
|
-
/** Validate
|
|
39
|
-
titleMatch?: RegExp;
|
|
40
|
-
requiredLabels?: string[];
|
|
41
|
-
}): (body: unknown) => boolean; /** Validate a comment body. */
|
|
42
|
-
comment(opts: {
|
|
43
|
-
maxLength?: number;
|
|
44
|
-
pattern?: RegExp;
|
|
45
|
-
}): (body: unknown) => boolean; /** Validate a GraphQL request — restrict to queries only, or specific operations. */
|
|
46
|
-
graphql(opts?: {
|
|
41
|
+
/** Validate a GraphQL request — restrict to queries only, or specific operations. */graphql(opts?: {
|
|
47
42
|
allowedOperations?: string[];
|
|
48
43
|
denyMutations?: boolean;
|
|
49
44
|
}): (body: unknown) => boolean;
|
|
50
45
|
};
|
|
51
46
|
//#endregion
|
|
52
|
-
|
|
47
|
+
//#region src/proxies/policy.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Shared policy evaluation for proxy requests.
|
|
50
|
+
*
|
|
51
|
+
* This is the canonical TypeScript implementation. The CLI's proxy-server.mjs
|
|
52
|
+
* has an identical zero-dependency copy (see packages/cli/src/sandbox/proxy-server.mjs).
|
|
53
|
+
*/
|
|
54
|
+
interface PolicyResult {
|
|
55
|
+
allowed: boolean;
|
|
56
|
+
reason: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Match a URL path against a glob-style pattern.
|
|
60
|
+
* `*` matches one path segment, `**` matches zero or more.
|
|
61
|
+
*/
|
|
62
|
+
declare function matchPath(pattern: string, path: string): boolean;
|
|
63
|
+
declare function matchMethod(ruleMethod: string | string[], requestMethod: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Evaluate the proxy policy for a request.
|
|
66
|
+
* Order: deny rules -> allow rules (with optional body + rate limits) -> base level.
|
|
67
|
+
*
|
|
68
|
+
* `ruleCounts` is optional — omit on Cloudflare where rate limits are not enforced.
|
|
69
|
+
* `body` validators on rules are skipped when not present (Cloudflare v1 stores
|
|
70
|
+
* policies without functions).
|
|
71
|
+
*/
|
|
72
|
+
declare function evaluatePolicy(method: string, path: string, parsedBody: unknown, policy: ProxyPolicy | null, ruleCounts?: Map<string, number>): PolicyResult;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { type PolicyResult, type PolicyRule, type ProxyFactory, type ProxyPolicy, type ProxyPresetResult, type ProxyService, anthropic, evaluatePolicy, github, githubBody, matchMethod, matchPath };
|
package/dist/proxies/index.mjs
CHANGED
|
@@ -2,37 +2,45 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Anthropic model provider proxy preset.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Strips all non-allowlisted headers for security.
|
|
5
|
+
* Returns a ProxyFactory that, when called with `{ apiKey }`, produces a
|
|
6
|
+
* ProxyService proxying requests to api.anthropic.com with the API key
|
|
7
|
+
* injected. Strips all non-allowlisted headers for security.
|
|
8
8
|
*/
|
|
9
9
|
function anthropic(opts) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
10
|
+
const factory = (secrets) => {
|
|
11
|
+
const { apiKey } = secrets;
|
|
12
|
+
return {
|
|
13
|
+
name: "anthropic",
|
|
14
|
+
target: "https://api.anthropic.com",
|
|
15
|
+
headers: {
|
|
16
|
+
"x-api-key": apiKey,
|
|
17
|
+
host: "api.anthropic.com"
|
|
18
|
+
},
|
|
19
|
+
transform: (req) => {
|
|
20
|
+
const safe = [
|
|
21
|
+
"content-type",
|
|
22
|
+
"content-length",
|
|
23
|
+
"accept",
|
|
24
|
+
"anthropic-version",
|
|
25
|
+
"anthropic-beta",
|
|
26
|
+
"user-agent"
|
|
27
|
+
];
|
|
28
|
+
const filtered = {};
|
|
29
|
+
for (const key of safe) if (req.headers[key]) filtered[key] = req.headers[key];
|
|
30
|
+
filtered["x-api-key"] = apiKey;
|
|
31
|
+
return { headers: filtered };
|
|
32
|
+
},
|
|
33
|
+
policy: opts?.policy ?? "allow-all",
|
|
34
|
+
isModelProvider: true,
|
|
35
|
+
providerConfig: {
|
|
36
|
+
providerKey: "anthropic",
|
|
37
|
+
options: { apiKey: "sk-dummy-value-real-key-injected-by-proxy" }
|
|
38
|
+
}
|
|
39
|
+
};
|
|
35
40
|
};
|
|
41
|
+
factory.secretsMap = { apiKey: "ANTHROPIC_API_KEY" };
|
|
42
|
+
factory.proxyName = "anthropic";
|
|
43
|
+
return factory;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
//#endregion
|
|
@@ -40,7 +48,8 @@ function anthropic(opts) {
|
|
|
40
48
|
/**
|
|
41
49
|
* GitHub proxy preset.
|
|
42
50
|
*
|
|
43
|
-
* Returns
|
|
51
|
+
* Returns a ProxyFactory that, when called with `{ token }`, produces two
|
|
52
|
+
* ProxyService objects:
|
|
44
53
|
* - `github-api`: unix socket proxy for REST/GraphQL API (api.github.com),
|
|
45
54
|
* used by the `gh` CLI and `curl`.
|
|
46
55
|
* - `github-git`: TCP port proxy for git smart HTTP (github.com),
|
|
@@ -49,40 +58,46 @@ function anthropic(opts) {
|
|
|
49
58
|
* Both share the same token and policy.
|
|
50
59
|
*/
|
|
51
60
|
function github(opts) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
61
|
+
const factory = (secrets) => {
|
|
62
|
+
const { token } = secrets;
|
|
63
|
+
const resolvedPolicy = resolveGitHubPolicy(opts?.policy);
|
|
64
|
+
const denyResponse = ({ method, path, reason }) => ({
|
|
65
|
+
status: 403,
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
message: `Blocked by flue proxy policy: ${method} ${path} — ${reason}`,
|
|
69
|
+
documentation_url: "https://flue.dev/docs/proxy-policy"
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
return [{
|
|
73
|
+
name: "github-api",
|
|
74
|
+
target: "https://api.github.com",
|
|
75
|
+
headers: {
|
|
76
|
+
authorization: `token ${token}`,
|
|
77
|
+
host: "api.github.com",
|
|
78
|
+
"user-agent": "flue-proxy"
|
|
79
|
+
},
|
|
80
|
+
policy: resolvedPolicy,
|
|
81
|
+
socket: true,
|
|
82
|
+
env: { GH_TOKEN: "proxy-placeholder" },
|
|
83
|
+
setup: ["gh config set http_unix_socket {{socketPath}} 2>/dev/null || true"],
|
|
84
|
+
instructions: ["The `gh` CLI is pre-configured with authentication.", "For GitHub API calls, prefer `gh api` over raw `curl`."].join(" "),
|
|
85
|
+
denyResponse
|
|
86
|
+
}, {
|
|
87
|
+
name: "github-git",
|
|
88
|
+
target: "https://github.com",
|
|
89
|
+
headers: {
|
|
90
|
+
authorization: `Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`,
|
|
91
|
+
"user-agent": "flue-proxy"
|
|
92
|
+
},
|
|
93
|
+
policy: resolvedPolicy,
|
|
94
|
+
setup: ["git config --global url.\"{{proxyUrl}}/\".insteadOf \"https://github.com/\"", "git config --global http.{{proxyUrl}}/.extraheader \"Authorization: Bearer proxy-placeholder\""],
|
|
95
|
+
denyResponse
|
|
96
|
+
}];
|
|
97
|
+
};
|
|
98
|
+
factory.secretsMap = { token: "GITHUB_TOKEN" };
|
|
99
|
+
factory.proxyName = "github";
|
|
100
|
+
return factory;
|
|
86
101
|
}
|
|
87
102
|
/**
|
|
88
103
|
* Resolve a GitHub policy into a concrete ProxyPolicy.
|
|
@@ -132,42 +147,123 @@ function resolveGitHubPolicy(policy) {
|
|
|
132
147
|
*
|
|
133
148
|
* These return validator functions suitable for `PolicyRule.body`.
|
|
134
149
|
*/
|
|
135
|
-
const githubBody = {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
150
|
+
const githubBody = { graphql(opts) {
|
|
151
|
+
return (body) => {
|
|
152
|
+
const b = body;
|
|
153
|
+
if (typeof b?.query !== "string") return false;
|
|
154
|
+
if (opts?.denyMutations !== false) {
|
|
155
|
+
const trimmed = b.query.replace(/\s+/g, " ").trim();
|
|
156
|
+
if (trimmed.startsWith("mutation") || /^\s*mutation\b/.test(trimmed)) return false;
|
|
157
|
+
}
|
|
158
|
+
if (opts?.allowedOperations) {
|
|
159
|
+
if (!b.operationName || !opts.allowedOperations.includes(b.operationName)) return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
} };
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/proxies/policy.ts
|
|
167
|
+
/**
|
|
168
|
+
* Match a URL path against a glob-style pattern.
|
|
169
|
+
* `*` matches one path segment, `**` matches zero or more.
|
|
170
|
+
*/
|
|
171
|
+
function matchPath(pattern, path) {
|
|
172
|
+
return matchParts(pattern.split("/").filter(Boolean), 0, path.split("/").filter(Boolean), 0);
|
|
173
|
+
}
|
|
174
|
+
function matchParts(pattern, pi, path, pai) {
|
|
175
|
+
while (pi < pattern.length && pai < path.length) {
|
|
176
|
+
if (pattern[pi] === "**") {
|
|
177
|
+
for (let skip = pai; skip <= path.length; skip++) if (matchParts(pattern, pi + 1, path, skip)) return true;
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (pattern[pi] === "*" || pattern[pi] === path[pai]) {
|
|
181
|
+
pi++;
|
|
182
|
+
pai++;
|
|
183
|
+
} else return false;
|
|
184
|
+
}
|
|
185
|
+
while (pi < pattern.length && pattern[pi] === "**") pi++;
|
|
186
|
+
return pi === pattern.length && pai === path.length;
|
|
187
|
+
}
|
|
188
|
+
function matchMethod(ruleMethod, requestMethod) {
|
|
189
|
+
if (ruleMethod === "*") return true;
|
|
190
|
+
if (Array.isArray(ruleMethod)) return ruleMethod.some((m) => m.toUpperCase() === requestMethod.toUpperCase());
|
|
191
|
+
return ruleMethod.toUpperCase() === requestMethod.toUpperCase();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Evaluate the proxy policy for a request.
|
|
195
|
+
* Order: deny rules -> allow rules (with optional body + rate limits) -> base level.
|
|
196
|
+
*
|
|
197
|
+
* `ruleCounts` is optional — omit on Cloudflare where rate limits are not enforced.
|
|
198
|
+
* `body` validators on rules are skipped when not present (Cloudflare v1 stores
|
|
199
|
+
* policies without functions).
|
|
200
|
+
*/
|
|
201
|
+
function evaluatePolicy(method, path, parsedBody, policy, ruleCounts) {
|
|
202
|
+
if (!policy) {
|
|
203
|
+
if ([
|
|
204
|
+
"GET",
|
|
205
|
+
"HEAD",
|
|
206
|
+
"OPTIONS"
|
|
207
|
+
].includes(method.toUpperCase())) return {
|
|
208
|
+
allowed: true,
|
|
209
|
+
reason: ""
|
|
145
210
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const b = body;
|
|
150
|
-
if (typeof b?.body !== "string") return false;
|
|
151
|
-
if (opts.maxLength && b.body.length > opts.maxLength) return false;
|
|
152
|
-
if (opts.pattern && !opts.pattern.test(b.body)) return false;
|
|
153
|
-
return true;
|
|
211
|
+
return {
|
|
212
|
+
allowed: false,
|
|
213
|
+
reason: "not allowed by default allow-read policy"
|
|
154
214
|
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
215
|
+
}
|
|
216
|
+
if (policy.deny) {
|
|
217
|
+
for (const rule of policy.deny) if (matchMethod(rule.method, method) && matchPath(rule.path, path)) {
|
|
218
|
+
if (rule.body === void 0 || rule.body(parsedBody) === true) return {
|
|
219
|
+
allowed: false,
|
|
220
|
+
reason: "matched deny rule"
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (policy.allow) for (let i = 0; i < policy.allow.length; i++) {
|
|
225
|
+
const rule = policy.allow[i];
|
|
226
|
+
if (matchMethod(rule.method, method) && matchPath(rule.path, path)) {
|
|
227
|
+
if (rule.body !== void 0 && rule.body(parsedBody) === false) continue;
|
|
228
|
+
if (rule.limit !== void 0 && ruleCounts) {
|
|
229
|
+
const key = `allow:${i}`;
|
|
230
|
+
const count = ruleCounts.get(key) || 0;
|
|
231
|
+
if (count >= rule.limit) return {
|
|
232
|
+
allowed: false,
|
|
233
|
+
reason: `limit reached (${count}/${rule.limit})`
|
|
234
|
+
};
|
|
235
|
+
ruleCounts.set(key, count + 1);
|
|
166
236
|
}
|
|
167
|
-
return
|
|
237
|
+
return {
|
|
238
|
+
allowed: true,
|
|
239
|
+
reason: ""
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
switch (policy.base || "allow-read") {
|
|
244
|
+
case "allow-all": return {
|
|
245
|
+
allowed: true,
|
|
246
|
+
reason: ""
|
|
168
247
|
};
|
|
248
|
+
case "deny-all": return {
|
|
249
|
+
allowed: false,
|
|
250
|
+
reason: "base policy: deny-all"
|
|
251
|
+
};
|
|
252
|
+
default:
|
|
253
|
+
if ([
|
|
254
|
+
"GET",
|
|
255
|
+
"HEAD",
|
|
256
|
+
"OPTIONS"
|
|
257
|
+
].includes(method.toUpperCase())) return {
|
|
258
|
+
allowed: true,
|
|
259
|
+
reason: ""
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
allowed: false,
|
|
263
|
+
reason: "not allowed by allow-read policy"
|
|
264
|
+
};
|
|
169
265
|
}
|
|
170
|
-
}
|
|
266
|
+
}
|
|
171
267
|
|
|
172
268
|
//#endregion
|
|
173
|
-
export { anthropic, github, githubBody };
|
|
269
|
+
export { anthropic, evaluatePolicy, github, githubBody, matchMethod, matchPath };
|
|
@@ -172,5 +172,18 @@ interface PolicyRule {
|
|
|
172
172
|
* The CLI auto-flattens with proxies.flat().
|
|
173
173
|
*/
|
|
174
174
|
type ProxyPresetResult = ProxyService | ProxyService[];
|
|
175
|
+
/**
|
|
176
|
+
* A preset function with deferred secrets. Call without secrets to get
|
|
177
|
+
* a factory; call the factory with secrets to get resolved ProxyService(s).
|
|
178
|
+
*
|
|
179
|
+
* `secretsMap` maps each TSecrets key to its conventional env var name
|
|
180
|
+
* (e.g., `{ apiKey: 'ANTHROPIC_API_KEY' }`). The CLI uses this to
|
|
181
|
+
* auto-resolve secrets from `process.env`.
|
|
182
|
+
*/
|
|
183
|
+
interface ProxyFactory<TSecrets extends Record<string, string>> {
|
|
184
|
+
(secrets: TSecrets): ProxyService | ProxyService[];
|
|
185
|
+
secretsMap: { [K in keyof TSecrets]: string };
|
|
186
|
+
proxyName: string;
|
|
187
|
+
}
|
|
175
188
|
//#endregion
|
|
176
|
-
export { ProxyService as i,
|
|
189
|
+
export { ProxyService as a, ProxyPresetResult as i, ProxyFactory as n, ProxyPolicy as r, PolicyRule as t };
|