cursedbelt-core 2.13.2 → 2.14.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/core/analytics/range.d.ts +2 -2
- package/dist/core/analytics/range.js +2 -2
- package/dist/core/analytics/schedule.d.ts +1 -1
- package/dist/core/analytics/schedule.js +3 -3
- package/dist/core/duplicates/index.d.ts +2 -2
- package/dist/core/duplicates/index.js +1 -1
- package/dist/core/file-tree/fileTreeModel.d.ts +3 -3
- package/dist/core/file-tree/fileTreeModel.js +2 -2
- package/dist/core/folder-tree/actions.d.ts +1 -1
- package/dist/core/folder-tree/actions.js +1 -1
- package/dist/core/master-lock/kdf.js +1 -1
- package/dist/core/master-lock/presence.d.ts +1 -1
- package/dist/core/master-lock/presence.js +1 -1
- package/dist/core/master-lock/wire.d.ts +1 -1
- package/dist/core/media/mediaItem.js +1 -1
- package/dist/core/media/types.d.ts +4 -4
- package/dist/core/media/videoCuts.d.ts +1 -1
- package/dist/core/media/videoCuts.js +2 -2
- package/dist/core/navigation/index.js +1 -1
- package/dist/core/navigation/navigationModel.d.ts +1 -1
- package/dist/core/navigation/navigationModel.js +1 -1
- package/dist/core/navigation/navigationSelectors.d.ts +2 -2
- package/dist/core/navigation/navigationSelectors.js +2 -2
- package/dist/core/navigation/searchCatalogue.d.ts +2 -2
- package/dist/core/navigation/searchCatalogue.js +4 -4
- package/dist/core/palette/fuzzyRank.d.ts +1 -1
- package/dist/core/palette/fuzzyRank.js +3 -3
- package/dist/core/tokens/registry.d.ts +1 -1
- package/dist/core/tokens/registry.js +1 -1
- package/dist/core/upload-precheck/contract.d.ts +71 -0
- package/dist/core/upload-precheck/contract.js +76 -0
- package/dist/core/upload-precheck/hash.d.ts +26 -0
- package/dist/core/upload-precheck/hash.js +46 -0
- package/dist/core/upload-precheck/index.d.ts +21 -0
- package/dist/core/upload-precheck/index.js +21 -0
- package/dist/core/upload-precheck/memo.d.ts +52 -0
- package/dist/core/upload-precheck/memo.js +160 -0
- package/dist/core/upload-precheck/plan.d.ts +112 -0
- package/dist/core/upload-precheck/plan.js +242 -0
- package/dist/core/upload-precheck/sha256.d.ts +27 -0
- package/dist/core/upload-precheck/sha256.js +124 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +2 -2
- package/dist/core/wire/index.d.ts +1 -1
- package/dist/core/wire/index.js +1 -1
- package/dist/core/wire/ingress.d.ts +4 -4
- package/dist/core/wire/ingress.js +12 -12
- package/dist/core/wire/marshalWorker.d.ts +1 -1
- package/dist/core/wire/marshalWorker.js +15 -15
- package/dist/core/wire/marshalling.d.ts +16 -16
- package/dist/core/wire/marshalling.js +13 -13
- package/dist/ctgrClient/convertRequest.js +2 -2
- package/dist/testing/bytes.d.ts +1 -1
- package/dist/testing/bytes.js +1 -1
- package/package.json +7 -1
- package/src/core/analytics/range.spec.ts +1 -1
- package/src/core/analytics/range.ts +2 -2
- package/src/core/analytics/schedule.spec.ts +3 -3
- package/src/core/analytics/schedule.ts +3 -3
- package/src/core/duplicates/index.ts +2 -2
- package/src/core/file-tree/fileTreeModel.ts +4 -4
- package/src/core/folder-tree/actions.ts +1 -1
- package/src/core/master-lock/kdf.ts +1 -1
- package/src/core/master-lock/presence.ts +1 -1
- package/src/core/master-lock/wire.ts +1 -1
- package/src/core/media/mediaItem.ts +1 -1
- package/src/core/media/types.ts +4 -4
- package/src/core/media/videoCuts.ts +2 -2
- package/src/core/navigation/index.ts +1 -1
- package/src/core/navigation/navigation.spec.ts +3 -3
- package/src/core/navigation/navigationModel.ts +2 -2
- package/src/core/navigation/navigationSelectors.ts +3 -3
- package/src/core/navigation/searchCatalogue.ts +4 -4
- package/src/core/palette/fuzzyRank.ts +3 -3
- package/src/core/tokens/registry.ts +2 -2
- package/src/core/upload-precheck/contract.spec.ts +67 -0
- package/src/core/upload-precheck/contract.ts +107 -0
- package/src/core/upload-precheck/hash.ts +57 -0
- package/src/core/upload-precheck/index.ts +46 -0
- package/src/core/upload-precheck/memo.ts +170 -0
- package/src/core/upload-precheck/plan.spec.ts +299 -0
- package/src/core/upload-precheck/plan.ts +315 -0
- package/src/core/upload-precheck/sha256.spec.ts +47 -0
- package/src/core/upload-precheck/sha256.ts +127 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +2 -2
- package/src/core/wire/index.ts +1 -1
- package/src/core/wire/ingress.spec.ts +12 -12
- package/src/core/wire/ingress.ts +15 -15
- package/src/core/wire/marshalWorker.spec.ts +7 -7
- package/src/core/wire/marshalWorker.ts +15 -15
- package/src/core/wire/marshalling.spec.ts +3 -3
- package/src/core/wire/marshalling.ts +25 -25
- package/src/ctgrClient/convertRequest.ts +2 -2
- package/src/namedSubpathsResolve.spec.ts +1 -1
- package/src/publishShape.spec.ts +1 -1
- package/src/shippedFilesAreTracked.spec.ts +2 -2
- package/src/testing/bytes.ts +1 -1
|
@@ -158,8 +158,8 @@ export const startOfBucket = (t: number, unit: BucketUnit): number =>
|
|
|
158
158
|
* duplicated or missing bar exactly once every spring and autumn — the kind of
|
|
159
159
|
* bug nobody reproduces.
|
|
160
160
|
*
|
|
161
|
-
* The time-of-day is carried through rather than
|
|
162
|
-
* caller here passes an already-aligned value, so
|
|
161
|
+
* The time-of-day is carried through rather than normalized to midnight: every
|
|
162
|
+
* caller here passes an already-aligned value, so normalizing would be
|
|
163
163
|
* invisible in this file and a trap for the first caller that does not.
|
|
164
164
|
*/
|
|
165
165
|
export const addDays = (t: number, n: number): number => {
|
|
@@ -25,7 +25,7 @@ describe('simulateSchedule', () => {
|
|
|
25
25
|
expect(simulateSchedule(items, { workers: 1 }).makespanMs).toBe(40 * MIN);
|
|
26
26
|
expect(simulateSchedule(items, { workers: 2 }).makespanMs).toBe(20 * MIN);
|
|
27
27
|
expect(simulateSchedule(items, { workers: 4 }).makespanMs).toBe(10 * MIN);
|
|
28
|
-
// Nothing left to
|
|
28
|
+
// Nothing left to parallelize — the fifth worker is inert.
|
|
29
29
|
expect(simulateSchedule(items, { workers: 5 }).makespanMs).toBe(10 * MIN);
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -50,12 +50,12 @@ describe('simulateSchedule', () => {
|
|
|
50
50
|
expect(simulateSchedule(items, { workers: 2 }).makespanMs).toBe(10 * MIN);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('
|
|
53
|
+
it('serializes a named group without needing a shared resource', () => {
|
|
54
54
|
const items = [item('a', 5, { group: 'gpu' }), item('b', 5, { group: 'gpu' })];
|
|
55
55
|
expect(simulateSchedule(items, { workers: 4 }).makespanMs).toBe(10 * MIN);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it('
|
|
58
|
+
it('honors dependencies', () => {
|
|
59
59
|
const items = [item('second', 10, { dependsOn: ['first'] }), item('first', 10)];
|
|
60
60
|
const r = simulateSchedule(items, { workers: 4 });
|
|
61
61
|
expect(r.makespanMs).toBe(20 * MIN);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Three separate claims hide in that, and a `remaining / rate` division answers
|
|
12
12
|
* none of them:
|
|
13
13
|
*
|
|
14
|
-
* 1. **Work does not
|
|
14
|
+
* 1. **Work does not parallelize freely.** Two tasks that write the same files
|
|
15
15
|
* may not run together whatever the fleet size — that is the orchestrator's
|
|
16
16
|
* own scope rule, and it is why a queue of ten tasks all scoped to one app
|
|
17
17
|
* drains at exactly the same speed with eight workers as with one.
|
|
@@ -312,10 +312,10 @@ function blocks(candidate: ScheduleItem, other: ScheduleItem): boolean {
|
|
|
312
312
|
/**
|
|
313
313
|
* A best-effort reason an item started late — for the table behind the chart.
|
|
314
314
|
*
|
|
315
|
-
* Deliberately approximate and
|
|
315
|
+
* Deliberately approximate and labeled as such: the precise answer would need
|
|
316
316
|
* the admission loop to record every refusal at every event, which is a lot of
|
|
317
317
|
* bookkeeping for a column. "Was it waiting on a dependency, on a conflicting
|
|
318
|
-
*
|
|
318
|
+
* neighbor, or simply for a free worker" is the distinction a reader acts on,
|
|
319
319
|
* and that is decidable from the item alone.
|
|
320
320
|
*/
|
|
321
321
|
function whyWaited(
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ── What is NOT here, deliberately ──────────────────────────────────────────────────────
|
|
19
19
|
* Anything that needs a database, a filesystem, a fetch or a picture decoder. This module is
|
|
20
|
-
* pure so both consumers can prove their
|
|
20
|
+
* pure so both consumers can prove their behavior with `bun test` instead of against a live
|
|
21
21
|
* store, and so a browser bundle may import it (music renders the pair side by side).
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -158,7 +158,7 @@ export function durationsClose(
|
|
|
158
158
|
/** One file, reduced to what this module can reason about. Units are the caller's — pass
|
|
159
159
|
* both durations in the same unit or not at all. */
|
|
160
160
|
export interface NamedFileFacts {
|
|
161
|
-
/** The name a person would
|
|
161
|
+
/** The name a person would recognize: a display title, else the last path segment. */
|
|
162
162
|
name: string | null | undefined;
|
|
163
163
|
sizeBytes: number;
|
|
164
164
|
/** Playing time, in whatever unit the caller uses consistently. Null = never measured. */
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
export type FileTreeNodeKind = 'folder' | 'file';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* How visible a node is,
|
|
27
|
+
* How visible a node is, generalized from collections' private/locked folders.
|
|
28
28
|
*
|
|
29
29
|
* 🔴 It is a LABEL, never an enforcement. The server decides what a session may see, and a
|
|
30
30
|
* tree that hides a row the server sent (or shows one it did not) is a second, weaker
|
|
@@ -117,7 +117,7 @@ export interface FileTreeNode {
|
|
|
117
117
|
*
|
|
118
118
|
* For a consumer that narrows the tree itself (by content, not only by name) and keeps
|
|
119
119
|
* the folders on the way to each hit: the hits are what the eye is scanning for, and the
|
|
120
|
-
* folders kept around them are context. Omit it and nothing is
|
|
120
|
+
* folders kept around them are context. Omit it and nothing is emphasized.
|
|
121
121
|
*/
|
|
122
122
|
matched?: boolean;
|
|
123
123
|
/**
|
|
@@ -230,7 +230,7 @@ export function flattenFileTree(
|
|
|
230
230
|
* "A row that claims to open onto nothing is its own small lie" was the old reasoning
|
|
231
231
|
* and it is answered by what VS Code does: the chevron says *folder*, not *occupied*,
|
|
232
232
|
* and opening an empty one onto nothing is the honest answer, not a broken promise.
|
|
233
|
-
* `node.expandable === false` is still
|
|
233
|
+
* `node.expandable === false` is still honored for a row that can never hold anything.
|
|
234
234
|
*/
|
|
235
235
|
const expandable = node.kind === 'folder' && (node.expandable ?? true);
|
|
236
236
|
const expanded = expandable && !shut.has(node.id);
|
|
@@ -447,7 +447,7 @@ export interface KeyboardMoveOptions {
|
|
|
447
447
|
* without asking which of several folders was meant — and **← promotes out to the folder its
|
|
448
448
|
* own folder is in**, which is `root` at depth 1.
|
|
449
449
|
*
|
|
450
|
-
* 🔴 Every refusal the DRAG
|
|
450
|
+
* 🔴 Every refusal the DRAG honors is honored here, from the same fields: `acceptsDrop`,
|
|
451
451
|
* the consumer's `canDrop`, and the descendant rule. A keyboard route that can reach a
|
|
452
452
|
* destination the pointer refuses is a second, weaker opinion about what is legal — and it
|
|
453
453
|
* is the one nobody looks at, because the pointer is what gets tested by hand.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* applies, with nothing destroyed. Deleting the CONTENTS destroys items, and this module
|
|
26
26
|
* has never seen an item; it only ever sees the paths they carry. So `purge` is a mode the
|
|
27
27
|
* menu offers and the CONSUMER implements against its own store, which is also the only
|
|
28
|
-
* place that can count what is about to be lost or refuse the operation.
|
|
28
|
+
* place that can count what is about to be lost or refuse the operation. Modeling it here
|
|
29
29
|
* would mean inventing an item, and an item this model cannot see is one it cannot protect.
|
|
30
30
|
*/
|
|
31
31
|
|
|
@@ -89,7 +89,7 @@ export async function deriveMasterLockVerifier(
|
|
|
89
89
|
}
|
|
90
90
|
const salt = unb64u(params.salt);
|
|
91
91
|
const kek = await pbkdf2(utf8(masterPassword), salt, params.iter);
|
|
92
|
-
// Same salt as the KEK, by design: it
|
|
92
|
+
// Same salt as the KEK, by design: it individualizes the verifier without a second
|
|
93
93
|
// stored value. One round, over KEK ‖ password.
|
|
94
94
|
const verifier = await pbkdf2(concat(kek, utf8(masterPassword)), salt, 1);
|
|
95
95
|
return b64u(verifier);
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ── 🔴 Why a decorative loop is NOT presence ────────────────────────────────
|
|
24
24
|
* The failure mode that would make this a security hole is an ambient background clip
|
|
25
25
|
* — muted, looping, autoplaying, running forever with nobody in the building. That is
|
|
26
|
-
* a
|
|
26
|
+
* a recognizable signature (`muted` AND `loop`) and it is refused. A film the owner is
|
|
27
27
|
* actually watching is not both at once; a muted film that loops is not something this
|
|
28
28
|
* fleet renders. Where a component knows better than the heuristic, it says so:
|
|
29
29
|
* `data-master-lock-presence="watch"` counts unconditionally, `"ignore"` never does.
|
|
@@ -123,7 +123,7 @@ export const MASTER_LOCK_STATE_HEADER = "x-master-lock-state";
|
|
|
123
123
|
* nothing in the fleet constructs one. So adding a required field to it is
|
|
124
124
|
* additive, and without this annotation it is the residual false MAJOR that
|
|
125
125
|
* Phase −1's M-B measured and had no cheap fix for (F4). `release/surface.ts`
|
|
126
|
-
* records the annotation; `release/classify.ts`
|
|
126
|
+
* records the annotation; `release/classify.ts` honors it. */
|
|
127
127
|
export interface MasterLockStatus {
|
|
128
128
|
/** Is a master password set at all? False only on an app that was never seeded,
|
|
129
129
|
* or — in per-person mode — for a person who has not chosen one yet. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure MediaItem helpers — kind predicates + dimension/aspect resolution. The React
|
|
2
|
-
//
|
|
2
|
+
// catalog maps items to layout photos through these, so the "what size is this
|
|
3
3
|
// thumbnail" logic is tested here, not buried in a component.
|
|
4
4
|
|
|
5
5
|
import type { MediaItem } from './types.js';
|
package/src/core/media/types.ts
CHANGED
|
@@ -66,11 +66,11 @@ export type VideoProgressEvent = {
|
|
|
66
66
|
error?: string;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
/** The three media kinds the
|
|
69
|
+
/** The three media kinds the catalog + lightbox understand. */
|
|
70
70
|
export type MediaKind = 'image' | 'video' | 'audio';
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* A URL-only
|
|
73
|
+
* A URL-only catalog item — the unit the `<MediaCatalogueViewer>` renders. It
|
|
74
74
|
* holds NO storage identity (no FsNode, no binary id): the app maps its own records
|
|
75
75
|
* to this and pre-resolves `url`/`thumbnailUrl` (signing private files as needed).
|
|
76
76
|
*/
|
|
@@ -92,7 +92,7 @@ export type MediaItem = {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* A gallery photo — a {@link MediaItem} specialized to images, plus the
|
|
95
|
+
* A gallery photo — a {@link MediaItem} specialized to images, plus the catalog affordances a
|
|
96
96
|
* photo grid renders (tags for the filter strip, createdAt for ordering, favorite for the ♥).
|
|
97
97
|
* Like every media-core unit it carries only resolved URLs — no storage identity.
|
|
98
98
|
*/
|
|
@@ -123,7 +123,7 @@ export type ProcessingStatus = 'pending' | 'processing' | 'ready' | 'failed';
|
|
|
123
123
|
/**
|
|
124
124
|
* A surface-agnostic description of a playable video with every URL already resolved —
|
|
125
125
|
* the unit the `<VideoPlayer>` primitive consumes (distinct from {@link MediaItem}, which
|
|
126
|
-
* is the lightbox
|
|
126
|
+
* is the lightbox catalog unit). It carries NO storage identity: a source pre-resolves
|
|
127
127
|
* the streaming URLs (signing/tokenising private ones), so the player couples to nothing.
|
|
128
128
|
* Richer than `MediaItem`: an HLS master + scrub-preview track for a real player HUD.
|
|
129
129
|
*/
|
|
@@ -74,7 +74,7 @@ export interface VideoCut {
|
|
|
74
74
|
* The shortest span worth cutting, in seconds.
|
|
75
75
|
*
|
|
76
76
|
* 🔴 Not zero, and not "whatever the person dragged". Below roughly a frame the request is
|
|
77
|
-
* indistinguishable from a mis-click, and it is a request ffmpeg cannot
|
|
77
|
+
* indistinguishable from a mis-click, and it is a request ffmpeg cannot honor anyway — a
|
|
78
78
|
* cut has to land on a frame boundary, so a 4 ms span asks the encoder to remove nothing and
|
|
79
79
|
* charges a full re-encode of the file for it. 40 ms is one frame at 25fps, the slowest rate
|
|
80
80
|
* anything in this library was shot at.
|
|
@@ -320,7 +320,7 @@ export function toggleSegment(plan: CutPlan, segment: VideoCut, duration: number
|
|
|
320
320
|
splits: plan.splits,
|
|
321
321
|
// Drop every removal this segment sits inside, then put back the parts of a WIDER
|
|
322
322
|
// removal that are still meant to go — un-marking one piece of a span that was marked
|
|
323
|
-
// before it was split must not resurrect its
|
|
323
|
+
// before it was split must not resurrect its neighbors.
|
|
324
324
|
removed: plan.removed.flatMap((cut) => {
|
|
325
325
|
if (!(cut.start <= current.start + 1e-6 && cut.end >= current.end - 1e-6)) return [cut];
|
|
326
326
|
const rest: VideoCut[] = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `cursedbelt-core/navigation` — the navigation contract + pure selectors. React-free and
|
|
2
2
|
// routing-free: the single source of truth (the IA tree) plus every derivation off
|
|
3
|
-
// it (side nav, hub tiles, breadcrumbs, search
|
|
3
|
+
// it (side nav, hub tiles, breadcrumbs, search catalog, coverage test, prefs
|
|
4
4
|
// migration). The React components in `cursedbelt/react` consume exactly this.
|
|
5
5
|
|
|
6
6
|
export {
|
|
@@ -245,7 +245,7 @@ describe('deriveBreadcrumbTrail', () => {
|
|
|
245
245
|
describe('deriveSearchCatalogue + rankCatalogue', () => {
|
|
246
246
|
const catalogue = deriveSearchCatalogue(model, allowAll);
|
|
247
247
|
|
|
248
|
-
test('every visible domain + item is
|
|
248
|
+
test('every visible domain + item is cataloged; deepLinkOnly is excluded', () => {
|
|
249
249
|
const ids = catalogue.map((e) => e.id);
|
|
250
250
|
expect(ids).toContain('fractal');
|
|
251
251
|
expect(ids).toContain('env-sets');
|
|
@@ -280,7 +280,7 @@ describe('deriveSearchCatalogue + rankCatalogue', () => {
|
|
|
280
280
|
expect(userCat.map((e) => e.id)).not.toContain('admin-users');
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
-
test("a DOMAIN's description + keywords reach the
|
|
283
|
+
test("a DOMAIN's description + keywords reach the catalog, so a leaf-shaped rail is findable by synonym", () => {
|
|
284
284
|
// The asymmetry this closes: an app whose places are leaf domains (no
|
|
285
285
|
// sub-items) used to be searchable by exact label only, while an identical
|
|
286
286
|
// rail expressed as items matched synonyms — a data-model gap, not a ranking one.
|
|
@@ -305,7 +305,7 @@ describe('deriveSearchCatalogue + rankCatalogue', () => {
|
|
|
305
305
|
expect(rankCatalogue(leafRail, 'review')[0]?.id).toBe('changes');
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
-
test('a domain without them still
|
|
308
|
+
test('a domain without them still catalogs cleanly (both stay optional)', () => {
|
|
309
309
|
const bare = deriveSearchCatalogue(
|
|
310
310
|
{ domains: [{ id: 'x', label: 'X', icon: 'a', path: '/x', groups: [] }] },
|
|
311
311
|
allowAll,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The single navigation source of truth — a declarative Information-Architecture
|
|
2
2
|
// tree (Domain → Group → Item) every nav surface derives from: the side nav, hub
|
|
3
|
-
// landing tiles, breadcrumbs, the universal-search
|
|
3
|
+
// landing tiles, breadcrumbs, the universal-search catalog, and the command
|
|
4
4
|
// palette. Authoring a feature into the model is the *only* way to surface it, so
|
|
5
5
|
// the historical "four hand-maintained lists drift out of sync → features get
|
|
6
6
|
// buried" bug is structurally impossible (the coverage test enforces it in CI).
|
|
@@ -135,7 +135,7 @@ export interface NavDomain {
|
|
|
135
135
|
* again the same as on a {@link NavItem}.
|
|
136
136
|
*
|
|
137
137
|
* Both of these exist because a domain IS a destination: `deriveSearchCatalogue`
|
|
138
|
-
* emits one
|
|
138
|
+
* emits one catalog entry per domain, so a rail of leaf-shaped domains (an app
|
|
139
139
|
* whose places have no sub-items) had no way to be found by synonym while an
|
|
140
140
|
* otherwise identical rail of items did. That asymmetry was in the data model,
|
|
141
141
|
* not in the ranker.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure derivations off the {@link NavigationModel}. Every nav surface (side nav,
|
|
2
|
-
// hub tiles, breadcrumbs, search
|
|
2
|
+
// hub tiles, breadcrumbs, search catalog, palette, the coverage test) reads the
|
|
3
3
|
// model exclusively through these — there is no second list to keep in sync. They
|
|
4
4
|
// import nothing from React, so they run identically in the client, in a server
|
|
5
5
|
// test, and in the CI coverage check.
|
|
@@ -38,7 +38,7 @@ export function normalizeNavPath(path: string): string {
|
|
|
38
38
|
return p || '/';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** A flattened, gated
|
|
41
|
+
/** A flattened, gated catalog entry — a leaf plus where it lives (for grouping). */
|
|
42
42
|
export interface SearchCatalogueEntry extends NavItem {
|
|
43
43
|
/** Owning domain id (absent for ungrouped items). */
|
|
44
44
|
domainId?: string;
|
|
@@ -160,7 +160,7 @@ export function deriveBreadcrumbTrail(
|
|
|
160
160
|
* The flat list of searchable destinations — every visible (gated,
|
|
161
161
|
* non-`deepLinkOnly`) domain landing + item, `searchOnly` ones included, each tagged with its domain/group so
|
|
162
162
|
* results can be grouped. Drives HeaderSearch's instant pages layer *and* the
|
|
163
|
-
* command palette; replaces every hand-listed search
|
|
163
|
+
* command palette; replaces every hand-listed search catalog (so a feature is
|
|
164
164
|
* findable the moment it's registered).
|
|
165
165
|
*/
|
|
166
166
|
export function deriveSearchCatalogue(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure matcher + grouper for the instant client-side "pages" search layer. Ranks a
|
|
2
|
-
// derived
|
|
2
|
+
// derived catalog ({@link deriveSearchCatalogue}) against a query and buckets the
|
|
3
3
|
// hits by their owning domain. Kept pure + framework-free so it's unit-testable and
|
|
4
4
|
// shared by HeaderSearch and (task 14) the command palette. The palette upgrades the
|
|
5
5
|
// *ranking* to uFuzzy; this is the dependency-free baseline both can fall back to.
|
|
@@ -49,8 +49,8 @@ function scoreEntry(entry: SearchCatalogueEntry, q: string): number {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Filter + rank a
|
|
53
|
-
* shows nothing until the user types). Stable: equal scores keep
|
|
52
|
+
* Filter + rank a catalog by `query`. Empty/blank query → `[]` (the instant layer
|
|
53
|
+
* shows nothing until the user types). Stable: equal scores keep catalog order.
|
|
54
54
|
*/
|
|
55
55
|
export function rankCatalogue(catalogue: SearchCatalogueEntry[], query: string): CatalogueHit[] {
|
|
56
56
|
const q = norm(query);
|
|
@@ -61,7 +61,7 @@ export function rankCatalogue(catalogue: SearchCatalogueEntry[], query: string):
|
|
|
61
61
|
if (score > 0) scored.push({ ...entry, score });
|
|
62
62
|
}
|
|
63
63
|
// Stable sort by score desc — Array.prototype.sort is stable in modern engines, so
|
|
64
|
-
// equal scores retain
|
|
64
|
+
// equal scores retain catalog (authoring) order.
|
|
65
65
|
return scored.sort((a, b) => b.score - a.score);
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The command-palette ranking layer — uFuzzy over the nav
|
|
1
|
+
// The command-palette ranking layer — uFuzzy over the nav catalog. This is the
|
|
2
2
|
// *upgrade* HeaderSearch's `rankCatalogue` (a substring scorer) cannot match: uFuzzy
|
|
3
3
|
// scores by contiguous-match proximity, so abbreviated/typeahead input ranks the
|
|
4
4
|
// right page first. Pure + synchronous + React-free (uFuzzy is regex-based, no DOM),
|
|
@@ -48,7 +48,7 @@ function haystackFor(item: NavItem): string {
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* The SECOND tier: what a destination holds rather than what it is called — its description,
|
|
51
|
-
* and the group and place it sits under when the entry carries them (
|
|
51
|
+
* and the group and place it sits under when the entry carries them (catalog entries do).
|
|
52
52
|
* Owner, 2026-10-09: *"I may not know where to go but typing something like 'api' brings up the
|
|
53
53
|
* relevant page links quickly"* — a page found only by its exact name is not found by someone
|
|
54
54
|
* who does not know the name.
|
|
@@ -70,7 +70,7 @@ function rankedIndexes(haystack: string[], q: string): number[] {
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Rank `items` against `query`, best match first; non-matches dropped. Generic over the
|
|
73
|
-
* item type (so callers can pass plain {@link NavItem}s *or* richer
|
|
73
|
+
* item type (so callers can pass plain {@link NavItem}s *or* richer catalog entries
|
|
74
74
|
* that carry domain/group tags for grouping — the extra fields ride straight through).
|
|
75
75
|
* An empty/blank query returns `[]` (the palette shows its cold state instead).
|
|
76
76
|
*
|
|
@@ -40,7 +40,7 @@ export interface TokenDef<S extends z.ZodType = z.ZodType> {
|
|
|
40
40
|
subtypeOf?: string;
|
|
41
41
|
/** Canonical payload injected by the V7.4 wire smoke probe. Must satisfy `schema`. */
|
|
42
42
|
sample: z.infer<S>;
|
|
43
|
-
/** True when this token receives third-party traffic — triggers the V3
|
|
43
|
+
/** True when this token receives third-party traffic — triggers the V3 marshaler gates. */
|
|
44
44
|
external?: boolean;
|
|
45
45
|
deprecated?: TokenDeprecation;
|
|
46
46
|
description: string;
|
|
@@ -493,7 +493,7 @@ export const BASE_TOKENS: readonly TokenDef[] = [
|
|
|
493
493
|
id: 'knowledge:records',
|
|
494
494
|
mode: 'value',
|
|
495
495
|
external: true,
|
|
496
|
-
description: 'Imported record rows (CSV/sheet/webhook) — external traffic,
|
|
496
|
+
description: 'Imported record rows (CSV/sheet/webhook) — external traffic, marshaler-gated (02 §7.1).',
|
|
497
497
|
schema: z.object({ rows: z.array(z.record(z.string(), z.unknown())) }),
|
|
498
498
|
sample: { rows: [{ id: '1' }] },
|
|
499
499
|
},
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { jsonList, PRECHECK_MAX_HASHES, PRECHECK_MAX_SIZES, parsePrecheckRequest } from "./contract.js";
|
|
4
|
+
import { NATIVE_HASH_MAX_BYTES, sha256OfBlob } from "./hash.js";
|
|
5
|
+
|
|
6
|
+
const sha = (n: number) => n.toString(16).padStart(64, "0");
|
|
7
|
+
|
|
8
|
+
describe("parsePrecheckRequest", () => {
|
|
9
|
+
test("both lists, deduplicated, hashes lower-cased", () => {
|
|
10
|
+
const parsed = parsePrecheckRequest({ sizes: [3, 3, 0], sha256: [sha(1).toUpperCase(), sha(1)] });
|
|
11
|
+
expect(parsed).toEqual({ ok: true, sizes: [3, 0], sha256: [sha(1)] });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("either list may be absent", () => {
|
|
15
|
+
expect(parsePrecheckRequest({})).toEqual({ ok: true, sizes: [], sha256: [] });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test.each([
|
|
19
|
+
[null],
|
|
20
|
+
[[1, 2]],
|
|
21
|
+
[{ sizes: "3" }],
|
|
22
|
+
[{ sizes: [-1] }],
|
|
23
|
+
[{ sizes: [1.5] }],
|
|
24
|
+
[{ sha256: ["abc"] }],
|
|
25
|
+
[{ sha256: [42] }],
|
|
26
|
+
[{ sizes: Array.from({ length: PRECHECK_MAX_SIZES + 1 }, (_, i) => i) }],
|
|
27
|
+
[{ sha256: Array.from({ length: PRECHECK_MAX_HASHES + 1 }, (_, i) => sha(i)) }],
|
|
28
|
+
])("refuses %j", (body) => {
|
|
29
|
+
expect(parsePrecheckRequest(body).ok).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("jsonList — one bound parameter for a whole batch", () => {
|
|
34
|
+
test("🔴 a full batch of hashes in ONE statement, answered from the index", () => {
|
|
35
|
+
// D1 refuses more than 100 bound parameters; this is the shape that never needs more than one.
|
|
36
|
+
const db = new Database(":memory:");
|
|
37
|
+
db.run("CREATE TABLE items (id INTEGER PRIMARY KEY, owner TEXT, checksum TEXT, size INTEGER)");
|
|
38
|
+
db.run("CREATE INDEX items_owner_checksum ON items(owner, checksum)");
|
|
39
|
+
const insert = db.prepare("INSERT INTO items (owner, checksum, size) VALUES (?, ?, ?)");
|
|
40
|
+
for (let i = 0; i < 5000; i++) insert.run(i % 2 ? "me" : "you", sha(i), i);
|
|
41
|
+
const asked = Array.from({ length: PRECHECK_MAX_HASHES }, (_, i) => sha(i * 3));
|
|
42
|
+
const sql = "SELECT checksum FROM items WHERE owner = ? AND checksum IN (SELECT value FROM json_each(?))";
|
|
43
|
+
const rows = db.prepare(sql).all("me", jsonList(asked)) as { checksum: string }[];
|
|
44
|
+
expect(rows.length).toBe(asked.filter((_, i) => (i * 3) % 2 === 1).length);
|
|
45
|
+
const plan = (db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all("me", jsonList(asked)) as { detail: string }[])
|
|
46
|
+
.map((r) => r.detail)
|
|
47
|
+
.join(" | ");
|
|
48
|
+
expect(plan).toContain("USING COVERING INDEX items_owner_checksum");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("sha256OfBlob", () => {
|
|
53
|
+
test("the native road and the streaming road give the same digest", async () => {
|
|
54
|
+
const bytes = new Uint8Array(5 * 1024 * 1024 + 13).map((_, i) => (i * 31) & 0xff);
|
|
55
|
+
const blob = new Blob([bytes]);
|
|
56
|
+
const native = await sha256OfBlob(blob);
|
|
57
|
+
let last = 0;
|
|
58
|
+
const streamed = await sha256OfBlob(blob, { nativeMaxBytes: 1024, onProgress: (f) => (last = f) });
|
|
59
|
+
expect(streamed).toBe(native);
|
|
60
|
+
expect(last).toBe(1);
|
|
61
|
+
expect(NATIVE_HASH_MAX_BYTES).toBeGreaterThan(64 * 1024 * 1024);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("the empty file", async () => {
|
|
65
|
+
expect(await sha256OfBlob(new Blob([]))).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract of an upload PRE-CHECK — "which of these do you already have?", asked before
|
|
3
|
+
* a single byte of the files is sent (forge task 3579).
|
|
4
|
+
*
|
|
5
|
+
* The owner, 2026-10-22: *"They should find and skip dups efficiently without wasting bandwidth or
|
|
6
|
+
* processing power … They need to know dups based on the hashes rather than a long check such as
|
|
7
|
+
* copying the file first … I will sometimes reimport from huge directories just in case they have a
|
|
8
|
+
* new file that was missed on previous uploads and I want it to be fast."*
|
|
9
|
+
*
|
|
10
|
+
* ── Two questions, one route ───────────────────────────────────────────────────────────────
|
|
11
|
+
* 1. **`sizes`** — which of these byte sizes does the library hold at all? A file whose size
|
|
12
|
+
* matches nothing stored CANNOT be a byte-identical copy, so it is uploaded without being hashed
|
|
13
|
+
* first (binary-server hashes what it receives, as it always has). On a folder of mostly new
|
|
14
|
+
* videos that is nearly every file, and nearly all of the hashing.
|
|
15
|
+
* 2. **`sha256`** — which of these whole-file SHA-256s does the library hold? Asked only for the
|
|
16
|
+
* files whose size DID match, so the proof (the hash) is paid exactly where it can decide
|
|
17
|
+
* something. A hit is never sent.
|
|
18
|
+
*
|
|
19
|
+
* A route answers both in one body, so a client may ask either or both. Every app's library is
|
|
20
|
+
* scoped by the app's own rules (an account, an owner) — the route answers for the CALLER only,
|
|
21
|
+
* which is also what keeps the answer from saying anything about anyone else's files.
|
|
22
|
+
*
|
|
23
|
+
* ── SQL: one bound parameter, any length ───────────────────────────────────────────────────
|
|
24
|
+
* D1 refuses a statement with more than 100 bound parameters, so `IN (?, ?, …)` over a batch of
|
|
25
|
+
* 1,000 hashes cannot be written. Bind the list ONCE as JSON instead —
|
|
26
|
+
* `WHERE checksum IN (SELECT value FROM json_each(?))` with {@link jsonList} — which D1 and
|
|
27
|
+
* `bun:sqlite` both run against the column's index.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The most sizes one pre-check may ask about. A drop of 5,000 files asks once. */
|
|
31
|
+
export const PRECHECK_MAX_SIZES = 5000;
|
|
32
|
+
|
|
33
|
+
/** The most hashes one pre-check may ask about — a client sends them in batches this size or less. */
|
|
34
|
+
export const PRECHECK_MAX_HASHES = 1000;
|
|
35
|
+
|
|
36
|
+
/** What the client sends. Either list may be absent or empty. */
|
|
37
|
+
export interface PrecheckRequest {
|
|
38
|
+
sizes?: number[];
|
|
39
|
+
sha256?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Why a hash is refused: the library holds it, or once held it and its owner deleted it. */
|
|
43
|
+
export type PrecheckReason = "exists" | "deleted";
|
|
44
|
+
|
|
45
|
+
/** One hash the library already answers for. */
|
|
46
|
+
export interface PrecheckHit {
|
|
47
|
+
reason: PrecheckReason;
|
|
48
|
+
/** What the library calls it — shown to the person beside the skipped file. */
|
|
49
|
+
name?: string;
|
|
50
|
+
/** The app's own id for the item, when it has one worth linking to. */
|
|
51
|
+
id?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the route answers. */
|
|
55
|
+
export interface PrecheckResponse {
|
|
56
|
+
/** The asked sizes that the library holds at least one file of. */
|
|
57
|
+
sizes: number[];
|
|
58
|
+
/** The asked hashes the library already answers for; any hash absent here is new. */
|
|
59
|
+
have: Record<string, PrecheckHit>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
63
|
+
|
|
64
|
+
/** Is this a lowercase hex SHA-256 — the only spelling every store in the fleet records? */
|
|
65
|
+
export function isSha256Hex(value: unknown): value is string {
|
|
66
|
+
return typeof value === "string" && SHA256_HEX.test(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ParsedPrecheck = { ok: true; sizes: number[]; sha256: string[] } | { ok: false; error: string };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate a pre-check body — the ONE parser every app's route calls, so the limits and the
|
|
73
|
+
* spelling are the same everywhere. Duplicates collapse; order is not kept.
|
|
74
|
+
*/
|
|
75
|
+
export function parsePrecheckRequest(body: unknown): ParsedPrecheck {
|
|
76
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
77
|
+
return { ok: false, error: "expected a JSON object with `sizes` and/or `sha256`" };
|
|
78
|
+
}
|
|
79
|
+
const { sizes: rawSizes, sha256: rawHashes } = body as Record<string, unknown>;
|
|
80
|
+
if (rawSizes !== undefined && !Array.isArray(rawSizes)) return { ok: false, error: "`sizes` must be an array" };
|
|
81
|
+
if (rawHashes !== undefined && !Array.isArray(rawHashes)) return { ok: false, error: "`sha256` must be an array" };
|
|
82
|
+
const sizes = new Set<number>();
|
|
83
|
+
for (const size of rawSizes ?? []) {
|
|
84
|
+
if (!Number.isSafeInteger(size) || (size as number) < 0) {
|
|
85
|
+
return { ok: false, error: "every size must be a whole number of bytes" };
|
|
86
|
+
}
|
|
87
|
+
sizes.add(size as number);
|
|
88
|
+
}
|
|
89
|
+
const hashes = new Set<string>();
|
|
90
|
+
for (const hash of rawHashes ?? []) {
|
|
91
|
+
const spelled = typeof hash === "string" ? hash.toLowerCase() : hash;
|
|
92
|
+
if (!isSha256Hex(spelled)) return { ok: false, error: "every sha256 must be 64 hex characters" };
|
|
93
|
+
hashes.add(spelled);
|
|
94
|
+
}
|
|
95
|
+
if (sizes.size > PRECHECK_MAX_SIZES) {
|
|
96
|
+
return { ok: false, error: `at most ${PRECHECK_MAX_SIZES} sizes per pre-check` };
|
|
97
|
+
}
|
|
98
|
+
if (hashes.size > PRECHECK_MAX_HASHES) {
|
|
99
|
+
return { ok: false, error: `at most ${PRECHECK_MAX_HASHES} hashes per pre-check` };
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, sizes: [...sizes], sha256: [...hashes] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A list bound as ONE SQL parameter, for `IN (SELECT value FROM json_each(?))`. */
|
|
105
|
+
export function jsonList(values: readonly (string | number)[]): string {
|
|
106
|
+
return JSON.stringify(values);
|
|
107
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Sha256 } from "./sha256.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The largest file hashed in ONE native `crypto.subtle.digest` call — read whole into memory.
|
|
5
|
+
*
|
|
6
|
+
* Native hashing ran 1,960 MiB/s on this Mac against 184 MiB/s for the incremental one, so a
|
|
7
|
+
* file is read whole whenever that is safe. 256 MiB is far under what a phone tab survives, and
|
|
8
|
+
* covers every photo, every song and most clips; past it the incremental hasher streams in
|
|
9
|
+
* {@link STREAM_SLICE_BYTES} slices at roughly the speed an external drive reads anyway.
|
|
10
|
+
*/
|
|
11
|
+
export const NATIVE_HASH_MAX_BYTES = 256 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One read while streaming. Small enough that the incremental hasher returns to the event loop
|
|
15
|
+
* every ~10 ms (a 2 MiB slice at ~190 MiB/s), so a tab hashing an 8 GB disc stays responsive.
|
|
16
|
+
*/
|
|
17
|
+
export const STREAM_SLICE_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
export interface HashOptions {
|
|
20
|
+
/** 0..1 as the bytes are read. */
|
|
21
|
+
onProgress?: (fraction: number) => void;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
/** Override {@link NATIVE_HASH_MAX_BYTES} — the specs force the streaming road with it. */
|
|
24
|
+
nativeMaxBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hex(digest: ArrayBuffer): string {
|
|
28
|
+
const view = new Uint8Array(digest);
|
|
29
|
+
let out = "";
|
|
30
|
+
for (let i = 0; i < view.length; i++) out += view[i].toString(16).padStart(2, "0");
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
35
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("The upload was canceled.", "AbortError");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The whole-file SHA-256 of a Blob (a browser `File`, a `Bun.file`) of any size, as lowercase hex
|
|
40
|
+
* — the same digest binary-server records in `file_index.checksum` for the same bytes.
|
|
41
|
+
*/
|
|
42
|
+
export async function sha256OfBlob(blob: Blob, options: HashOptions = {}): Promise<string> {
|
|
43
|
+
const { onProgress, signal, nativeMaxBytes = NATIVE_HASH_MAX_BYTES } = options;
|
|
44
|
+
throwIfAborted(signal);
|
|
45
|
+
if (blob.size <= nativeMaxBytes) {
|
|
46
|
+
const digest = await crypto.subtle.digest("SHA-256", await blob.arrayBuffer());
|
|
47
|
+
onProgress?.(1);
|
|
48
|
+
return hex(digest);
|
|
49
|
+
}
|
|
50
|
+
const hasher = new Sha256();
|
|
51
|
+
for (let offset = 0; offset < blob.size; offset += STREAM_SLICE_BYTES) {
|
|
52
|
+
throwIfAborted(signal);
|
|
53
|
+
hasher.update(new Uint8Array(await blob.slice(offset, offset + STREAM_SLICE_BYTES).arrayBuffer()));
|
|
54
|
+
onProgress?.(Math.min(1, (offset + STREAM_SLICE_BYTES) / blob.size));
|
|
55
|
+
}
|
|
56
|
+
return hasher.hex();
|
|
57
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-core/upload-precheck` — skip a duplicate upload by its hash BEFORE sending it
|
|
3
|
+
* (forge task 3579). Every app's dropzone, uploader and Mac ingest uses this one module, so the
|
|
4
|
+
* order of the work (sizes → memo → hash → ask) and the route's contract are the same everywhere.
|
|
5
|
+
*
|
|
6
|
+
* - `planUploads` — the client side: decide which files to send.
|
|
7
|
+
* - `parsePrecheckRequest`, `jsonList` — the server side: each app's route parses with the first
|
|
8
|
+
* and binds its lists with the second.
|
|
9
|
+
* - `sha256OfBlob` — the whole-file SHA-256 binary-server records, of a Blob of any size.
|
|
10
|
+
* - `indexedDbHashMemo`, `memoryHashMemo` — the memo that makes a re-import of the same folder
|
|
11
|
+
* read nothing.
|
|
12
|
+
*
|
|
13
|
+
* Pure of any framework; a browser bundle and a Bun script may both import it. Its sibling
|
|
14
|
+
* `cursedbelt-core/duplicates` answers the other question — two files that are LIKELY the same
|
|
15
|
+
* (a re-encode, a copy under another name) — which is never enough to skip an upload on.
|
|
16
|
+
*/
|
|
17
|
+
export {
|
|
18
|
+
isSha256Hex,
|
|
19
|
+
jsonList,
|
|
20
|
+
PRECHECK_MAX_HASHES,
|
|
21
|
+
PRECHECK_MAX_SIZES,
|
|
22
|
+
type ParsedPrecheck,
|
|
23
|
+
type PrecheckHit,
|
|
24
|
+
type PrecheckReason,
|
|
25
|
+
type PrecheckRequest,
|
|
26
|
+
type PrecheckResponse,
|
|
27
|
+
parsePrecheckRequest,
|
|
28
|
+
} from "./contract.js";
|
|
29
|
+
export { type HashOptions, NATIVE_HASH_MAX_BYTES, STREAM_SLICE_BYTES, sha256OfBlob } from "./hash.js";
|
|
30
|
+
export {
|
|
31
|
+
BROWSER_MEMO_MAX_ENTRIES,
|
|
32
|
+
type HashMemo,
|
|
33
|
+
indexedDbHashMemo,
|
|
34
|
+
memoKey,
|
|
35
|
+
memoryHashMemo,
|
|
36
|
+
statIdentity,
|
|
37
|
+
} from "./memo.js";
|
|
38
|
+
export {
|
|
39
|
+
type PlanOptions,
|
|
40
|
+
type PlanResult,
|
|
41
|
+
type PlanStats,
|
|
42
|
+
type PrecheckAsk,
|
|
43
|
+
planUploads,
|
|
44
|
+
type Verdict,
|
|
45
|
+
} from "./plan.js";
|
|
46
|
+
export { Sha256 } from "./sha256.js";
|