@flue/client 0.0.22 → 0.0.24
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/README.md +19 -6
- package/dist/index.d.mts +24 -15
- package/dist/index.mjs +15 -20
- package/dist/proxies/index.d.mts +42 -20
- package/dist/proxies/index.mjs +213 -115
- package/dist/{types-Ch4cwdD8.d.mts → types-DnTJOlQ7.d.mts} +31 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,10 +69,23 @@ const summary = await flue.prompt('Summarize these test failures: ...', {
|
|
|
69
69
|
|
|
70
70
|
Options: `result`, `model`
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
## Proxies (Sandbox Mode)
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
In sandbox mode, the AI agent runs inside a sandbox container with no access to sensitive host credentials. Proxies let the sandbox talk to external services without leaking any actual credentials into the sandbox.
|
|
75
|
+
|
|
76
|
+
Flue ships with built-in presets for popular services. Every proxy supports an access control policy (`policy`) option for advanced control over what the sandbox has access to do. Built-in levels like `'allow-read'` and `'allow-all'` cover common service-specific policy rules, and you can extend them with explicit allow/deny rules for fine-grained control:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { anthropic, github } from '@flue/client/proxies';
|
|
80
|
+
|
|
81
|
+
export const proxies = [
|
|
82
|
+
anthropic(),
|
|
83
|
+
github({
|
|
84
|
+
token: process.env.GH_TOKEN!,
|
|
85
|
+
policy: {
|
|
86
|
+
base: 'allow-read',
|
|
87
|
+
allow: [{ method: 'POST', path: '/repos/withastro/astro/issues/*/comments', limit: 1 }],
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
```
|
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,34 @@ 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
|
+
/**
|
|
90
|
+
* Custom fetch implementation for reaching the OpenCode server.
|
|
91
|
+
* Use this when the OpenCode server is not reachable via global fetch
|
|
92
|
+
* (e.g. from a Cloudflare Worker, route through sandbox.containerFetch).
|
|
93
|
+
*
|
|
94
|
+
* When omitted, the SDK's default fetch (globalThis.fetch) is used.
|
|
95
|
+
*/
|
|
96
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
97
|
+
/**
|
|
98
|
+
* Custom shell implementation for executing commands.
|
|
99
|
+
* Use this when child_process is not available (e.g. Cloudflare Workers)
|
|
100
|
+
* and commands should be routed through sandbox.exec() instead.
|
|
101
|
+
*
|
|
102
|
+
* When omitted, commands run via Node.js child_process.exec.
|
|
103
|
+
*/
|
|
104
|
+
shell?: (command: string, options?: ShellOptions) => Promise<ShellResult>;
|
|
93
105
|
}
|
|
94
106
|
interface SkillOptions<S extends v.GenericSchema | undefined = undefined> {
|
|
95
107
|
/** Key-value args serialized into the prompt. */
|
|
@@ -128,16 +140,13 @@ interface ShellResult {
|
|
|
128
140
|
}
|
|
129
141
|
//#endregion
|
|
130
142
|
//#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>;
|
|
143
|
+
declare class FlueClient {
|
|
136
144
|
private readonly workdir;
|
|
137
145
|
private readonly proxyInstructions;
|
|
138
146
|
private readonly model?;
|
|
139
147
|
private readonly client;
|
|
140
|
-
|
|
148
|
+
private readonly shellFn?;
|
|
149
|
+
constructor(options: FlueClientOptions);
|
|
141
150
|
/** Run a named skill with a result schema. */
|
|
142
151
|
skill<S extends v.GenericSchema>(name: string, options: SkillOptions<S> & {
|
|
143
152
|
result: S;
|
|
@@ -156,4 +165,4 @@ declare class Flue {
|
|
|
156
165
|
close(): Promise<void>;
|
|
157
166
|
}
|
|
158
167
|
//#endregion
|
|
159
|
-
export {
|
|
168
|
+
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
|
@@ -326,9 +326,9 @@ function extractLastResultBlock(text) {
|
|
|
326
326
|
//#endregion
|
|
327
327
|
//#region src/skill.ts
|
|
328
328
|
/** How often to poll and log progress (ms). */
|
|
329
|
-
const POLL_INTERVAL =
|
|
329
|
+
const POLL_INTERVAL = 15e3;
|
|
330
330
|
/** Max times we'll see 0 assistant messages before giving up. */
|
|
331
|
-
const MAX_EMPTY_POLLS =
|
|
331
|
+
const MAX_EMPTY_POLLS = 20;
|
|
332
332
|
/** Max time to poll before timing out (ms) - 45 minutes. */
|
|
333
333
|
const MAX_POLL_TIME = 2700 * 1e3;
|
|
334
334
|
/**
|
|
@@ -430,7 +430,7 @@ async function pollUntilIdle(client, sessionId, workdir, label, startTime) {
|
|
|
430
430
|
const parts = await fetchAllAssistantParts(client, sessionId, workdir);
|
|
431
431
|
if (parts.length === 0) {
|
|
432
432
|
emptyPolls++;
|
|
433
|
-
if (emptyPolls %
|
|
433
|
+
if (emptyPolls % 4 === 0) {
|
|
434
434
|
console.log(`[flue] ${label}: status result: ${JSON.stringify({
|
|
435
435
|
hasData: !!statusResult.data,
|
|
436
436
|
sessionIds: statusResult.data ? Object.keys(statusResult.data) : [],
|
|
@@ -455,7 +455,7 @@ async function pollUntilIdle(client, sessionId, workdir, label, startTime) {
|
|
|
455
455
|
}
|
|
456
456
|
return parts;
|
|
457
457
|
}
|
|
458
|
-
if (pollCount %
|
|
458
|
+
if (pollCount % 4 === 0) console.log(`[flue] ${label}: running (${elapsed}s)`);
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
/**
|
|
@@ -478,33 +478,26 @@ function sleep(ms) {
|
|
|
478
478
|
|
|
479
479
|
//#endregion
|
|
480
480
|
//#region src/flue.ts
|
|
481
|
-
var
|
|
482
|
-
/** Working branch for commits. */
|
|
483
|
-
branch;
|
|
484
|
-
/** Workflow arguments passed by the runner. */
|
|
485
|
-
args;
|
|
481
|
+
var FlueClient = class {
|
|
486
482
|
workdir;
|
|
487
483
|
proxyInstructions;
|
|
488
484
|
model;
|
|
489
485
|
client;
|
|
486
|
+
shellFn;
|
|
490
487
|
constructor(options) {
|
|
491
|
-
this.
|
|
492
|
-
this.args = options.args ?? {};
|
|
493
|
-
this.proxyInstructions = options.proxyInstructions ?? [];
|
|
488
|
+
this.proxyInstructions = options.proxies?.map((p) => p.instructions).filter((i) => !!i) ?? [];
|
|
494
489
|
this.workdir = options.workdir;
|
|
495
490
|
this.model = options.model;
|
|
491
|
+
this.shellFn = options.shell;
|
|
496
492
|
this.client = createOpencodeClient({
|
|
497
493
|
baseUrl: options.opencodeUrl ?? "http://localhost:48765",
|
|
498
|
-
directory: this.workdir
|
|
494
|
+
directory: this.workdir,
|
|
495
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
499
496
|
});
|
|
500
497
|
}
|
|
501
498
|
async skill(name, options) {
|
|
502
499
|
const mergedOptions = {
|
|
503
500
|
...options,
|
|
504
|
-
args: this.args || options?.args ? {
|
|
505
|
-
...this.args,
|
|
506
|
-
...options?.args
|
|
507
|
-
} : void 0,
|
|
508
501
|
model: options?.model ?? this.model
|
|
509
502
|
};
|
|
510
503
|
return runSkill(this.client, this.workdir, name, mergedOptions, this.proxyInstructions);
|
|
@@ -527,14 +520,16 @@ var Flue = class {
|
|
|
527
520
|
}
|
|
528
521
|
/** Execute a shell command with scoped environment variables. */
|
|
529
522
|
async shell(command, options) {
|
|
530
|
-
|
|
523
|
+
const mergedOptions = {
|
|
531
524
|
...options,
|
|
532
525
|
cwd: options?.cwd ?? this.workdir
|
|
533
|
-
}
|
|
526
|
+
};
|
|
527
|
+
if (this.shellFn) return this.shellFn(command, mergedOptions);
|
|
528
|
+
return runShell(command, mergedOptions);
|
|
534
529
|
}
|
|
535
530
|
/** Close the OpenCode client connection. */
|
|
536
531
|
async close() {}
|
|
537
532
|
};
|
|
538
533
|
|
|
539
534
|
//#endregion
|
|
540
|
-
export {
|
|
535
|
+
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,62 +58,70 @@ 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
|
-
* Resolve GitHub
|
|
103
|
+
* Resolve a GitHub policy into a concrete ProxyPolicy.
|
|
89
104
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
105
|
+
* Accepts a core policy level string ('allow-read', 'allow-all', 'deny-all')
|
|
106
|
+
* or a ProxyPolicy object. Defaults to 'allow-read' if omitted.
|
|
107
|
+
*
|
|
108
|
+
* When the base is 'allow-read', GitHub-specific allow rules are prepended
|
|
109
|
+
* so that the gh CLI (GraphQL queries) and git clone/fetch work out of the box.
|
|
110
|
+
* Other base levels are used as-is.
|
|
94
111
|
*/
|
|
95
112
|
function resolveGitHubPolicy(policy) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
const base = typeof policy === "string" ? policy : policy?.base ?? "allow-read";
|
|
114
|
+
const userAllow = typeof policy === "object" ? policy.allow ?? [] : [];
|
|
115
|
+
const userDeny = typeof policy === "object" ? policy.deny ?? [] : [];
|
|
116
|
+
switch (base) {
|
|
117
|
+
case "allow-all":
|
|
118
|
+
case "deny-all": return {
|
|
119
|
+
base,
|
|
120
|
+
allow: userAllow,
|
|
121
|
+
deny: userDeny
|
|
105
122
|
};
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
default: return {
|
|
124
|
+
base: "allow-read",
|
|
108
125
|
allow: [
|
|
109
126
|
{
|
|
110
127
|
method: "POST",
|
|
@@ -118,11 +135,11 @@ function resolveGitHubPolicy(policy) {
|
|
|
118
135
|
{
|
|
119
136
|
method: "GET",
|
|
120
137
|
path: "/*/info/refs"
|
|
121
|
-
}
|
|
122
|
-
|
|
138
|
+
},
|
|
139
|
+
...userAllow
|
|
140
|
+
],
|
|
141
|
+
deny: userDeny
|
|
123
142
|
};
|
|
124
|
-
case "allow-all": return { default: "allow-all" };
|
|
125
|
-
default: throw new Error(`Unknown github() policy level: '${policy}'`);
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
/**
|
|
@@ -130,42 +147,123 @@ function resolveGitHubPolicy(policy) {
|
|
|
130
147
|
*
|
|
131
148
|
* These return validator functions suitable for `PolicyRule.body`.
|
|
132
149
|
*/
|
|
133
|
-
const githubBody = {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
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: ""
|
|
143
210
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const b = body;
|
|
148
|
-
if (typeof b?.body !== "string") return false;
|
|
149
|
-
if (opts.maxLength && b.body.length > opts.maxLength) return false;
|
|
150
|
-
if (opts.pattern && !opts.pattern.test(b.body)) return false;
|
|
151
|
-
return true;
|
|
211
|
+
return {
|
|
212
|
+
allowed: false,
|
|
213
|
+
reason: "not allowed by default allow-read policy"
|
|
152
214
|
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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);
|
|
164
236
|
}
|
|
165
|
-
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: ""
|
|
247
|
+
};
|
|
248
|
+
case "deny-all": return {
|
|
249
|
+
allowed: false,
|
|
250
|
+
reason: "base policy: deny-all"
|
|
166
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
|
+
};
|
|
167
265
|
}
|
|
168
|
-
}
|
|
266
|
+
}
|
|
169
267
|
|
|
170
268
|
//#endregion
|
|
171
|
-
export { anthropic, github, githubBody };
|
|
269
|
+
export { anthropic, evaluatePolicy, github, githubBody, matchMethod, matchPath };
|
|
@@ -63,14 +63,14 @@ interface ProxyService {
|
|
|
63
63
|
/**
|
|
64
64
|
* Access control policy for this proxy.
|
|
65
65
|
*
|
|
66
|
-
* String shorthand: a
|
|
67
|
-
* Object form: full control with
|
|
66
|
+
* String shorthand: a core policy level ('allow-read', 'allow-all', 'deny-all').
|
|
67
|
+
* Object form: full control with base level + allow/deny rules.
|
|
68
68
|
*
|
|
69
|
-
* When a string is provided, it is equivalent to {
|
|
69
|
+
* When a string is provided, it is equivalent to { base: theString }.
|
|
70
70
|
*
|
|
71
|
-
* If omitted, defaults to 'read
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* If omitted, defaults to 'allow-read' (GET/HEAD/OPTIONS only).
|
|
72
|
+
* Presets may extend the base level with additional allow rules
|
|
73
|
+
* appropriate for their service.
|
|
74
74
|
*/
|
|
75
75
|
policy?: string | ProxyPolicy;
|
|
76
76
|
/**
|
|
@@ -108,16 +108,22 @@ interface ProxyService {
|
|
|
108
108
|
}
|
|
109
109
|
interface ProxyPolicy {
|
|
110
110
|
/**
|
|
111
|
-
* The base policy level
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* The base policy level that applies when no allow/deny rule matches.
|
|
112
|
+
*
|
|
113
|
+
* - `'allow-read'`: allow GET/HEAD/OPTIONS, deny everything else
|
|
114
|
+
* - `'allow-all'`: allow everything
|
|
115
|
+
* - `'deny-all'`: deny everything
|
|
116
|
+
*
|
|
117
|
+
* Presets may extend the base level with additional allow rules
|
|
118
|
+
* (e.g., the GitHub preset adds GraphQL query and git clone support
|
|
119
|
+
* on top of `'allow-read'`).
|
|
114
120
|
*/
|
|
115
|
-
|
|
121
|
+
base: string;
|
|
116
122
|
/**
|
|
117
123
|
* Explicit allow rules. Evaluated after deny rules.
|
|
118
124
|
* A request matching an allow rule is permitted (subject to rate limits).
|
|
119
125
|
* If a request matches method + path but fails body validation, the rule
|
|
120
|
-
* does not match and evaluation continues to the next rule or
|
|
126
|
+
* does not match and evaluation continues to the next rule or base level.
|
|
121
127
|
*/
|
|
122
128
|
allow?: PolicyRule[];
|
|
123
129
|
/**
|
|
@@ -166,5 +172,18 @@ interface PolicyRule {
|
|
|
166
172
|
* The CLI auto-flattens with proxies.flat().
|
|
167
173
|
*/
|
|
168
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
|
+
}
|
|
169
188
|
//#endregion
|
|
170
|
-
export { ProxyService as i,
|
|
189
|
+
export { ProxyService as a, ProxyPresetResult as i, ProxyFactory as n, ProxyPolicy as r, PolicyRule as t };
|