@workerkit/core 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +10 -3
  3. package/SECURITY.md +34 -0
  4. package/dist/client.d.ts +5 -4
  5. package/dist/client.js +51 -18
  6. package/dist/client.js.map +1 -1
  7. package/dist/descriptors/decision-authoring.d.ts +0 -1
  8. package/dist/descriptors/decision-receipt.d.ts +0 -1
  9. package/dist/descriptors/directory.d.ts +0 -1
  10. package/dist/descriptors/directory.js +2 -2
  11. package/dist/descriptors/directory.js.map +1 -1
  12. package/dist/descriptors/index.d.ts +1 -2
  13. package/dist/descriptors/index.js +1 -1
  14. package/dist/descriptors/index.js.map +1 -1
  15. package/dist/descriptors/manage.d.ts +0 -1
  16. package/dist/descriptors/manage.js +37 -15
  17. package/dist/descriptors/manage.js.map +1 -1
  18. package/dist/descriptors/onboarding.d.ts +0 -1
  19. package/dist/descriptors/types.d.ts +2 -1
  20. package/dist/descriptors/types.js.map +1 -1
  21. package/dist/errors.d.ts +0 -1
  22. package/dist/execute.d.ts +0 -1
  23. package/dist/execute.js +5 -1
  24. package/dist/execute.js.map +1 -1
  25. package/dist/index.d.ts +0 -1
  26. package/dist/redact.d.ts +0 -1
  27. package/dist/zod.d.ts +0 -1
  28. package/docs/hybrid-workers.md +62 -0
  29. package/package.json +10 -5
  30. package/dist/client.d.ts.map +0 -1
  31. package/dist/descriptors/decision-authoring.d.ts.map +0 -1
  32. package/dist/descriptors/decision-receipt.d.ts.map +0 -1
  33. package/dist/descriptors/directory.d.ts.map +0 -1
  34. package/dist/descriptors/index.d.ts.map +0 -1
  35. package/dist/descriptors/manage.d.ts.map +0 -1
  36. package/dist/descriptors/onboarding.d.ts.map +0 -1
  37. package/dist/descriptors/types.d.ts.map +0 -1
  38. package/dist/errors.d.ts.map +0 -1
  39. package/dist/execute.d.ts.map +0 -1
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/redact.d.ts.map +0 -1
  42. package/dist/zod.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ and the package adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.8] - 2026-09-29
10
+
11
+ ### Added
12
+
13
+ Adds worker_decision_set with strict classifier, answer and revision inputs. Hybrid instruction and receipt contracts preserve classification, agent evidence, separate usage and actual wallet charges. Explicit worker_run waits use a bounded per-request timeout; POST requests remain non-retryable.
14
+
15
+ ### Fixed
16
+
17
+ - Reserve authentication and computed headers regardless of casing; keep anonymous discovery free of bearer credentials.
18
+ - Exclude credentials and query strings from request logs; cancel active requests and retry waits promptly.
19
+ - Validate client resource limits and reject credential-bearing base URLs.
20
+ - Update Undici to the patched 7.29.1 release.
21
+
22
+ ### Changed
23
+
24
+ - Build clean package contents and validate versions, release notes, installed packages and release tags before publication.
25
+ - Publish npm packages with provenance and attach the package to a matching GitHub Release.
26
+
9
27
  ## [0.3.7] - 2026-09-20
10
28
 
11
29
  ### Added
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
  [![license](https://img.shields.io/npm/l/%40workerkit%2Fcore.svg)](LICENSE)
7
7
 
8
8
  The shareable [WorkerKit](https://workerkit.ai) surface: an HTTP API client, the
9
- declarative tool-descriptor registry (84 authenticated fleet-management tools +
9
+ declarative tool-descriptor registry (85 authenticated fleet-management tools +
10
10
  9 anonymous kits-directory tools), and the single wire-execution path that turns
11
- a descriptor plus parameters into exactly one upstream request.
11
+ a descriptor plus parameters into one HTTP operation.
12
12
 
13
13
  Deliberately MCP-free: descriptors carry MCP-shaped annotations and zod schemas,
14
14
  but nothing here depends on an MCP SDK, so an MCP server, a CLI, or docs tooling
@@ -46,7 +46,7 @@ if (isSuccess(result)) {
46
46
 
47
47
  // Authenticated: list your worker fleet with a manager key.
48
48
  const list = byName("workers_list")!;
49
- const fleet = await executeTool(client, list, {}, { token: process.env.WORKERKIT_MANAGER_KEY });
49
+ const fleet = await executeTool(client, list, {}, { token: process.env.WK_MANAGER_KEY });
50
50
 
51
51
  await client.close();
52
52
  ```
@@ -61,6 +61,13 @@ whole-call deadline, bounded retries for idempotent methods only, a streamed
61
61
  response-size cap, and quota/rate-limit header extraction. `scrubSecrets` helps
62
62
  keep credentials out of logs.
63
63
 
64
+ ## Classification followed by an agent task
65
+
66
+ A hybrid worker classifies a bounded source window and runs one language-model agent
67
+ on the selected batch. Core exposes the configuration and a single run receipt;
68
+ model execution, permissions and billing remain server responsibilities. See the
69
+ [hybrid worker guide](https://github.com/workerkit/core/blob/main/docs/hybrid-workers.md).
70
+
64
71
  ## Development
65
72
 
66
73
  ```bash
package/SECURITY.md ADDED
@@ -0,0 +1,34 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Email **security@workerkit.ai**.
6
+ Please do not open public issues for security reports.
7
+
8
+ This document covers the `@workerkit/core` library specifically. For the platform, see the
9
+ [Security Overview](https://workerkit.ai/security), [Privacy Policy](https://workerkit.ai/privacy),
10
+ and [Terms of Service](https://workerkit.ai/terms).
11
+
12
+ ## Scope
13
+
14
+ `@workerkit/core` is a client library. It stores no credentials and keeps no state: bearer
15
+ tokens are supplied per call by the consumer and forwarded on the one request they authorize,
16
+ never logged or persisted. The `scrubSecrets` helper exists so consumers can redact key
17
+ material (`pe_mgr_` prefixes and bearer headers) from their own logs.
18
+
19
+ Properties relevant to a security review:
20
+
21
+ - Descriptors marked `anonymous` never forward an Authorization header, even when the caller
22
+ supplies a token.
23
+ - The HTTP client pins its origin at construction; descriptor paths cannot redirect a request
24
+ to another host.
25
+ - Responses are read through a hard size cap and per-attempt timeouts under a whole-call
26
+ deadline; retries apply to idempotent methods only and never to 429s.
27
+ - The package has two runtime dependencies (`undici`, `zod`), both exact-pinned.
28
+
29
+ ## Supported versions
30
+
31
+ The latest published version. Releases are tag-driven from
32
+ [github.com/workerkit/core](https://github.com/workerkit/core) via npm trusted publishing, and
33
+ every version carries a provenance attestation linking the tarball to the commit and workflow
34
+ that built it.
package/dist/client.d.ts CHANGED
@@ -29,15 +29,16 @@ export interface ClientOptions {
29
29
  requestIdProvider?: () => string | undefined;
30
30
  }
31
31
  export interface RequestOpts {
32
+ /** Override only this request, e.g. an explicit run wait. POST is still never retried. */
33
+ timeoutMs?: number;
32
34
  /** Bearer forwarded upstream. Omit for anonymous endpoints. */
33
35
  token?: string;
34
36
  params?: Record<string, unknown>;
35
37
  body?: unknown;
36
38
  signal?: AbortSignal;
37
39
  /**
38
- * Extra request headers. Every computed header (Authorization, X-Request-Id,
39
- * User-Agent, Accept, Content-Type) is assigned AFTER this spread and wins,
40
- * so these can never override one.
40
+ * Extra request headers, such as Idempotency-Key. Authentication, routing and
41
+ * computed headers are reserved, regardless of casing. Use token for authentication.
41
42
  */
42
43
  headers?: Record<string, string>;
43
44
  }
@@ -85,6 +86,7 @@ export declare class PortEdenClient {
85
86
  patch<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>>;
86
87
  put<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>>;
87
88
  delete<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>>;
89
+ private requestTimeout;
88
90
  private doRequest;
89
91
  private singleAttempt;
90
92
  private buildUrl;
@@ -94,4 +96,3 @@ export declare class PortEdenClient {
94
96
  }
95
97
  /** Brand-facing alias — the same client, published under the WorkerKit name. */
96
98
  export { PortEdenClient as WorkerKitClient };
97
- //# sourceMappingURL=client.d.ts.map
package/dist/client.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Agent, request as undiciRequest } from "undici";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { scrubSecrets } from "./redact.js";
3
5
  /**
4
6
  * The upstream response was well-formed but larger than the configured read budget, so we
5
7
  * abandoned it mid-stream. Typed because it must not be retried: the response is deterministic,
@@ -29,6 +31,20 @@ const BACKOFF_MS = [200, 500]; // between attempts 1→2 and 2→3
29
31
  // Ceiling on any Retry-After honoured below. Dormant today: no RETRYABLE_STATUS carries
30
32
  // one, so quota.retryAfter is always null there. Read the 429 note above before changing.
31
33
  const MAX_BACKOFF_MS = 2_000;
34
+ const RESERVED_HEADERS = new Set([
35
+ "authorization", "proxy-authorization", "host", "x-request-id",
36
+ "user-agent", "accept", "content-type", "content-length", "transfer-encoding",
37
+ ]);
38
+ function positiveInteger(value, name) {
39
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647) {
40
+ throw new RangeError(`${name} must be a positive integer no greater than 2147483647`);
41
+ }
42
+ return value;
43
+ }
44
+ function logPath(path, token) {
45
+ const pathname = path.split(/[?#]/, 1)[0] ?? "/";
46
+ return scrubSecrets(token ? pathname.split(token).join("[REDACTED]") : pathname);
47
+ }
32
48
  const NOOP_LOGGER = {
33
49
  info() {
34
50
  /* no-op */
@@ -50,10 +66,14 @@ export class PortEdenClient {
50
66
  logger;
51
67
  requestIdProvider;
52
68
  constructor(options) {
53
- this.baseUrl = options.baseUrl;
54
- this.baseOrigin = new URL(options.baseUrl).origin;
55
- this.timeoutMs = options.timeoutMs ?? 30_000;
56
- this.maxResponseBodyBytes = options.maxResponseBodyBytes ?? 10 * 1024 * 1024;
69
+ const base = new URL(options.baseUrl);
70
+ if (!["http:", "https:"].includes(base.protocol) || base.username || base.password) {
71
+ throw new Error("baseUrl must be an HTTP(S) URL without embedded credentials");
72
+ }
73
+ this.baseUrl = base.toString();
74
+ this.baseOrigin = base.origin;
75
+ this.timeoutMs = positiveInteger(options.timeoutMs ?? 30_000, "timeoutMs");
76
+ this.maxResponseBodyBytes = positiveInteger(options.maxResponseBodyBytes ?? 10 * 1024 * 1024, "maxResponseBodyBytes");
57
77
  this.userAgent = options.userAgent;
58
78
  this.logger = options.logger ?? NOOP_LOGGER;
59
79
  this.requestIdProvider = options.requestIdProvider;
@@ -61,7 +81,7 @@ export class PortEdenClient {
61
81
  keepAliveTimeout: 30_000,
62
82
  keepAliveMaxTimeout: 60_000,
63
83
  pipelining: 1,
64
- connections: options.maxConnections ?? 16,
84
+ connections: positiveInteger(options.maxConnections ?? 16, "maxConnections"),
65
85
  connect: { rejectUnauthorized: true },
66
86
  // Defence in depth: the per-attempt AbortSignal below already bounds each
67
87
  // call, but undici's own limits default to ~300s — pin them to the same
@@ -85,6 +105,10 @@ export class PortEdenClient {
85
105
  async delete(path, opts) {
86
106
  return this.doRequest("DELETE", path, opts);
87
107
  }
108
+ requestTimeout(opts) {
109
+ return opts.timeoutMs !== undefined && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0
110
+ ? Math.max(1, Math.floor(Math.min(300_000, opts.timeoutMs))) : this.timeoutMs;
111
+ }
88
112
  async doRequest(method, path, opts) {
89
113
  const isIdempotent = IDEMPOTENT_METHODS.has(method);
90
114
  const maxAttempts = isIdempotent ? MAX_ATTEMPTS : 1;
@@ -92,7 +116,7 @@ export class PortEdenClient {
92
116
  // worst case against a slow-then-failing upstream is ~3 x timeoutMs plus
93
117
  // backoff, holding a concurrency slot for ~90s. Two timeout budgets is
94
118
  // enough for one slow attempt plus a meaningful retry.
95
- const totalDeadlineAt = performance.now() + this.timeoutMs * 2;
119
+ const totalDeadlineAt = performance.now() + this.requestTimeout(opts) * 2;
96
120
  let result;
97
121
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
98
122
  if (opts.signal?.aborted) {
@@ -101,6 +125,7 @@ export class PortEdenClient {
101
125
  data: null,
102
126
  quota: { limit: null, used: null, remaining: null, retryAfter: null },
103
127
  requestId: randomUUID(),
128
+ retryable: false,
104
129
  };
105
130
  }
106
131
  result = await this.singleAttempt(method, path, opts, attempt, totalDeadlineAt);
@@ -119,7 +144,7 @@ export class PortEdenClient {
119
144
  this.logger.info({
120
145
  msg: "api_retry_budget_exhausted",
121
146
  method,
122
- path,
147
+ path: logPath(path, opts.token),
123
148
  status: result.status,
124
149
  attempt,
125
150
  requestId: result.requestId,
@@ -129,13 +154,18 @@ export class PortEdenClient {
129
154
  this.logger.info({
130
155
  msg: "api_retry",
131
156
  method,
132
- path,
157
+ path: logPath(path, opts.token),
133
158
  status: result.status,
134
159
  attempt,
135
160
  nextDelayMs: delayMs,
136
161
  requestId: result.requestId,
137
162
  });
138
- await new Promise((resolve) => setTimeout(resolve, delayMs));
163
+ try {
164
+ await delay(delayMs, undefined, { signal: opts.signal });
165
+ }
166
+ catch {
167
+ // The next iteration returns the caller-cancel result without another attempt.
168
+ }
139
169
  }
140
170
  return result;
141
171
  }
@@ -149,7 +179,7 @@ export class PortEdenClient {
149
179
  const url = this.buildUrl(path, opts.params);
150
180
  const start = performance.now();
151
181
  const headers = {
152
- ...opts.headers,
182
+ ...Object.fromEntries(Object.entries(opts.headers ?? {}).filter(([name]) => !RESERVED_HEADERS.has(name.toLowerCase()))),
153
183
  "X-Request-Id": requestId,
154
184
  Accept: "application/json",
155
185
  };
@@ -179,7 +209,7 @@ export class PortEdenClient {
179
209
  // THEN cap at timeoutMs. Flooring last would hand a 1s budget to a
180
210
  // deployment configured with a shorter timeout.
181
211
  const remainingMs = totalDeadlineAt - performance.now();
182
- const timeoutSignal = AbortSignal.timeout(Math.min(this.timeoutMs, Math.max(1_000, remainingMs)));
212
+ const timeoutSignal = AbortSignal.timeout(Math.floor(Math.min(this.requestTimeout(opts), Math.max(1_000, remainingMs))));
183
213
  const signal = opts.signal
184
214
  ? AbortSignal.any([opts.signal, timeoutSignal])
185
215
  : timeoutSignal;
@@ -190,6 +220,8 @@ export class PortEdenClient {
190
220
  body: hasBody ? JSON.stringify(opts.body) : undefined,
191
221
  signal,
192
222
  dispatcher: this.agent,
223
+ headersTimeout: this.requestTimeout(opts),
224
+ bodyTimeout: this.requestTimeout(opts),
193
225
  });
194
226
  const status = response.statusCode;
195
227
  const responseBody = await this.readBodyWithLimit(response.body);
@@ -213,7 +245,7 @@ export class PortEdenClient {
213
245
  this.logger[logAtWarn ? "warn" : "info"]({
214
246
  msg: "api_call",
215
247
  method,
216
- path,
248
+ path: logPath(path, opts.token),
217
249
  status,
218
250
  elapsed,
219
251
  requestId,
@@ -236,11 +268,12 @@ export class PortEdenClient {
236
268
  this.logger.error({
237
269
  msg: "api_error",
238
270
  method,
239
- path,
271
+ path: logPath(path, opts.token),
240
272
  elapsed,
241
273
  requestId,
242
274
  attempt,
243
- error: err instanceof Error ? err.message : String(err),
275
+ // Validation errors can contain header values. Never log exception messages.
276
+ error: tooLarge ? "response_too_large" : callerAborted ? "canceled" : timedOut ? "timeout" : "request_failed",
244
277
  callerAborted,
245
278
  timedOut,
246
279
  isTransport,
@@ -254,7 +287,7 @@ export class PortEdenClient {
254
287
  // An oversized response reports status 0 like a transport error, but re-requesting it is
255
288
  // guaranteed to fail the same way — mark it so the retry loop doesn't spend two more
256
289
  // attempts (and both backoffs) proving that.
257
- ...(tooLarge ? { retryable: false } : {}),
290
+ ...(tooLarge || callerAborted ? { retryable: false } : {}),
258
291
  };
259
292
  }
260
293
  }
@@ -264,11 +297,11 @@ export class PortEdenClient {
264
297
  // against future bugs (typo, unencoded user input flowing into path) that could
265
298
  // pivot the proxy into making requests to an attacker-controlled host.
266
299
  if (!path.startsWith("/")) {
267
- throw new Error(`Invalid upstream path: must start with '/' (got ${path.slice(0, 64)})`);
300
+ throw new Error("Invalid upstream path: must start with '/'");
268
301
  }
269
302
  const url = new URL(path, this.baseUrl);
270
- if (url.origin !== this.baseOrigin) {
271
- throw new Error(`Upstream origin mismatch: ${url.origin} != ${this.baseOrigin}`);
303
+ if (url.origin !== this.baseOrigin || url.username || url.password) {
304
+ throw new Error("Upstream path must stay on the configured origin without embedded credentials");
272
305
  }
273
306
  if (params) {
274
307
  for (const [key, value] of Object.entries(params)) {
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, request as undiciRequest } from \"undici\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Minimal structured-logging surface the client emits into. Matches pino's\n * object-first calling convention (`logger.info({ msg, ... })`) so a pino\n * instance satisfies it directly; defaults to a no-op when omitted.\n */\nexport interface ClientLogger {\n info(o: object, msg?: string): void;\n warn(o: object, msg?: string): void;\n error(o: object, msg?: string): void;\n}\n\nexport interface ClientOptions {\n /** Upstream API base URL, e.g. \"https://api.workerkit.ai\". */\n baseUrl: string;\n /** Per-attempt request timeout in milliseconds. Default 30000. */\n timeoutMs?: number;\n /** Streamed response body cap in bytes. Default 10 MiB. */\n maxResponseBodyBytes?: number;\n /** Connection-pool size. Default 16. */\n maxConnections?: number;\n /** Emitted as the User-Agent header on every request when set. */\n userAgent?: string;\n /** Structured logger; defaults to a no-op. */\n logger?: ClientLogger;\n /**\n * Supplies the current correlation id (e.g. from AsyncLocalStorage in a\n * server). When it returns a value, each attempt's X-Request-Id is derived\n * from it (`<id>-aN`); otherwise a fresh UUID is minted per attempt.\n */\n requestIdProvider?: () => string | undefined;\n}\n\nexport interface RequestOpts {\n /** Bearer forwarded upstream. Omit for anonymous endpoints. */\n token?: string;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n /**\n * Extra request headers. Every computed header (Authorization, X-Request-Id,\n * User-Agent, Accept, Content-Type) is assigned AFTER this spread and wins,\n * so these can never override one.\n */\n headers?: Record<string, string>;\n}\n\nexport interface QuotaInfo {\n limit: number | null;\n used: number | null;\n remaining: number | null;\n retryAfter: number | null;\n}\n\nexport interface ApiResult<T = unknown> {\n status: number;\n data: T;\n quota: QuotaInfo;\n requestId: string;\n /**\n * Set to false when the failure is deterministic and re-issuing the identical request cannot\n * change the outcome. Overrides RETRYABLE_STATUSES, which classifies by status alone and so\n * can't tell a transport blip (status 0, worth retrying) from a local failure that happens to\n * report the same status. Absent means \"decide by status\", the normal path.\n */\n retryable?: boolean;\n}\n\n/**\n * The upstream response was well-formed but larger than the configured read budget, so we\n * abandoned it mid-stream. Typed because it must not be retried: the response is deterministic,\n * so the next two attempts re-download the same oversized body, burn the same seconds, and fail\n * identically.\n */\nexport class ResponseTooLargeError extends Error {\n constructor(public readonly limitBytes: number) {\n super(`Response body exceeded ${limitBytes} bytes`);\n this.name = \"ResponseTooLargeError\";\n }\n}\n\n// Retry policy — only applied to idempotent HTTP methods on transient upstream failures.\n// POST/PATCH are excluded because retrying a non-idempotent write could duplicate side\n// effects. Status 0 = transport error, 504 = local timeout (see doRequest).\n//\n// 429 must NEVER join RETRYABLE_STATUSES: upstream's Retry-After on a QUOTA_EXCEEDED 429\n// is secondsUntilReset (weeks, not seconds), MAX_BACKOFF_MS would clamp it to 2s, and a\n// billing-period quota wall would become a tight retry loop. It is surfaced to the\n// caller, which branches on errorCode, and never retried here.\nconst IDEMPOTENT_METHODS = new Set([\"GET\", \"PUT\", \"DELETE\"]);\nconst RETRYABLE_STATUSES = new Set([0, 502, 503, 504]);\nconst MAX_ATTEMPTS = 3;\nconst BACKOFF_MS = [200, 500]; // between attempts 1→2 and 2→3\n// Ceiling on any Retry-After honoured below. Dormant today: no RETRYABLE_STATUS carries\n// one, so quota.retryAfter is always null there. Read the 429 note above before changing.\nconst MAX_BACKOFF_MS = 2_000;\n\nconst NOOP_LOGGER: ClientLogger = {\n info() {\n /* no-op */\n },\n warn() {\n /* no-op */\n },\n error() {\n /* no-op */\n },\n};\n\nexport class PortEdenClient {\n private agent: Agent;\n private baseUrl: string;\n private baseOrigin: string;\n private timeoutMs: number;\n private maxResponseBodyBytes: number;\n private userAgent: string | undefined;\n private logger: ClientLogger;\n private requestIdProvider: (() => string | undefined) | undefined;\n\n constructor(options: ClientOptions) {\n this.baseUrl = options.baseUrl;\n this.baseOrigin = new URL(options.baseUrl).origin;\n this.timeoutMs = options.timeoutMs ?? 30_000;\n this.maxResponseBodyBytes = options.maxResponseBodyBytes ?? 10 * 1024 * 1024;\n this.userAgent = options.userAgent;\n this.logger = options.logger ?? NOOP_LOGGER;\n this.requestIdProvider = options.requestIdProvider;\n this.agent = new Agent({\n keepAliveTimeout: 30_000,\n keepAliveMaxTimeout: 60_000,\n pipelining: 1,\n connections: options.maxConnections ?? 16,\n connect: { rejectUnauthorized: true },\n // Defence in depth: the per-attempt AbortSignal below already bounds each\n // call, but undici's own limits default to ~300s — pin them to the same\n // budget so a hung upstream can never outlive the request either way.\n headersTimeout: this.timeoutMs,\n bodyTimeout: this.timeoutMs,\n });\n }\n\n async get<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"GET\", path, opts);\n }\n\n async post<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"POST\", path, opts);\n }\n\n async patch<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"PATCH\", path, opts);\n }\n\n async put<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"PUT\", path, opts);\n }\n\n async delete<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"DELETE\", path, opts);\n }\n\n private async doRequest<T>(\n method: string,\n path: string,\n opts: RequestOpts\n ): Promise<ApiResult<T>> {\n const isIdempotent = IDEMPOTENT_METHODS.has(method);\n const maxAttempts = isIdempotent ? MAX_ATTEMPTS : 1;\n\n // Total deadline across ALL attempts and backoff delays. Without it, the\n // worst case against a slow-then-failing upstream is ~3 x timeoutMs plus\n // backoff, holding a concurrency slot for ~90s. Two timeout budgets is\n // enough for one slow attempt plus a meaningful retry.\n const totalDeadlineAt = performance.now() + this.timeoutMs * 2;\n\n let result: ApiResult<T> | undefined;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n if (opts.signal?.aborted) {\n return {\n status: 0,\n data: null as T,\n quota: { limit: null, used: null, remaining: null, retryAfter: null },\n requestId: randomUUID(),\n };\n }\n\n result = await this.singleAttempt<T>(method, path, opts, attempt, totalDeadlineAt);\n\n if (\n result.retryable === false ||\n !RETRYABLE_STATUSES.has(result.status) ||\n attempt === maxAttempts\n ) {\n return result;\n }\n\n const baseDelay = BACKOFF_MS[attempt - 1] ?? BACKOFF_MS[BACKOFF_MS.length - 1];\n const retryAfterMs = (result.quota.retryAfter ?? 0) * 1000;\n const delayMs = Math.min(Math.max(baseDelay, retryAfterMs), MAX_BACKOFF_MS);\n\n // Skip the retry when the remaining budget can't fit the backoff plus a\n // useful slice of the next attempt — return the transient failure instead\n // of holding the slot past the deadline.\n if (performance.now() + delayMs + 1_000 > totalDeadlineAt) {\n this.logger.info({\n msg: \"api_retry_budget_exhausted\",\n method,\n path,\n status: result.status,\n attempt,\n requestId: result.requestId,\n });\n return result;\n }\n\n this.logger.info({\n msg: \"api_retry\",\n method,\n path,\n status: result.status,\n attempt,\n nextDelayMs: delayMs,\n requestId: result.requestId,\n });\n\n await new Promise<void>((resolve) => setTimeout(resolve, delayMs));\n }\n return result!;\n }\n\n private async singleAttempt<T>(\n method: string,\n path: string,\n opts: RequestOpts,\n attempt: number,\n totalDeadlineAt: number\n ): Promise<ApiResult<T>> {\n // Derive from the inbound correlation id so the caller's request log, the\n // api_call lines and the upstream API's own logs all join on one key; the\n // -aN suffix keeps each retry attempt distinguishable while staying unique\n // per upstream request.\n const correlationId = this.requestIdProvider?.();\n const requestId = correlationId ? `${correlationId}-a${attempt}` : randomUUID();\n const url = this.buildUrl(path, opts.params);\n const start = performance.now();\n\n const headers: Record<string, string> = {\n ...opts.headers,\n \"X-Request-Id\": requestId,\n Accept: \"application/json\",\n };\n if (this.userAgent) {\n headers[\"User-Agent\"] = this.userAgent;\n }\n if (opts.token) {\n headers.Authorization = `Bearer ${opts.token}`;\n }\n\n const hasBody = opts.body !== undefined && method !== \"GET\";\n if (hasBody) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n // Compose the caller's cancellation signal with a per-attempt timeout.\n // Using only opts.signal would let a long upstream call hang indefinitely;\n // using only the timeout would ignore client cancellation.\n //\n // The per-attempt budget is CLAMPED to what is left of the whole-call\n // deadline. Without the clamp, totalDeadlineAt gated only the DECISION to\n // start another attempt: the final attempt then got a fresh full timeoutMs\n // and the call could run ~3 x timeoutMs (~90s on defaults), holding a\n // concurrency slot and an upstream connection long after the caller had\n // given up.\n //\n // Order matters: floor at 1s (doRequest's gate already guarantees roughly\n // that much budget at attempt start, so this only guards arithmetic edges),\n // THEN cap at timeoutMs. Flooring last would hand a 1s budget to a\n // deployment configured with a shorter timeout.\n const remainingMs = totalDeadlineAt - performance.now();\n const timeoutSignal = AbortSignal.timeout(\n Math.min(this.timeoutMs, Math.max(1_000, remainingMs))\n );\n const signal = opts.signal\n ? AbortSignal.any([opts.signal, timeoutSignal])\n : timeoutSignal;\n\n try {\n const response = await undiciRequest(url, {\n method: method as \"GET\" | \"POST\" | \"PATCH\" | \"PUT\" | \"DELETE\",\n headers,\n body: hasBody ? JSON.stringify(opts.body) : undefined,\n signal,\n dispatcher: this.agent,\n });\n\n const status = response.statusCode;\n const responseBody = await this.readBodyWithLimit(response.body);\n const elapsed = Math.round(performance.now() - start);\n\n let data: T;\n try {\n data = JSON.parse(responseBody) as T;\n } catch {\n data = (responseBody || null) as T;\n }\n\n const quota = this.extractQuota(response.headers as Record<string, string>);\n\n // An upstream 5xx is a service-level error: the request reached the API and\n // it failed to serve it. It goes through this success branch (undici resolved),\n // so without bumping the level here it would be logged at INFO — buried among\n // healthy 200s and invisible to log-based WARNING/ERROR views and alerts.\n // WARN (not ERROR): \"something is wrong with the service\", but often transient\n // and retried. 4xx stays at INFO — those are client/auth/quota/validation\n // outcomes, not outages.\n const logAtWarn = status >= 500;\n this.logger[logAtWarn ? \"warn\" : \"info\"]({\n msg: \"api_call\",\n method,\n path,\n status,\n elapsed,\n requestId,\n attempt,\n quotaRemaining: quota.remaining,\n });\n\n return { status, data, quota, requestId };\n } catch (err) {\n const elapsed = Math.round(performance.now() - start);\n // Distinguish caller-cancel from local timeout: timeoutSignal aborts when the\n // per-attempt budget is exhausted; opts.signal aborts when the caller\n // gave up. Treat caller-cancel as a non-retryable transport failure.\n const callerAborted = opts.signal?.aborted === true;\n const timedOut = !callerAborted && timeoutSignal.aborted;\n const isTransport =\n err instanceof Error &&\n \"code\" in err &&\n (err.code === \"ECONNRESET\" || err.code === \"ECONNREFUSED\" || err.code === \"ETIMEDOUT\");\n const tooLarge = err instanceof ResponseTooLargeError;\n\n this.logger.error({\n msg: \"api_error\",\n method,\n path,\n elapsed,\n requestId,\n attempt,\n error: err instanceof Error ? err.message : String(err),\n callerAborted,\n timedOut,\n isTransport,\n tooLarge,\n });\n\n return {\n status: timedOut ? 504 : 0,\n data: null as T,\n quota: { limit: null, used: null, remaining: null, retryAfter: null },\n requestId,\n // An oversized response reports status 0 like a transport error, but re-requesting it is\n // guaranteed to fail the same way — mark it so the retry loop doesn't spend two more\n // attempts (and both backoffs) proving that.\n ...(tooLarge ? { retryable: false } : {}),\n };\n }\n }\n\n private buildUrl(path: string, params?: Record<string, unknown>): string {\n // SSRF defence: reject absolute URLs and ensure the resolved origin matches the\n // configured upstream. Tool paths are hard-coded today, but this is a cheap guard\n // against future bugs (typo, unencoded user input flowing into path) that could\n // pivot the proxy into making requests to an attacker-controlled host.\n if (!path.startsWith(\"/\")) {\n throw new Error(`Invalid upstream path: must start with '/' (got ${path.slice(0, 64)})`);\n }\n const url = new URL(path, this.baseUrl);\n if (url.origin !== this.baseOrigin) {\n throw new Error(`Upstream origin mismatch: ${url.origin} != ${this.baseOrigin}`);\n }\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n // Array params repeat the key (?userIds=a&userIds=b) — that is what the\n // backend's [FromQuery] List<T> binding expects. The scalar String(value)\n // path below would comma-join an array into ONE value (\"a,b\"), which binds\n // as a single-element list and silently matches nothing upstream. Params\n // the backend takes as a comma-separated string instead are typed\n // z.string() by their tools and never reach this branch.\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) url.searchParams.append(key, String(item));\n }\n continue;\n }\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n // Anchored rather than a bare parseInt, which is lenient: it takes the leading digit\n // run and ignores the rest, so an ISO-8601 timestamp (the shape of the resetsAt that\n // rides the QUOTA_EXCEEDED body) would parse to 2026 and read as a plausible 33-minute\n // backoff, with no NaN to catch it. Anchoring fails closed on that and on the HTTP-date\n // form alike. `-?` keeps any negative X-Monthly-* sentinel; a negative Retry-After is\n // rejected at the use site.\n private extractQuota(headers: Record<string, string | string[] | undefined>): QuotaInfo {\n const toNum = (key: string): number | null => {\n const val = headers[key];\n const str = (Array.isArray(val) ? val[0] : val)?.trim();\n if (!str || !/^-?\\d+$/.test(str)) return null;\n const n = Number(str);\n return Number.isSafeInteger(n) ? n : null;\n };\n\n return {\n limit: toNum(\"x-monthly-limit\"),\n used: toNum(\"x-monthly-used\"),\n remaining: toNum(\"x-monthly-remaining\"),\n retryAfter: toNum(\"retry-after\"),\n };\n }\n\n private async readBodyWithLimit(body: import(\"undici\").Dispatcher.ResponseData[\"body\"]): Promise<string> {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n for await (const chunk of body) {\n totalBytes += chunk.length;\n if (totalBytes > this.maxResponseBodyBytes) {\n // Consume remaining to avoid connection leaks, then throw\n body.destroy();\n throw new ResponseTooLargeError(this.maxResponseBodyBytes);\n }\n chunks.push(Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n async close(): Promise<void> {\n await this.agent.close();\n }\n}\n\n/** Brand-facing alias — the same client, published under the WorkerKit name. */\nexport { PortEdenClient as WorkerKitClient };\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, request as undiciRequest } from \"undici\";\nimport { randomUUID } from \"node:crypto\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { scrubSecrets } from \"./redact.js\";\n\n/**\n * Minimal structured-logging surface the client emits into. Matches pino's\n * object-first calling convention (`logger.info({ msg, ... })`) so a pino\n * instance satisfies it directly; defaults to a no-op when omitted.\n */\nexport interface ClientLogger {\n info(o: object, msg?: string): void;\n warn(o: object, msg?: string): void;\n error(o: object, msg?: string): void;\n}\n\nexport interface ClientOptions {\n /** Upstream API base URL, e.g. \"https://api.workerkit.ai\". */\n baseUrl: string;\n /** Per-attempt request timeout in milliseconds. Default 30000. */\n timeoutMs?: number;\n /** Streamed response body cap in bytes. Default 10 MiB. */\n maxResponseBodyBytes?: number;\n /** Connection-pool size. Default 16. */\n maxConnections?: number;\n /** Emitted as the User-Agent header on every request when set. */\n userAgent?: string;\n /** Structured logger; defaults to a no-op. */\n logger?: ClientLogger;\n /**\n * Supplies the current correlation id (e.g. from AsyncLocalStorage in a\n * server). When it returns a value, each attempt's X-Request-Id is derived\n * from it (`<id>-aN`); otherwise a fresh UUID is minted per attempt.\n */\n requestIdProvider?: () => string | undefined;\n}\n\nexport interface RequestOpts {\n /** Override only this request, e.g. an explicit run wait. POST is still never retried. */\n timeoutMs?: number;\n /** Bearer forwarded upstream. Omit for anonymous endpoints. */\n token?: string;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n /**\n * Extra request headers, such as Idempotency-Key. Authentication, routing and\n * computed headers are reserved, regardless of casing. Use token for authentication.\n */\n headers?: Record<string, string>;\n}\n\nexport interface QuotaInfo {\n limit: number | null;\n used: number | null;\n remaining: number | null;\n retryAfter: number | null;\n}\n\nexport interface ApiResult<T = unknown> {\n status: number;\n data: T;\n quota: QuotaInfo;\n requestId: string;\n /**\n * Set to false when the failure is deterministic and re-issuing the identical request cannot\n * change the outcome. Overrides RETRYABLE_STATUSES, which classifies by status alone and so\n * can't tell a transport blip (status 0, worth retrying) from a local failure that happens to\n * report the same status. Absent means \"decide by status\", the normal path.\n */\n retryable?: boolean;\n}\n\n/**\n * The upstream response was well-formed but larger than the configured read budget, so we\n * abandoned it mid-stream. Typed because it must not be retried: the response is deterministic,\n * so the next two attempts re-download the same oversized body, burn the same seconds, and fail\n * identically.\n */\nexport class ResponseTooLargeError extends Error {\n constructor(public readonly limitBytes: number) {\n super(`Response body exceeded ${limitBytes} bytes`);\n this.name = \"ResponseTooLargeError\";\n }\n}\n\n// Retry policy — only applied to idempotent HTTP methods on transient upstream failures.\n// POST/PATCH are excluded because retrying a non-idempotent write could duplicate side\n// effects. Status 0 = transport error, 504 = local timeout (see doRequest).\n//\n// 429 must NEVER join RETRYABLE_STATUSES: upstream's Retry-After on a QUOTA_EXCEEDED 429\n// is secondsUntilReset (weeks, not seconds), MAX_BACKOFF_MS would clamp it to 2s, and a\n// billing-period quota wall would become a tight retry loop. It is surfaced to the\n// caller, which branches on errorCode, and never retried here.\nconst IDEMPOTENT_METHODS = new Set([\"GET\", \"PUT\", \"DELETE\"]);\nconst RETRYABLE_STATUSES = new Set([0, 502, 503, 504]);\nconst MAX_ATTEMPTS = 3;\nconst BACKOFF_MS = [200, 500]; // between attempts 1→2 and 2→3\n// Ceiling on any Retry-After honoured below. Dormant today: no RETRYABLE_STATUS carries\n// one, so quota.retryAfter is always null there. Read the 429 note above before changing.\nconst MAX_BACKOFF_MS = 2_000;\nconst RESERVED_HEADERS = new Set([\n \"authorization\", \"proxy-authorization\", \"host\", \"x-request-id\",\n \"user-agent\", \"accept\", \"content-type\", \"content-length\", \"transfer-encoding\",\n]);\n\nfunction positiveInteger(value: number, name: string): number {\n if (!Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647) {\n throw new RangeError(`${name} must be a positive integer no greater than 2147483647`);\n }\n return value;\n}\n\nfunction logPath(path: string, token?: string): string {\n const pathname = path.split(/[?#]/, 1)[0] ?? \"/\";\n return scrubSecrets(token ? pathname.split(token).join(\"[REDACTED]\") : pathname);\n}\n\nconst NOOP_LOGGER: ClientLogger = {\n info() {\n /* no-op */\n },\n warn() {\n /* no-op */\n },\n error() {\n /* no-op */\n },\n};\n\nexport class PortEdenClient {\n private agent: Agent;\n private baseUrl: string;\n private baseOrigin: string;\n private timeoutMs: number;\n private maxResponseBodyBytes: number;\n private userAgent: string | undefined;\n private logger: ClientLogger;\n private requestIdProvider: (() => string | undefined) | undefined;\n\n constructor(options: ClientOptions) {\n const base = new URL(options.baseUrl);\n if (![\"http:\", \"https:\"].includes(base.protocol) || base.username || base.password) {\n throw new Error(\"baseUrl must be an HTTP(S) URL without embedded credentials\");\n }\n this.baseUrl = base.toString();\n this.baseOrigin = base.origin;\n this.timeoutMs = positiveInteger(options.timeoutMs ?? 30_000, \"timeoutMs\");\n this.maxResponseBodyBytes = positiveInteger(options.maxResponseBodyBytes ?? 10 * 1024 * 1024, \"maxResponseBodyBytes\");\n this.userAgent = options.userAgent;\n this.logger = options.logger ?? NOOP_LOGGER;\n this.requestIdProvider = options.requestIdProvider;\n this.agent = new Agent({\n keepAliveTimeout: 30_000,\n keepAliveMaxTimeout: 60_000,\n pipelining: 1,\n connections: positiveInteger(options.maxConnections ?? 16, \"maxConnections\"),\n connect: { rejectUnauthorized: true },\n // Defence in depth: the per-attempt AbortSignal below already bounds each\n // call, but undici's own limits default to ~300s — pin them to the same\n // budget so a hung upstream can never outlive the request either way.\n headersTimeout: this.timeoutMs,\n bodyTimeout: this.timeoutMs,\n });\n }\n\n async get<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"GET\", path, opts);\n }\n\n async post<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"POST\", path, opts);\n }\n\n async patch<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"PATCH\", path, opts);\n }\n\n async put<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"PUT\", path, opts);\n }\n\n async delete<T = unknown>(path: string, opts: RequestOpts): Promise<ApiResult<T>> {\n return this.doRequest<T>(\"DELETE\", path, opts);\n }\n\n private requestTimeout(opts: RequestOpts): number {\n return opts.timeoutMs !== undefined && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0\n ? Math.max(1, Math.floor(Math.min(300_000, opts.timeoutMs))) : this.timeoutMs;\n }\n\n private async doRequest<T>(\n method: string,\n path: string,\n opts: RequestOpts\n ): Promise<ApiResult<T>> {\n const isIdempotent = IDEMPOTENT_METHODS.has(method);\n const maxAttempts = isIdempotent ? MAX_ATTEMPTS : 1;\n\n // Total deadline across ALL attempts and backoff delays. Without it, the\n // worst case against a slow-then-failing upstream is ~3 x timeoutMs plus\n // backoff, holding a concurrency slot for ~90s. Two timeout budgets is\n // enough for one slow attempt plus a meaningful retry.\n const totalDeadlineAt = performance.now() + this.requestTimeout(opts) * 2;\n\n let result: ApiResult<T> | undefined;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n if (opts.signal?.aborted) {\n return {\n status: 0,\n data: null as T,\n quota: { limit: null, used: null, remaining: null, retryAfter: null },\n requestId: randomUUID(),\n retryable: false,\n };\n }\n\n result = await this.singleAttempt<T>(method, path, opts, attempt, totalDeadlineAt);\n\n if (\n result.retryable === false ||\n !RETRYABLE_STATUSES.has(result.status) ||\n attempt === maxAttempts\n ) {\n return result;\n }\n\n const baseDelay = BACKOFF_MS[attempt - 1] ?? BACKOFF_MS[BACKOFF_MS.length - 1];\n const retryAfterMs = (result.quota.retryAfter ?? 0) * 1000;\n const delayMs = Math.min(Math.max(baseDelay, retryAfterMs), MAX_BACKOFF_MS);\n\n // Skip the retry when the remaining budget can't fit the backoff plus a\n // useful slice of the next attempt — return the transient failure instead\n // of holding the slot past the deadline.\n if (performance.now() + delayMs + 1_000 > totalDeadlineAt) {\n this.logger.info({\n msg: \"api_retry_budget_exhausted\",\n method,\n path: logPath(path, opts.token),\n status: result.status,\n attempt,\n requestId: result.requestId,\n });\n return result;\n }\n\n this.logger.info({\n msg: \"api_retry\",\n method,\n path: logPath(path, opts.token),\n status: result.status,\n attempt,\n nextDelayMs: delayMs,\n requestId: result.requestId,\n });\n\n try {\n await delay(delayMs, undefined, { signal: opts.signal });\n } catch {\n // The next iteration returns the caller-cancel result without another attempt.\n }\n }\n return result!;\n }\n\n private async singleAttempt<T>(\n method: string,\n path: string,\n opts: RequestOpts,\n attempt: number,\n totalDeadlineAt: number\n ): Promise<ApiResult<T>> {\n // Derive from the inbound correlation id so the caller's request log, the\n // api_call lines and the upstream API's own logs all join on one key; the\n // -aN suffix keeps each retry attempt distinguishable while staying unique\n // per upstream request.\n const correlationId = this.requestIdProvider?.();\n const requestId = correlationId ? `${correlationId}-a${attempt}` : randomUUID();\n const url = this.buildUrl(path, opts.params);\n const start = performance.now();\n\n const headers: Record<string, string> = {\n ...Object.fromEntries(Object.entries(opts.headers ?? {}).filter(([name]) => !RESERVED_HEADERS.has(name.toLowerCase()))),\n \"X-Request-Id\": requestId,\n Accept: \"application/json\",\n };\n if (this.userAgent) {\n headers[\"User-Agent\"] = this.userAgent;\n }\n if (opts.token) {\n headers.Authorization = `Bearer ${opts.token}`;\n }\n\n const hasBody = opts.body !== undefined && method !== \"GET\";\n if (hasBody) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n // Compose the caller's cancellation signal with a per-attempt timeout.\n // Using only opts.signal would let a long upstream call hang indefinitely;\n // using only the timeout would ignore client cancellation.\n //\n // The per-attempt budget is CLAMPED to what is left of the whole-call\n // deadline. Without the clamp, totalDeadlineAt gated only the DECISION to\n // start another attempt: the final attempt then got a fresh full timeoutMs\n // and the call could run ~3 x timeoutMs (~90s on defaults), holding a\n // concurrency slot and an upstream connection long after the caller had\n // given up.\n //\n // Order matters: floor at 1s (doRequest's gate already guarantees roughly\n // that much budget at attempt start, so this only guards arithmetic edges),\n // THEN cap at timeoutMs. Flooring last would hand a 1s budget to a\n // deployment configured with a shorter timeout.\n const remainingMs = totalDeadlineAt - performance.now();\n const timeoutSignal = AbortSignal.timeout(\n Math.floor(Math.min(this.requestTimeout(opts), Math.max(1_000, remainingMs)))\n );\n const signal = opts.signal\n ? AbortSignal.any([opts.signal, timeoutSignal])\n : timeoutSignal;\n\n try {\n const response = await undiciRequest(url, {\n method: method as \"GET\" | \"POST\" | \"PATCH\" | \"PUT\" | \"DELETE\",\n headers,\n body: hasBody ? JSON.stringify(opts.body) : undefined,\n signal,\n dispatcher: this.agent,\n headersTimeout: this.requestTimeout(opts),\n bodyTimeout: this.requestTimeout(opts),\n });\n\n const status = response.statusCode;\n const responseBody = await this.readBodyWithLimit(response.body);\n const elapsed = Math.round(performance.now() - start);\n\n let data: T;\n try {\n data = JSON.parse(responseBody) as T;\n } catch {\n data = (responseBody || null) as T;\n }\n\n const quota = this.extractQuota(response.headers as Record<string, string>);\n\n // An upstream 5xx is a service-level error: the request reached the API and\n // it failed to serve it. It goes through this success branch (undici resolved),\n // so without bumping the level here it would be logged at INFO — buried among\n // healthy 200s and invisible to log-based WARNING/ERROR views and alerts.\n // WARN (not ERROR): \"something is wrong with the service\", but often transient\n // and retried. 4xx stays at INFO — those are client/auth/quota/validation\n // outcomes, not outages.\n const logAtWarn = status >= 500;\n this.logger[logAtWarn ? \"warn\" : \"info\"]({\n msg: \"api_call\",\n method,\n path: logPath(path, opts.token),\n status,\n elapsed,\n requestId,\n attempt,\n quotaRemaining: quota.remaining,\n });\n\n return { status, data, quota, requestId };\n } catch (err) {\n const elapsed = Math.round(performance.now() - start);\n // Distinguish caller-cancel from local timeout: timeoutSignal aborts when the\n // per-attempt budget is exhausted; opts.signal aborts when the caller\n // gave up. Treat caller-cancel as a non-retryable transport failure.\n const callerAborted = opts.signal?.aborted === true;\n const timedOut = !callerAborted && timeoutSignal.aborted;\n const isTransport =\n err instanceof Error &&\n \"code\" in err &&\n (err.code === \"ECONNRESET\" || err.code === \"ECONNREFUSED\" || err.code === \"ETIMEDOUT\");\n const tooLarge = err instanceof ResponseTooLargeError;\n\n this.logger.error({\n msg: \"api_error\",\n method,\n path: logPath(path, opts.token),\n elapsed,\n requestId,\n attempt,\n // Validation errors can contain header values. Never log exception messages.\n error: tooLarge ? \"response_too_large\" : callerAborted ? \"canceled\" : timedOut ? \"timeout\" : \"request_failed\",\n callerAborted,\n timedOut,\n isTransport,\n tooLarge,\n });\n\n return {\n status: timedOut ? 504 : 0,\n data: null as T,\n quota: { limit: null, used: null, remaining: null, retryAfter: null },\n requestId,\n // An oversized response reports status 0 like a transport error, but re-requesting it is\n // guaranteed to fail the same way — mark it so the retry loop doesn't spend two more\n // attempts (and both backoffs) proving that.\n ...(tooLarge || callerAborted ? { retryable: false } : {}),\n };\n }\n }\n\n private buildUrl(path: string, params?: Record<string, unknown>): string {\n // SSRF defence: reject absolute URLs and ensure the resolved origin matches the\n // configured upstream. Tool paths are hard-coded today, but this is a cheap guard\n // against future bugs (typo, unencoded user input flowing into path) that could\n // pivot the proxy into making requests to an attacker-controlled host.\n if (!path.startsWith(\"/\")) {\n throw new Error(\"Invalid upstream path: must start with '/'\");\n }\n const url = new URL(path, this.baseUrl);\n if (url.origin !== this.baseOrigin || url.username || url.password) {\n throw new Error(\"Upstream path must stay on the configured origin without embedded credentials\");\n }\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n // Array params repeat the key (?userIds=a&userIds=b) — that is what the\n // backend's [FromQuery] List<T> binding expects. The scalar String(value)\n // path below would comma-join an array into ONE value (\"a,b\"), which binds\n // as a single-element list and silently matches nothing upstream. Params\n // the backend takes as a comma-separated string instead are typed\n // z.string() by their tools and never reach this branch.\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) url.searchParams.append(key, String(item));\n }\n continue;\n }\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n // Anchored rather than a bare parseInt, which is lenient: it takes the leading digit\n // run and ignores the rest, so an ISO-8601 timestamp (the shape of the resetsAt that\n // rides the QUOTA_EXCEEDED body) would parse to 2026 and read as a plausible 33-minute\n // backoff, with no NaN to catch it. Anchoring fails closed on that and on the HTTP-date\n // form alike. `-?` keeps any negative X-Monthly-* sentinel; a negative Retry-After is\n // rejected at the use site.\n private extractQuota(headers: Record<string, string | string[] | undefined>): QuotaInfo {\n const toNum = (key: string): number | null => {\n const val = headers[key];\n const str = (Array.isArray(val) ? val[0] : val)?.trim();\n if (!str || !/^-?\\d+$/.test(str)) return null;\n const n = Number(str);\n return Number.isSafeInteger(n) ? n : null;\n };\n\n return {\n limit: toNum(\"x-monthly-limit\"),\n used: toNum(\"x-monthly-used\"),\n remaining: toNum(\"x-monthly-remaining\"),\n retryAfter: toNum(\"retry-after\"),\n };\n }\n\n private async readBodyWithLimit(body: import(\"undici\").Dispatcher.ResponseData[\"body\"]): Promise<string> {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n for await (const chunk of body) {\n totalBytes += chunk.length;\n if (totalBytes > this.maxResponseBodyBytes) {\n // Consume remaining to avoid connection leaks, then throw\n body.destroy();\n throw new ResponseTooLargeError(this.maxResponseBodyBytes);\n }\n chunks.push(Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n async close(): Promise<void> {\n await this.agent.close();\n }\n}\n\n/** Brand-facing alias — the same client, published under the WorkerKit name. */\nexport { PortEdenClient as WorkerKitClient };\n"]}
@@ -50,4 +50,3 @@ export declare const decisionQuestion: z.ZodDiscriminatedUnion<"type", [z.ZodObj
50
50
  falseMeaning?: string | undefined;
51
51
  }>]>;
52
52
  export declare const createDecisionWorker: ToolDescriptor;
53
- //# sourceMappingURL=decision-authoring.d.ts.map
@@ -1,3 +1,2 @@
1
1
  /** Keep exact decision rows once in LLM-facing receipts. The API remains unchanged. */
2
2
  export declare function compactDecisionReceipt(data: unknown): unknown;
3
- //# sourceMappingURL=decision-receipt.d.ts.map
@@ -22,4 +22,3 @@ export declare function publisherPageData(data: unknown, params: Record<string,
22
22
  export declare function publisherPageFooter(data: unknown): string | undefined;
23
23
  export declare const directoryDescriptors: readonly ToolDescriptor[];
24
24
  export {};
25
- //# sourceMappingURL=directory.d.ts.map
@@ -137,7 +137,7 @@ const searchKits = {
137
137
  appCategory: z.string().optional().describe("Capability-category code, e.g. 'team-collaboration': matches kits that define that install-time slot OR pin one of its member apps — broader than the app filter. Unknown codes error (the error names the fix). Valid codes: directory_overview → appCategories."),
138
138
  model: z.string().max(80).optional().describe("Substring match on a scored model name, e.g. 'sonnet' or 'gpt'. Valid names: directory_overview → models."),
139
139
  trigger: z.enum(["onDemand", "scheduled", "event"]).optional().describe("How the kit's worker runs: onDemand (a human or agent triggers it), scheduled (ships recurring schedules), event (webhook-triggered)."),
140
- modelType: z.enum(["language", "decision"]).optional().describe("language = an instruction the installer runs on a model of their choice; decision = a routing table the decision model applies per item (triage, sorting, labelling, filtering — no instruction, no model to pick). Omit for both."),
140
+ modelType: z.enum(["language", "decision", "hybrid"]).optional().describe("hybrid = bounded classification followed by one language-model agent; language = an instruction the installer runs on a model of their choice; decision = a routing table the decision model applies per item (triage, sorting, labelling, filtering — no instruction, no model to pick). Omit for both."),
141
141
  publisher: z.string().optional().describe("Publisher slug (publisherSlug on any card) — that publisher's kits only."),
142
142
  sort: z.enum(["downloads", "new", "name", "stars"]).default("downloads").describe("downloads = most installed (the popularity signal), new = latest published, name = A-Z, stars = most starred."),
143
143
  page: z.number().int().min(1).default(1).describe("1-based page number."),
@@ -156,7 +156,7 @@ const searchKits = {
156
156
  const getKit = {
157
157
  name: "kit_get",
158
158
  title: "Get Kit",
159
- description: "One kit's full public dossier — what a human, or an agent advising one, evaluates before installing. Fields: description + appDescriptions (the author's per-app / per-tool notes); modelType ('language', the default, or 'decision'); instructionContent — a language kit's job instruction VERBATIM, the core of what you are evaluating (empty when isProtected, with instructionLength still real; startCommand / endCommand / whenToUse are null on a protected kit, indistinguishable from 'none'); on a DECISION kit evaluate decisionNarration instead (what it reads, the questions it asks each item, the rules that route them, what happens below the confidence floor — served on protected kits too) and decisionSetup (the questions its installer answers); permissionsManifest — the EXACT data access an install grants (per-app operations / fields / scopes; the numeric value is authoritative, names are display) — summarize THIS when asked what the kit can touch; requiredInputs + memorySetup — the install form (fields the kit needs, questions whose answers become the worker's memory); memoryProfile ('contextual' = the worker sees its own recent runs) and selfFactsEnabled (it saves durable facts on its own); schedules / triggers / usageWindows; skillResources — reference docs the worker fetches on demand (key / kind / name always; description / contentLength unless protected; bodies via includeResourceBodies); hasExternalMcpShells — TRUE means installing creates an MCP gateway in the installer's account pointing at the publisher's own upstream URL (permissionsManifest.mcpServers.shells): surface it when recommending; scan — the security scan of the text being served: outcome 'cleared' or 'underReview' (both are live, installable listings — 'underReview' means a reviewer has not settled a raised check, never that the kit is unsafe), scannedAtUtc (null = published before the scanner existed, most of the catalog), checks (the families that ran on THIS kit — 'cleared' on ['static'] alone is weaker than on all three) and recommendation ('safe' | 'caution' | 'doNotInstall', the SCANNER's own advice, independent of outcome and possibly disagreeing with it: attribute it to SkillSpector, and read 'caution' as 'worth a look' — it is also emitted when the analysis was incomplete); scan is absent when there is nothing to report and never carries findings; modelScores (with notes), downloadCount / starCount, and pageUrl — the install page to hand to the human. 404 = no published kit with that slug." +
159
+ description: "One kit's full public dossier — what a human, or an agent advising one, evaluates before installing. Fields: description + appDescriptions (the author's per-app / per-tool notes); modelType ('hybrid' combines classification and an instruction; 'language', the default, or 'decision'); instructionContent — a language kit's job instruction VERBATIM, the core of what you are evaluating (empty when isProtected, with instructionLength still real; startCommand / endCommand / whenToUse are null on a protected kit, indistinguishable from 'none'); on a DECISION kit evaluate decisionNarration instead (what it reads, the questions it asks each item, the rules that route them, what happens below the confidence floor — served on protected kits too) and decisionSetup (the questions its installer answers); permissionsManifest — the EXACT data access an install grants (per-app operations / fields / scopes; the numeric value is authoritative, names are display) — summarize THIS when asked what the kit can touch; requiredInputs + memorySetup — the install form (fields the kit needs, questions whose answers become the worker's memory); memoryProfile ('contextual' = the worker sees its own recent runs) and selfFactsEnabled (it saves durable facts on its own); schedules / triggers / usageWindows; skillResources — reference docs the worker fetches on demand (key / kind / name always; description / contentLength unless protected; bodies via includeResourceBodies); hasExternalMcpShells — TRUE means installing creates an MCP gateway in the installer's account pointing at the publisher's own upstream URL (permissionsManifest.mcpServers.shells): surface it when recommending; scan — the security scan of the text being served: outcome 'cleared' or 'underReview' (both are live, installable listings — 'underReview' means a reviewer has not settled a raised check, never that the kit is unsafe), scannedAtUtc (null = published before the scanner existed, most of the catalog), checks (the families that ran on THIS kit — 'cleared' on ['static'] alone is weaker than on all three) and recommendation ('safe' | 'caution' | 'doNotInstall', the SCANNER's own advice, independent of outcome and possibly disagreeing with it: attribute it to SkillSpector, and read 'caution' as 'worth a look' — it is also emitted when the analysis was incomplete); scan is absent when there is nothing to report and never carries findings; modelScores (with notes), downloadCount / starCount, and pageUrl — the install page to hand to the human. 404 = no published kit with that slug." +
160
160
  DIRECTORY_NOTE,
161
161
  auth: "anonymous",
162
162
  method: "get",