@volter/twin-turbopuffer 0.1.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.
Files changed (52) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +145 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +27 -0
  5. package/dist/src/generated/surface.gen.json +1 -0
  6. package/dist/src/generated/ui.gen.json +1 -0
  7. package/dist/src/index.d.ts +11 -0
  8. package/dist/src/index.js +65 -0
  9. package/dist/src/key-gate.d.ts +3 -0
  10. package/dist/src/key-gate.js +39 -0
  11. package/dist/src/manifest.d.ts +2 -0
  12. package/dist/src/manifest.js +28 -0
  13. package/dist/src/screens/dashboard.d.ts +11 -0
  14. package/dist/src/screens/dashboard.js +191 -0
  15. package/dist/src/semantics/namespaces.d.ts +5 -0
  16. package/dist/src/semantics/namespaces.js +17 -0
  17. package/dist/src/turbopuffer-capabilities.d.ts +6 -0
  18. package/dist/src/turbopuffer-capabilities.js +442 -0
  19. package/dist/src/turbopuffer-conformance.d.ts +8 -0
  20. package/dist/src/turbopuffer-conformance.js +102 -0
  21. package/dist/src/turbopuffer-connector.d.ts +34 -0
  22. package/dist/src/turbopuffer-connector.js +152 -0
  23. package/dist/src/turbopuffer-filter.d.ts +57 -0
  24. package/dist/src/turbopuffer-filter.js +286 -0
  25. package/dist/src/turbopuffer-server.d.ts +23 -0
  26. package/dist/src/turbopuffer-server.js +72 -0
  27. package/dist/src/turbopuffer-stem.d.ts +1 -0
  28. package/dist/src/turbopuffer-stem.js +133 -0
  29. package/dist/src/turbopuffer-store.d.ts +80 -0
  30. package/dist/src/turbopuffer-store.js +1304 -0
  31. package/dist/src/turbopuffer-text.d.ts +44 -0
  32. package/dist/src/turbopuffer-text.js +189 -0
  33. package/dist/src/turbopuffer-twin.d.ts +25 -0
  34. package/dist/src/turbopuffer-twin.js +406 -0
  35. package/package.json +56 -0
  36. package/src/cli.ts +28 -0
  37. package/src/generated/surface.gen.json +1 -0
  38. package/src/generated/ui.gen.json +1 -0
  39. package/src/index.ts +92 -0
  40. package/src/key-gate.ts +39 -0
  41. package/src/manifest.ts +63 -0
  42. package/src/screens/dashboard.tsx +214 -0
  43. package/src/semantics/namespaces.ts +30 -0
  44. package/src/turbopuffer-capabilities.ts +455 -0
  45. package/src/turbopuffer-conformance.ts +101 -0
  46. package/src/turbopuffer-connector.ts +153 -0
  47. package/src/turbopuffer-filter.ts +277 -0
  48. package/src/turbopuffer-server.ts +81 -0
  49. package/src/turbopuffer-stem.ts +104 -0
  50. package/src/turbopuffer-store.ts +1157 -0
  51. package/src/turbopuffer-text.ts +204 -0
  52. package/src/turbopuffer-twin.ts +429 -0
@@ -0,0 +1,63 @@
1
+ // Turbopuffer's manifest: the vendor facts its published spec does not carry (docs/contributing/architecture.md,
2
+ // "Protocol 3"). The surface is generated (./generated/surface.gen.json, from ../spec by scripts/derive-pack.ts): the
3
+ // OpenAPI document @turbopuffer/turbopuffer@2.8.0 was generated from, with its three generator pseudo-paths folded
4
+ // into the operations turbopuffer documents (../spec/patches.json). The spec has no operationIds, so an operation's id
5
+ // is the reader's `method_path` slug (`post_v2_namespaces_namespace` is the write).
6
+ //
7
+ // THE HANDLERS. Every operation the twin serves is a semantics handler (semantics/namespaces.ts) over the pack's
8
+ // request handler (turbopuffer-twin.ts), whose store and filter/BM25 code (turbopuffer-store.ts, -filter.ts,
9
+ // -text.ts) are the vendor's behaviour. The spec names one resource (`NamespaceSummary`, what the list answers);
10
+ // every other answer is a computed or non-resource body (a write's result, a query's rows, a schema), so the derived
11
+ // core owns no operation and none is declared here. An operation no handler serves answers the gap (../src/turbopuffer-server.ts).
12
+ //
13
+ // WHY NO `auth`. Turbopuffer reads `Authorization: Bearer <API key>` (https://turbopuffer.com/docs/auth). The request
14
+ // handler refuses a request with no key; the key gate (../key-gate.ts) refuses a key the dashboard expired or never made,
15
+ // and a key without the list or admin permission asking to list namespaces. A served twin may also be started demanding
16
+ // one key (`--token`), which the kernel's `auth` (keys the twin refuses, never the one it accepts) cannot state.
17
+ //
18
+ // THE STATUSES THE KEY GATE ANSWERS: the twin's reading. turbopuffer's pages print the error body and a 429, and no
19
+ // status for a bad key or a missing permission; 401 and 403 are the statuses its own client maps to AuthenticationError
20
+ // and PermissionDeniedError (https://raw.githubusercontent.com/turbopuffer/turbopuffer-typescript/v2.8.0/src/core/error.ts).
21
+ //
22
+ // THE KEYS' PERMISSIONS: where the evidence stops. turbopuffer names read/write permissions (changelog, March 2024), a
23
+ // list permission (May 2026) and admin, and says which one an endpoint needs only for listing namespaces
24
+ // (https://turbopuffer.com/docs/namespaces). The twin enforces that one; every other operation admits any live key, the
25
+ // twin's reading, since no page says whether a read key may write, warm a cache or delete a namespace.
26
+ //
27
+ // THE ERROR BODY. `{"status": "error", "error": "<message>"}`, as https://turbopuffer.com/docs/auth prints it and the
28
+ // spec's ErrorResponse types it. The messages are the twin's; where the documentation stops, no page prints the text
29
+ // of a particular refusal.
30
+ //
31
+ // THE STATE. One `namespace` subject per namespace (its schema, distance metric, vector dimensions and times) and one
32
+ // `document` subject per document, keyed `<namespace>/<n|s>:<id>` (turbopuffer-store.ts). A write request is one
33
+ // kernel action: the namespace's fields and a projection of the documents it touched.
34
+ import type { DerivedManifest } from '@volter/world-core';
35
+
36
+ export const manifest: DerivedManifest = {
37
+ vendor: 'turbopuffer',
38
+ service: 'turbopuffer',
39
+ body: {},
40
+ // the core mints no id (it owns no operation); a document's id is the caller's
41
+ ids: { template: '{prefix}_{n}' },
42
+ time: 'iso',
43
+ error: { status: 'error', error: '{message}' },
44
+ readOnly: { status: 405, message: 'read_only: this twin was started read-only; writes are refused' },
45
+ malformedBody: { status: 400, message: 'invalid request: body is not valid JSON' },
46
+ notFound: { status: 404, message: 'not found' },
47
+ // GET /v1/namespaces answers `{namespaces, next_cursor}`, paged by `cursor` and `page_size`
48
+ // (https://turbopuffer.com/docs/namespaces); the handler answers it. Required by the type; the core lists nothing.
49
+ list: { style: 'envelope', envelope: { namespaces: '{data}' }, limit: { param: 'page_size', default: 100, max: 1000 } },
50
+ deleted: {},
51
+ // turbopuffer's dashboard (https://turbopuffer.com/docs/auth: "You can create and expire tokens in the dashboard"):
52
+ // its sign-in, and the page where an organization's API keys are made and expired (screens/dashboard.tsx)
53
+ screens: [{
54
+ id: 'login', kind: 'flow', host: 'turbopuffer.com', path: '/login',
55
+ demand: 'every dashboard page asks who is signed in', status: 'done',
56
+ controls: ['Email', 'Password', 'Continue'], source: 'https://turbopuffer.com/docs/auth',
57
+ }, {
58
+ id: 'api-keys', kind: 'workspace', host: 'turbopuffer.com', path: '/dashboard/api-keys',
59
+ demand: 'every API key a customer uses is made here, and expired here when it is rotated or the customer leaves', status: 'done',
60
+ controls: ['Name', 'Read', 'Write', 'List', 'Admin', 'Create API key', 'Expire'], source: 'https://turbopuffer.com/docs/auth',
61
+ }],
62
+ resources: {},
63
+ };
@@ -0,0 +1,214 @@
1
+ // TURBOPUFFER'S DASHBOARD — the pages an organization's person signs in to and makes its API keys on
2
+ // (docs/contributing/architecture.md, "Screens" and "Who is on a screen"). No API operation makes or expires a key: "All
3
+ // API calls require authenticating with your API key. You can create and expire tokens in the dashboard"
4
+ // (https://turbopuffer.com/docs/auth), so every key a customer uses comes from here. Built from @volter/world-ui's
5
+ // sign-in and settings pieces under turbopuffer's skin; nothing of turbopuffer's page is copied.
6
+ //
7
+ // - turbopuffer.com/login: the sign-in (turbopuffer's page asks for an Email and has Continue). A dashboard page asked for
8
+ // by nobody signed in sends its visitor here, and the sign-in returns them there.
9
+ // - turbopuffer.com/dashboard/api-keys: the organization's API keys. Create API key with its Name and its permissions,
10
+ // shown once in the page's notice; each live key's Expire. The permissions are the ones turbopuffer's pages name:
11
+ // "API Key read/write permissions" (https://turbopuffer.com/docs/roadmap, March 2024), "New API key permission to
12
+ // `list` namespaces" (the same, May 2026), and admin ("available to API keys with list or admin permissions",
13
+ // https://turbopuffer.com/docs/namespaces). A key is named ("Named API Keys", the same changelog) and marked expired
14
+ // (the audit log's `api-key-marked-as-expired`, https://turbopuffer.com/docs/audit-logs).
15
+ // - POST /_twin/users/:email/password {password, organization, plan}: stands in for turbopuffer's sign-up
16
+ // (turbopuffer.com/join: Email, a plan, Continue), as upstash's and planetscale's doors of the same path do. The World
17
+ // keeps the password's hash, never the password.
18
+ //
19
+ // Where turbopuffer's documentation stops and the twin decides: a person signs in with an email and the password the
20
+ // World gave them (turbopuffer's own sign-in continues by email or GitHub or Google, which the twin does not model); the
21
+ // session cookie is named `tpuf_session`; the keys page is at /dashboard/api-keys; a key's permissions are four
22
+ // checkboxes (Read, Write, List, Admin; turbopuffer does not document the control) and a key needs at least one; a key
23
+ // is `tpuf_` and 32 letters and digits (the length is the twin's; the prefix is the docs' own placeholder for a key,
24
+ // `"source_api_key": "tpuf_A1..."` in https://turbopuffer.com/docs/write's copy example); a refused sign-in shows the form again with
25
+ // "Incorrect email or password." (200); a name already used by a live key, or none, answers the page with a notice (422).
26
+ import { createHash } from 'node:crypto';
27
+ import { cookieOf, flowPage, Portal, PORTAL_CSS, SignIn, SIGN_IN_CSS } from '@volter/world-ui';
28
+ import type { SemanticsContext } from '@volter/world-core';
29
+
30
+ export const DASHBOARD_HOST = 'turbopuffer.com';
31
+ const COOKIE = 'tpuf_session';
32
+ const KEYS = '/dashboard/api-keys';
33
+ export const GRANTS = ['read', 'write', 'list', 'admin'] as const;
34
+ export type Grant = (typeof GRANTS)[number];
35
+ const LABEL: Record<Grant, string> = { read: 'Read', write: 'Write', list: 'List', admin: 'Admin' };
36
+
37
+ const hex = (s: string): string => createHash('sha256').update(s).digest('hex');
38
+ const passwordHash = (email: string, password: string): string => hex(`password:${email.toLowerCase()}:${password}`);
39
+ const ALNUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
40
+
41
+ // turbopuffer's skin: its warm off-white page and near-black ink and buttons
42
+ const SKIN = `
43
+ body { background: #faf8f5; color: #111111; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
44
+ .sign-in-mark { background: #111111; }
45
+ .sign-in-box { background: #ffffff; border-color: #e5e1da; }
46
+ .sign-in-error { background: #fff4f2; border-color: #f3c7bf; color: #111111; }
47
+ .sign-in-submit { background: #111111; border-color: #111111; color: #ffffff; }
48
+ .portal-side { background: #f3f0ea; border-right: 1px solid #e5e1da; }
49
+ .portal-section h2 { border-color: #e5e1da; color: #57534e; }
50
+ .portal-notice { background: #f3f0ea; border: 1px solid #e5e1da; word-break: break-all; }
51
+ .portal-button { background: #ffffff; border: 1px solid #d6d1c8; border-radius: 4px; padding: 6px 12px; }
52
+ .portal-primary { background: #111111; border-color: #111111; color: #ffffff; }
53
+ `;
54
+
55
+ type Row = Record<string, unknown>;
56
+
57
+ /** A return_to the dashboard follows: a path on turbopuffer.com, never another site. */
58
+ const returnTo = (raw: string | null | undefined): string => (raw && raw.startsWith('/') && !raw.startsWith('//') ? raw : KEYS);
59
+
60
+ function signInPage(back: string, email?: string, error?: string): Response {
61
+ return flowPage({
62
+ title: 'Log in · turbopuffer',
63
+ css: [SIGN_IN_CSS, SKIN],
64
+ body: (
65
+ <SignIn
66
+ heading="Log in"
67
+ action="/login"
68
+ fields={{ return_to: back }}
69
+ account={{ name: 'email', label: 'Email', ...(email ? { value: email } : {}) }}
70
+ password={{ name: 'password', label: 'Password' }}
71
+ submit="Continue"
72
+ {...(error ? { error } : {})}
73
+ />
74
+ ),
75
+ });
76
+ }
77
+
78
+ /** The person the request's session cookie names, with their organization, or undefined. */
79
+ function signedIn(ctx: SemanticsContext, request: Request): Row | undefined {
80
+ const token = cookieOf(request, COOKIE);
81
+ const session = token ? ctx.rowsRaw('_web_session').find((s) => s.token === token) : undefined;
82
+ return session ? ctx.rowsRaw('_person').find((p) => p.email === session.email) : undefined;
83
+ }
84
+
85
+ /** The organization's live and expired keys, oldest first. */
86
+ export function keysOf(ctx: SemanticsContext, organization: string): Row[] {
87
+ return ctx.rowsRaw('_api_key').filter((k) => k.organization === organization);
88
+ }
89
+
90
+ function keysPage(ctx: SemanticsContext, person: Row, notice?: string, status = 200): Response {
91
+ const organization = String(person.organization);
92
+ const keys = keysOf(ctx, organization);
93
+ const grantsOf = (k: Row): string => (Array.isArray(k.grants) ? (k.grants as Grant[]).map((g) => LABEL[g]).join(', ') : '');
94
+ return flowPage({
95
+ title: 'API keys · turbopuffer',
96
+ css: [PORTAL_CSS, SKIN],
97
+ status,
98
+ body: (
99
+ <Portal
100
+ merchant={`${organization} · ${String(person.email)}`}
101
+ back={{ href: '/dashboard', label: 'Dashboard' }}
102
+ {...(notice ? { notice } : {})}
103
+ sections={[{
104
+ heading: 'API keys',
105
+ empty: 'No API keys yet.',
106
+ items: keys.map((k) => ({
107
+ title: String(k.name),
108
+ detail: grantsOf(k),
109
+ note: k.expired_at ? `Expired ${String(k.expired_at).slice(0, 10)}` : `Created ${String(k.created_at).slice(0, 10)} · ends ${String(k.suffix)}`,
110
+ ...(k.expired_at ? { badge: 'Expired' } : { actions: [{ label: 'Expire', action: `${KEYS}/expire`, tone: 'danger' as const, fields: { name: String(k.name) } }] }),
111
+ })),
112
+ }]}
113
+ forms={[{
114
+ heading: 'Create API key',
115
+ action: KEYS,
116
+ fields: [
117
+ { id: 'name', label: 'Name' },
118
+ ...GRANTS.map((g) => ({ id: g, label: LABEL[g], type: 'checkbox' })),
119
+ ],
120
+ submit: { label: 'Create API key' },
121
+ }]}
122
+ />
123
+ ),
124
+ });
125
+ }
126
+
127
+ /** A key's value: `tpuf_` and 32 letters and digits, derived from the organization, the name and the moment. */
128
+ function keyValue(organization: string, name: string, at: string, n: number): string {
129
+ const digest = createHash('sha256').update(`apikey:${organization}:${name}:${at}:${n}`).digest();
130
+ return `tpuf_${Array.from(digest.subarray(0, 32), (b) => ALNUM[b % ALNUM.length]).join('')}`;
131
+ }
132
+
133
+ async function formOf(request: Request): Promise<Record<string, string>> {
134
+ return Object.fromEntries(new URLSearchParams(await request.text()));
135
+ }
136
+
137
+ async function createKey(ctx: SemanticsContext, person: Row, form: Record<string, string>): Promise<Response> {
138
+ const organization = String(person.organization);
139
+ const name = (form.name ?? '').trim();
140
+ const grants = GRANTS.filter((g) => form[g] !== undefined);
141
+ if (!name) return keysPage(ctx, person, 'An API key needs a name.', 422);
142
+ if (keysOf(ctx, organization).some((k) => k.name === name && !k.expired_at)) return keysPage(ctx, person, `There is already an API key named ${name}.`, 422);
143
+ if (!grants.length) return keysPage(ctx, person, 'Choose at least one permission.', 422);
144
+ const n = ctx.rowsRaw('_api_key').length + 1;
145
+ const key = keyValue(organization, name, ctx.occurredAt, n);
146
+ await ctx.record('_api_key', {
147
+ organization, name, grants, created_at: ctx.occurredAt, created_by: person.email, expired_at: null,
148
+ suffix: key.slice(-4), key_sha256: hex(key),
149
+ }, `apikey:${organization}:${n}`);
150
+ return keysPage(ctx, person, `API key ${name} created: ${key} — copy it now, it will not be shown again.`);
151
+ }
152
+
153
+ /** Expire: the key stops working at once (key-gate.ts admits no expired key). */
154
+ async function expireKey(ctx: SemanticsContext, person: Row, name: string): Promise<Response> {
155
+ const organization = String(person.organization);
156
+ const key = keysOf(ctx, organization).find((k) => k.name === name && !k.expired_at);
157
+ if (!key) return keysPage(ctx, person, `There is no live API key named ${name}.`, 422);
158
+ await ctx.record('_api_key', { ...ctx.own(key), expired_at: ctx.occurredAt, expired_by: person.email }, String(key.id));
159
+ return keysPage(ctx, person, `API key ${name} expired.`);
160
+ }
161
+
162
+ /** turbopuffer.com's pages and the World's sign-up door, or undefined for any other path. `contextFor` gives the
163
+ * pack's semantics context for a request. */
164
+ export async function dashboard(request: Request, contextFor: (request: Request) => Promise<SemanticsContext>): Promise<Response | undefined> {
165
+ const url = new URL(request.url);
166
+ const path = url.pathname.replace(/\/+$/, '') || '/';
167
+ const ctx = await contextFor(new Request(request.url));
168
+ if (path === '/login' && request.method === 'GET') return signInPage(returnTo(url.searchParams.get('return_to')));
169
+ if (path === '/login' && request.method === 'POST') {
170
+ const f = await formOf(request);
171
+ const email = (f.email ?? '').trim().toLowerCase();
172
+ const back = returnTo(f.return_to);
173
+ const person = ctx.rowsRaw('_person').find((p) => p.email === email);
174
+ if (!email || !person || person.hash !== passwordHash(email, f.password ?? '')) return signInPage(back, email, 'Incorrect email or password.');
175
+ const token = hex(`session:${email}:${ctx.occurredAt}:${ctx.rowsRaw('_web_session').length + 1}`).slice(0, 48);
176
+ await ctx.record('_web_session', { token, email, created_at: ctx.occurredAt }, `websession:${token}`);
177
+ const headers = new Headers({ location: back });
178
+ headers.append('set-cookie', `${COOKIE}=${token}; path=/; HttpOnly; Secure; SameSite=Lax`);
179
+ return new Response(null, { status: 302, headers });
180
+ }
181
+ const door = /^\/_twin\/users\/([^/]+)\/password$/.exec(path);
182
+ if (door && request.method === 'POST') return signUpDoor(ctx, decodeURIComponent(door[1]!).toLowerCase(), request);
183
+ // a path the host's pages do not have is the caller's (its 404)
184
+ return path === '/dashboard' || path === KEYS || path === `${KEYS}/expire` ? keysPages(ctx, request, url, path) : undefined;
185
+ }
186
+
187
+ /** /dashboard (to the keys page), the keys page, its Create API key and its Expire, for the person signed in. */
188
+ async function keysPages(ctx: SemanticsContext, request: Request, url: URL, path: string): Promise<Response | undefined> {
189
+ const person = signedIn(ctx, request);
190
+ if (!person) return new Response(null, { status: 302, headers: { location: `/login?return_to=${encodeURIComponent(url.pathname + url.search)}` } });
191
+ if (request.method === 'GET') return path === '/dashboard' ? new Response(null, { status: 302, headers: { location: KEYS } }) : keysPage(ctx, person);
192
+ if (request.method !== 'POST' || path === '/dashboard') return undefined;
193
+ const form = await formOf(request);
194
+ return path === KEYS ? createKey(ctx, person, form) : expireKey(ctx, person, (form.name ?? '').trim());
195
+ }
196
+
197
+ /** The sign-up door's refusal of a call missing what it needs. */
198
+ function badSignUp(): Response {
199
+ return Response.json({ status: 'error', error: 'an email, a password of at least 8 characters, an organization and a plan (launch or scale) are required' }, { status: 400 });
200
+ }
201
+
202
+ /** POST /_twin/users/:email/password {password, organization, plan}: the person, their organization and its plan, as
203
+ * turbopuffer's sign-up makes them (turbopuffer.com/join: "Select plan launch ($16/mo minimum) … scale ($256/mo
204
+ * minimum)"); 204. */
205
+ async function signUpDoor(ctx: SemanticsContext, email: string, request: Request): Promise<Response> {
206
+ let body: Record<string, unknown> = {};
207
+ try { body = JSON.parse(await request.text()) as Record<string, unknown>; } catch { /* refused below */ }
208
+ const password = typeof body.password === 'string' ? body.password : '';
209
+ const organization = typeof body.organization === 'string' ? body.organization.trim() : '';
210
+ const plan = body.plan === undefined ? 'launch' : body.plan;
211
+ if (!/^[^@\s]+@[^@\s]+$/.test(email) || password.length < 8 || !organization || (plan !== 'launch' && plan !== 'scale')) return badSignUp();
212
+ await ctx.record('_person', { email, hash: passwordHash(email, password), organization, plan, set_at: ctx.occurredAt }, `person:${email}`);
213
+ return new Response(null, { status: 204 });
214
+ }
@@ -0,0 +1,30 @@
1
+ // Turbopuffer's handlers, by operationId (docs/contributing/architecture.md, "The semantics layer"): every operation of
2
+ // the spec's surface (../generated/surface.gen.json) the twin serves. Each delegates to the pack's request handler
3
+ // (../turbopuffer-twin.ts), which keeps turbopuffer's order and answers: the API key, then the namespace name, then
4
+ // the operation over the store (../turbopuffer-store.ts: writes, schema, queries, BM25 and filters), in the
5
+ // `{status: "error", error}` body. An operation of the surface not named here is the gap: namespace pinning
6
+ // (PATCH /v1/namespaces/{namespace}/metadata), query plans (explain_query) and recall evaluation (_debug/recall).
7
+ //
8
+ // These are dispatch handlers, not `bindSemantics` ones: a write request is one kernel action over the namespace and
9
+ // every document it touches (a projection), which a semantics context's single-subject write cannot make, as
10
+ // tinybird's handlers are for its row-per-event appends.
11
+ import type { DerivedCall, DerivedHandler } from '@volter/world-core';
12
+
13
+ /** The operations served here, every one the twin serves. */
14
+ export const HANDLED = [
15
+ // namespaces
16
+ 'get_v1_namespaces', 'get_v2_namespaces_namespace_metadata', 'get_v1_namespaces_namespace_metadata', 'get_v1_namespaces_namespace_hint_cache_warm',
17
+ 'delete_v2_namespaces_namespace',
18
+ // an asynchronous copy's operation, polled (the API overview's Asynchronous requests)
19
+ 'get_v1_namespaces_namespace_operations_token',
20
+ // a namespace's schema
21
+ 'get_v1_namespaces_namespace_schema', 'post_v1_namespaces_namespace_schema',
22
+ // documents: the write (upserts, patches and deletes, by id or by filter) and the query (one query, or several)
23
+ 'post_v2_namespaces_namespace', 'post_v2_namespaces_namespace_query',
24
+ ] as const;
25
+
26
+ /** A handler per operation above, answering through `answer` (the pack's request handler as a fetch). */
27
+ export function turbopufferHandlers(answer: (request: Request) => Promise<Response>): Record<string, DerivedHandler> {
28
+ const serve: DerivedHandler = ({ request }: DerivedCall) => answer(request);
29
+ return Object.fromEntries(HANDLED.map((id) => [id, serve]));
30
+ }