@substrat-run/vertical-host 0.137.0 → 0.139.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.
@@ -1 +1 @@
1
- {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE9E;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,qEAAqE;AACrE,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC,2GAA2G;AAC3G,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;AAE1F,qGAAqG;AAErG,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACnD,oFAAoF;IACpF,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;IACxE;;;OAGG;IACH,UAAU,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC9D,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAeD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,GAAG,EAE1C,GAAG,EAAE,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EACtB,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC3B,IAAI,CAsBN"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE9E;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,qEAAqE;AACrE,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC,2GAA2G;AAC3G,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;AAE1F,qGAAqG;AAErG,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACnD,oFAAoF;IACpF,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;IACxE;;;OAGG;IACH,UAAU,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC9D,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,SAAuC,GAAG,QAAQ,CAErG;AAED,gCAAgC;AAChC,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,SAA4D,GAAG,QAAQ,CAEtH;AAeD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,GAAG,EAE1C,GAAG,EAAE,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EACtB,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC3B,IAAI,CAYN"}
package/dist/live.js CHANGED
@@ -8,6 +8,20 @@ import { isUpgradeRequest } from '@substrat-run/kernel';
8
8
  export { isUpgradeRequest };
9
9
  /** Where `mountLiveReads` mounts the route unless told otherwise. */
10
10
  export const LIVE_PATH = '/api/live';
11
+ /**
12
+ * The two refusals a client reads as "no push here" (#1853): `426` for a request that is not
13
+ * a WebSocket handshake, and `501` for a host that cannot push, each with the
14
+ * `LIVE_MODE_HEADER` value the client keys its fallback on. Exported so a vertical's own
15
+ * live route — one that cannot use `mountLiveReads`'s same-origin gate, like a widget on
16
+ * somebody else's page — answers in the same words.
17
+ */
18
+ export function refuseNotAnUpgrade(c, error = 'live reads are a WebSocket surface') {
19
+ return c.json({ error }, 426, { [LIVE_MODE_HEADER]: 'not-an-upgrade' });
20
+ }
21
+ /** See `refuseNotAnUpgrade`. */
22
+ export function refuseLivePoll(c, error = 'live reads are not available on this host; poll instead') {
23
+ return c.json({ error }, 501, { [LIVE_MODE_HEADER]: 'poll' });
24
+ }
11
25
  /**
12
26
  * Whether a handshake's `Origin` is the page this vertical serves.
13
27
  *
@@ -28,18 +42,14 @@ export function mountLiveReads(
28
42
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
43
  app, options) {
30
44
  app.get(options.path ?? LIVE_PATH, async (c) => {
31
- if (!isUpgradeRequest(c.req.raw)) {
32
- return c.json({ error: 'live reads are a WebSocket surface' }, 426, {
33
- [LIVE_MODE_HEADER]: 'not-an-upgrade',
34
- });
35
- }
45
+ if (!isUpgradeRequest(c.req.raw))
46
+ return refuseNotAnUpgrade(c);
36
47
  if (!sameOrigin(c.req.raw)) {
37
48
  return c.json({ error: 'live reads are only offered to this app’s own pages' }, 403);
38
49
  }
39
50
  const live = options.live(c);
40
- if (!live) {
41
- return c.json({ error: 'live reads are not available on this host; poll instead' }, 501, { [LIVE_MODE_HEADER]: 'poll' });
42
- }
51
+ if (!live)
52
+ return refuseLivePoll(c);
43
53
  const who = await options.subscriber(c);
44
54
  if (!who)
45
55
  return c.json({ error: 'unauthorized' }, 401);
package/dist/live.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAE,gBAAgB,EAAoB,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAwB,MAAM,sBAAsB,CAAC;AAE9E;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,qEAAqE;AACrE,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AAmBrC;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,GAAY;IAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;AAC/D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc;AAC5B,8DAA8D;AAC9D,GAAsB,EACtB,OAA4B;IAE5B,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC7C,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,EAAE,GAAG,EAAE;gBAClE,CAAC,gBAAgB,CAAC,EAAE,gBAAsC;aAC3D,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qDAAqD,EAAE,EAAE,GAAG,CAAC,CAAC;QACvF,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,yDAAyD,EAAE,EACpE,GAAG,EACH,EAAE,CAAC,gBAAgB,CAAC,EAAE,MAA4B,EAAE,CACrD,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAE,gBAAgB,EAAoB,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAwB,MAAM,sBAAsB,CAAC;AAE9E;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,qEAAqE;AACrE,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AAmBrC;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAAU,EAAE,KAAK,GAAG,oCAAoC;IACzF,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,gBAAsC,EAAE,CAAC,CAAC;AAChG,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,cAAc,CAAC,CAAU,EAAE,KAAK,GAAG,yDAAyD;IAC1G,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,MAA4B,EAAE,CAAC,CAAC;AACtF,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,GAAY;IAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;AAC/D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc;AAC5B,8DAA8D;AAC9D,GAAsB,EACtB,OAA4B;IAE5B,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC7C,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qDAAqD,EAAE,EAAE,GAAG,CAAC,CAAC;QACvF,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI;YAAE,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Request rate limiting for a host with no router in front (#130).
3
+ *
4
+ * On the hosted path the router counts every request before it reaches a vertical, and a
5
+ * vertical mounts nothing. A node host — self-host, a dev server, a container — has no
6
+ * router, so this is the same choke point moved onto its own Hono app: the keys, the
7
+ * headers and the 429 are `@substrat-run/contracts`' `rate-limit.ts`, so the two paths
8
+ * cannot disagree about what a refusal looks like.
9
+ *
10
+ * Opt-in, and never mounted on a Cloudflare vertical: an isolate's memory is not a counter,
11
+ * and the router already counted the request.
12
+ *
13
+ * ```ts
14
+ * app.use('*', rateLimit({ nodeOf: () => ({ tenantId, scopeId }), clientIp: (c) => … }));
15
+ * ```
16
+ */
17
+ import type { Context, MiddlewareHandler } from 'hono';
18
+ import { type RateLimitBucket, type RateLimiter, type RateLimitPolicy } from '@substrat-run/contracts';
19
+ export interface RateLimitOptions {
20
+ /**
21
+ * The tenant and scope a request is for — the prefix of every key, which is what keeps one
22
+ * tenant from spending another's budget. A single-install host answers a constant.
23
+ */
24
+ nodeOf: (c: Context) => {
25
+ tenantId: string;
26
+ scopeId: string;
27
+ } | Promise<{
28
+ tenantId: string;
29
+ scopeId: string;
30
+ }>;
31
+ /**
32
+ * The client address, for the address budget and for a request with no credential. Only
33
+ * the host knows which proxy header it can trust, so nothing is read by default; absent,
34
+ * every anonymous caller shares one budget.
35
+ */
36
+ clientIp?: (c: Context) => string | null | undefined;
37
+ /** Budgets per bucket. Defaults to `DEFAULT_RATE_LIMITS`. */
38
+ limits?: Partial<Record<RateLimitBucket, RateLimitPolicy>>;
39
+ /**
40
+ * The counters. Defaults to one {@link memoryRateLimiter} per bucket — exact for this
41
+ * process, and only for it: several replicas want a shared counter here.
42
+ */
43
+ limiters?: Partial<Record<RateLimitBucket, RateLimiter>>;
44
+ }
45
+ /**
46
+ * Count each request against its budgets before any route runs. Refuses with a 429
47
+ * `application/problem+json` (`rate_limited`) and `Retry-After`; fails OPEN with a log line
48
+ * when a counter throws, for the reason the contracts module gives.
49
+ */
50
+ export declare function rateLimit(options: RateLimitOptions): MiddlewareHandler;
51
+ /** The keys a {@link memoryRateLimiter} holds at most, by default. */
52
+ export declare const MEMORY_RATE_LIMITER_MAX_KEYS = 50000;
53
+ /**
54
+ * A fixed-window counter in this process's memory — the `RateLimiter` {@link rateLimit}
55
+ * counts with by default.
56
+ *
57
+ * Exact for ONE process. Several replicas behind a balancer each count their own share, so
58
+ * the effective budget is the limit times the replica count; a deployment like that wants a
59
+ * shared counter behind the same interface.
60
+ *
61
+ * Bounded at `maxKeys` windows. Expired windows are swept at most once a period as keys are
62
+ * touched; a NEW key arriving at capacity first sweeps whatever has expired, and if the map is
63
+ * still full it evicts the oldest window. Eviction, not refusal, deliberately: refusing a
64
+ * newcomer because a crowd of other addresses filled the map would let that crowd deny
65
+ * service to everyone else, while forgetting the oldest count only hands that key a fresh
66
+ * budget — the limiter degrading toward open, the same direction it fails in everywhere else.
67
+ * A caller rotating credentials from one address cannot fill it: `evaluateRateLimits` counts
68
+ * the address first and never mints a credential key for a request the address refused.
69
+ */
70
+ export declare function memoryRateLimiter(policy: RateLimitPolicy, { now, maxKeys }?: {
71
+ now?: () => number;
72
+ maxKeys?: number;
73
+ }): RateLimiter & {
74
+ readonly size: number;
75
+ };
76
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/G;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrD,6DAA6D;IAC7D,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC,CAAC;IAC3D;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC;CAC1D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,iBAAiB,CAuBtE;AAED,sEAAsE;AACtE,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,eAAe,EACvB,EAAE,GAAc,EAAE,OAAsC,EAAE,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACxG,WAAW,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAkCzC"}
@@ -0,0 +1,88 @@
1
+ import { DEFAULT_RATE_LIMITS, evaluateRateLimits, RATE_LIMIT_BUCKETS, RATE_LIMIT_POLICY_HEADER, } from '@substrat-run/contracts';
2
+ /**
3
+ * Count each request against its budgets before any route runs. Refuses with a 429
4
+ * `application/problem+json` (`rate_limited`) and `Retry-After`; fails OPEN with a log line
5
+ * when a counter throws, for the reason the contracts module gives.
6
+ */
7
+ export function rateLimit(options) {
8
+ const policies = { ...DEFAULT_RATE_LIMITS, ...options.limits };
9
+ const limiters = Object.fromEntries(RATE_LIMIT_BUCKETS.map((bucket) => [bucket, options.limiters?.[bucket] ?? memoryRateLimiter(policies[bucket])]));
10
+ return async (c, next) => {
11
+ const at = await options.nodeOf(c);
12
+ const { refusal, policyHeader, unavailable } = await evaluateRateLimits({
13
+ subject: { ...at, headers: c.req.raw.headers, clientIp: options.clientIp?.(c), method: c.req.method },
14
+ policies,
15
+ limiters,
16
+ instance: c.req.path,
17
+ });
18
+ if (unavailable)
19
+ console.error(JSON.stringify({ host: 'rate-limit-unavailable', ...at, ...unavailable }));
20
+ if (refusal)
21
+ return c.body(JSON.stringify(refusal.body), refusal.status, refusal.headers);
22
+ await next();
23
+ // Rebuilt rather than set in place: a handler may answer with a response whose headers
24
+ // are immutable (one it fetched). An upgrade carries a socket a rebuild would drop.
25
+ if (c.res.status === 101 || !policyHeader)
26
+ return;
27
+ c.res = new Response(c.res.body, c.res);
28
+ c.res.headers.set(RATE_LIMIT_POLICY_HEADER, policyHeader);
29
+ };
30
+ }
31
+ /** The keys a {@link memoryRateLimiter} holds at most, by default. */
32
+ export const MEMORY_RATE_LIMITER_MAX_KEYS = 50_000;
33
+ /**
34
+ * A fixed-window counter in this process's memory — the `RateLimiter` {@link rateLimit}
35
+ * counts with by default.
36
+ *
37
+ * Exact for ONE process. Several replicas behind a balancer each count their own share, so
38
+ * the effective budget is the limit times the replica count; a deployment like that wants a
39
+ * shared counter behind the same interface.
40
+ *
41
+ * Bounded at `maxKeys` windows. Expired windows are swept at most once a period as keys are
42
+ * touched; a NEW key arriving at capacity first sweeps whatever has expired, and if the map is
43
+ * still full it evicts the oldest window. Eviction, not refusal, deliberately: refusing a
44
+ * newcomer because a crowd of other addresses filled the map would let that crowd deny
45
+ * service to everyone else, while forgetting the oldest count only hands that key a fresh
46
+ * budget — the limiter degrading toward open, the same direction it fails in everywhere else.
47
+ * A caller rotating credentials from one address cannot fill it: `evaluateRateLimits` counts
48
+ * the address first and never mints a credential key for a request the address refused.
49
+ */
50
+ export function memoryRateLimiter(policy, { now = Date.now, maxKeys = MEMORY_RATE_LIMITER_MAX_KEYS } = {}) {
51
+ // Insertion order is window-start order: a key whose window resets is deleted and
52
+ // re-inserted, so the first entry is always the oldest window.
53
+ const windows = new Map();
54
+ const periodMs = policy.period * 1000;
55
+ let lastSweep = now();
56
+ const sweep = (t) => {
57
+ for (const [k, w] of windows) {
58
+ if (t - w.start < periodMs)
59
+ break;
60
+ windows.delete(k);
61
+ }
62
+ lastSweep = t;
63
+ };
64
+ return {
65
+ get size() {
66
+ return windows.size;
67
+ },
68
+ async limit({ key }) {
69
+ const t = now();
70
+ if (t - lastSweep >= periodMs)
71
+ sweep(t);
72
+ let w = windows.get(key);
73
+ if (!w || t - w.start >= periodMs) {
74
+ windows.delete(key);
75
+ if (windows.size >= maxKeys) {
76
+ sweep(t);
77
+ if (windows.size >= maxKeys)
78
+ windows.delete(windows.keys().next().value);
79
+ }
80
+ w = { start: t, count: 0 };
81
+ windows.set(key, w);
82
+ }
83
+ w.count += 1;
84
+ return { success: w.count <= policy.limit };
85
+ },
86
+ };
87
+ }
88
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/vertical-host",
3
- "version": "0.137.0",
3
+ "version": "0.139.0",
4
4
  "description": "The platform's /internal/* surface + error envelope, authored once — mount it in a vertical worker instead of re-copying the control-plane contract",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -26,18 +26,21 @@
26
26
  "dist"
27
27
  ],
28
28
  "dependencies": {
29
- "ai": "7.0.114",
30
- "hono": "^4.13.9",
29
+ "ai": "7.0.127",
30
+ "hono": "^4.13.12",
31
31
  "workers-ai-provider": "^4.0.0",
32
- "@substrat-run/contracts": "^0.137.0",
33
- "@substrat-run/model-providers": "^0.5.27",
34
- "@substrat-run/kernel": "^0.137.0"
32
+ "@substrat-run/contracts": "^0.139.0",
33
+ "@substrat-run/model-providers": "^0.5.29",
34
+ "@substrat-run/kernel": "^0.139.0"
35
35
  },
36
36
  "devDependencies": {
37
+ "@types/better-sqlite3": "^9.6.0",
38
+ "better-sqlite3": "^13.0.3",
37
39
  "typescript": "^7.0.0",
38
40
  "vitest": "^3.2.7",
39
- "@substrat-run/adapter-sqlite": "^0.137.0",
40
- "@substrat-run/contract-tests": "^0.137.0"
41
+ "@substrat-run/adapter-sqlite": "^0.139.0",
42
+ "@substrat-run/contract-tests": "^0.139.0",
43
+ "@substrat-run/vertical-auth": "^0.20.0"
41
44
  },
42
45
  "publishConfig": {
43
46
  "access": "public"