@flue/client 0.0.23 → 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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as ProxyService, n as ProxyPolicy, r as ProxyPresetResult, t as PolicyRule } from "./types-37Tgp-H9.mjs";
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 FlueOptions {
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
- /** Working branch for commits. */
83
- branch?: string;
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 Flue {
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
- constructor(options: FlueOptions);
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 { Flue, type FlueEvent, type FlueOptions, type PolicyRule, type PromptOptions, type ProxyPolicy, type ProxyPresetResult, type ProxyService, type ShellOptions, type ShellResult, type SkillOptions, SkillOutputError, transformEvent };
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 = 5e3;
329
+ const POLL_INTERVAL = 15e3;
330
330
  /** Max times we'll see 0 assistant messages before giving up. */
331
- const MAX_EMPTY_POLLS = 60;
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 % 12 === 0) {
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 % 12 === 0) console.log(`[flue] ${label}: running (${elapsed}s)`);
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 Flue = class {
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.branch = options.branch ?? "main";
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
- return runShell(command, {
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 { Flue, SkillOutputError, transformEvent };
535
+ export { FlueClient, SkillOutputError, transformEvent };
@@ -1,23 +1,25 @@
1
- import { i as ProxyService, n as ProxyPolicy, r as ProxyPresetResult, t as PolicyRule } from "../types-37Tgp-H9.mjs";
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
- * Proxies requests to api.anthropic.com with the API key injected.
8
- * Reads ANTHROPIC_API_KEY from the environment by default.
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
- }): ProxyService;
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 two `ProxyService` objects:
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
- }): ProxyService[];
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 an issue creation body. */issue(opts: {
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
- export { type PolicyRule, type ProxyPolicy, type ProxyPresetResult, type ProxyService, anthropic, github, githubBody };
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 };
@@ -2,37 +2,45 @@
2
2
  /**
3
3
  * Anthropic model provider proxy preset.
4
4
  *
5
- * Proxies requests to api.anthropic.com with the API key injected.
6
- * Reads ANTHROPIC_API_KEY from the environment by default.
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 apiKey = opts?.apiKey || process.env.ANTHROPIC_API_KEY;
11
- if (!apiKey) throw new Error("anthropic() proxy requires ANTHROPIC_API_KEY. Set it in your environment or pass { apiKey } explicitly.");
12
- return {
13
- name: "anthropic",
14
- target: "https://api.anthropic.com",
15
- transform: (req) => {
16
- const safe = [
17
- "content-type",
18
- "content-length",
19
- "accept",
20
- "anthropic-version",
21
- "anthropic-beta",
22
- "user-agent"
23
- ];
24
- const filtered = {};
25
- for (const key of safe) if (req.headers[key]) filtered[key] = req.headers[key];
26
- filtered["x-api-key"] = apiKey;
27
- return { headers: filtered };
28
- },
29
- policy: opts?.policy ?? "allow-all",
30
- isModelProvider: true,
31
- providerConfig: {
32
- providerKey: "anthropic",
33
- options: { apiKey: "sk-dummy-value-real-key-injected-by-proxy" }
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 two `ProxyService` objects:
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 resolvedPolicy = resolveGitHubPolicy(opts.policy);
53
- const denyResponse = ({ method, path, reason }) => ({
54
- status: 403,
55
- headers: { "Content-Type": "application/json" },
56
- body: JSON.stringify({
57
- message: `Blocked by flue proxy policy: ${method} ${path} — ${reason}`,
58
- documentation_url: "https://flue.dev/docs/proxy-policy"
59
- })
60
- });
61
- return [{
62
- name: "github-api",
63
- target: "https://api.github.com",
64
- headers: {
65
- authorization: `token ${opts.token}`,
66
- host: "api.github.com",
67
- "user-agent": "flue-proxy"
68
- },
69
- policy: resolvedPolicy,
70
- socket: true,
71
- env: { GH_TOKEN: "proxy-placeholder" },
72
- setup: ["gh config set http_unix_socket {{socketPath}} 2>/dev/null || true"],
73
- instructions: ["The `gh` CLI is pre-configured with authentication.", "For GitHub API calls, prefer `gh api` over raw `curl`."].join(" "),
74
- denyResponse
75
- }, {
76
- name: "github-git",
77
- target: "https://github.com",
78
- headers: {
79
- authorization: `Basic ${Buffer.from(`x-access-token:${opts.token}`).toString("base64")}`,
80
- "user-agent": "flue-proxy"
81
- },
82
- policy: resolvedPolicy,
83
- setup: ["git config --global url.\"{{proxyUrl}}/\".insteadOf \"https://github.com/\"", "git config --global http.{{proxyUrl}}/.extraheader \"Authorization: Bearer proxy-placeholder\""],
84
- denyResponse
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
- issue(opts) {
137
- return (body) => {
138
- const b = body;
139
- if (opts.titleMatch && !opts.titleMatch.test(b?.title ?? "")) return false;
140
- if (opts.requiredLabels) {
141
- const labels = (b?.labels ?? []).map((l) => typeof l === "string" ? l : l?.name);
142
- if (!opts.requiredLabels.every((r) => labels.includes(r))) return false;
143
- }
144
- return true;
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
- comment(opts) {
148
- return (body) => {
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
- graphql(opts) {
157
- return (body) => {
158
- const b = body;
159
- if (typeof b?.query !== "string") return false;
160
- if (opts?.denyMutations !== false) {
161
- const trimmed = b.query.replace(/\s+/g, " ").trim();
162
- if (trimmed.startsWith("mutation") || /^\s*mutation\b/.test(trimmed)) return false;
163
- }
164
- if (opts?.allowedOperations) {
165
- if (!b.operationName || !opts.allowedOperations.includes(b.operationName)) return false;
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 true;
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, ProxyPolicy as n, ProxyPresetResult as r, PolicyRule as t };
189
+ export { ProxyService as a, ProxyPresetResult as i, ProxyFactory as n, ProxyPolicy as r, PolicyRule as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flue/client",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {