cursedbelt-server 4.18.1 → 4.19.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 +61 -0
- package/dist/server/activity/index.d.ts +2 -1
- package/dist/server/activity/index.js +2 -1
- package/dist/server/auth/passwordCost.d.ts +21 -0
- package/dist/server/auth/passwordCost.js +80 -0
- package/dist/server/bench/index.d.ts +1 -0
- package/dist/server/bench/index.js +1 -0
- package/dist/server/bench/tail.d.ts +110 -0
- package/dist/server/bench/tail.js +182 -0
- package/dist/server/d1/index.d.ts +1 -2
- package/dist/server/d1/index.js +9 -9
- package/dist/server/engagement/api.d.ts +71 -0
- package/dist/server/engagement/api.js +84 -0
- package/dist/server/engagement/env.d.ts +18 -0
- package/dist/server/engagement/env.js +52 -0
- package/dist/server/engagement/index.d.ts +55 -0
- package/dist/server/engagement/index.js +55 -0
- package/dist/server/engagement/places.d.ts +22 -0
- package/dist/server/engagement/places.js +63 -0
- package/dist/server/engagement/policy.d.ts +168 -0
- package/dist/server/engagement/policy.js +202 -0
- package/dist/server/engagement/store.d.ts +92 -0
- package/dist/server/engagement/store.js +223 -0
- package/dist/server/engagement/summary.d.ts +102 -0
- package/dist/server/engagement/summary.js +127 -0
- package/dist/server/engagement/types.d.ts +42 -0
- package/dist/server/engagement/types.js +12 -0
- package/dist/server/maps-budget/mapsBudget.d.ts +194 -0
- package/dist/server/maps-budget/mapsBudget.js +193 -0
- package/dist/server/satellite/config.d.ts +173 -0
- package/dist/server/satellite/config.js +259 -0
- package/dist/server/satellite/door.d.ts +112 -0
- package/dist/server/satellite/door.js +149 -0
- package/dist/server/storage/binaryStore.d.ts +18 -0
- package/dist/server/storage/binaryStore.js +32 -1
- package/dist/server/storage/derivatives.d.ts +253 -0
- package/dist/server/storage/derivatives.js +266 -0
- package/dist/server/storage/uploadSession.d.ts +75 -0
- package/dist/server/storage/uploadSession.js +74 -0
- package/dist/subpathReach.d.ts +32 -0
- package/dist/subpathReach.js +58 -0
- package/docs/THE-DEV-DEPENDENCY-CYCLE.md +55 -0
- package/docs/activity.md +43 -0
- package/docs/engagement.md +47 -0
- package/docs/notifications.md +43 -0
- package/docs/retention.md +81 -0
- package/docs/skipped-tests.md +19 -0
- package/package.json +48 -9
- package/src/barrelsReachNoOptionalPeer.spec.ts +1 -47
- package/src/leafSubpathsImportNothing.spec.ts +49 -0
- package/src/readmeInstallTable.spec.ts +48 -0
- package/src/server/activity/index.ts +2 -1
- package/src/server/auth/passwordCost.spec.ts +42 -0
- package/src/server/auth/passwordCost.ts +87 -0
- package/src/server/bench/index.ts +13 -0
- package/src/server/bench/tail.spec.ts +126 -0
- package/src/server/bench/tail.ts +237 -0
- package/src/server/d1/index.ts +9 -9
- package/src/server/engagement/api.ts +119 -0
- package/src/server/engagement/engagement.spec.ts +462 -0
- package/src/server/engagement/env.ts +73 -0
- package/src/server/engagement/index.ts +92 -0
- package/src/server/engagement/places.ts +76 -0
- package/src/server/engagement/policy.ts +250 -0
- package/src/server/engagement/store.ts +272 -0
- package/src/server/engagement/summary.ts +216 -0
- package/src/server/engagement/types.ts +61 -0
- package/src/server/maps-budget/mapsBudget.spec.ts +366 -0
- package/src/server/maps-budget/mapsBudget.ts +304 -0
- package/src/server/satellite/config.ts +389 -0
- package/src/server/satellite/door.ts +169 -0
- package/src/server/satellite/satellite.spec.ts +161 -0
- package/src/server/serveBunOverload.spec.ts +52 -0
- package/src/server/storage/binaryStore.ts +31 -1
- package/src/server/storage/derivatives.spec.ts +125 -0
- package/src/server/storage/derivatives.ts +329 -0
- package/src/server/storage/uploadSession.spec.ts +132 -0
- package/src/server/storage/uploadSession.ts +114 -0
- package/src/subpathReach.ts +61 -0
- package/dist/server/d1/kysely.d.ts +0 -56
- package/dist/server/d1/kysely.js +0 -138
- package/src/server/d1/kysely.spec.ts +0 -145
- package/src/server/d1/kysely.ts +0 -169
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Engagement — product analytics, and which side of the gate a receiver mounts on
|
|
2
|
+
|
|
3
|
+
`cursedbelt-server/engagement` is the server half (recorder, store, policy, summary); the beacon
|
|
4
|
+
is the browser half in the design system's tier. Four apps copied `src/kit/engagement/` and cited
|
|
5
|
+
a feedback-kit standard for its mount-order rule; neither existed here until 2026-09-23 (tasks
|
|
6
|
+
280 and 324). This page is that rule and the privacy promise it serves.
|
|
7
|
+
|
|
8
|
+
## 1. The privacy promise is three structural rules (`src/server/engagement/policy.ts`)
|
|
9
|
+
|
|
10
|
+
1. **A place is an allowlisted id, never a path.** The allowlist is the app's own
|
|
11
|
+
`routes.manifest.json`; anything a client claims that is not on it records as `other`.
|
|
12
|
+
2. **Some apps are excluded by name, in the library** (`ENGAGEMENT_EXCLUDED_APPS` — the vault).
|
|
13
|
+
No environment variable can turn an excluded app back on.
|
|
14
|
+
3. **Counts and clocks only.** One row per (user, place, UTC day); there is no event log, so a
|
|
15
|
+
timeline of a household's private apps cannot be reconstructed from the store.
|
|
16
|
+
|
|
17
|
+
The user key comes ONLY from the app's `resolveUser(c)` — never from the request body — so the
|
|
18
|
+
beacon cannot be used to write somebody else's history.
|
|
19
|
+
|
|
20
|
+
## 2. Mount order — the decision four apps had to agree on
|
|
21
|
+
|
|
22
|
+
| receiver | its credential | mounts |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| the engagement recorder (`POST <mount>/view`) | none of its own — it needs the SESSION's user | **AFTER** the session gate |
|
|
25
|
+
| a sync receiver (a feed another machine pushes into) | its own bearer | **BEFORE** the session gate |
|
|
26
|
+
|
|
27
|
+
The two are mirror images for one reason: **a door mounts on the side of the gate that supplies
|
|
28
|
+
the identity it trusts.** The recorder trusts the session, so the gate must have run and put the
|
|
29
|
+
user where `resolveUser` reads it; mounted before, every view is a 401 and nothing records. A
|
|
30
|
+
sync receiver trusts its bearer and has no session at all; mounted after, the gate refuses the
|
|
31
|
+
machine before its own check can run, and the feed goes silently dark.
|
|
32
|
+
|
|
33
|
+
## 3. What the app keeps
|
|
34
|
+
|
|
35
|
+
Its `routes.manifest.json` and the test that proves it parses. `engagementPlacesFor` returns `[]`
|
|
36
|
+
for anything it cannot read — deliberately silent, so telemetry never takes a boot down — which
|
|
37
|
+
degrades the allowlist to nothing while the board keeps rendering. Only the app can prove its own
|
|
38
|
+
manifest parses (`apps/roms/routes.manifest.test.ts` is the pattern).
|
|
39
|
+
|
|
40
|
+
## 4. Where the data lives
|
|
41
|
+
|
|
42
|
+
`engagement.sqlite` beside the app's own database (`readEngagementEnv` resolves it with
|
|
43
|
+
`cursedbelt-server/satellite-config`'s `resolveAppDataDir`), or nowhere: a test runner, an
|
|
44
|
+
excluded app, `SATELLITE_ENGAGEMENT=0` or an unresolvable data directory each turn recording OFF
|
|
45
|
+
and say why, and the app still boots.
|
|
46
|
+
|
|
47
|
+
Cite this page from an app as `cursedbelt-server/docs/engagement.md`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Notifications — the bell and inbox contract
|
|
2
|
+
|
|
3
|
+
`cursedbelt-server/notifications` (routes, SSE hub, migration) over the engine in
|
|
4
|
+
`cwip/notifications` is the code, and `cursedbelt/react/notifications` is the client. This page
|
|
5
|
+
is the contract a producer in any app is agreeing to. Written 2026-09-23 (task 324); `family`
|
|
6
|
+
cited it four times before it existed.
|
|
7
|
+
|
|
8
|
+
## 1. A notification is a row before it is an event
|
|
9
|
+
|
|
10
|
+
Delivery (SSE) is a cache of who is listening; the poll transport is the backstop and is not
|
|
11
|
+
optional. Nothing is lost to a closed laptop.
|
|
12
|
+
|
|
13
|
+
## 2. Coalesce what RECURS — never coalesce what is individually ACTIONABLE
|
|
14
|
+
|
|
15
|
+
A `groupKey` folds repeats into one row with a count, and it is enforced by the database (a
|
|
16
|
+
partial unique index on `(user_id, group_key) WHERE group_key IS NOT NULL AND done = 0`), not
|
|
17
|
+
by a debounce, so it holds under concurrent producers.
|
|
18
|
+
|
|
19
|
+
The rule for choosing one:
|
|
20
|
+
|
|
21
|
+
- **Give it a `groupKey`** when the twentieth instance tells the reader nothing the first did:
|
|
22
|
+
"a sync failed", "new comments on this thread", "a backup is overdue". One row, a count, one
|
|
23
|
+
thing to go and look at.
|
|
24
|
+
- **Give it NO `groupKey`** when each instance is its own thing to go and look at: two
|
|
25
|
+
relatives shared with the same person are two people to open (`apps/family/src/server/shares.ts`,
|
|
26
|
+
`shareNotificationDraft`), two uploads that need review are two reviews. Coalescing those
|
|
27
|
+
hides work behind a number.
|
|
28
|
+
|
|
29
|
+
When in doubt, do not coalesce: an extra row costs a glance, a swallowed one costs the thing
|
|
30
|
+
the notification was for.
|
|
31
|
+
|
|
32
|
+
## 3. The routes address the session's user and nothing else
|
|
33
|
+
|
|
34
|
+
There is no route that notifies somebody else. Producing is server-side code calling
|
|
35
|
+
`center.notify*`; the mounted routes list, read and mark the CURRENT user's rows.
|
|
36
|
+
|
|
37
|
+
## 4. A notification is written once and read later — so it names nothing the reader may not see
|
|
38
|
+
|
|
39
|
+
It is not filtered by the reader's scope at read time and cannot be. Put what is safe for the
|
|
40
|
+
recipient in the title and body, and let the LINK carry them to the thing, where every normal
|
|
41
|
+
read filter applies (`family`'s share notification does not name the relative for this reason).
|
|
42
|
+
|
|
43
|
+
Cite this page from an app as `cursedbelt-server/docs/notifications.md`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Retention — the fleet standard
|
|
2
|
+
|
|
3
|
+
`cursedbelt-server/retention` is the code; this page is the contract every app that deletes
|
|
4
|
+
old rows is agreeing to. Four apps cited a `docs/retention.md` for two generations and it did
|
|
5
|
+
not exist here until 2026-09-23 (task 324). Where this page and the code disagree, the code
|
|
6
|
+
wins and this page is the bug.
|
|
7
|
+
|
|
8
|
+
## 1. Retention is OPT-IN, and by CATEGORY — never by table
|
|
9
|
+
|
|
10
|
+
A resource can be swept only if it declares a `RetentionCategory`
|
|
11
|
+
(`src/server/retention/policy.ts`, `RETENTION_CATEGORIES`). Every category describes one kind
|
|
12
|
+
of data: **a record that exists so a human can find out what happened, and whose loss costs
|
|
13
|
+
nothing but hindsight** — `audit-log`, `event-log`, `run-log`, `transcript`, `metric`, `cache`,
|
|
14
|
+
`replica-mirror`, `queue-result`, `scratch-artifact`, and the one exception, `archived-history`.
|
|
15
|
+
|
|
16
|
+
Why by category rather than by table:
|
|
17
|
+
|
|
18
|
+
- **Long-term data is protected by ABSENCE.** There is no category that can describe a note, a
|
|
19
|
+
setting, a password, a save state, a playlist or a vault entry. "Should this table have a
|
|
20
|
+
policy?" is answered by whether an honest category exists, not by somebody's judgement on
|
|
21
|
+
the day — and a new table is safe by default, because it is not swept until it is declared.
|
|
22
|
+
- **The number is the category's, not the app's.** Every deleting category defaults to the
|
|
23
|
+
owner's two-week standard or shorter. A deleting category that wanted a longer default would
|
|
24
|
+
be describing data that belongs to the user — the case this vocabulary refuses to express.
|
|
25
|
+
- **The owner, not the app, moves the number.** An app publishes what it owns
|
|
26
|
+
(`publishRetention`); overrides are read from one file the station edits
|
|
27
|
+
(`writeRetentionOverride`); both the station's view and every sweep resolve the policy with
|
|
28
|
+
the same function (`resolveRetentionPolicy`), so the screen can never promise a date the
|
|
29
|
+
sweeper does not keep.
|
|
30
|
+
|
|
31
|
+
## 2. `archived-history` is the only category allowed a longer window
|
|
32
|
+
|
|
33
|
+
It keeps a bounded window on the instance and writes everything older to a verified,
|
|
34
|
+
append-only archive BEFORE a row is deleted. `archivesBeforeDelete` is a property of the
|
|
35
|
+
category: the plain-delete targets in `src/server/retention/target.ts` refuse a declaration
|
|
36
|
+
that carries it, and the archiving target refuses to delete a row it cannot prove it archived.
|
|
37
|
+
|
|
38
|
+
## 3. Two floors that hold even when a policy is wrong
|
|
39
|
+
|
|
40
|
+
- `floorItems` — no policy, however mis-set, can empty a resource.
|
|
41
|
+
- `RETENTION_MIN_DAYS` / `RETENTION_MAX_DAYS` bound every override.
|
|
42
|
+
|
|
43
|
+
## 4. Declaring nothing is a legitimate answer
|
|
44
|
+
|
|
45
|
+
An audit of destructive actions that writes a handful of rows a month is right to declare no
|
|
46
|
+
category and say so in a comment — a 14-day cutoff must not reach an audit trail of production
|
|
47
|
+
deletions. Opting in is a decision about the data, not a checkbox every table ticks.
|
|
48
|
+
|
|
49
|
+
## 5. Anything that grows without bound and is NOT sweepable is a design bug
|
|
50
|
+
|
|
51
|
+
If a resource grows for ever and no category fits it, the answer is not a longer retention —
|
|
52
|
+
it is that the thing should not be accumulating.
|
|
53
|
+
|
|
54
|
+
## 6. A threshold is tuned against the CORPUS, never against the story
|
|
55
|
+
|
|
56
|
+
`apps/music/scripts/backup.ts:172-190` records the failure: a refusal keyed on "a catalogue
|
|
57
|
+
with tracks and no organizing work must be a stage database" was written from the story, and
|
|
58
|
+
the live corpus at cutover (5,391 tracks, 0 playlists, 0 tags) would have tripped it nightly on
|
|
59
|
+
a healthy library. Before a retention or refusal number ships, measure it against what the app
|
|
60
|
+
actually holds, and write the measurement beside the number.
|
|
61
|
+
|
|
62
|
+
## Adopting it
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createRetentionSweeper, createSqliteTableTarget } from "cursedbelt-server/retention";
|
|
66
|
+
|
|
67
|
+
createRetentionSweeper({
|
|
68
|
+
app: "myapp",
|
|
69
|
+
targets: [
|
|
70
|
+
createSqliteTableTarget({
|
|
71
|
+
db, table: "events", timeColumn: "at",
|
|
72
|
+
declaration: { app: "myapp", id: "events", label: "Event log", category: "event-log",
|
|
73
|
+
unit: "rows", location: "myapp.sqlite · events",
|
|
74
|
+
consequence: "Who did what, for hindsight. Losing it costs no data." },
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
}).start();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Cite this page from an app as `cursedbelt-server/docs/retention.md` — with the package prefix,
|
|
81
|
+
because a bare `docs/retention.md` is a promise about the citing app's own `docs/`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Skipped tests — what each one is waiting for
|
|
2
|
+
|
|
3
|
+
`src/skippedTestsAreAnswered.spec.ts` keeps this table honest in both directions:
|
|
4
|
+
every file with a skip site must have a row here, and a row whose file has stopped
|
|
5
|
+
carrying one goes red until it is removed. Inherited from `cursedbelt` at the
|
|
6
|
+
2026-09-15 split (task 148) — rows below are the ones whose files moved here.
|
|
7
|
+
|
|
8
|
+
| file | waiting for |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `src/server/search/pgSearchProvider.pg.test.ts` | 24 | 22 | 2026-09-13 |
|
|
11
|
+
| `src/server/storage/transcode.integration.spec.ts` | 7 | 5 | 2026-09-13 |
|
|
12
|
+
| `src/server/storage/binaryServerProcessing.integration.spec.ts` | 4 | 2 | 2026-09-13 |
|
|
13
|
+
| `src/server/storage/chunked.integration.spec.ts` | 3 | 1 | 2026-09-13 |
|
|
14
|
+
| `src/server/media-bun/image.spec.ts` | the optional `sharp` peer | no |
|
|
15
|
+
| `src/server/media-bun/video.spec.ts` | `ffmpeg` + `ffprobe` on `PATH` | no |
|
|
16
|
+
| `src/server/storage/imageThumbnail.spec.ts` | `ffmpeg` on `PATH` | no |
|
|
17
|
+
| `src/server/storage/videoProcessor.posterOnly.spec.ts` | `ffmpeg` on `PATH` | no |
|
|
18
|
+
| `src/server/storage/files/adapter.photo.display.spec.ts` | `ffmpeg` on `PATH` | no |
|
|
19
|
+
| `src/server/storage/fileUploadClient-chunked.integration.spec.ts` | a real `binary-server` checkout — set `BINARY_SERVER_DIR`. No default by design: a published package must not go looking in one machine's directory layout, and `binary-server` has not been re-homed into this generation, so it skips on every host today. Moved here from `cursedbelt` at the 2026-09-15 split, with the storage internals it drives. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.19.1",
|
|
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.",
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
-
"src"
|
|
31
|
+
"src",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"!src/server/typetests"
|
|
32
35
|
],
|
|
33
36
|
"exports": {
|
|
34
37
|
".": {
|
|
@@ -67,6 +70,12 @@
|
|
|
67
70
|
"source": "./src/server/storage/binaryStore.ts",
|
|
68
71
|
"import": "./dist/server/storage/binaryStore.js"
|
|
69
72
|
},
|
|
73
|
+
"./binary-store/upload-session": {
|
|
74
|
+
"types": "./dist/server/storage/uploadSession.d.ts",
|
|
75
|
+
"bun": "./src/server/storage/uploadSession.ts",
|
|
76
|
+
"source": "./src/server/storage/uploadSession.ts",
|
|
77
|
+
"import": "./dist/server/storage/uploadSession.js"
|
|
78
|
+
},
|
|
70
79
|
"./binary-store/testing": {
|
|
71
80
|
"types": "./dist/server/storage/binaryStoreFake.d.ts",
|
|
72
81
|
"bun": "./src/server/storage/binaryStoreFake.ts",
|
|
@@ -91,12 +100,6 @@
|
|
|
91
100
|
"source": "./src/server/d1/localBackup.ts",
|
|
92
101
|
"import": "./dist/server/_bunOnly.js"
|
|
93
102
|
},
|
|
94
|
-
"./d1/kysely": {
|
|
95
|
-
"types": "./dist/server/d1/kysely.d.ts",
|
|
96
|
-
"bun": "./src/server/d1/kysely.ts",
|
|
97
|
-
"source": "./src/server/d1/kysely.ts",
|
|
98
|
-
"import": "./dist/server/d1/kysely.js"
|
|
99
|
-
},
|
|
100
103
|
"./d1/testing": {
|
|
101
104
|
"types": "./dist/server/d1/fakeD1.d.ts",
|
|
102
105
|
"bun": "./src/server/d1/fakeD1.ts",
|
|
@@ -109,6 +112,12 @@
|
|
|
109
112
|
"source": "./src/server/dropzone/index.ts",
|
|
110
113
|
"import": "./dist/server/dropzone/index.js"
|
|
111
114
|
},
|
|
115
|
+
"./engagement": {
|
|
116
|
+
"types": "./dist/server/engagement/index.d.ts",
|
|
117
|
+
"bun": "./src/server/engagement/index.ts",
|
|
118
|
+
"source": "./src/server/engagement/index.ts",
|
|
119
|
+
"import": "./dist/server/_bunOnly.js"
|
|
120
|
+
},
|
|
112
121
|
"./errors": {
|
|
113
122
|
"types": "./dist/server/errors.d.ts",
|
|
114
123
|
"bun": "./src/server/errors.ts",
|
|
@@ -157,6 +166,12 @@
|
|
|
157
166
|
"source": "./src/server/auth/loginThrottle.ts",
|
|
158
167
|
"import": "./dist/server/auth/loginThrottle.js"
|
|
159
168
|
},
|
|
169
|
+
"./maps-budget": {
|
|
170
|
+
"types": "./dist/server/maps-budget/mapsBudget.d.ts",
|
|
171
|
+
"bun": "./src/server/maps-budget/mapsBudget.ts",
|
|
172
|
+
"source": "./src/server/maps-budget/mapsBudget.ts",
|
|
173
|
+
"import": "./dist/server/maps-budget/mapsBudget.js"
|
|
174
|
+
},
|
|
160
175
|
"./master-lock": {
|
|
161
176
|
"types": "./dist/server/master-lock/index.d.ts",
|
|
162
177
|
"bun": "./src/server/master-lock/index.ts",
|
|
@@ -187,6 +202,12 @@
|
|
|
187
202
|
"source": "./src/server/net/outboundGuard.ts",
|
|
188
203
|
"import": "./dist/server/net/outboundGuard.js"
|
|
189
204
|
},
|
|
205
|
+
"./password": {
|
|
206
|
+
"types": "./dist/server/auth/passwordCost.d.ts",
|
|
207
|
+
"bun": "./src/server/auth/passwordCost.ts",
|
|
208
|
+
"source": "./src/server/auth/passwordCost.ts",
|
|
209
|
+
"import": "./dist/server/_bunOnly.js"
|
|
210
|
+
},
|
|
190
211
|
"./notifications": {
|
|
191
212
|
"types": "./dist/server/notifications/index.d.ts",
|
|
192
213
|
"bun": "./src/server/notifications/index.ts",
|
|
@@ -205,6 +226,18 @@
|
|
|
205
226
|
"source": "./src/server/retention/index.ts",
|
|
206
227
|
"import": "./dist/server/retention/index.js"
|
|
207
228
|
},
|
|
229
|
+
"./satellite-config": {
|
|
230
|
+
"types": "./dist/server/satellite/config.d.ts",
|
|
231
|
+
"bun": "./src/server/satellite/config.ts",
|
|
232
|
+
"source": "./src/server/satellite/config.ts",
|
|
233
|
+
"import": "./dist/server/_bunOnly.js"
|
|
234
|
+
},
|
|
235
|
+
"./satellite-door": {
|
|
236
|
+
"types": "./dist/server/satellite/door.d.ts",
|
|
237
|
+
"bun": "./src/server/satellite/door.ts",
|
|
238
|
+
"source": "./src/server/satellite/door.ts",
|
|
239
|
+
"import": "./dist/server/satellite/door.js"
|
|
240
|
+
},
|
|
208
241
|
"./search": {
|
|
209
242
|
"types": "./dist/server/search/index.d.ts",
|
|
210
243
|
"bun": "./src/server/search/index.ts",
|
|
@@ -229,6 +262,12 @@
|
|
|
229
262
|
"source": "./src/server/storage/index.ts",
|
|
230
263
|
"import": "./dist/server/storage/index.js"
|
|
231
264
|
},
|
|
265
|
+
"./storage/derivatives": {
|
|
266
|
+
"types": "./dist/server/storage/derivatives.d.ts",
|
|
267
|
+
"bun": "./src/server/storage/derivatives.ts",
|
|
268
|
+
"source": "./src/server/storage/derivatives.ts",
|
|
269
|
+
"import": "./dist/server/storage/derivatives.js"
|
|
270
|
+
},
|
|
232
271
|
"./storage/types": {
|
|
233
272
|
"types": "./dist/server/storage/types.d.ts",
|
|
234
273
|
"bun": "./src/server/storage/types.ts",
|
|
@@ -250,6 +289,7 @@
|
|
|
250
289
|
},
|
|
251
290
|
"dependencies": {
|
|
252
291
|
"cursedbelt-core": "^2.1.1",
|
|
292
|
+
"cursedops": "^0.5.2",
|
|
253
293
|
"cwip": "^4.6.0",
|
|
254
294
|
"jose": "^6.2.3"
|
|
255
295
|
},
|
|
@@ -290,7 +330,6 @@
|
|
|
290
330
|
"@types/bun": "^1.3.14",
|
|
291
331
|
"@types/node": "^24",
|
|
292
332
|
"cursedbelt": "^4.5.0",
|
|
293
|
-
"cursedops": "^0.2.7",
|
|
294
333
|
"hono": "4.12.28",
|
|
295
334
|
"kysely": "^0.28.17",
|
|
296
335
|
"kysely-bun-sqlite": "^0.4.0",
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'bun:test';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import pkg from '../package.json';
|
|
5
|
+
import { MAY_DRAG, MAY_NEED_BUN } from './subpathReach.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* A public subpath must not STATICALLY drag anything its consumers cannot have — an
|
|
@@ -99,53 +100,6 @@ import pkg from '../package.json';
|
|
|
99
100
|
|
|
100
101
|
const REPO = fileURLToPath(new URL('..', import.meta.url));
|
|
101
102
|
|
|
102
|
-
/**
|
|
103
|
-
* The subpaths whose whole PURPOSE is the optional peer they pull, mapped to what they are
|
|
104
|
-
* allowed to pull and why. An entry here is a promise that the peer is the point of the
|
|
105
|
-
* subpath — not a place to park a new drag.
|
|
106
|
-
*
|
|
107
|
-
* 🔴 Measured 2026-09-18: these three are the ONLY subpaths in the map that reach an
|
|
108
|
-
* optional peer. Everything else is clean, which is what makes this list an allowlist
|
|
109
|
-
* rather than a baseline — it can only shrink.
|
|
110
|
-
*/
|
|
111
|
-
const MAY_DRAG: Record<string, readonly string[]> = {
|
|
112
|
-
// The whole-package barrel. It exists to re-export everything, so it necessarily reaches
|
|
113
|
-
// what the specific subpaths reach. An app importing `cursedbelt-server` whole is asking
|
|
114
|
-
// for that; an app importing `cursedbelt-server/d1` is not, and that is the distinction
|
|
115
|
-
// this spec protects.
|
|
116
|
-
'.': ['kysely', 'kysely-bun-sqlite', 'otplib', 'plainjob'],
|
|
117
|
-
// `createD1Kysely` IS the kysely adapter. Split out of `./d1` on 2026-09-18 precisely so
|
|
118
|
-
// the seam itself stops paying for it — see the header.
|
|
119
|
-
'./d1/kysely': ['kysely'],
|
|
120
|
-
// The job queue is plainjob. Nothing else here is.
|
|
121
|
-
'./jobs': ['plainjob'],
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The subpaths whose whole PURPOSE is a bun-specific runtime, mapped to the builtins they
|
|
126
|
-
* are allowed to reach. Everything NOT listed here must be importable on `workerd`, which
|
|
127
|
-
* is the property the D1 seam exists to deliver.
|
|
128
|
-
*
|
|
129
|
-
* 🔴 Measured 2026-09-18 after the `./d1/backup-local` split: these five are the ONLY
|
|
130
|
-
* subpaths in the map that reach a bun builtin at all. Like {@link MAY_DRAG} it is an
|
|
131
|
-
* allowlist, not a baseline — it can only shrink, and an entry that stops being true is a
|
|
132
|
-
* failure rather than a tidy-up.
|
|
133
|
-
*/
|
|
134
|
-
const MAY_NEED_BUN: Record<string, readonly string[]> = {
|
|
135
|
-
// The whole-package barrel — it re-exports everything, including the Mac-hosted halves.
|
|
136
|
-
// An app importing `cursedbelt-server` whole is a Bun server by construction; an app
|
|
137
|
-
// importing `cursedbelt-server/d1` is not, and that is the distinction this protects.
|
|
138
|
-
'.': ['bun:sqlite'],
|
|
139
|
-
// The local backup IS `VACUUM INTO` against a live `bun:sqlite` handle. Split out of
|
|
140
|
-
// `./d1` on 2026-09-18 precisely so the seam itself stops paying for it — see the header.
|
|
141
|
-
'./d1/backup-local': ['bun:sqlite'],
|
|
142
|
-
// The guard's revocation store is a `bun:sqlite` table, and `./guard` mounts it.
|
|
143
|
-
'./guard': ['bun:sqlite'],
|
|
144
|
-
'./guard/revocations': ['bun:sqlite'],
|
|
145
|
-
// `./sqlite` is the bun:sqlite tier. Naming it is the point of the subpath.
|
|
146
|
-
'./sqlite': ['bun:sqlite'],
|
|
147
|
-
};
|
|
148
|
-
|
|
149
103
|
const OPTIONAL_PEERS = new Set(
|
|
150
104
|
Object.entries(
|
|
151
105
|
(pkg as { peerDependenciesMeta?: Record<string, { optional?: boolean }> }).peerDependenciesMeta ?? {},
|
|
@@ -161,6 +161,50 @@ const LEAVES = [
|
|
|
161
161
|
*/
|
|
162
162
|
evaluates: 'createGoogleTokenMinter',
|
|
163
163
|
},
|
|
164
|
+
{
|
|
165
|
+
subpath: './maps-budget',
|
|
166
|
+
/**
|
|
167
|
+
* The Google Maps spend ceiling (4.19.0, task 345) — copied into `family` and `station`
|
|
168
|
+
* under two different file names. The store is injected, so the module needs nothing.
|
|
169
|
+
*/
|
|
170
|
+
evaluates: 'createMapsBudget',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
subpath: './binary-store/upload-session',
|
|
174
|
+
/**
|
|
175
|
+
* The direct-upload mint (4.19.0, task 343). Its one import is `import type` of the
|
|
176
|
+
* `BinaryStore` interface, which is erased — so it keeps `./binary-store`'s promise.
|
|
177
|
+
*/
|
|
178
|
+
evaluates: 'createUploadSession',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
subpath: './storage/derivatives',
|
|
182
|
+
/**
|
|
183
|
+
* How an app READS binary-server's derivatives (4.19.0, task 344). Collections serves it
|
|
184
|
+
* from a Worker, so it may reach nothing at all — except the key contract it re-exports,
|
|
185
|
+
* which is itself import-free and is copied beside it rather than excused.
|
|
186
|
+
*/
|
|
187
|
+
evaluates: 'resolveVideoPlayback',
|
|
188
|
+
siblings: ['src/server/storage/videoDerivativeKeys.ts'],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
subpath: './satellite-door',
|
|
192
|
+
/**
|
|
193
|
+
* The pure half of the satellite config contract (4.19.0, task 281) — what a Worker's door
|
|
194
|
+
* imports INSTEAD of the Node half. `music`'s Worker dragged `node:fs` and the whole data-dir
|
|
195
|
+
* chain into its bundle to reach two of these functions; this leaf is why it no longer must.
|
|
196
|
+
*/
|
|
197
|
+
evaluates: 'allowedEmailsFor',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
subpath: './password',
|
|
201
|
+
/**
|
|
202
|
+
* The argon2id cost decision (4.19.0, task 321). It shares its test-runtime predicate with
|
|
203
|
+
* `./satellite-door` rather than copying it, and that sibling is itself import-free.
|
|
204
|
+
*/
|
|
205
|
+
evaluates: 'argon2Cost',
|
|
206
|
+
siblings: ['src/server/satellite/door.ts'],
|
|
207
|
+
},
|
|
164
208
|
] as const;
|
|
165
209
|
|
|
166
210
|
/**
|
|
@@ -269,6 +313,11 @@ const run = async (): Promise<Record<string, Outcome>> => {
|
|
|
269
313
|
// copy made through it can land in a mock the spawned child cannot see. Same trap as
|
|
270
314
|
// publishShape.spec.ts.
|
|
271
315
|
await Bun.write(`${pkgDir}/${source}`, Bun.file(`${REPO}/${source}`));
|
|
316
|
+
// A leaf that re-exports an import-free sibling ships with exactly that sibling and no
|
|
317
|
+
// other — so the sibling is held to the same promise rather than excused from it.
|
|
318
|
+
for (const sibling of 'siblings' in leaf ? leaf.siblings : []) {
|
|
319
|
+
await Bun.write(`${pkgDir}/${sibling}`, Bun.file(`${REPO}/${sibling}`));
|
|
320
|
+
}
|
|
272
321
|
targets.push({ name: leaf.subpath, specifier: `${pkg.name}/${leaf.subpath.slice(2)}` });
|
|
273
322
|
}
|
|
274
323
|
await Bun.write(`${pkgDir}/${CONTROL}`, Bun.file(`${REPO}/${CONTROL}`));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* README.md's "What you install" table is what an importing app reads (task 104) — so it is held
|
|
3
|
+
* to the same data the bundler check proves (`subpathReach.ts`), row for row, in both directions.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'bun:test';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { MAY_DRAG, MAY_NEED_BUN } from './subpathReach.js';
|
|
9
|
+
|
|
10
|
+
const README = readFileSync(fileURLToPath(new URL('../README.md', import.meta.url)), 'utf8');
|
|
11
|
+
|
|
12
|
+
/** `| \`./x\` | \`a\`, \`b\` | — |` → { './x': { peers: [a, b], bun: [] } } */
|
|
13
|
+
function tableRows(): Map<string, { peers: string[]; bun: string[] }> {
|
|
14
|
+
const section = README.split('## What you install')[1]?.split('\n## ')[0] ?? '';
|
|
15
|
+
const rows = new Map<string, { peers: string[]; bun: string[] }>();
|
|
16
|
+
for (const line of section.split('\n')) {
|
|
17
|
+
const cells = line.split('|').map((c) => c.trim());
|
|
18
|
+
const subpath = /^`(\.[^`]*)`$/.exec(cells[1] ?? '')?.[1];
|
|
19
|
+
if (!subpath) continue;
|
|
20
|
+
const named = (cell: string | undefined) => [...(cell ?? '').matchAll(/`([^`]+)`/g)].map((m) => m[1] as string).sort();
|
|
21
|
+
rows.set(subpath, { peers: named(cells[2]), bun: named(cells[3]) });
|
|
22
|
+
}
|
|
23
|
+
return rows;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('README install table', () => {
|
|
27
|
+
const rows = tableRows();
|
|
28
|
+
const expected = new Set([...Object.keys(MAY_DRAG), ...Object.keys(MAY_NEED_BUN)]);
|
|
29
|
+
|
|
30
|
+
it('has a row for every subpath that reaches an optional peer or a bun builtin — and no other', () => {
|
|
31
|
+
expect(rows.size).toBeGreaterThan(0);
|
|
32
|
+
expect([...rows.keys()].sort()).toEqual([...expected].sort());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('names exactly what each subpath reaches', () => {
|
|
36
|
+
for (const subpath of expected) {
|
|
37
|
+
expect({ subpath, ...rows.get(subpath) }).toEqual({
|
|
38
|
+
subpath,
|
|
39
|
+
peers: [...(MAY_DRAG[subpath] ?? [])].sort(),
|
|
40
|
+
bun: [...(MAY_NEED_BUN[subpath] ?? [])].sort(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('warns that the root export is a barrel', () => {
|
|
46
|
+
expect(README).toContain('The root export is a BARREL');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ── Retention ───────────────────────────────────────────────────────────────
|
|
12
12
|
* This package never deletes an event. An app whose stream is high-volume opts
|
|
13
|
-
* into the fleet standard (`cursedbelt-server/retention
|
|
13
|
+
* into the fleet standard (`cursedbelt-server/retention`; the contract is
|
|
14
|
+
* `docs/retention.md` in this package, and this stream's own is `docs/activity.md`) by
|
|
14
15
|
* declaring the table, which puts the number under the owner's control in the
|
|
15
16
|
* station rather than in a constant here:
|
|
16
17
|
*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The KDF cost decision — and the one direction it may never move by accident.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 The failure worth a test is a PRODUCTION process paying test cost: a weakened argon2id on
|
|
5
|
+
* the owner's real password hash, silently. So the production path is asserted against an env
|
|
6
|
+
* with every marker absent, not merely "whatever this runner has".
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'bun:test';
|
|
9
|
+
import { TEST_RUNTIME_ENV_KEYS } from '../satellite/door.js';
|
|
10
|
+
import { argon2Cost, hashSecret, PRODUCTION_ARGON2, TEST_ARGON2, verifySecret } from './passwordCost.js';
|
|
11
|
+
|
|
12
|
+
describe('argon2Cost', () => {
|
|
13
|
+
it('🔴 a process with NO test marker pays production cost', () => {
|
|
14
|
+
expect(argon2Cost({ NODE_ENV: 'production' })).toBe(PRODUCTION_ARGON2);
|
|
15
|
+
expect(argon2Cost({})).toBe(PRODUCTION_ARGON2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('each marker the data-dir guard trusts lowers the cost — and nothing else does', () => {
|
|
19
|
+
expect(argon2Cost({ NODE_ENV: 'test' })).toBe(TEST_ARGON2);
|
|
20
|
+
expect(argon2Cost({ BUN_TEST: '1' })).toBe(TEST_ARGON2);
|
|
21
|
+
expect(argon2Cost({ SATELLITE_TEST_RUNTIME: '1' })).toBe(TEST_ARGON2);
|
|
22
|
+
expect(argon2Cost({ NODE_ENV: 'development' })).toBe(PRODUCTION_ARGON2);
|
|
23
|
+
expect(TEST_RUNTIME_ENV_KEYS).toEqual(['NODE_ENV', 'BUN_TEST', 'SATELLITE_TEST_RUNTIME']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('never changes the ALGORITHM, so a hash made under either cost verifies under either', async () => {
|
|
27
|
+
expect(TEST_ARGON2.algorithm).toBe('argon2id');
|
|
28
|
+
expect(PRODUCTION_ARGON2.algorithm).toBe('argon2id');
|
|
29
|
+
const cheap = await Bun.password.hash('correct horse', TEST_ARGON2);
|
|
30
|
+
expect(await verifySecret('correct horse', cheap)).toBe(true);
|
|
31
|
+
expect(await verifySecret('wrong horse', cheap)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('hashSecret / verifySecret', () => {
|
|
36
|
+
it('round-trips, and a malformed stored hash is `false` — never a throw', async () => {
|
|
37
|
+
const hash = await hashSecret('s3cret-enough');
|
|
38
|
+
expect(hash.startsWith('$argon2id$')).toBe(true);
|
|
39
|
+
expect(await verifySecret('s3cret-enough', hash)).toBe(true);
|
|
40
|
+
expect(await verifySecret('s3cret-enough', 'not-a-phc-string')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW EXPENSIVE ARGON2ID SHOULD BE IN THIS PROCESS — production's cost, unless a test
|
|
3
|
+
* runner is in charge.
|
|
4
|
+
*
|
|
5
|
+
* ## The measurement (2026-09-09)
|
|
6
|
+
*
|
|
7
|
+
* Argon2id is slow ON PURPOSE: one hash costs ~66 ms on this Mac, and that slowness IS
|
|
8
|
+
* the security property. It is also the wrong price to pay six hundred times in a unit
|
|
9
|
+
* test whose assertion is *"a wrong password is refused"* — that asserts the comparison,
|
|
10
|
+
* never the cost of the KDF.
|
|
11
|
+
*
|
|
12
|
+
* `apps/auth`'s own suite, measured both ways with nothing else changed:
|
|
13
|
+
*
|
|
14
|
+
* production cost 52.85 s
|
|
15
|
+
* test cost 7.32 s
|
|
16
|
+
*
|
|
17
|
+
* Eighty-six per cent of what was, that morning, the largest single workspace in
|
|
18
|
+
* `verify:scoped` — paid by every agent on every branch touching auth or any shared
|
|
19
|
+
* package, and again on every union gate.
|
|
20
|
+
*
|
|
21
|
+
* ## Why this is safe to key on the test runtime, stated plainly
|
|
22
|
+
*
|
|
23
|
+
* A weakened KDF is the most dangerous thing this module could do, so the argument has
|
|
24
|
+
* to be better than "the flag is only set in tests".
|
|
25
|
+
*
|
|
26
|
+
* `TEST_RUNTIME_ENV_KEYS` is the fleet's existing answer to *"is a test runner in
|
|
27
|
+
* charge"*, and `app-data` **already refuses to open the owner's real database** when it
|
|
28
|
+
* is true. A process carrying one of these markers therefore has no production data to
|
|
29
|
+
* write a weak hash into — it is the same claim, load-bearing in the same direction,
|
|
30
|
+
* decided in one place. If that guarantee ever weakens, it weakens for the database
|
|
31
|
+
* first, which is the louder failure.
|
|
32
|
+
*
|
|
33
|
+
* The ALGORITHM never changes. Only `memoryCost`/`timeCost` move, argon2 encodes its own
|
|
34
|
+
* parameters into the hash, and verification is therefore identical — so a stored hash
|
|
35
|
+
* made under either cost verifies under either cost.
|
|
36
|
+
*
|
|
37
|
+
* ## `cursedbelt-server/password`, since 4.19.0 (task 321)
|
|
38
|
+
|
|
39
|
+
It was `src/kit/passwordCost.ts` in `auth`, `collections`, `patterns` and `vault` — four copies of
|
|
40
|
+
a security-relevant cost decision, one of them (`vault`) already carrying a `verifySecret` the
|
|
41
|
+
others lacked. The test-runtime rule it keys on is `./satellite-door`'s `isTestRuntime`, the same
|
|
42
|
+
predicate that refuses a test the owner's real database, so the two can never disagree about
|
|
43
|
+
whether a runner is in charge. It is not in `cursedauth` because that package has no
|
|
44
|
+
dependencies and this decision must share its predicate, not copy it.
|
|
45
|
+
|
|
46
|
+
## Why it lives in one place and not in each app
|
|
47
|
+
*
|
|
48
|
+
* Five call sites hashed at production cost when this was written — `apps/auth`,
|
|
49
|
+
* `apps/collections` (×2), `apps/patterns`, `apps/vault` — and a per-app copy of a
|
|
50
|
+
* security-relevant cost decision is how four of them end up agreeing and the fifth does
|
|
51
|
+
* not. One module, one test, one place to read.
|
|
52
|
+
*/
|
|
53
|
+
import { isTestRuntime } from "../satellite/door.js";
|
|
54
|
+
|
|
55
|
+
/** Production: the runtime's own argon2id defaults, with nothing pinned by hand. */
|
|
56
|
+
export const PRODUCTION_ARGON2: Bun.Password.Argon2Algorithm = { algorithm: "argon2id" };
|
|
57
|
+
|
|
58
|
+
/** The floor argon2id accepts — free, and still argon2id. */
|
|
59
|
+
export const TEST_ARGON2: Bun.Password.Argon2Algorithm = {
|
|
60
|
+
algorithm: "argon2id",
|
|
61
|
+
memoryCost: 8,
|
|
62
|
+
timeCost: 1,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Which cost this process should pay. Takes `env` explicitly so the failure path is
|
|
67
|
+
* drivable: a check that can only be run under the runner it is deciding about is a
|
|
68
|
+
* check nobody can prove.
|
|
69
|
+
*/
|
|
70
|
+
export function argon2Cost(env: NodeJS.ProcessEnv = process.env): Bun.Password.Argon2Algorithm {
|
|
71
|
+
return isTestRuntime(env) ? TEST_ARGON2 : PRODUCTION_ARGON2;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Hash a secret at the right cost for this process. The one call every app should use;
|
|
76
|
+
* a bare `Bun.password.hash(x, "argon2id")` pays production's cost in every test.
|
|
77
|
+
*/
|
|
78
|
+
export const hashSecret = (secret: string): Promise<string> =>
|
|
79
|
+
Bun.password.hash(secret, argon2Cost());
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Verify a secret against a stored argon2id PHC string — `false`, never a throw, on anything
|
|
83
|
+
* malformed. `Bun.password` on the Mac; on the Worker the same global is `worker/password.ts`'s
|
|
84
|
+
* pure-JS shim, which reproduces Bun's hashes byte for byte, so every stored verifier keeps working.
|
|
85
|
+
*/
|
|
86
|
+
export const verifySecret = (secret: string, hash: string): Promise<boolean> =>
|
|
87
|
+
Bun.password.verify(secret, hash).catch(() => false);
|
|
@@ -80,3 +80,16 @@ export {
|
|
|
80
80
|
type RouteCpuStats,
|
|
81
81
|
} from './recorder.js';
|
|
82
82
|
export { BENCH_ORIGIN, type BenchCase, runCpuBench, type RunCpuBenchOpts } from './runBench.js';
|
|
83
|
+
export {
|
|
84
|
+
parseTailLines,
|
|
85
|
+
type RecordTailTracesOpts,
|
|
86
|
+
recordTailTraces,
|
|
87
|
+
routeMatcher,
|
|
88
|
+
TAIL_CPU_SOURCE,
|
|
89
|
+
type TailRecordResult,
|
|
90
|
+
type TailTraceLike,
|
|
91
|
+
type TailTrafficRate,
|
|
92
|
+
tailCpuClock,
|
|
93
|
+
tailTrafficRate,
|
|
94
|
+
UNMATCHED_ROUTE,
|
|
95
|
+
} from './tail.js';
|