cursedbelt-server 4.46.1 β 4.47.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/dist/server/storage/binaryServerMeta.d.ts +42 -0
- package/dist/server/storage/binaryServerMeta.js +57 -0
- package/dist/server/storage/binaryStore.d.ts +2 -0
- package/dist/server/storage/binaryStore.js +2 -0
- package/package.json +1 -1
- package/src/leafSubpathsImportNothing.spec.ts +2 -0
- package/src/server/storage/binaryServerMeta.spec.ts +52 -0
- package/src/server/storage/binaryServerMeta.ts +81 -0
- package/src/server/storage/binaryStore.ts +3 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What binary-server holds under MANY keys β `POST /internal/ops/meta`, one request per
|
|
3
|
+
* {@link BINARY_META_KEYS_MAX} keys (forge task 3385).
|
|
4
|
+
*
|
|
5
|
+
* π΄ One client, because two hand-rolled copies drifted into being: collections' media-cipher
|
|
6
|
+
* sweep (forge task 2634 β ~880 per-key `/meta` asks a run against its 8/s bulk ceiling, ~2,300
|
|
7
|
+
* refusals an hour) and music's nightly missing-bytes check (forge task 3382) each carried the
|
|
8
|
+
* loop and a copied `1_000`. A change to the route's cap or shape is now one edit here, and
|
|
9
|
+
* binary-server takes its own cap from {@link BINARY_META_KEYS_MAX}, so the two cannot disagree.
|
|
10
|
+
*
|
|
11
|
+
* The route is internal-secret gated and loopback-only (`/internal/ops/*` 404s on the public
|
|
12
|
+
* hostname), so `baseUrl` is binary-server's INTERNAL origin, never the tunnel's.
|
|
13
|
+
*/
|
|
14
|
+
/** Keys per `POST /internal/ops/meta` β binary-server's `META_KEYS_MAX` is this constant. */
|
|
15
|
+
export declare const BINARY_META_KEYS_MAX = 1000;
|
|
16
|
+
/** One key's answer: an indexed, active row whose bytes exist. */
|
|
17
|
+
export interface StoredMeta {
|
|
18
|
+
size: number;
|
|
19
|
+
mime: string | null;
|
|
20
|
+
checksum: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface MetaManyTarget {
|
|
23
|
+
/** binary-server's internal origin (`BINARY_SERVER_INTERNAL_URL`). */
|
|
24
|
+
baseUrl: string | undefined;
|
|
25
|
+
internalSecret: string | undefined;
|
|
26
|
+
/** The tenant the keys belong to. */
|
|
27
|
+
app: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MetaManyOptions {
|
|
30
|
+
fetchImpl?: (url: string, init: RequestInit) => Promise<Response>;
|
|
31
|
+
/** Per batch. Default {@link BINARY_META_BATCH_TIMEOUT_MS}. */
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every asked key β its {@link StoredMeta}, or `null` when the store holds nothing live there.
|
|
36
|
+
*
|
|
37
|
+
* The whole answer is `null` on ANY failure β no origin or secret, a refusal (an older
|
|
38
|
+
* binary-server without the route answers 404; a batch over its cap, 400), a timeout, a body of
|
|
39
|
+
* another shape. Callers then ask key by key, as before: slower, never wrong. A partial map is
|
|
40
|
+
* never returned, so "absent from the map" cannot be mistaken for "not held".
|
|
41
|
+
*/
|
|
42
|
+
export declare function metaMany(to: MetaManyTarget, keys: readonly string[], opts?: MetaManyOptions): Promise<Map<string, StoredMeta | null> | null>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What binary-server holds under MANY keys β `POST /internal/ops/meta`, one request per
|
|
3
|
+
* {@link BINARY_META_KEYS_MAX} keys (forge task 3385).
|
|
4
|
+
*
|
|
5
|
+
* π΄ One client, because two hand-rolled copies drifted into being: collections' media-cipher
|
|
6
|
+
* sweep (forge task 2634 β ~880 per-key `/meta` asks a run against its 8/s bulk ceiling, ~2,300
|
|
7
|
+
* refusals an hour) and music's nightly missing-bytes check (forge task 3382) each carried the
|
|
8
|
+
* loop and a copied `1_000`. A change to the route's cap or shape is now one edit here, and
|
|
9
|
+
* binary-server takes its own cap from {@link BINARY_META_KEYS_MAX}, so the two cannot disagree.
|
|
10
|
+
*
|
|
11
|
+
* The route is internal-secret gated and loopback-only (`/internal/ops/*` 404s on the public
|
|
12
|
+
* hostname), so `baseUrl` is binary-server's INTERNAL origin, never the tunnel's.
|
|
13
|
+
*/
|
|
14
|
+
/** Keys per `POST /internal/ops/meta` β binary-server's `META_KEYS_MAX` is this constant. */
|
|
15
|
+
export const BINARY_META_KEYS_MAX = 1_000;
|
|
16
|
+
/** How long one batch may take: a batch of keys the store's memo lacks is HEADed eight at a time. */
|
|
17
|
+
const BINARY_META_BATCH_TIMEOUT_MS = 180_000;
|
|
18
|
+
/**
|
|
19
|
+
* Every asked key β its {@link StoredMeta}, or `null` when the store holds nothing live there.
|
|
20
|
+
*
|
|
21
|
+
* The whole answer is `null` on ANY failure β no origin or secret, a refusal (an older
|
|
22
|
+
* binary-server without the route answers 404; a batch over its cap, 400), a timeout, a body of
|
|
23
|
+
* another shape. Callers then ask key by key, as before: slower, never wrong. A partial map is
|
|
24
|
+
* never returned, so "absent from the map" cannot be mistaken for "not held".
|
|
25
|
+
*/
|
|
26
|
+
export async function metaMany(to, keys, opts = {}) {
|
|
27
|
+
if (!to.baseUrl || !to.internalSecret)
|
|
28
|
+
return null;
|
|
29
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
30
|
+
const url = `${to.baseUrl.replace(/\/+$/, '')}/internal/ops/meta`;
|
|
31
|
+
const out = new Map();
|
|
32
|
+
try {
|
|
33
|
+
for (let i = 0; i < keys.length; i += BINARY_META_KEYS_MAX) {
|
|
34
|
+
const batch = keys.slice(i, i + BINARY_META_KEYS_MAX);
|
|
35
|
+
const res = await doFetch(url, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'content-type': 'application/json', 'x-internal-secret': to.internalSecret },
|
|
38
|
+
body: JSON.stringify({ app: to.app, keys: batch }),
|
|
39
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? BINARY_META_BATCH_TIMEOUT_MS),
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
return null;
|
|
43
|
+
const body = (await res.json());
|
|
44
|
+
if (!body?.metas || typeof body.metas !== 'object')
|
|
45
|
+
return null;
|
|
46
|
+
const metas = body.metas;
|
|
47
|
+
for (const key of batch) {
|
|
48
|
+
const m = metas[key];
|
|
49
|
+
out.set(key, m && typeof m.size === 'number' ? { size: m.size, mime: m.mime ?? null, checksum: m.checksum ?? null } : null);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
@@ -6,6 +6,8 @@ import type { FileTokenClaims } from './types.js';
|
|
|
6
6
|
* this module read unchanged.
|
|
7
7
|
*/
|
|
8
8
|
export type { FileTokenClaims };
|
|
9
|
+
/** The batched `/internal/ops/meta` client β its own module so the store's graph stays the store's. */
|
|
10
|
+
export { BINARY_META_KEYS_MAX, metaMany, type StoredMeta } from './binaryServerMeta.js';
|
|
9
11
|
export interface BinaryStoreConfig {
|
|
10
12
|
/** e.g. `http://127.0.0.1:3099` or `https://binary-server.cursedalchemy.com`. */
|
|
11
13
|
baseUrl: string;
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
* behavioural difference in any of the three, so nothing was dropped to land the union.
|
|
42
42
|
*/
|
|
43
43
|
import { createHash, createPrivateKey, createPublicKey, sign as cryptoSign } from 'node:crypto';
|
|
44
|
+
/** The batched `/internal/ops/meta` client β its own module so the store's graph stays the store's. */
|
|
45
|
+
export { BINARY_META_KEYS_MAX, metaMany } from './binaryServerMeta.js';
|
|
44
46
|
/*
|
|
45
47
|
* ββ The process-wide repeat register (Β§ the repeat-key counter) βββββββββββββββ
|
|
46
48
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.47.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split β storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -144,8 +144,10 @@ const LEAVES = [
|
|
|
144
144
|
* in `src/server/storage/` beside the file-catalogue and transcode graph, which is
|
|
145
145
|
* exactly the barrel this must never be reached through: `./storage`'s index is heavy
|
|
146
146
|
* and this module's whole point is that it costs `node:crypto` and nothing else.
|
|
147
|
+
* Its batched `/internal/ops/meta` client (4.47.0) is a sibling that imports nothing.
|
|
147
148
|
*/
|
|
148
149
|
evaluates: 'createBinaryStore',
|
|
150
|
+
siblings: ['src/server/storage/binaryServerMeta.ts'],
|
|
149
151
|
},
|
|
150
152
|
{
|
|
151
153
|
subpath: './binary-store/testing',
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { BINARY_META_KEYS_MAX, metaMany } from './binaryStore.js';
|
|
3
|
+
|
|
4
|
+
type Call = { url: string; init: RequestInit };
|
|
5
|
+
const target = { baseUrl: 'http://127.0.0.1:3099/', internalSecret: 's3cret', app: 'music' };
|
|
6
|
+
|
|
7
|
+
/** A store that holds every key whose name starts with `held/`, answering in the route's shape. */
|
|
8
|
+
function store(calls: Call[], answer?: (keys: string[]) => Response) {
|
|
9
|
+
return async (url: string, init: RequestInit) => {
|
|
10
|
+
calls.push({ url, init });
|
|
11
|
+
const { keys } = JSON.parse(String(init.body)) as { keys: string[] };
|
|
12
|
+
if (answer) return answer(keys);
|
|
13
|
+
const metas = Object.fromEntries(keys.map((k) => [k, k.startsWith('held/') ? { size: 7, mime: 'audio/mp4', checksum: 'abc' } : null]));
|
|
14
|
+
return Response.json({ app: 'music', metas });
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('metaMany', () => {
|
|
19
|
+
it('asks one request per BINARY_META_KEYS_MAX keys, and answers every key', async () => {
|
|
20
|
+
const calls: Call[] = [];
|
|
21
|
+
const keys = Array.from({ length: BINARY_META_KEYS_MAX * 2 + 1 }, (_, i) => (i % 2 ? `held/${i}` : `gone/${i}`));
|
|
22
|
+
const got = await metaMany(target, keys, { fetchImpl: store(calls) });
|
|
23
|
+
expect(calls.map((c) => (JSON.parse(String(c.init.body)) as { keys: string[] }).keys.length)).toEqual([BINARY_META_KEYS_MAX, BINARY_META_KEYS_MAX, 1]);
|
|
24
|
+
expect(calls[0]?.url).toBe('http://127.0.0.1:3099/internal/ops/meta');
|
|
25
|
+
expect(calls[0]?.init.headers).toMatchObject({ 'x-internal-secret': 's3cret' });
|
|
26
|
+
expect(got?.size).toBe(keys.length);
|
|
27
|
+
expect(got?.get('held/1')).toEqual({ size: 7, mime: 'audio/mp4', checksum: 'abc' });
|
|
28
|
+
expect(got?.get('gone/0')).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('is null β never a partial map β when any batch fails', async () => {
|
|
32
|
+
let n = 0;
|
|
33
|
+
const keys = Array.from({ length: BINARY_META_KEYS_MAX + 1 }, (_, i) => `held/${i}`);
|
|
34
|
+
const second404 = store([], (asked) => (n++ === 0 ? Response.json({ metas: Object.fromEntries(asked.map((k) => [k, null])) }) : new Response('nope', { status: 404 })));
|
|
35
|
+
expect(await metaMany(target, keys, { fetchImpl: second404 })).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('is null on a body of another shape, a throw, or no origin/secret β and asks nothing then', async () => {
|
|
39
|
+
expect(await metaMany(target, ['k'], { fetchImpl: store([], () => Response.json({ ok: true })) })).toBeNull();
|
|
40
|
+
expect(await metaMany(target, ['k'], { fetchImpl: async () => { throw new Error('ECONNREFUSED'); } })).toBeNull();
|
|
41
|
+
const calls: Call[] = [];
|
|
42
|
+
expect(await metaMany({ ...target, internalSecret: undefined }, ['k'], { fetchImpl: store(calls) })).toBeNull();
|
|
43
|
+
expect(await metaMany({ ...target, baseUrl: '' }, ['k'], { fetchImpl: store(calls) })).toBeNull();
|
|
44
|
+
expect(calls).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('reads a key the store left out of its answer as not held', async () => {
|
|
48
|
+
const got = await metaMany(target, ['held/a', 'held/b'], { fetchImpl: store([], () => Response.json({ metas: { 'held/a': { size: 1 } } })) });
|
|
49
|
+
expect(got?.get('held/a')).toEqual({ size: 1, mime: null, checksum: null });
|
|
50
|
+
expect(got?.get('held/b')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What binary-server holds under MANY keys β `POST /internal/ops/meta`, one request per
|
|
3
|
+
* {@link BINARY_META_KEYS_MAX} keys (forge task 3385).
|
|
4
|
+
*
|
|
5
|
+
* π΄ One client, because two hand-rolled copies drifted into being: collections' media-cipher
|
|
6
|
+
* sweep (forge task 2634 β ~880 per-key `/meta` asks a run against its 8/s bulk ceiling, ~2,300
|
|
7
|
+
* refusals an hour) and music's nightly missing-bytes check (forge task 3382) each carried the
|
|
8
|
+
* loop and a copied `1_000`. A change to the route's cap or shape is now one edit here, and
|
|
9
|
+
* binary-server takes its own cap from {@link BINARY_META_KEYS_MAX}, so the two cannot disagree.
|
|
10
|
+
*
|
|
11
|
+
* The route is internal-secret gated and loopback-only (`/internal/ops/*` 404s on the public
|
|
12
|
+
* hostname), so `baseUrl` is binary-server's INTERNAL origin, never the tunnel's.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Keys per `POST /internal/ops/meta` β binary-server's `META_KEYS_MAX` is this constant. */
|
|
16
|
+
export const BINARY_META_KEYS_MAX = 1_000;
|
|
17
|
+
|
|
18
|
+
/** How long one batch may take: a batch of keys the store's memo lacks is HEADed eight at a time. */
|
|
19
|
+
const BINARY_META_BATCH_TIMEOUT_MS = 180_000;
|
|
20
|
+
|
|
21
|
+
/** One key's answer: an indexed, active row whose bytes exist. */
|
|
22
|
+
export interface StoredMeta {
|
|
23
|
+
size: number;
|
|
24
|
+
mime: string | null;
|
|
25
|
+
checksum: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MetaManyTarget {
|
|
29
|
+
/** binary-server's internal origin (`BINARY_SERVER_INTERNAL_URL`). */
|
|
30
|
+
baseUrl: string | undefined;
|
|
31
|
+
internalSecret: string | undefined;
|
|
32
|
+
/** The tenant the keys belong to. */
|
|
33
|
+
app: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MetaManyOptions {
|
|
37
|
+
fetchImpl?: (url: string, init: RequestInit) => Promise<Response>;
|
|
38
|
+
/** Per batch. Default {@link BINARY_META_BATCH_TIMEOUT_MS}. */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every asked key β its {@link StoredMeta}, or `null` when the store holds nothing live there.
|
|
44
|
+
*
|
|
45
|
+
* The whole answer is `null` on ANY failure β no origin or secret, a refusal (an older
|
|
46
|
+
* binary-server without the route answers 404; a batch over its cap, 400), a timeout, a body of
|
|
47
|
+
* another shape. Callers then ask key by key, as before: slower, never wrong. A partial map is
|
|
48
|
+
* never returned, so "absent from the map" cannot be mistaken for "not held".
|
|
49
|
+
*/
|
|
50
|
+
export async function metaMany(
|
|
51
|
+
to: MetaManyTarget,
|
|
52
|
+
keys: readonly string[],
|
|
53
|
+
opts: MetaManyOptions = {},
|
|
54
|
+
): Promise<Map<string, StoredMeta | null> | null> {
|
|
55
|
+
if (!to.baseUrl || !to.internalSecret) return null;
|
|
56
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
57
|
+
const url = `${to.baseUrl.replace(/\/+$/, '')}/internal/ops/meta`;
|
|
58
|
+
const out = new Map<string, StoredMeta | null>();
|
|
59
|
+
try {
|
|
60
|
+
for (let i = 0; i < keys.length; i += BINARY_META_KEYS_MAX) {
|
|
61
|
+
const batch = keys.slice(i, i + BINARY_META_KEYS_MAX);
|
|
62
|
+
const res = await doFetch(url, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'content-type': 'application/json', 'x-internal-secret': to.internalSecret },
|
|
65
|
+
body: JSON.stringify({ app: to.app, keys: batch }),
|
|
66
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? BINARY_META_BATCH_TIMEOUT_MS),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) return null;
|
|
69
|
+
const body = (await res.json()) as { metas?: unknown } | null;
|
|
70
|
+
if (!body?.metas || typeof body.metas !== 'object') return null;
|
|
71
|
+
const metas = body.metas as Record<string, Partial<StoredMeta> | null | undefined>;
|
|
72
|
+
for (const key of batch) {
|
|
73
|
+
const m = metas[key];
|
|
74
|
+
out.set(key, m && typeof m.size === 'number' ? { size: m.size, mime: m.mime ?? null, checksum: m.checksum ?? null } : null);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
@@ -51,6 +51,9 @@ import type { FileTokenClaims } from './types.js';
|
|
|
51
51
|
*/
|
|
52
52
|
export type { FileTokenClaims };
|
|
53
53
|
|
|
54
|
+
/** The batched `/internal/ops/meta` client β its own module so the store's graph stays the store's. */
|
|
55
|
+
export { BINARY_META_KEYS_MAX, metaMany, type StoredMeta } from './binaryServerMeta.js';
|
|
56
|
+
|
|
54
57
|
export interface BinaryStoreConfig {
|
|
55
58
|
/** e.g. `http://127.0.0.1:3099` or `https://binary-server.cursedalchemy.com`. */
|
|
56
59
|
baseUrl: string;
|