@noy-db/in-rest 0.5.0 → 0.6.0-pre.1

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
@@ -14,7 +14,44 @@ pnpm add @noy-db/hub @noy-db/in-rest
14
14
 
15
15
  ## What it is
16
16
 
17
- Framework-neutral REST API integration for noy-db — createRestHandler with Hono, Express, Fastify, and Nitro subpath adapters.
17
+ A framework-neutral HTTP server that exposes a noy-db **store** — and nothing more.
18
+ `createRestHandler` turns any `NoydbStore` into a single `POST {basePath}/rpc` endpoint
19
+ that proxies the six store methods (`get`/`put`/`delete`/`list`/`loadAll`/`saveAll`, plus
20
+ optional `ping`/`listSince`/`listPage`/`listVaults`) over the wire. Hono, Express, Fastify,
21
+ and Nitro subpath adapters marshal your framework's request/response into it.
22
+
23
+ **The server only ever sees ciphertext.** It never receives the vault passphrase, never
24
+ unlocks a vault, and never runs a query — it forwards `EncryptedEnvelope`s straight to the
25
+ store and returns them unchanged. Unlock, decryption, and the query DSL all happen in the
26
+ **client**, exactly as with every other noy-db storage backend. The REST server is a dumb,
27
+ untrusted ciphertext relay, consistent with the zero-knowledge model.
28
+
29
+ The matching client — a `to-rest` `NoydbStore` that POSTs to this endpoint and re-hydrates
30
+ `ConflictError` from a `409` — lives in the [`noy-db-to`](https://github.com/vLannaAi/noy-db-to)
31
+ companion repo (the `to-*` store family), so it never has to ship here.
32
+
33
+ ## Usage
34
+
35
+ ```ts
36
+ import { createRestHandler } from '@noy-db/in-rest'
37
+ import { honoAdapter } from '@noy-db/in-rest/hono'
38
+
39
+ // `store` is any ciphertext NoydbStore (to-file, to-postgres, to-aws-s3, …).
40
+ const handler = createRestHandler({
41
+ store,
42
+ // Authorization is FAIL-CLOSED: with no `authorize`, every request is 401.
43
+ // You own the policy — check a bearer token, an mTLS header, a JWT, whatever.
44
+ authorize: (req) => req.headers['authorization'] === `Bearer ${process.env.REST_TOKEN}`,
45
+ // Optional: restrict to a read-only relay.
46
+ // allow: new Set(['get', 'list', 'loadAll']),
47
+ })
48
+
49
+ app.route('/', honoAdapter(handler)) // or expressAdapter / fastifyPlugin / nitroAdapter
50
+ ```
51
+
52
+ `RestHandlerOptions`: `{ store, authorize?, allow?, basePath? }`. Success → `200` with the
53
+ raw store result; a CAS conflict → `409 { error: { name: 'ConflictError', version } }`;
54
+ unauthorized → `401`; unknown/malformed → `400`; disallowed method → `403`.
18
55
 
19
56
  ## Status
20
57
 
package/dist/index.d.ts CHANGED
@@ -3,12 +3,22 @@ import { NoydbStore } from '@noy-db/hub';
3
3
  /**
4
4
  * **@noy-db/in-rest** — Framework-neutral REST API integration for noy-db.
5
5
  *
6
+ * A thin RPC dispatcher — the HTTP twin of `@noy-db/by-peer`'s
7
+ * `servePeerStore` — that forwards the 6 `NoydbStore` methods straight to
8
+ * the caller's ciphertext store. The server NEVER sees a passphrase, never
9
+ * calls `createNoydb`/`openVault`, and never decrypts anything: every
10
+ * request/response body is an `EncryptedEnvelope` (or a plain id/list of
11
+ * one) round-tripped as-is.
12
+ *
6
13
  * @example
7
14
  * ```ts
8
15
  * import { createRestHandler } from '@noy-db/in-rest'
9
16
  * import { honoAdapter } from '@noy-db/in-rest/hono'
10
17
  *
11
- * const handler = createRestHandler({ store, user: 'api' })
18
+ * const handler = createRestHandler({
19
+ * store,
20
+ * authorize: (req) => req.headers['authorization'] === `Bearer ${API_KEY}`,
21
+ * })
12
22
  * app.route('/api/noydb', honoAdapter(handler))
13
23
  * ```
14
24
  *
@@ -32,8 +42,17 @@ interface NoydbRestHandler {
32
42
  }
33
43
  interface RestHandlerOptions {
34
44
  readonly store: NoydbStore;
35
- readonly user: string;
36
- readonly ttlSeconds?: number;
45
+ /**
46
+ * Authorize each request. Return `true` to allow. If OMITTED, the
47
+ * handler is FAIL-CLOSED — every `/rpc` request is rejected with 401.
48
+ * The caller MUST supply an authorizer to accept any traffic.
49
+ */
50
+ readonly authorize?: (req: RestRequest) => boolean | Promise<boolean>;
51
+ /**
52
+ * Optional method allowlist (e.g. a read-only relay). When set, a
53
+ * method not in the set is rejected with 403.
54
+ */
55
+ readonly allow?: ReadonlySet<string>;
37
56
  readonly basePath?: string;
38
57
  }
39
58
  declare function createRestHandler(options: RestHandlerOptions): NoydbRestHandler;
package/dist/index.js CHANGED
@@ -1,165 +1,5 @@
1
- // src/sessions.ts
2
- var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
3
- var SessionStore = class {
4
- sessions = /* @__PURE__ */ new Map();
5
- ttlMs;
6
- sweepTimer;
7
- constructor(ttlSeconds, options = {}) {
8
- this.ttlMs = ttlSeconds * 1e3;
9
- const sweepMs = options.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
10
- if (sweepMs > 0) {
11
- const timer = setInterval(() => this.sweep(), sweepMs);
12
- if (typeof timer.unref === "function") {
13
- timer.unref();
14
- }
15
- this.sweepTimer = timer;
16
- } else {
17
- this.sweepTimer = null;
18
- }
19
- }
20
- create(db) {
21
- const token = crypto.randomUUID();
22
- this.sessions.set(token, { db, expiresAt: Date.now() + this.ttlMs });
23
- return token;
24
- }
25
- /**
26
- * Look up a session and refresh its sliding-window TTL. Call from
27
- * auth-guarded routes that are treated as "activity".
28
- */
29
- get(token) {
30
- const session = this.sessions.get(token);
31
- if (!session) return null;
32
- if (Date.now() > session.expiresAt) {
33
- this.sessions.delete(token);
34
- return null;
35
- }
36
- session.expiresAt = Date.now() + this.ttlMs;
37
- return session.db;
38
- }
39
- delete(token) {
40
- this.sessions.delete(token);
41
- }
42
- /**
43
- * Non-refreshing existence check. Used by polling endpoints like
44
- * `GET /sessions/current` that should not extend the session merely
45
- * by being queried.
46
- */
47
- peek(token) {
48
- const session = this.sessions.get(token);
49
- if (!session) return false;
50
- if (Date.now() > session.expiresAt) {
51
- this.sessions.delete(token);
52
- return false;
53
- }
54
- return true;
55
- }
56
- /**
57
- * Drop every session whose TTL has elapsed. Called automatically by
58
- * the internal interval timer; exposed so tests can deterministically
59
- * trigger the sweep without waiting. Returns the count of entries
60
- * removed — useful for logging / metrics hooks.
61
- */
62
- sweep() {
63
- const now = Date.now();
64
- let removed = 0;
65
- for (const [token, session] of this.sessions) {
66
- if (now > session.expiresAt) {
67
- this.sessions.delete(token);
68
- removed++;
69
- }
70
- }
71
- return removed;
72
- }
73
- /**
74
- * Current number of tracked sessions (expired or not). Primarily for
75
- * tests that verify the sweep is working; operators wanting live
76
- * metrics should wrap the store or emit their own counters.
77
- */
78
- size() {
79
- return this.sessions.size;
80
- }
81
- /**
82
- * Stop the background sweep timer and drop every session. Call this
83
- * on shutdown to allow the event loop to exit cleanly — `unref()`
84
- * handles most cases, but explicit cleanup is cheaper than relying
85
- * on finalization.
86
- */
87
- close() {
88
- if (this.sweepTimer !== null) clearInterval(this.sweepTimer);
89
- this.sessions.clear();
90
- }
91
- };
92
-
93
- // src/router.ts
94
- import { createNoydb, PermissionDeniedError, NotFoundError, ConflictError, ValidationError } from "@noy-db/hub";
95
-
96
- // src/query-params.ts
97
- var OP_MAP = {
98
- eq: "==",
99
- neq: "!=",
100
- gt: ">",
101
- gte: ">=",
102
- lt: "<",
103
- lte: "<="
104
- };
105
- function parseQueryParams(searchParams) {
106
- const wheres = searchParams.getAll("where");
107
- const orderByParam = searchParams.get("orderBy");
108
- const limitParam = searchParams.get("limit");
109
- const whereClauses = [];
110
- for (const clause of wheres) {
111
- const parts = clause.split(":");
112
- if (parts.length < 3) {
113
- return {
114
- error: { error: "invalid_where", op: clause },
115
- apply: (q) => q,
116
- limit: null
117
- };
118
- }
119
- const field = parts[0];
120
- const opStr = parts[1];
121
- const value = parts.slice(2).join(":");
122
- const op = OP_MAP[opStr];
123
- if (!op) {
124
- return {
125
- error: { error: "invalid_op", op: opStr },
126
- apply: (q) => q,
127
- limit: null
128
- };
129
- }
130
- whereClauses.push({ field, op, value: coerce(value) });
131
- }
132
- let orderBy = null;
133
- if (orderByParam) {
134
- const obParts = orderByParam.split(":");
135
- const obField = obParts[0];
136
- const obDir = obParts[1];
137
- orderBy = { field: obField, dir: obDir === "desc" ? "desc" : "asc" };
138
- }
139
- const limit = limitParam ? parseInt(limitParam, 10) : null;
140
- return {
141
- apply(q) {
142
- let result = q;
143
- for (const { field, op, value } of whereClauses) {
144
- result = result.where(field, op, value);
145
- }
146
- if (orderBy) {
147
- result = result.orderBy(orderBy.field, orderBy.dir);
148
- }
149
- return result;
150
- },
151
- limit
152
- };
153
- }
154
- function coerce(raw) {
155
- if (raw === "true") return true;
156
- if (raw === "false") return false;
157
- const n = Number(raw);
158
- if (!isNaN(n) && raw.trim() !== "") return n;
159
- return raw;
160
- }
161
-
162
1
  // src/router.ts
2
+ import { isConflictError } from "@noy-db/hub";
163
3
  function json(status, body) {
164
4
  return {
165
5
  status,
@@ -167,12 +7,75 @@ function json(status, body) {
167
7
  body: JSON.stringify(body)
168
8
  };
169
9
  }
170
- function extractToken(req) {
171
- const auth = req.headers["authorization"] ?? req.headers["Authorization"];
172
- if (!auth?.startsWith("Bearer ")) return null;
173
- return auth.slice(7);
10
+ var CORE_METHODS = /* @__PURE__ */ new Set([
11
+ "get",
12
+ "put",
13
+ "delete",
14
+ "list",
15
+ "loadAll",
16
+ "saveAll",
17
+ "ping",
18
+ "listSince",
19
+ "listPage",
20
+ "listVaults"
21
+ ]);
22
+ var UnknownMethodError = class extends Error {
23
+ };
24
+ var UnsupportedMethodError = class extends Error {
25
+ };
26
+ async function dispatch(store, method, args) {
27
+ switch (method) {
28
+ case "get": {
29
+ const [vault, collection, id] = args;
30
+ return store.get(vault, collection, id);
31
+ }
32
+ case "put": {
33
+ const [vault, collection, id, envelope, expectedVersion] = args;
34
+ await store.put(vault, collection, id, envelope, expectedVersion);
35
+ return null;
36
+ }
37
+ case "delete": {
38
+ const [vault, collection, id] = args;
39
+ await store.delete(vault, collection, id);
40
+ return null;
41
+ }
42
+ case "list": {
43
+ const [vault, collection] = args;
44
+ return store.list(vault, collection);
45
+ }
46
+ case "loadAll": {
47
+ const [vault] = args;
48
+ return store.loadAll(vault);
49
+ }
50
+ case "saveAll": {
51
+ const [vault, data] = args;
52
+ await store.saveAll(vault, data);
53
+ return null;
54
+ }
55
+ case "ping": {
56
+ if (!store.ping) return true;
57
+ return store.ping();
58
+ }
59
+ case "listSince": {
60
+ if (!store.listSince) throw new UnsupportedMethodError("listSince not supported by this store");
61
+ const [vault, collection, since] = args;
62
+ return store.listSince(vault, collection, since);
63
+ }
64
+ case "listPage": {
65
+ if (!store.listPage) throw new UnsupportedMethodError("listPage not supported by this store");
66
+ const [vault, collection, cursor, limit] = args;
67
+ return store.listPage(vault, collection, cursor, limit);
68
+ }
69
+ case "listVaults": {
70
+ if (!store.listVaults) throw new UnsupportedMethodError("listVaults not supported by this store");
71
+ return store.listVaults();
72
+ }
73
+ }
74
+ throw new UnknownMethodError(`Unhandled method: ${method}`);
174
75
  }
175
- function buildRouter(store, user, sessions, basePath) {
76
+ function buildRouter(opts) {
77
+ const { store, authorize, allow } = opts;
78
+ const basePath = opts.basePath ?? "";
176
79
  function stripBase(pathname) {
177
80
  if (!basePath) return pathname;
178
81
  if (pathname === basePath) return "/";
@@ -182,130 +85,54 @@ function buildRouter(store, user, sessions, basePath) {
182
85
  return async function route(req) {
183
86
  const path = stripBase(req.pathname);
184
87
  const method = req.method.toUpperCase();
185
- if (method === "POST" && path === "/sessions/unlock/secret") {
186
- let body;
187
- try {
188
- body = await req.json();
189
- } catch {
190
- return json(400, { error: "invalid_json" });
191
- }
192
- const secret = body?.secret;
193
- if (typeof secret !== "string" || !secret) {
194
- return json(400, { error: "secret_required" });
195
- }
196
- try {
197
- const db2 = await createNoydb({ store, user, secret });
198
- const token2 = sessions.create(db2);
199
- return json(200, { token: token2 });
200
- } catch (err) {
201
- if (err instanceof ValidationError) {
202
- return json(400, { error: "weak_secret", message: err.message });
203
- }
204
- return json(401, { error: "invalid_secret" });
205
- }
88
+ if (method !== "POST" || path !== "/rpc") {
89
+ return json(404, { error: { name: "NotFound", message: "no such route" } });
206
90
  }
207
- if (method === "GET" && path === "/sessions/current") {
208
- const token2 = extractToken(req);
209
- const active = token2 !== null && sessions.peek(token2);
210
- return json(200, { active });
91
+ let authorized;
92
+ try {
93
+ authorized = authorize ? await authorize(req) : false;
94
+ } catch {
95
+ return json(500, { error: { name: "Error", message: "authorization failed" } });
211
96
  }
212
- if (method === "DELETE" && path === "/sessions/current") {
213
- const token2 = extractToken(req);
214
- if (!token2 || !sessions.peek(token2)) return json(401, { error: "unauthorized" });
215
- sessions.delete(token2);
216
- return { status: 204, headers: {}, body: null };
97
+ if (!authorized) {
98
+ return json(401, { error: { name: "Unauthorized", message: "unauthorized" } });
217
99
  }
218
- const token = extractToken(req);
219
- const db = token ? sessions.get(token) : null;
220
- if (!db) return json(401, { error: "unauthorized" });
221
- if (method === "GET" && path === "/vaults") {
222
- try {
223
- if (typeof store.listVaults === "function") {
224
- const vaults = await db.listAccessibleVaults();
225
- return json(200, vaults.map((v) => v.id));
226
- }
227
- return json(200, []);
228
- } catch {
229
- return json(200, []);
230
- }
100
+ let body;
101
+ try {
102
+ body = await req.json();
103
+ } catch {
104
+ return json(400, { error: { name: "BadRequest", message: "invalid JSON body" } });
231
105
  }
232
- const recordMatch = path.match(/^\/vaults\/([^/]+)\/collections\/([^/]+)\/([^/]+)$/);
233
- if (recordMatch) {
234
- const vaultName = recordMatch[1];
235
- const collName = recordMatch[2];
236
- const id = recordMatch[3];
237
- if (vaultName === void 0 || collName === void 0 || id === void 0) {
238
- return json(500, { error: "internal_error" });
239
- }
240
- try {
241
- const vault = await db.openVault(vaultName);
242
- const coll = vault.collection(collName);
243
- if (method === "GET") {
244
- const record = await coll.get(id);
245
- if (!record) return json(404, { error: "not_found" });
246
- return json(200, record);
247
- }
248
- if (method === "POST") {
249
- let body;
250
- try {
251
- body = await req.json();
252
- } catch {
253
- return json(400, { error: "invalid_json" });
254
- }
255
- await coll.put(id, body);
256
- return json(200, { ok: true });
257
- }
258
- if (method === "DELETE") {
259
- await coll.delete(id);
260
- return json(200, { ok: true });
261
- }
262
- return {
263
- status: 405,
264
- headers: { "content-type": "application/json", allow: "GET, POST, DELETE" },
265
- body: JSON.stringify({ error: "method_not_allowed" })
266
- };
267
- } catch (err) {
268
- if (err instanceof PermissionDeniedError) return json(403, { error: "forbidden" });
269
- if (err instanceof NotFoundError) return json(404, { error: "not_found" });
270
- if (err instanceof ConflictError) return json(409, { error: "conflict" });
271
- return json(500, { error: "internal_error" });
272
- }
106
+ const rpcMethod = body?.method;
107
+ const rpcArgs = body?.args;
108
+ if (typeof rpcMethod !== "string" || !Array.isArray(rpcArgs)) {
109
+ return json(400, { error: { name: "BadRequest", message: "body must be { method: string, args: unknown[] }" } });
273
110
  }
274
- const collMatch = path.match(/^\/vaults\/([^/]+)\/collections\/([^/]+)$/);
275
- if (collMatch) {
276
- if (method !== "GET") {
277
- return {
278
- status: 405,
279
- headers: { "content-type": "application/json", allow: "GET" },
280
- body: JSON.stringify({ error: "method_not_allowed" })
281
- };
282
- }
283
- const vaultName = collMatch[1];
284
- const collName = collMatch[2];
285
- if (vaultName === void 0 || collName === void 0) {
286
- return json(500, { error: "internal_error" });
111
+ if (!CORE_METHODS.has(rpcMethod)) {
112
+ return json(400, { error: { name: "BadRequest", message: `unknown method: ${rpcMethod}` } });
113
+ }
114
+ if (allow && !allow.has(rpcMethod)) {
115
+ return json(403, { error: { name: "Forbidden", message: `method not allowed: ${rpcMethod}` } });
116
+ }
117
+ try {
118
+ const result = await dispatch(store, rpcMethod, rpcArgs);
119
+ return json(200, result ?? null);
120
+ } catch (err) {
121
+ if (isConflictError(err)) {
122
+ return json(409, { error: { name: "ConflictError", message: err.message, version: err.version } });
287
123
  }
288
- const params = parseQueryParams(req.searchParams);
289
- if (params.error) return json(400, params.error);
290
- try {
291
- const vault = await db.openVault(vaultName);
292
- const coll = vault.collection(collName);
293
- let results = params.apply(coll.query()).toArray();
294
- if (params.limit !== null) results = results.slice(0, params.limit);
295
- return json(200, results);
296
- } catch (err) {
297
- if (err instanceof PermissionDeniedError) return json(403, { error: "forbidden" });
298
- return json(500, { error: "internal_error" });
124
+ if (err instanceof UnsupportedMethodError) {
125
+ return json(501, { error: { name: "NotImplemented", message: err.message } });
299
126
  }
127
+ const e = err;
128
+ return json(500, { error: { name: e.name ?? "Error", message: "store error" } });
300
129
  }
301
- return json(404, { error: "not_found" });
302
130
  };
303
131
  }
304
132
 
305
133
  // src/index.ts
306
134
  function createRestHandler(options) {
307
- const sessions = new SessionStore(options.ttlSeconds ?? 900);
308
- const route = buildRouter(options.store, options.user, sessions, options.basePath ?? "");
135
+ const route = buildRouter(options);
309
136
  return { handle: route };
310
137
  }
311
138
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sessions.ts","../src/router.ts","../src/query-params.ts","../src/index.ts"],"sourcesContent":["import type { Noydb } from '@noy-db/hub'\n\ninterface Session {\n db: Noydb\n expiresAt: number\n}\n\n/**\n * Options for `SessionStore`.\n *\n * `sweepIntervalMs` controls the periodic background sweep that drops\n * expired sessions from the map. Without it, expired\n * tokens only clear when a matching `get()` / `peek()` call happens —\n * a long-running process with many short-lived sessions accumulates\n * dead entries unbounded. Default is 5 minutes; pass `0` to disable\n * the sweep (appropriate for tests or very short-lived CLI processes).\n */\nexport interface SessionStoreOptions {\n sweepIntervalMs?: number\n}\n\nconst DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1000\n\nexport class SessionStore {\n private readonly sessions = new Map<string, Session>()\n private readonly ttlMs: number\n private readonly sweepTimer: ReturnType<typeof setInterval> | null\n\n constructor(ttlSeconds: number, options: SessionStoreOptions = {}) {\n this.ttlMs = ttlSeconds * 1000\n const sweepMs = options.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS\n if (sweepMs > 0) {\n // Node's setInterval returns a Timeout whose `.unref()` prevents\n // the timer from keeping the event loop alive. That matters in\n // CLI / serverless contexts where the Node process should exit\n // cleanly once the request cycle ends.\n const timer = setInterval(() => this.sweep(), sweepMs)\n if (typeof (timer as { unref?: () => void }).unref === 'function') {\n (timer as { unref: () => void }).unref()\n }\n this.sweepTimer = timer\n } else {\n this.sweepTimer = null\n }\n }\n\n create(db: Noydb): string {\n const token = crypto.randomUUID()\n this.sessions.set(token, { db, expiresAt: Date.now() + this.ttlMs })\n return token\n }\n\n /**\n * Look up a session and refresh its sliding-window TTL. Call from\n * auth-guarded routes that are treated as \"activity\".\n */\n get(token: string): Noydb | null {\n const session = this.sessions.get(token)\n if (!session) return null\n if (Date.now() > session.expiresAt) {\n this.sessions.delete(token)\n return null\n }\n session.expiresAt = Date.now() + this.ttlMs\n return session.db\n }\n\n delete(token: string): void {\n this.sessions.delete(token)\n }\n\n /**\n * Non-refreshing existence check. Used by polling endpoints like\n * `GET /sessions/current` that should not extend the session merely\n * by being queried.\n */\n peek(token: string): boolean {\n const session = this.sessions.get(token)\n if (!session) return false\n if (Date.now() > session.expiresAt) {\n this.sessions.delete(token)\n return false\n }\n return true\n }\n\n /**\n * Drop every session whose TTL has elapsed. Called automatically by\n * the internal interval timer; exposed so tests can deterministically\n * trigger the sweep without waiting. Returns the count of entries\n * removed — useful for logging / metrics hooks.\n */\n sweep(): number {\n const now = Date.now()\n let removed = 0\n for (const [token, session] of this.sessions) {\n if (now > session.expiresAt) {\n this.sessions.delete(token)\n removed++\n }\n }\n return removed\n }\n\n /**\n * Current number of tracked sessions (expired or not). Primarily for\n * tests that verify the sweep is working; operators wanting live\n * metrics should wrap the store or emit their own counters.\n */\n size(): number {\n return this.sessions.size\n }\n\n /**\n * Stop the background sweep timer and drop every session. Call this\n * on shutdown to allow the event loop to exit cleanly — `unref()`\n * handles most cases, but explicit cleanup is cheaper than relying\n * on finalization.\n */\n close(): void {\n if (this.sweepTimer !== null) clearInterval(this.sweepTimer)\n this.sessions.clear()\n }\n}\n","import { createNoydb, PermissionDeniedError, NotFoundError, ConflictError, ValidationError } from '@noy-db/hub'\nimport type { NoydbStore } from '@noy-db/hub'\nimport type { RestRequest, RestResponse } from './index.js'\nimport type { SessionStore } from './sessions.js'\nimport { parseQueryParams } from './query-params.js'\n\nfunction json(status: number, body: unknown): RestResponse {\n return {\n status,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n }\n}\n\nfunction extractToken(req: RestRequest): string | null {\n // HTTP headers are case-insensitive; check both common casings.\n const auth = req.headers['authorization'] ?? req.headers['Authorization']\n if (!auth?.startsWith('Bearer ')) return null\n return auth.slice(7)\n}\n\nexport function buildRouter(store: NoydbStore, user: string, sessions: SessionStore, basePath: string) {\n function stripBase(pathname: string): string {\n // Segment-aware prefix match: basePath '/api' matches '/api' or '/api/...'\n // but NOT '/apifoo' or '/api-other/...'.\n if (!basePath) return pathname\n if (pathname === basePath) return '/'\n if (pathname.startsWith(basePath + '/')) return pathname.slice(basePath.length)\n return pathname\n }\n\n return async function route(req: RestRequest): Promise<RestResponse> {\n const path = stripBase(req.pathname)\n const method = req.method.toUpperCase()\n\n // ── Session routes (no auth required) ─────────────────────────\n\n if (method === 'POST' && path === '/sessions/unlock/secret') {\n let body: unknown\n try { body = await req.json() } catch { return json(400, { error: 'invalid_json' }) }\n const secret = (body as Record<string, unknown>)?.secret\n if (typeof secret !== 'string' || !secret) {\n return json(400, { error: 'secret_required' })\n }\n try {\n const db = await createNoydb({ store, user, secret: secret })\n const token = sessions.create(db)\n return json(200, { token })\n } catch (err) {\n if (err instanceof ValidationError) {\n return json(400, { error: 'weak_secret', message: err.message })\n }\n return json(401, { error: 'invalid_secret' })\n }\n }\n\n if (method === 'GET' && path === '/sessions/current') {\n const token = extractToken(req)\n const active = token !== null && sessions.peek(token)\n return json(200, { active })\n }\n\n if (method === 'DELETE' && path === '/sessions/current') {\n const token = extractToken(req)\n if (!token || !sessions.peek(token)) return json(401, { error: 'unauthorized' })\n sessions.delete(token)\n return { status: 204, headers: {}, body: null }\n }\n\n // ── Auth guard ────────────────────────────────────────────────\n\n const token = extractToken(req)\n const db = token ? sessions.get(token) : null\n if (!db) return json(401, { error: 'unauthorized' })\n\n // ── Vault routes ──────────────────────────────────────────────\n\n if (method === 'GET' && path === '/vaults') {\n try {\n if (typeof store.listVaults === 'function') {\n const vaults = await db.listAccessibleVaults()\n return json(200, vaults.map(v => v.id))\n }\n return json(200, [])\n } catch {\n return json(200, [])\n }\n }\n\n // Match /vaults/:vault/collections/:collection/:id\n const recordMatch = path.match(/^\\/vaults\\/([^/]+)\\/collections\\/([^/]+)\\/([^/]+)$/)\n if (recordMatch) {\n const vaultName = recordMatch[1]\n const collName = recordMatch[2]\n const id = recordMatch[3]\n if (vaultName === undefined || collName === undefined || id === undefined) {\n return json(500, { error: 'internal_error' })\n }\n try {\n const vault = await db.openVault(vaultName)\n const coll = vault.collection<Record<string, unknown>>(collName)\n\n if (method === 'GET') {\n const record = await coll.get(id)\n if (!record) return json(404, { error: 'not_found' })\n return json(200, record)\n }\n\n if (method === 'POST') {\n let body: unknown\n try { body = await req.json() } catch { return json(400, { error: 'invalid_json' }) }\n await coll.put(id, body as Record<string, unknown>)\n return json(200, { ok: true })\n }\n\n if (method === 'DELETE') {\n await coll.delete(id)\n return json(200, { ok: true })\n }\n\n return {\n status: 405,\n headers: { 'content-type': 'application/json', allow: 'GET, POST, DELETE' },\n body: JSON.stringify({ error: 'method_not_allowed' }),\n }\n } catch (err) {\n if (err instanceof PermissionDeniedError) return json(403, { error: 'forbidden' })\n if (err instanceof NotFoundError) return json(404, { error: 'not_found' })\n if (err instanceof ConflictError) return json(409, { error: 'conflict' })\n return json(500, { error: 'internal_error' })\n }\n }\n\n // Match /vaults/:vault/collections/:collection (list)\n const collMatch = path.match(/^\\/vaults\\/([^/]+)\\/collections\\/([^/]+)$/)\n if (collMatch) {\n if (method !== 'GET') {\n return {\n status: 405,\n headers: { 'content-type': 'application/json', allow: 'GET' },\n body: JSON.stringify({ error: 'method_not_allowed' }),\n }\n }\n const vaultName = collMatch[1]\n const collName = collMatch[2]\n if (vaultName === undefined || collName === undefined) {\n return json(500, { error: 'internal_error' })\n }\n const params = parseQueryParams(req.searchParams)\n if (params.error) return json(400, params.error)\n try {\n const vault = await db.openVault(vaultName)\n const coll = vault.collection<Record<string, unknown>>(collName)\n let results = params.apply(coll.query()).toArray()\n if (params.limit !== null) results = results.slice(0, params.limit)\n return json(200, results)\n } catch (err) {\n if (err instanceof PermissionDeniedError) return json(403, { error: 'forbidden' })\n return json(500, { error: 'internal_error' })\n }\n }\n\n return json(404, { error: 'not_found' })\n }\n}\n","import type { Query } from '@noy-db/hub'\n\ntype QueryOp = '==' | '!=' | '>' | '>=' | '<' | '<='\n\nconst OP_MAP: Record<string, QueryOp> = {\n eq: '==',\n neq: '!=',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n}\n\nexport interface ParsedQueryParams {\n error?: { error: string; op?: string }\n apply<T>(q: Query<T>): Query<T>\n limit: number | null\n}\n\nexport function parseQueryParams(searchParams: URLSearchParams): ParsedQueryParams {\n const wheres = searchParams.getAll('where')\n const orderByParam = searchParams.get('orderBy')\n const limitParam = searchParams.get('limit')\n\n const whereClauses: Array<{ field: string; op: QueryOp; value: unknown }> = []\n\n for (const clause of wheres) {\n const parts = clause.split(':')\n if (parts.length < 3) {\n return {\n error: { error: 'invalid_where', op: clause },\n apply: (q) => q,\n limit: null,\n }\n }\n const field = parts[0] as string\n const opStr = parts[1] as string\n const value = parts.slice(2).join(':')\n const op = OP_MAP[opStr]\n if (!op) {\n return {\n error: { error: 'invalid_op', op: opStr },\n apply: (q) => q,\n limit: null,\n }\n }\n whereClauses.push({ field, op, value: coerce(value) })\n }\n\n let orderBy: { field: string; dir: 'asc' | 'desc' } | null = null\n if (orderByParam) {\n const obParts = orderByParam.split(':')\n const obField = obParts[0] as string\n const obDir = obParts[1]\n orderBy = { field: obField, dir: obDir === 'desc' ? 'desc' : 'asc' }\n }\n\n const limit = limitParam ? parseInt(limitParam, 10) : null\n\n return {\n apply<T>(q: Query<T>): Query<T> {\n let result = q\n for (const { field, op, value } of whereClauses) {\n result = result.where(field, op, value as T[keyof T & string])\n }\n if (orderBy) {\n result = result.orderBy(orderBy.field, orderBy.dir)\n }\n return result\n },\n limit,\n }\n}\n\nfunction coerce(raw: string): unknown {\n if (raw === 'true') return true\n if (raw === 'false') return false\n const n = Number(raw)\n if (!isNaN(n) && raw.trim() !== '') return n\n return raw\n}\n","/**\n * **@noy-db/in-rest** — Framework-neutral REST API integration for noy-db.\n *\n * @example\n * ```ts\n * import { createRestHandler } from '@noy-db/in-rest'\n * import { honoAdapter } from '@noy-db/in-rest/hono'\n *\n * const handler = createRestHandler({ store, user: 'api' })\n * app.route('/api/noydb', honoAdapter(handler))\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore } from '@noy-db/hub'\nimport { SessionStore } from './sessions.js'\nimport { buildRouter } from './router.js'\n\nexport interface RestRequest {\n readonly method: string\n readonly pathname: string\n readonly searchParams: URLSearchParams\n readonly headers: Record<string, string>\n json(): Promise<unknown>\n}\n\nexport interface RestResponse {\n readonly status: number\n readonly headers: Record<string, string>\n readonly body: string | Uint8Array | null\n}\n\nexport interface NoydbRestHandler {\n handle(req: RestRequest): Promise<RestResponse>\n}\n\nexport interface RestHandlerOptions {\n readonly store: NoydbStore\n readonly user: string\n readonly ttlSeconds?: number\n readonly basePath?: string\n}\n\nexport function createRestHandler(options: RestHandlerOptions): NoydbRestHandler {\n const sessions = new SessionStore(options.ttlSeconds ?? 900)\n const route = buildRouter(options.store, options.user, sessions, options.basePath ?? '')\n return { handle: route }\n}\n"],"mappings":"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","names":["db","token"]}
1
+ {"version":3,"sources":["../src/router.ts","../src/index.ts"],"sourcesContent":["import { isConflictError } from '@noy-db/hub'\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport type { RestRequest, RestResponse, RestHandlerOptions } from './index.js'\n\nfunction json(status: number, body: unknown): RestResponse {\n return {\n status,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n }\n}\n\n/**\n * The 6 required `NoydbStore` methods plus the optional sync/pagination\n * extensions — the exact set `by-peer`'s `servePeerStore` exposes. The\n * router never decrypts or interprets an argument; it forwards the\n * positional tuple straight to `store.*` and returns the raw result.\n */\nconst CORE_METHODS = new Set<string>([\n 'get',\n 'put',\n 'delete',\n 'list',\n 'loadAll',\n 'saveAll',\n 'ping',\n 'listSince',\n 'listPage',\n 'listVaults',\n])\n\nclass UnknownMethodError extends Error {}\nclass UnsupportedMethodError extends Error {}\n\nasync function dispatch(store: NoydbStore, method: string, args: readonly unknown[]): Promise<unknown> {\n switch (method) {\n case 'get': {\n const [vault, collection, id] = args as [string, string, string]\n return store.get(vault, collection, id)\n }\n case 'put': {\n const [vault, collection, id, envelope, expectedVersion] = args as [\n string,\n string,\n string,\n EncryptedEnvelope,\n number | undefined,\n ]\n await store.put(vault, collection, id, envelope, expectedVersion)\n return null\n }\n case 'delete': {\n const [vault, collection, id] = args as [string, string, string]\n await store.delete(vault, collection, id)\n return null\n }\n case 'list': {\n const [vault, collection] = args as [string, string]\n return store.list(vault, collection)\n }\n case 'loadAll': {\n const [vault] = args as [string]\n return store.loadAll(vault)\n }\n case 'saveAll': {\n const [vault, data] = args as [string, VaultSnapshot]\n await store.saveAll(vault, data)\n return null\n }\n case 'ping': {\n if (!store.ping) return true\n return store.ping()\n }\n case 'listSince': {\n if (!store.listSince) throw new UnsupportedMethodError('listSince not supported by this store')\n const [vault, collection, since] = args as [string, string, string]\n return store.listSince(vault, collection, since)\n }\n case 'listPage': {\n if (!store.listPage) throw new UnsupportedMethodError('listPage not supported by this store')\n const [vault, collection, cursor, limit] = args as [\n string,\n string,\n string | undefined,\n number | undefined,\n ]\n return store.listPage(vault, collection, cursor, limit)\n }\n case 'listVaults': {\n if (!store.listVaults) throw new UnsupportedMethodError('listVaults not supported by this store')\n return store.listVaults()\n }\n }\n /* istanbul ignore next — CORE_METHODS gate makes this unreachable */\n throw new UnknownMethodError(`Unhandled method: ${method}`)\n}\n\nexport function buildRouter(opts: RestHandlerOptions) {\n const { store, authorize, allow } = opts\n const basePath = opts.basePath ?? ''\n\n function stripBase(pathname: string): string {\n // Segment-aware prefix match: basePath '/api' matches '/api' or '/api/...'\n // but NOT '/apifoo' or '/api-other/...'.\n if (!basePath) return pathname\n if (pathname === basePath) return '/'\n if (pathname.startsWith(basePath + '/')) return pathname.slice(basePath.length)\n return pathname\n }\n\n return async function route(req: RestRequest): Promise<RestResponse> {\n const path = stripBase(req.pathname)\n const method = req.method.toUpperCase()\n\n if (method !== 'POST' || path !== '/rpc') {\n return json(404, { error: { name: 'NotFound', message: 'no such route' } })\n }\n\n // Auth first, and fail-closed: an omitted authorizer denies every\n // request. The caller MUST supply one to accept any traffic. A throwing\n // authorizer also fails closed — a structured 500 with no leaked detail,\n // never an open request or an uncaught rejection out of handle().\n let authorized: boolean\n try {\n authorized = authorize ? await authorize(req) : false\n } catch {\n return json(500, { error: { name: 'Error', message: 'authorization failed' } })\n }\n if (!authorized) {\n return json(401, { error: { name: 'Unauthorized', message: 'unauthorized' } })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return json(400, { error: { name: 'BadRequest', message: 'invalid JSON body' } })\n }\n const rpcMethod = (body as Record<string, unknown> | null)?.method\n const rpcArgs = (body as Record<string, unknown> | null)?.args\n if (typeof rpcMethod !== 'string' || !Array.isArray(rpcArgs)) {\n return json(400, { error: { name: 'BadRequest', message: 'body must be { method: string, args: unknown[] }' } })\n }\n\n if (!CORE_METHODS.has(rpcMethod)) {\n return json(400, { error: { name: 'BadRequest', message: `unknown method: ${rpcMethod}` } })\n }\n if (allow && !allow.has(rpcMethod)) {\n return json(403, { error: { name: 'Forbidden', message: `method not allowed: ${rpcMethod}` } })\n }\n\n try {\n const result = await dispatch(store, rpcMethod, rpcArgs)\n return json(200, result ?? null)\n } catch (err) {\n if (isConflictError(err)) {\n return json(409, { error: { name: 'ConflictError', message: err.message, version: err.version } })\n }\n if (err instanceof UnsupportedMethodError) {\n // The request was well-formed; the backing store just lacks this\n // optional method. 501 (not 400) lets a client feature-detect.\n return json(501, { error: { name: 'NotImplemented', message: err.message } })\n }\n // Preserve the error NAME so a client can branch / re-hydrate, but do\n // NOT echo the raw store message — it may embed operational internals\n // (connection strings, paths). Operators read the detail from logs.\n const e = err as Error\n return json(500, { error: { name: e.name ?? 'Error', message: 'store error' } })\n }\n }\n}\n","/**\n * **@noy-db/in-rest** — Framework-neutral REST API integration for noy-db.\n *\n * A thin RPC dispatcher — the HTTP twin of `@noy-db/by-peer`'s\n * `servePeerStore` — that forwards the 6 `NoydbStore` methods straight to\n * the caller's ciphertext store. The server NEVER sees a passphrase, never\n * calls `createNoydb`/`openVault`, and never decrypts anything: every\n * request/response body is an `EncryptedEnvelope` (or a plain id/list of\n * one) round-tripped as-is.\n *\n * @example\n * ```ts\n * import { createRestHandler } from '@noy-db/in-rest'\n * import { honoAdapter } from '@noy-db/in-rest/hono'\n *\n * const handler = createRestHandler({\n * store,\n * authorize: (req) => req.headers['authorization'] === `Bearer ${API_KEY}`,\n * })\n * app.route('/api/noydb', honoAdapter(handler))\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore } from '@noy-db/hub'\nimport { buildRouter } from './router.js'\n\nexport interface RestRequest {\n readonly method: string\n readonly pathname: string\n readonly searchParams: URLSearchParams\n readonly headers: Record<string, string>\n json(): Promise<unknown>\n}\n\nexport interface RestResponse {\n readonly status: number\n readonly headers: Record<string, string>\n readonly body: string | Uint8Array | null\n}\n\nexport interface NoydbRestHandler {\n handle(req: RestRequest): Promise<RestResponse>\n}\n\nexport interface RestHandlerOptions {\n readonly store: NoydbStore\n /**\n * Authorize each request. Return `true` to allow. If OMITTED, the\n * handler is FAIL-CLOSED — every `/rpc` request is rejected with 401.\n * The caller MUST supply an authorizer to accept any traffic.\n */\n readonly authorize?: (req: RestRequest) => boolean | Promise<boolean>\n /**\n * Optional method allowlist (e.g. a read-only relay). When set, a\n * method not in the set is rejected with 403.\n */\n readonly allow?: ReadonlySet<string>\n readonly basePath?: string\n}\n\nexport function createRestHandler(options: RestHandlerOptions): NoydbRestHandler {\n const route = buildRouter(options)\n return { handle: route }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/in-rest",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-pre.1",
4
4
  "description": "Framework-neutral REST API integration for noy-db — createRestHandler with Hono, Express, Fastify, and Nitro subpath adapters.",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -48,7 +48,7 @@
48
48
  "node": ">=22.0.0"
49
49
  },
50
50
  "peerDependencies": {
51
- "@noy-db/hub": "0.5.0"
51
+ "@noy-db/hub": "0.6.0-pre.1"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "hono": {
@@ -70,7 +70,7 @@
70
70
  "fastify": "^5.0.0",
71
71
  "h3": "^1.13.0",
72
72
  "hono": "^4.0.0",
73
- "@noy-db/hub": "0.5.0"
73
+ "@noy-db/hub": "0.6.0-pre.1"
74
74
  },
75
75
  "keywords": [
76
76
  "noy-db",