@volter/twin-upstash 0.1.1 → 0.1.3

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.
@@ -13,7 +13,7 @@
13
13
  // the SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
14
14
  // (`createTwinFetchFromHandler`): replies carry ONLY the handler's own headers (the REST envelope
15
15
  // declares its own content type; a 405 carries none at all), and the clock is an injectable seam.
16
- import { serveHttp } from '@volter/world-core';
16
+ import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
17
17
  import { handleUpstashRedisTwinRequest } from "./upstash-twin.js";
18
18
  import { upstashRedisOwners } from "./upstash-store.js";
19
19
  import { API_HOST, CONSOLE_HOST, createUpstashApiLaneFetch } from "../api/src/fetch.js";
@@ -26,12 +26,16 @@ const QSTASH_HOST = /^qstash(-[a-z0-9-]+)?\.upstash\.io$/;
26
26
  const QSTASH_PATH = /^\/(v2\/|_twin\/(deliveries|destinations)(\/|$))/;
27
27
  /** The pack's fetch, with `owners()`: per id of Redis's command table, the command core (`handler`) or the gap. */
28
28
  export function createUpstashRedisTwinFetch(options = {}) {
29
- const readOnly = options.readOnly ?? false;
30
29
  const now = options.now ?? (() => worldNow());
31
30
  // api.upstash.com's Developer API and console.upstash.com's pages are the pack's `api` lane (../api/src/fetch.ts)
31
+ // the lanes' own read-only twin; a read-only REQUEST is refused by the kernel's scope around them (withRequestScopes)
32
+ const readOnly = options.readOnly ?? false;
32
33
  const api = createUpstashApiLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
33
34
  const qstash = options.qstash ?? createQstashLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
34
- const fetch = async function upstashRedisTwinFetch(request) {
35
+ // a read-only request (x-volter-read-only) is a request to a read-only twin: a command that writes
36
+ // (by GET too: Upstash runs any command from the path) is refused, and the kernel refuses any that slip
37
+ const fetch = withRequestScopes(async function upstashRedisTwinFetch(request) {
38
+ const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
35
39
  const url = new URL(request.url);
36
40
  // the vendor host a redirected request names in x-volter-twin-original-host (the injector's fetch path, a hosted
37
41
  // World), else its Host, else its URL's (vercel-server.ts's precedent): the lane's two hosts go to the lane
@@ -64,7 +68,7 @@ export function createUpstashRedisTwinFetch(options = {}) {
64
68
  if (out === null)
65
69
  return new Response(null, { status, headers: { ...(outHeaders ?? {}) } });
66
70
  return new Response(JSON.stringify(out), { status, headers: { ...(outHeaders ?? {}) } });
67
- };
71
+ }, { refuse: () => Response.json({ error: 'NOPERM this request is read-only' }, { status: 403 }) });
68
72
  return Object.assign(fetch, { owners: upstashRedisOwners });
69
73
  }
70
74
  /** `GET /twin`, the discovery manifest. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-upstash",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local Upstash Redis (REST API) twin: a real stateful Redis command core (strings/counters/TTL/hashes/sets/sorted-sets/keyspace) served over Upstash's per-database REST protocol — single-command POST /, path-style GET /get/key, POST /pipeline, POST /multi-exec, Bearer-token auth, the {result}/{error} envelope, Upstash-Encoding: base64 — plus an EVAL/EVALSHA Lua-subset interpreter that runs @upstash/ratelimit's real fixed-window/sliding-window/token-bucket scripts. Built on @volter/world-core.",
5
5
  "author": "Volter (https://github.com/volter-ai)",
6
6
  "license": "Apache-2.0",
@@ -37,17 +37,17 @@
37
37
  "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
38
38
  },
39
39
  "dependencies": {
40
- "@volter/world-ui": "0.1.1",
40
+ "@volter/world-ui": "0.1.3",
41
41
  "react": "^19.2.7",
42
42
  "react-dom": "^19.2.7"
43
43
  },
44
44
  "peerDependencies": {
45
- "@volter/world-core": "2.0.1"
45
+ "@volter/world-core": "2.0.3"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/bun": "^1.2.20",
49
49
  "@types/node": "^24.0.0",
50
- "@volter/world-core": "2.0.1",
50
+ "@volter/world-core": "2.0.3",
51
51
  "@volter/world-tooling": "0.1.0",
52
52
  "@upstash/redis": "1.35.7",
53
53
  "@upstash/ratelimit": "2.0.8",
@@ -13,7 +13,7 @@
13
13
  // the SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
14
14
  // (`createTwinFetchFromHandler`): replies carry ONLY the handler's own headers (the REST envelope
15
15
  // declares its own content type; a 405 carries none at all), and the clock is an injectable seam.
16
- import { serveHttp } from '@volter/world-core';
16
+ import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
17
17
  import { handleUpstashRedisTwinRequest } from './upstash-twin.ts';
18
18
  import { upstashRedisOwners } from './upstash-store.ts';
19
19
  import { API_HOST, CONSOLE_HOST, createUpstashApiLaneFetch } from '../api/src/fetch.ts';
@@ -50,12 +50,16 @@ const QSTASH_PATH = /^\/(v2\/|_twin\/(deliveries|destinations)(\/|$))/;
50
50
 
51
51
  /** The pack's fetch, with `owners()`: per id of Redis's command table, the command core (`handler`) or the gap. */
52
52
  export function createUpstashRedisTwinFetch(options: UpstashRedisTwinFetchOptions = {}): ((request: Request) => Promise<Response>) & { owners: typeof upstashRedisOwners } {
53
- const readOnly = options.readOnly ?? false;
54
53
  const now = options.now ?? (() => worldNow());
55
54
  // api.upstash.com's Developer API and console.upstash.com's pages are the pack's `api` lane (../api/src/fetch.ts)
55
+ // the lanes' own read-only twin; a read-only REQUEST is refused by the kernel's scope around them (withRequestScopes)
56
+ const readOnly = options.readOnly ?? false;
56
57
  const api = createUpstashApiLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
57
58
  const qstash = options.qstash ?? createQstashLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
58
- const fetch = async function upstashRedisTwinFetch(request: Request): Promise<Response> {
59
+ // a read-only request (x-volter-read-only) is a request to a read-only twin: a command that writes
60
+ // (by GET too: Upstash runs any command from the path) is refused, and the kernel refuses any that slip
61
+ const fetch = withRequestScopes(async function upstashRedisTwinFetch(request: Request): Promise<Response> {
62
+ const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
59
63
  const url = new URL(request.url);
60
64
  // the vendor host a redirected request names in x-volter-twin-original-host (the injector's fetch path, a hosted
61
65
  // World), else its Host, else its URL's (vercel-server.ts's precedent): the lane's two hosts go to the lane
@@ -84,7 +88,7 @@ export function createUpstashRedisTwinFetch(options: UpstashRedisTwinFetchOption
84
88
  // the real service answers those with zero bytes and, for 405, no content-type either.
85
89
  if (out === null) return new Response(null, { status, headers: { ...(outHeaders ?? {}) } });
86
90
  return new Response(JSON.stringify(out), { status, headers: { ...(outHeaders ?? {}) } });
87
- };
91
+ }, { refuse: () => Response.json({ error: 'NOPERM this request is read-only' }, { status: 403 }) });
88
92
  return Object.assign(fetch, { owners: upstashRedisOwners });
89
93
  }
90
94