@noidmejs/atomkit-http 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,8 +17,11 @@ const listUsers = {
17
17
  url: 'https://api.example.com/users',
18
18
  query: { q: '{{params.q}}' },
19
19
  auth: { type: 'bearer', token: '{{secret.API_TOKEN}}' },
20
- retry: { attempts: 3, backoffMs: 300 },
20
+ retry: { attempts: 3, backoffMs: 300 }, // GET/HEAD/OPTIONS only, unless you opt in
21
21
  cache: { ttlMs: 30000 },
22
+ timeoutMs: 10000, // default; never unbounded
23
+ maxBytes: 5_000_000, // default response cap
24
+ expose: true, // REQUIRED to be reachable via handleRequest — default false
22
25
  transform: 'data.items',
23
26
  };
24
27
  ```
@@ -31,7 +34,8 @@ import { createProxy } from '@noidmejs/atomkit-http';
31
34
  const proxy = createProxy({
32
35
  sources: { listUsers },
33
36
  secrets: { API_TOKEN: process.env.API_TOKEN! },
34
- allowHosts: ['api.example.com'], // SSRF guard
37
+ allowHosts: ['api.example.com'], // SSRF guard: where a request may GO
38
+ authorize: async (req, sourceId, params) => isLoggedIn(req), // WHO may trigger it
35
39
  });
36
40
 
37
41
  // Next route handler / Cloudflare Worker:
@@ -42,6 +46,13 @@ The client POSTs `{ sourceId, params }`; the proxy injects secrets, enforces the
42
46
  host allow-list, calls the API, and returns the (transformed) JSON. Tokens stay
43
47
  on the server.
44
48
 
49
+ > **`handleRequest` authenticates nobody.** The allow-list governs where a request
50
+ > goes, never who may trigger it, and every call spends your server's
51
+ > secret-authenticated upstream quota with caller-chosen params. A source is
52
+ > therefore unreachable unless it sets `expose: true`, and any source returning
53
+ > personal data needs `authorize` (or host auth in front of the route) **plus rate
54
+ > limiting**, which this package does not provide.
55
+
45
56
  ## Client: run a public source directly
46
57
 
47
58
  ```ts
@@ -57,11 +68,17 @@ const res = await runSource(publicSource, { params: { q: 'hi' } });
57
68
  (JSON path). `{{params.x}}` interpolates caller params; `{{secret.X}}` resolves
58
69
  **server-side only**, in the proxy.
59
70
 
60
- ## Security status (v0.1 — read this)
71
+ ## Security status (v0.2 — read this)
72
+
73
+ Server-only secrets + a hostname **allow-list SSRF tier** that now **re-validates
74
+ every redirect hop** (an allow-listed host can't `30x`-escape to a private IP),
75
+ **strips credentials on cross-host redirects**, **redacts upstream errors** to the
76
+ client (full detail → your `onError` sink), and proto-guarded templating. See
77
+ [SECURITY.md](./SECURITY.md).
61
78
 
62
- Hostname allow-list SSRF tier + server-only secrets + prototype-pollution-guarded
63
- templating. It is **not yet pen-tested**, and it does **not** pin DNS/IPs (a
64
- resolved hostname could still point at a private IP). Do not expose it to
65
- untrusted multi-tenant input without adding IP-pinning and a security review.
79
+ It is **not yet pen-tested**, and it does **not** pin DNS/IPs — a hostname you
80
+ allow-list (or redirect to) whose DNS resolves to a private IP is still reached.
81
+ Do not expose it to untrusted multi-tenant input without adding IP-pinning and a
82
+ security review. Report vulnerabilities via a GitHub Security Advisory.
66
83
 
67
84
  MIT © noidmejs
@@ -0,0 +1,28 @@
1
+ import type { ResponseCache } from './types.js';
2
+ /** Default cap on entries held by one cache instance. */
3
+ export declare const DEFAULT_MAX_ENTRIES = 500;
4
+ /**
5
+ * A bounded LRU with TTL eviction and single-flight.
6
+ *
7
+ * Replaces a module-global `Map` that was keyed by `source.id + params`, never
8
+ * evicted, and shared across every `createProxy()` in the process. That map had
9
+ * three defects this fixes:
10
+ *
11
+ * - **Cross-instance bleed.** Two proxies (say, per tenant) each declaring a
12
+ * source named `users` served each other's responses, because neither the URL,
13
+ * the auth headers, nor the owning instance were part of the key.
14
+ * - **Unbounded growth.** Entries were only ignored after their TTL, never
15
+ * deleted, and the key space included client-controlled params — so any caller
16
+ * of a public proxy could grow memory until OOM.
17
+ * - **Thundering herd.** No in-flight dedup, so every concurrent request for a
18
+ * hot key stampeded the upstream the moment its TTL lapsed.
19
+ */
20
+ export declare function createCache(maxEntries?: number): ResponseCache;
21
+ /** Process-wide fallback used when no per-instance cache is supplied. Bounded and
22
+ * evicting, but PREFER a per-instance cache (createProxy makes one for you). */
23
+ export declare const defaultCache: ResponseCache;
24
+ /** Stable, non-secret-bearing cache key: method + fully-resolved URL + a hash of
25
+ * the request headers. Two callers with different bearer tokens must never share
26
+ * an entry, but the token itself must not sit in a key we may log. */
27
+ export declare function cacheKeyFor(method: string, url: string, headers: Record<string, string>): string;
28
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,UAAU,GAAE,MAA4B,GAAG,aAAa,CAuCnF;AAED;iFACiF;AACjF,eAAO,MAAM,YAAY,EAAE,aAA6B,CAAC;AAEzD;;uEAEuE;AACvE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAShG"}
package/dist/cache.js ADDED
@@ -0,0 +1,78 @@
1
+ /** Default cap on entries held by one cache instance. */
2
+ export const DEFAULT_MAX_ENTRIES = 500;
3
+ /**
4
+ * A bounded LRU with TTL eviction and single-flight.
5
+ *
6
+ * Replaces a module-global `Map` that was keyed by `source.id + params`, never
7
+ * evicted, and shared across every `createProxy()` in the process. That map had
8
+ * three defects this fixes:
9
+ *
10
+ * - **Cross-instance bleed.** Two proxies (say, per tenant) each declaring a
11
+ * source named `users` served each other's responses, because neither the URL,
12
+ * the auth headers, nor the owning instance were part of the key.
13
+ * - **Unbounded growth.** Entries were only ignored after their TTL, never
14
+ * deleted, and the key space included client-controlled params — so any caller
15
+ * of a public proxy could grow memory until OOM.
16
+ * - **Thundering herd.** No in-flight dedup, so every concurrent request for a
17
+ * hot key stampeded the upstream the moment its TTL lapsed.
18
+ */
19
+ export function createCache(maxEntries = DEFAULT_MAX_ENTRIES) {
20
+ const entries = new Map();
21
+ const pending = new Map();
22
+ return {
23
+ get(key, ttlMs) {
24
+ const hit = entries.get(key);
25
+ if (!hit)
26
+ return undefined;
27
+ if (Date.now() - hit.at >= ttlMs) {
28
+ entries.delete(key); // evict on expired read — do not retain forever
29
+ return undefined;
30
+ }
31
+ entries.delete(key); // re-insert to mark most-recently-used
32
+ entries.set(key, hit);
33
+ return hit.data;
34
+ },
35
+ set(key, data) {
36
+ entries.delete(key);
37
+ entries.set(key, { at: Date.now(), data });
38
+ while (entries.size > maxEntries) {
39
+ const oldest = entries.keys().next();
40
+ if (oldest.done)
41
+ break;
42
+ entries.delete(oldest.value);
43
+ }
44
+ },
45
+ inflight(key, run) {
46
+ const existing = pending.get(key);
47
+ if (existing)
48
+ return existing;
49
+ const p = run().finally(() => pending.delete(key));
50
+ pending.set(key, p);
51
+ return p;
52
+ },
53
+ clear() {
54
+ entries.clear();
55
+ },
56
+ get size() {
57
+ return entries.size;
58
+ },
59
+ };
60
+ }
61
+ /** Process-wide fallback used when no per-instance cache is supplied. Bounded and
62
+ * evicting, but PREFER a per-instance cache (createProxy makes one for you). */
63
+ export const defaultCache = createCache();
64
+ /** Stable, non-secret-bearing cache key: method + fully-resolved URL + a hash of
65
+ * the request headers. Two callers with different bearer tokens must never share
66
+ * an entry, but the token itself must not sit in a key we may log. */
67
+ export function cacheKeyFor(method, url, headers) {
68
+ const canonical = Object.keys(headers)
69
+ .sort()
70
+ .map((k) => `${k}:${headers[k]}`)
71
+ .join('\n');
72
+ // djb2 — not cryptographic; enough to separate credential scopes in-process.
73
+ let h = 5381;
74
+ for (let i = 0; i < canonical.length; i++)
75
+ h = ((h << 5) + h + canonical.charCodeAt(i)) | 0;
76
+ return `${method} ${url} #${(h >>> 0).toString(36)}`;
77
+ }
78
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types.js';
2
2
  export { interpolate, interpolateDeep, getPath } from './template.js';
3
- export { runSource } from './run.js';
3
+ export { runSource, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_BYTES } from './run.js';
4
4
  export { createProxy, type ProxyConfig } from './proxy.js';
5
+ export { createCache, defaultCache, cacheKeyFor, DEFAULT_MAX_ENTRIES } from './cache.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Call any API by config; keep secrets server-only behind an SSRF-guarded proxy.
3
3
  export * from './types.js';
4
4
  export { interpolate, interpolateDeep, getPath } from './template.js';
5
- export { runSource } from './run.js';
5
+ export { runSource, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_BYTES } from './run.js';
6
6
  export { createProxy } from './proxy.js';
7
+ export { createCache, defaultCache, cacheKeyFor, DEFAULT_MAX_ENTRIES } from './cache.js';
7
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,iFAAiF;AACjF,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,WAAW,EAAoB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,iFAAiF;AACjF,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAoB,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
package/dist/proxy.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DataSource, RunResult } from './types.js';
1
+ import type { DataSource, ResponseCache, RunResult } from './types.js';
2
2
  export interface ProxyConfig {
3
3
  sources: Record<string, DataSource>;
4
4
  /** Server-only secret values referenced as {{secret.NAME}}. Never leaves the server. */
@@ -6,6 +6,26 @@ export interface ProxyConfig {
6
6
  /** Allow-listed request hosts — exact ("api.example.com") or suffix (".example.com"). */
7
7
  allowHosts: string[];
8
8
  fetchImpl?: typeof fetch;
9
+ /**
10
+ * Server-side error sink. `handleRequest` REDACTS the upstream error before it
11
+ * crosses to the client (it can leak internal hosts / secret-bearing URLs); the
12
+ * unredacted detail is handed here for your logs/telemetry instead.
13
+ */
14
+ onError?: (sourceId: string, result: RunResult) => void;
15
+ /** Max redirect hops the SSRF guard follows before failing (default 5). */
16
+ maxRedirects?: number;
17
+ /**
18
+ * Caller authorization for `handleRequest`. Return false to reject with 403.
19
+ *
20
+ * The SSRF allow-list governs where a request GOES, never WHO may trigger it.
21
+ * `handleRequest` authenticates nobody, and every call spends the server's
22
+ * secret-authenticated upstream quota — so a source returning personal data
23
+ * needs this hook (or host-level auth in front of the route), plus rate limiting.
24
+ */
25
+ authorize?: (req: Request, sourceId: string, params: Record<string, unknown>) => boolean | Promise<boolean>;
26
+ /** Response cache. Defaults to a fresh per-proxy instance — never shared across
27
+ * proxies, so two tenants cannot read each other's cached responses. */
28
+ cache?: ResponseCache;
9
29
  }
10
30
  /**
11
31
  * A server-side proxy: resolves a named source with secrets injected, enforcing a
@@ -20,5 +40,6 @@ export interface ProxyConfig {
20
40
  export declare function createProxy(config: ProxyConfig): {
21
41
  resolve: (sourceId: string, params?: Record<string, unknown>) => Promise<RunResult>;
22
42
  handleRequest: (req: Request) => Promise<Response>;
43
+ clearCache: () => void;
23
44
  };
24
45
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAIxD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,yFAAyF;IACzF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;wBACZ,MAAM,WAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAQ,OAAO,CAAC,SAAS,CAAC;yBAahE,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;EAqB9D"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAKvE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,yFAAyF;IACzF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IACxD,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5G;6EACyE;IACzE,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AA2BD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;wBAMZ,MAAM,WAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAQ,OAAO,CAAC,SAAS,CAAC;yBA8BhE,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;sBAmCtC,IAAI;EAK5B"}
package/dist/proxy.js CHANGED
@@ -1,5 +1,20 @@
1
1
  import { interpolate } from './template.js';
2
2
  import { runSource } from './run.js';
3
+ import { createCache } from './cache.js';
4
+ /** Generic, non-revealing client message for a failed status. */
5
+ function genericError(status) {
6
+ if (status === 400)
7
+ return 'bad request';
8
+ if (status === 401 || status === 403)
9
+ return 'forbidden';
10
+ if (status === 404)
11
+ return 'not found';
12
+ if (status === 429)
13
+ return 'rate limited';
14
+ if (status === 0)
15
+ return 'upstream unavailable';
16
+ return 'upstream request failed';
17
+ }
3
18
  function hostAllowed(rawUrl, allow) {
4
19
  let host;
5
20
  try {
@@ -27,21 +42,43 @@ function hostAllowed(rawUrl, allow) {
27
42
  * multi-tenant input without adding IP-pinning + a security review.
28
43
  */
29
44
  export function createProxy(config) {
45
+ // One cache per proxy. The old module-global map was keyed by `source.id`
46
+ // alone, so two proxies both declaring a source named "users" served each
47
+ // other's cached responses.
48
+ const cache = config.cache ?? createCache();
30
49
  async function resolve(sourceId, params = {}) {
31
50
  const source = config.sources[sourceId];
32
51
  if (!source)
33
52
  return { ok: false, status: 404, error: `unknown source "${sourceId}"` };
34
- // Compute the effective host from params (secrets stripped) and allow-list it,
35
- // so neither a static URL nor a param-injected one can reach an off-list host.
53
+ // Cheap pre-check on the secret-stripped URL: reject an off-list host before
54
+ // any secret is interpolated. NOT sufficient on its own a secret sitting in
55
+ // the host position would pass this and then be fetched — so `runSource` also
56
+ // re-validates the fully-resolved URL immediately before fetch.
36
57
  const withoutSecrets = source.url.replace(/\{\{\s*secret\.[\w.$-]+\s*\}\}/g, '');
37
58
  const checkUrl = interpolate(withoutSecrets, { params, secret: {} });
38
59
  if (!hostAllowed(checkUrl, config.allowHosts))
39
60
  return { ok: false, status: 403, error: 'host not allowed' };
40
- return runSource(source, { params, secrets: config.secrets, fetchImpl: config.fetchImpl });
61
+ return runSource(source, {
62
+ params,
63
+ secrets: config.secrets,
64
+ fetchImpl: config.fetchImpl,
65
+ cache,
66
+ // Authoritative check against the URL actually fetched, secrets included.
67
+ allowRequestHost: (u) => hostAllowed(u, config.allowHosts),
68
+ // Re-validate every redirect hop against the SAME allow-list so a 30x can't
69
+ // escape it to a private IP / off-list host.
70
+ allowRedirectHost: (u) => hostAllowed(u, config.allowHosts),
71
+ maxRedirects: config.maxRedirects,
72
+ });
41
73
  }
42
74
  /** Web-standard handler: POST { sourceId, params } → JSON RunResult. Mount in a
43
- * Next route handler, a Cloudflare Worker, an Express adapter, etc. */
75
+ * Next route handler, a Cloudflare Worker, an Express adapter, etc.
76
+ *
77
+ * Only sources declaring `expose: true` are reachable, and `config.authorize`
78
+ * (if set) gates every call. This handler has no rate limiting of its own —
79
+ * put the host's in front of it. */
44
80
  async function handleRequest(req) {
81
+ const json = (body, status) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } });
45
82
  let payload = {};
46
83
  try {
47
84
  payload = (await req.json());
@@ -49,18 +86,32 @@ export function createProxy(config) {
49
86
  catch {
50
87
  /* invalid body → handled below */
51
88
  }
52
- if (!payload.sourceId) {
53
- return new Response(JSON.stringify({ ok: false, status: 400, error: 'missing sourceId' }), {
54
- status: 400,
55
- headers: { 'content-type': 'application/json' },
56
- });
89
+ if (!payload.sourceId)
90
+ return json({ ok: false, status: 400, error: 'missing sourceId' }, 400);
91
+ const params = payload.params ?? {};
92
+ // Same response for "no such source" and "not exposed": do not let a caller
93
+ // enumerate which sources exist.
94
+ const source = config.sources[payload.sourceId];
95
+ if (!source || source.expose !== true)
96
+ return json({ ok: false, status: 404, error: 'not found' }, 404);
97
+ if (config.authorize && !(await config.authorize(req, payload.sourceId, params))) {
98
+ return json({ ok: false, status: 403, error: 'forbidden' }, 403);
57
99
  }
58
- const result = await resolve(payload.sourceId, payload.params ?? {});
59
- return new Response(JSON.stringify(result), {
60
- status: result.ok ? 200 : result.status || 500,
61
- headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
62
- });
100
+ const result = await resolve(payload.sourceId, params);
101
+ // Redact before crossing to the client: the raw `error` (fetch messages,
102
+ // resolved URLs, upstream bodies) can leak internal hosts or secret-bearing
103
+ // URLs. Log the detail server-side, return a generic message.
104
+ let client = result;
105
+ if (!result.ok) {
106
+ config.onError?.(payload.sourceId, result);
107
+ client = { ok: false, status: result.status, error: genericError(result.status) };
108
+ }
109
+ return json(client, result.ok ? 200 : result.status || 500);
110
+ }
111
+ /** Drop every cached response held by this proxy (erasure of cached personal data). */
112
+ function clearCache() {
113
+ cache.clear();
63
114
  }
64
- return { resolve, handleRequest };
115
+ return { resolve, handleRequest, clearCache };
65
116
  }
66
117
  //# sourceMappingURL=proxy.js.map
package/dist/proxy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
package/dist/run.d.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import type { DataSource, RunContext, RunResult } from './types.js';
2
+ /** Applied whenever `source.timeoutMs` is unset. An absent timeout let a hung or
3
+ * slow-loris upstream hold a request for the platform default (~300s), multiplied
4
+ * by every retry — behind a publicly mounted proxy that exhausts sockets. */
5
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
6
+ /** Cap on a buffered upstream response body. The SSRF allow-list constrains WHERE
7
+ * a request goes, not how much it returns; without a cap one large (or endless)
8
+ * response is fully buffered per concurrent request. */
9
+ export declare const DEFAULT_MAX_BYTES: number;
2
10
  /**
3
- * Resolve a data source: build the request from config, fetch (with retry/backoff/
4
- * timeout), extract via `transform`, and cache successful GETs for `cache.ttlMs`.
11
+ * Resolve a data source: build the request from config, fetch (with timeout,
12
+ * jittered retry, SSRF re-validation), extract via `transform`, and cache
13
+ * successful GETs for `cache.ttlMs`.
14
+ *
5
15
  * Works client- or server-side; pass `secrets` only on the server.
6
16
  */
7
17
  export declare function runSource<T = unknown>(source: DataSource, ctx?: RunContext): Promise<RunResult<T>>;
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA8BpE;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CA2C5G"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAIpE;;8EAE8E;AAC9E,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC;;yDAEyD;AACzD,eAAO,MAAM,iBAAiB,QAAkB,CAAC;AAkFjD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAgI5G"}
package/dist/run.js CHANGED
@@ -1,5 +1,14 @@
1
1
  import { interpolate, interpolateDeep, getPath } from './template.js';
2
- const memo = new Map();
2
+ import { defaultCache, cacheKeyFor } from './cache.js';
3
+ /** Applied whenever `source.timeoutMs` is unset. An absent timeout let a hung or
4
+ * slow-loris upstream hold a request for the platform default (~300s), multiplied
5
+ * by every retry — behind a publicly mounted proxy that exhausts sockets. */
6
+ export const DEFAULT_TIMEOUT_MS = 10_000;
7
+ /** Cap on a buffered upstream response body. The SSRF allow-list constrains WHERE
8
+ * a request goes, not how much it returns; without a cap one large (or endless)
9
+ * response is fully buffered per concurrent request. */
10
+ export const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
11
+ const IDEMPOTENT = new Set(['GET', 'HEAD', 'OPTIONS']);
3
12
  function buildUrl(source, scope) {
4
13
  let url = interpolate(source.url, scope);
5
14
  const entries = [];
@@ -29,56 +38,220 @@ function buildHeaders(source, scope) {
29
38
  h[a.header.toLowerCase()] = interpolate(a.key ?? '', scope);
30
39
  return h;
31
40
  }
41
+ /** Only a network error, a timeout, 408, 429 or a 5xx can succeed on a retry.
42
+ * Re-sending a 400/401/403/404 is guaranteed-futile load on a struggling upstream. */
43
+ function isRetryableStatus(status) {
44
+ return status === 408 || status === 429 || status >= 500;
45
+ }
46
+ /** `Retry-After: <seconds>` or an HTTP-date. Returns ms, or undefined. */
47
+ function retryAfterMs(res) {
48
+ const raw = res.headers.get('retry-after');
49
+ if (!raw)
50
+ return undefined;
51
+ const secs = Number(raw);
52
+ if (Number.isFinite(secs))
53
+ return Math.max(0, secs * 1000);
54
+ const when = Date.parse(raw);
55
+ if (Number.isFinite(when))
56
+ return Math.max(0, when - Date.now());
57
+ return undefined;
58
+ }
59
+ /** Full jitter (AWS): random in [0, base * 2^attempt]. A fixed linear backoff made
60
+ * a fleet of clients retry in lockstep against an upstream that was already down. */
61
+ function jitteredBackoff(base, attempt) {
62
+ const ceiling = Math.min(base * 2 ** attempt, 30_000);
63
+ return Math.random() * ceiling;
64
+ }
65
+ /** Read a response body with a hard byte cap, streaming where possible so an
66
+ * endless body is abandoned rather than buffered. */
67
+ async function readCapped(res, maxBytes) {
68
+ const declared = Number(res.headers.get('content-length'));
69
+ if (Number.isFinite(declared) && declared > maxBytes)
70
+ throw new Error('response too large');
71
+ if (!res.body) {
72
+ const text = await res.text();
73
+ if (text.length > maxBytes)
74
+ throw new Error('response too large');
75
+ return text;
76
+ }
77
+ const reader = res.body.getReader();
78
+ const chunks = [];
79
+ let total = 0;
80
+ for (;;) {
81
+ const { done, value } = await reader.read();
82
+ if (done)
83
+ break;
84
+ total += value.byteLength;
85
+ if (total > maxBytes) {
86
+ await reader.cancel();
87
+ throw new Error('response too large');
88
+ }
89
+ chunks.push(value);
90
+ }
91
+ const buf = new Uint8Array(total);
92
+ let off = 0;
93
+ for (const c of chunks) {
94
+ buf.set(c, off);
95
+ off += c.byteLength;
96
+ }
97
+ return new TextDecoder().decode(buf);
98
+ }
32
99
  /**
33
- * Resolve a data source: build the request from config, fetch (with retry/backoff/
34
- * timeout), extract via `transform`, and cache successful GETs for `cache.ttlMs`.
100
+ * Resolve a data source: build the request from config, fetch (with timeout,
101
+ * jittered retry, SSRF re-validation), extract via `transform`, and cache
102
+ * successful GETs for `cache.ttlMs`.
103
+ *
35
104
  * Works client- or server-side; pass `secrets` only on the server.
36
105
  */
37
106
  export async function runSource(source, ctx = {}) {
107
+ const started = Date.now();
38
108
  const scope = { params: ctx.params ?? {}, secret: ctx.secrets ?? {} };
39
109
  const method = source.method ?? 'GET';
40
- const cacheKey = source.cache?.ttlMs && method === 'GET' ? `${source.id}:${JSON.stringify(ctx.params ?? {})}` : '';
41
- if (cacheKey) {
42
- const hit = memo.get(cacheKey);
43
- if (hit && Date.now() - hit.at < source.cache.ttlMs)
44
- return { ok: true, status: 200, data: hit.data };
110
+ const timeoutMs = source.timeoutMs ?? DEFAULT_TIMEOUT_MS;
111
+ const maxBytes = source.maxBytes ?? DEFAULT_MAX_BYTES;
112
+ let url;
113
+ try {
114
+ url = buildUrl(source, scope);
115
+ }
116
+ catch {
117
+ return { ok: false, status: 0, error: 'invalid url', attempts: 0, durationMs: Date.now() - started };
45
118
  }
46
- const url = buildUrl(source, scope);
47
119
  const headers = buildHeaders(source, scope);
48
- let body;
49
- if (source.body !== undefined && method !== 'GET' && method !== 'HEAD') {
50
- const b = interpolateDeep(source.body, scope);
51
- body = typeof b === 'string' ? b : JSON.stringify(b);
52
- if (typeof b !== 'string' && !headers['content-type'])
53
- headers['content-type'] = 'application/json';
120
+ // SSRF: validate the URL we are ACTUALLY about to fetch — after secrets are
121
+ // interpolated not a secret-stripped surrogate checked by the caller.
122
+ if (ctx.allowRequestHost && !ctx.allowRequestHost(url)) {
123
+ return { ok: false, status: 403, error: 'host not allowed', attempts: 0, durationMs: Date.now() - started };
124
+ }
125
+ // Key on the resolved request, so two sources sharing an `id` — or two tenants
126
+ // with different credentials — can never read each other's entries.
127
+ const cache = ctx.cache ?? defaultCache;
128
+ const ttlMs = source.cache?.ttlMs;
129
+ const useCache = !!ttlMs && method === 'GET';
130
+ const key = useCache ? cacheKeyFor(method, url, headers) : '';
131
+ if (useCache) {
132
+ const hit = cache.get(key, ttlMs);
133
+ if (hit !== undefined)
134
+ return { ok: true, status: 200, data: hit, cached: true, attempts: 0, durationMs: Date.now() - started };
54
135
  }
55
- const f = ctx.fetchImpl ?? fetch;
56
- const attempts = Math.max(1, source.retry?.attempts ?? 1);
57
- const backoff = source.retry?.backoffMs ?? 300;
58
- let last = { ok: false, status: 0, error: 'not run' };
59
- for (let i = 0; i < attempts; i++) {
60
- try {
61
- const ctrl = source.timeoutMs ? new AbortController() : null;
62
- const timer = ctrl ? setTimeout(() => ctrl.abort(), source.timeoutMs) : null;
63
- const res = await f(url, { method, headers, body, signal: ctrl?.signal ?? null });
64
- if (timer)
136
+ const exec = async () => {
137
+ // Re-check after winning the single-flight race: the first caller may have
138
+ // already populated the entry while we queued behind it.
139
+ if (useCache) {
140
+ const hit = cache.get(key, ttlMs);
141
+ if (hit !== undefined)
142
+ return { ok: true, status: 200, data: hit, cached: true, attempts: 0, durationMs: Date.now() - started };
143
+ }
144
+ let body;
145
+ if (source.body !== undefined && method !== 'GET' && method !== 'HEAD') {
146
+ const b = interpolateDeep(source.body, scope);
147
+ body = typeof b === 'string' ? b : JSON.stringify(b);
148
+ if (typeof b !== 'string' && !headers['content-type'])
149
+ headers['content-type'] = 'application/json';
150
+ }
151
+ const f = ctx.fetchImpl ?? fetch;
152
+ const requested = Math.max(1, source.retry?.attempts ?? 1);
153
+ // A non-idempotent verb is not retried unless the caller asserts it is safe:
154
+ // a write that landed upstream but timed out here would otherwise run twice.
155
+ const canRetry = IDEMPOTENT.has(method) || source.retry?.retryNonIdempotent === true;
156
+ const attempts = canRetry ? requested : 1;
157
+ const backoff = source.retry?.backoffMs ?? 300;
158
+ const guardRedirects = typeof ctx.allowRedirectHost === 'function';
159
+ const maxRedirects = Math.max(0, ctx.maxRedirects ?? 5);
160
+ let last = { ok: false, status: 0, error: 'not run' };
161
+ let tried = 0;
162
+ for (let i = 0; i < attempts; i++) {
163
+ tried = i + 1;
164
+ // Always abort: an unset timeoutMs previously meant no AbortController at all.
165
+ const ctrl = new AbortController();
166
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
167
+ let waitMs;
168
+ try {
169
+ // Redirect handling. Default: let the platform follow (redirect:'follow').
170
+ // When an SSRF guard is supplied, follow MANUALLY and re-validate each hop —
171
+ // an allow-listed host can otherwise 30x-redirect to a private IP / off-list
172
+ // host and the platform would follow it, bypassing the allow-list.
173
+ const originHost = (() => { try {
174
+ return new URL(url).host;
175
+ }
176
+ catch {
177
+ return '';
178
+ } })();
179
+ let curUrl = url;
180
+ let curMethod = method;
181
+ const curHeaders = { ...headers };
182
+ let curBody = body;
183
+ let res = await f(curUrl, {
184
+ method: curMethod, headers: curHeaders, body: curBody,
185
+ signal: ctrl.signal, redirect: guardRedirects ? 'manual' : 'follow',
186
+ });
187
+ if (guardRedirects) {
188
+ let hops = 0;
189
+ while (res.status >= 300 && res.status < 400 && res.headers.get('location')) {
190
+ if (hops++ >= maxRedirects) {
191
+ clearTimeout(timer);
192
+ return { ok: false, status: 0, error: 'too many redirects', attempts: tried, durationMs: Date.now() - started };
193
+ }
194
+ let loc;
195
+ try {
196
+ loc = new URL(res.headers.get('location'), curUrl).toString();
197
+ }
198
+ catch {
199
+ clearTimeout(timer);
200
+ return { ok: false, status: 0, error: 'invalid redirect location', attempts: tried, durationMs: Date.now() - started };
201
+ }
202
+ if (!ctx.allowRedirectHost(loc)) {
203
+ clearTimeout(timer);
204
+ return { ok: false, status: 403, error: 'redirect host not allowed', attempts: tried, durationMs: Date.now() - started };
205
+ }
206
+ // Cross-host hop → drop credentials so bearer/cookie can't leak off-origin.
207
+ if (new URL(loc).host !== originHost) {
208
+ delete curHeaders['authorization'];
209
+ delete curHeaders['cookie'];
210
+ }
211
+ // 303 (and 301/302 on a non-idempotent method) → switch to GET, drop body;
212
+ // 307/308 preserve method + body.
213
+ if (res.status !== 307 && res.status !== 308 && curMethod !== 'GET' && curMethod !== 'HEAD') {
214
+ curMethod = 'GET';
215
+ curBody = undefined;
216
+ }
217
+ curUrl = loc;
218
+ res = await f(curUrl, { method: curMethod, headers: curHeaders, body: curBody, signal: ctrl.signal, redirect: 'manual' });
219
+ }
220
+ }
221
+ if (!res.ok) {
222
+ clearTimeout(timer);
223
+ last = { ok: false, status: res.status, error: `HTTP ${res.status}`, attempts: tried, durationMs: Date.now() - started };
224
+ if (!isRetryableStatus(res.status))
225
+ return last; // futile to re-send
226
+ waitMs = retryAfterMs(res); // honour the upstream's own pacing
227
+ }
228
+ else {
229
+ const text = await readCapped(res, maxBytes);
230
+ clearTimeout(timer);
231
+ const ct = res.headers.get('content-type') ?? '';
232
+ const raw = ct.includes('json') ? (text ? JSON.parse(text) : undefined) : text;
233
+ const data = (source.transform ? getPath(raw, source.transform) : raw);
234
+ if (useCache)
235
+ cache.set(key, data);
236
+ return { ok: true, status: res.status, data, cached: false, attempts: tried, durationMs: Date.now() - started };
237
+ }
238
+ }
239
+ catch (e) {
65
240
  clearTimeout(timer);
66
- const ct = res.headers.get('content-type') ?? '';
67
- const raw = ct.includes('json') ? await res.json() : await res.text();
68
- const data = (source.transform ? getPath(raw, source.transform) : raw);
69
- last = { ok: res.ok, status: res.status, data, error: res.ok ? undefined : `HTTP ${res.status}` };
70
- if (res.ok) {
71
- if (cacheKey)
72
- memo.set(cacheKey, { at: Date.now(), data });
73
- return last;
241
+ const aborted = e instanceof Error && e.name === 'AbortError';
242
+ last = {
243
+ ok: false, status: 0,
244
+ error: aborted ? `timeout after ${timeoutMs}ms` : e instanceof Error ? e.message : 'fetch failed',
245
+ attempts: tried, durationMs: Date.now() - started,
246
+ };
74
247
  }
248
+ if (i < attempts - 1)
249
+ await new Promise((r) => setTimeout(r, waitMs ?? jitteredBackoff(backoff, i)));
75
250
  }
76
- catch (e) {
77
- last = { ok: false, status: 0, error: e instanceof Error ? e.message : 'fetch failed' };
78
- }
79
- if (i < attempts - 1)
80
- await new Promise((r) => setTimeout(r, backoff * (i + 1)));
81
- }
82
- return last;
251
+ return { ...last, attempts: tried, durationMs: Date.now() - started };
252
+ };
253
+ // Share one in-flight upstream request per cache key: when a hot key expires,
254
+ // concurrent callers await the same promise instead of stampeding the upstream.
255
+ return useCache ? cache.inflight(key, exec) : exec();
83
256
  }
84
257
  //# sourceMappingURL=run.js.map
package/dist/run.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -14,10 +14,32 @@ export interface AuthConfig {
14
14
  export interface RetryConfig {
15
15
  attempts?: number;
16
16
  backoffMs?: number;
17
+ /**
18
+ * Retry non-idempotent verbs (POST/PUT/PATCH/DELETE). Default false.
19
+ *
20
+ * A write that SUCCEEDED upstream but timed out client-side is indistinguishable
21
+ * from one that never landed, so retrying it silently duplicates the write. Only
22
+ * enable when the upstream is idempotent (e.g. you send an idempotency key).
23
+ */
24
+ retryNonIdempotent?: boolean;
17
25
  }
18
26
  export interface CacheConfig {
19
27
  ttlMs?: number;
20
28
  }
29
+ /**
30
+ * A bounded, TTL-evicting response cache with single-flight. Create one PER
31
+ * proxy/tenant: entries are keyed by the fully-resolved request (method + URL +
32
+ * a hash of auth headers), never by `source.id` alone.
33
+ */
34
+ export interface ResponseCache {
35
+ get(key: string, ttlMs: number): unknown | undefined;
36
+ set(key: string, data: unknown): void;
37
+ /** Share one in-flight request per key (stops a TTL-expiry stampede). */
38
+ inflight<T>(key: string, run: () => Promise<T>): Promise<T>;
39
+ /** Drop all entries — required for erasure of cached personal data. */
40
+ clear(): void;
41
+ readonly size: number;
42
+ }
21
43
  /** A declarative data source. url/headers/query/body may contain {{params.x}}
22
44
  * and (server-side only) {{secret.X}} template references. */
23
45
  export interface DataSource {
@@ -28,9 +50,20 @@ export interface DataSource {
28
50
  query?: Record<string, string>;
29
51
  body?: unknown;
30
52
  auth?: AuthConfig;
53
+ /** Per-request timeout. Defaults to `DEFAULT_TIMEOUT_MS` (10s) — never unbounded. */
31
54
  timeoutMs?: number;
32
55
  retry?: RetryConfig;
33
56
  cache?: CacheConfig;
57
+ /** Cap on the upstream response body in bytes. Default `DEFAULT_MAX_BYTES` (5 MiB). */
58
+ maxBytes?: number;
59
+ /**
60
+ * Reachable through `createProxy().handleRequest`? Default FALSE.
61
+ *
62
+ * `handleRequest` performs no authentication of its own, and each call spends the
63
+ * server's secret-authenticated upstream quota. Opting a source in makes it
64
+ * invokable by any client that can reach the route, with attacker-chosen params.
65
+ */
66
+ expose?: boolean;
34
67
  /** Dot/bracket path into the JSON response to extract, e.g. "data.items". */
35
68
  transform?: string;
36
69
  }
@@ -40,11 +73,37 @@ export interface RunContext {
40
73
  secrets?: Record<string, string>;
41
74
  /** Override fetch (for tests / custom runtimes). */
42
75
  fetchImpl?: typeof fetch;
76
+ /**
77
+ * SSRF hardening: when provided, redirects are followed MANUALLY and every hop's
78
+ * URL is re-validated with this predicate (return `true` to allow). Without it,
79
+ * the platform default (`redirect: 'follow'`) is used and off-list redirect
80
+ * targets are NOT re-checked — so the proxy always sets this. Credentials
81
+ * (`authorization` / `cookie`) are stripped on a cross-host hop.
82
+ */
83
+ allowRedirectHost?: (url: string) => boolean;
84
+ /** Max redirect hops to follow when `allowRedirectHost` is set (default 5). */
85
+ maxRedirects?: number;
86
+ /**
87
+ * SSRF hardening for the INITIAL request. Checked against the fully-resolved URL
88
+ * — after `{{secret.X}}` interpolation — immediately before fetch. The proxy used
89
+ * to allow-list a secret-STRIPPED surrogate while `runSource` fetched the real
90
+ * one, so a secret in the host position (`api.example.com{{secret.SUFFIX}}`)
91
+ * passed the check and reached a different host.
92
+ */
93
+ allowRequestHost?: (url: string) => boolean;
94
+ /** Per-instance response cache. Omit to use the process-wide default cache. */
95
+ cache?: ResponseCache;
43
96
  }
44
97
  export interface RunResult<T = unknown> {
45
98
  ok: boolean;
46
99
  status: number;
47
100
  data?: T;
48
101
  error?: string;
102
+ /** Observability: how many attempts were made (1 = no retry). */
103
+ attempts?: number;
104
+ /** Observability: served from cache without an upstream request. */
105
+ cached?: boolean;
106
+ /** Observability: wall-clock ms for the whole resolve. */
107
+ durationMs?: number;
49
108
  }
50
109
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAExF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpC,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;+DAC+D;AAC/D,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;IACpC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@noidmejs/atomkit-http",
3
- "version": "0.1.0",
4
- "description": "Declarative HTTP/backend connector: call any API by config (all verbs, headers, query, body, auth, retries, cache, transforms). Secrets stay server-only via a proxy with an SSRF host allow-list.",
3
+ "version": "0.3.0",
4
+ "description": "Declarative HTTP/backend connector: call any API by config (all verbs, headers, query, body, auth, retries, cache, transforms). Secrets stay server-only via a proxy with an SSRF host allow-list + per-redirect-hop re-validation.",
5
5
  "keywords": ["atomkit", "http", "connector", "declarative", "fetch", "api", "proxy", "ssrf", "no-code"],
6
6
  "license": "MIT",
7
7
  "author": "noidmejs",
@@ -19,5 +19,7 @@
19
19
  "devDependencies": { "typescript": "^5.6.0", "@types/node": "^20.0.0" },
20
20
  "publishConfig": { "access": "public" },
21
21
  "repository": { "type": "git", "url": "git+https://github.com/noidme-git/atomkit-http.git" },
22
+ "bugs": { "url": "https://github.com/noidme-git/atomkit-http/issues" },
23
+ "homepage": "https://github.com/noidme-git/atomkit-http#readme",
22
24
  "engines": { "node": ">=18" }
23
25
  }