@boxxy/server 0.1.3 → 0.1.5
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.
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { WatchReport } from '@boxxy/dispatch';
|
|
1
2
|
import type { FakeForge } from '@boxxy/dispatch/test-utils/create-fake-forge';
|
|
2
3
|
import type { TestDispatch } from '@boxxy/dispatch/test-utils/create-test-dispatch';
|
|
3
|
-
import type {
|
|
4
|
+
import type { ClosableClaimStore } from '@boxxy/dispatch/test-utils/types';
|
|
5
|
+
import type { AttemptRow, CallerProof, Clock, PepysReader, PhaseName } from '@boxxy/model';
|
|
4
6
|
import type { FakeNotifier } from '@boxxy/model/test-utils/create-fake-notifier';
|
|
5
7
|
import type { FakePepysReader } from '@boxxy/model/test-utils/create-fake-pepys-reader';
|
|
6
8
|
import type { TickReport } from '../tick/run-tick.ts';
|
|
@@ -9,6 +11,8 @@ import type { FakeCallerProof } from './create-fake-caller-proof.ts';
|
|
|
9
11
|
export interface TestServerOptions {
|
|
10
12
|
readonly hasPepys?: boolean;
|
|
11
13
|
readonly makeCallerProof?: (clock: Clock) => Promise<CallerProof>;
|
|
14
|
+
readonly pepys?: PepysReader;
|
|
15
|
+
readonly store?: ClosableClaimStore;
|
|
12
16
|
}
|
|
13
17
|
export interface TestServer {
|
|
14
18
|
readonly dispatch: TestDispatch;
|
|
@@ -25,6 +29,7 @@ export interface TestServer {
|
|
|
25
29
|
readonly owner: (request: Request) => Promise<Response>;
|
|
26
30
|
readonly worker: (request: Request) => Promise<Response>;
|
|
27
31
|
readonly tick: () => Promise<TickReport>;
|
|
32
|
+
readonly watch: () => Promise<WatchReport>;
|
|
28
33
|
readonly seedStartedAttempt: (phase: PhaseName) => Promise<AttemptRow>;
|
|
29
34
|
}
|
|
30
35
|
export declare function createTestServer(options?: TestServerOptions): Promise<TestServer>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxxy/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "The boxxy server: the API behind its three doors, and the runner loop",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/boxxy/tree/main/apps/server#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/boxxy/issues",
|
|
@@ -32,19 +32,19 @@
|
|
|
32
32
|
"typecheck": "tsrs --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@boxxy/caller-proof-imp": "0.1.
|
|
36
|
-
"@boxxy/closeout": "0.1.
|
|
37
|
-
"@boxxy/content-hash": "0.1.
|
|
38
|
-
"@boxxy/contract": "0.1.
|
|
39
|
-
"@boxxy/dispatch": "0.1.
|
|
40
|
-
"@boxxy/forge-github": "0.1.
|
|
41
|
-
"@boxxy/model": "0.1.
|
|
42
|
-
"@boxxy/notifier-atc": "0.1.
|
|
43
|
-
"@boxxy/pepys": "0.1.
|
|
44
|
-
"@boxxy/planning": "0.1.
|
|
45
|
-
"@boxxy/rules": "0.1.
|
|
46
|
-
"@boxxy/store-sqlite": "0.1.
|
|
47
|
-
"@boxxy/worker-atc": "0.1.
|
|
35
|
+
"@boxxy/caller-proof-imp": "0.1.5",
|
|
36
|
+
"@boxxy/closeout": "0.1.5",
|
|
37
|
+
"@boxxy/content-hash": "0.1.5",
|
|
38
|
+
"@boxxy/contract": "0.1.5",
|
|
39
|
+
"@boxxy/dispatch": "0.1.5",
|
|
40
|
+
"@boxxy/forge-github": "0.1.5",
|
|
41
|
+
"@boxxy/model": "0.1.5",
|
|
42
|
+
"@boxxy/notifier-atc": "0.1.5",
|
|
43
|
+
"@boxxy/pepys": "0.1.5",
|
|
44
|
+
"@boxxy/planning": "0.1.5",
|
|
45
|
+
"@boxxy/rules": "0.1.5",
|
|
46
|
+
"@boxxy/store-sqlite": "0.1.5",
|
|
47
|
+
"@boxxy/worker-atc": "0.1.5",
|
|
48
48
|
"@modelcontextprotocol/server": "2.4.0",
|
|
49
49
|
"@orpc/client": "2.0.0-beta.50",
|
|
50
50
|
"@orpc/mcp": "2.0.0-beta.50",
|
|
@@ -1,25 +1,36 @@
|
|
|
1
|
-
import { readdir } from 'node:fs/promises';
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
export type IssuersAnswer =
|
|
5
5
|
| { readonly kind: 'read'; readonly issuers: readonly unknown[] }
|
|
6
6
|
| { readonly kind: 'invalid'; readonly message: string };
|
|
7
7
|
|
|
8
|
-
// The trust documents, one per file of each folder, as parsed JSON in name order. The
|
|
9
|
-
// checks their shape; this reads them only.
|
|
8
|
+
// The trust documents, one per regular file of each folder, as parsed JSON in name order. The
|
|
9
|
+
// adapter checks their shape; this reads them only. Folders that hold no document refuse, so a
|
|
10
|
+
// configured but empty mount never starts a server that refuses every session in silence.
|
|
10
11
|
export async function readTrustedIssuers(folders: readonly string[]): Promise<IssuersAnswer> {
|
|
11
12
|
const issuers: unknown[] = [];
|
|
12
13
|
|
|
13
14
|
for (const folder of folders) {
|
|
14
|
-
const
|
|
15
|
+
const names = await readEntryNames(folder);
|
|
15
16
|
|
|
16
|
-
if (
|
|
17
|
+
if (names === null) {
|
|
17
18
|
return { kind: 'invalid', message: `no trusted issuers folder at ${folder}` };
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
for (const name of
|
|
21
|
+
for (const name of names) {
|
|
21
22
|
const file = path.join(folder, name);
|
|
22
23
|
|
|
24
|
+
const entry = await readEntryKind(file);
|
|
25
|
+
|
|
26
|
+
if (entry === 'missing') {
|
|
27
|
+
return { kind: 'invalid', message: `the trust document at ${file} points nowhere` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (entry === 'other') {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
const text = await Bun.file(file).text();
|
|
24
35
|
|
|
25
36
|
const json = parseJSON(text);
|
|
@@ -32,23 +43,38 @@ export async function readTrustedIssuers(folders: readonly string[]): Promise<Is
|
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
if (issuers.length === 0) {
|
|
47
|
+
return { kind: 'invalid', message: `no trust document in ${folders.join(', ')}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
return { kind: 'read', issuers };
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
// null: the folder does not exist
|
|
39
|
-
|
|
53
|
+
// null: the folder does not exist. A Kubernetes volume keeps its real files under `..data` and
|
|
54
|
+
// `..<timestamp>` and shows each key as a symlink beside them, so `..` names are skipped.
|
|
55
|
+
async function readEntryNames(folder: string): Promise<string[] | null> {
|
|
40
56
|
try {
|
|
41
|
-
const
|
|
57
|
+
const names = await readdir(folder);
|
|
42
58
|
|
|
43
|
-
return
|
|
44
|
-
.filter((
|
|
45
|
-
.map((entry) => entry.name)
|
|
59
|
+
return names
|
|
60
|
+
.filter((name) => !name.startsWith('..'))
|
|
46
61
|
.toSorted((left, right) => left.localeCompare(right));
|
|
47
62
|
} catch {
|
|
48
63
|
return null;
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
// `stat` follows a symlink to its target; a symlink that points nowhere is `missing`
|
|
68
|
+
async function readEntryKind(file: string): Promise<'file' | 'other' | 'missing'> {
|
|
69
|
+
try {
|
|
70
|
+
const stats = await stat(file);
|
|
71
|
+
|
|
72
|
+
return stats.isFile() ? 'file' : 'other';
|
|
73
|
+
} catch {
|
|
74
|
+
return 'missing';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
function parseJSON(text: string): unknown {
|
|
53
79
|
try {
|
|
54
80
|
const value: unknown = JSON.parse(text);
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { watchPullRequests } from '@boxxy/dispatch';
|
|
2
|
+
import type { WatchReport } from '@boxxy/dispatch';
|
|
1
3
|
import type { FakeForge } from '@boxxy/dispatch/test-utils/create-fake-forge';
|
|
2
4
|
import { createFakeForge } from '@boxxy/dispatch/test-utils/create-fake-forge';
|
|
3
5
|
import type { TestDispatch } from '@boxxy/dispatch/test-utils/create-test-dispatch';
|
|
4
6
|
import { createTestDispatch } from '@boxxy/dispatch/test-utils/create-test-dispatch';
|
|
7
|
+
import type { ClosableClaimStore } from '@boxxy/dispatch/test-utils/types';
|
|
5
8
|
import type {
|
|
6
9
|
AttemptId,
|
|
7
10
|
AttemptRow,
|
|
8
11
|
CallerProof,
|
|
9
12
|
Clock,
|
|
13
|
+
PepysReader,
|
|
10
14
|
PhaseName,
|
|
11
15
|
SessionReading,
|
|
12
16
|
} from '@boxxy/model';
|
|
@@ -33,6 +37,12 @@ export interface TestServerOptions {
|
|
|
33
37
|
// the server's real proof in place of the fake one, built on the test's clock; the fake that
|
|
34
38
|
// `callerProof` returns then proves nothing
|
|
35
39
|
readonly makeCallerProof?: (clock: Clock) => Promise<CallerProof>;
|
|
40
|
+
|
|
41
|
+
// the reader of a pepys under test in place of the fake one, which `pepys` then still returns
|
|
42
|
+
readonly pepys?: PepysReader;
|
|
43
|
+
|
|
44
|
+
// the memory store when left out
|
|
45
|
+
readonly store?: ClosableClaimStore;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
export interface TestServer {
|
|
@@ -57,6 +67,9 @@ export interface TestServer {
|
|
|
57
67
|
// one runner tick at the clock's time
|
|
58
68
|
readonly tick: () => Promise<TickReport>;
|
|
59
69
|
|
|
70
|
+
// one pass of the pull request watch at the clock's time
|
|
71
|
+
readonly watch: () => Promise<WatchReport>;
|
|
72
|
+
|
|
60
73
|
// an item at the phase, claimed and started by the fake worker; its row carries the caller
|
|
61
74
|
// that its session reports
|
|
62
75
|
readonly seedStartedAttempt: (phase: PhaseName) => Promise<AttemptRow>;
|
|
@@ -65,7 +78,9 @@ export interface TestServer {
|
|
|
65
78
|
// The server's two listeners on the memory store, the fake worker, the fake clock, a fake pepys
|
|
66
79
|
// reader, a fake notifier and a fake forge, with the example rules. No port is opened.
|
|
67
80
|
export async function createTestServer(options: TestServerOptions = {}): Promise<TestServer> {
|
|
68
|
-
const
|
|
81
|
+
const dispatchOptions = options.store === undefined ? {} : { store: options.store };
|
|
82
|
+
|
|
83
|
+
const dispatch = await createTestDispatch(dispatchOptions);
|
|
69
84
|
|
|
70
85
|
const callerProof = createFakeCallerProof();
|
|
71
86
|
const pepys = createFakePepysReader();
|
|
@@ -95,7 +110,7 @@ export async function createTestServer(options: TestServerOptions = {}): Promise
|
|
|
95
110
|
? callerProof
|
|
96
111
|
: await options.makeCallerProof(dispatch.clock),
|
|
97
112
|
workerTokenDigest: buildTokenDigest(workerToken),
|
|
98
|
-
pepys: options.hasPepys === false ? null : pepys,
|
|
113
|
+
pepys: options.hasPepys === false ? null : (options.pepys ?? pepys),
|
|
99
114
|
forge,
|
|
100
115
|
outbox: createOffOutbox(dispatch.store, dispatch.clock),
|
|
101
116
|
readings,
|
|
@@ -129,6 +144,15 @@ export async function createTestServer(options: TestServerOptions = {}): Promise
|
|
|
129
144
|
readings,
|
|
130
145
|
log,
|
|
131
146
|
}),
|
|
147
|
+
watch: () =>
|
|
148
|
+
watchPullRequests({
|
|
149
|
+
store: dispatch.store,
|
|
150
|
+
forge,
|
|
151
|
+
rules: dispatch.rules(),
|
|
152
|
+
runner: dispatch.runner,
|
|
153
|
+
now: dispatch.clock.now(),
|
|
154
|
+
newId: dispatch.newId,
|
|
155
|
+
}),
|
|
132
156
|
seedStartedAttempt: async (phase) => {
|
|
133
157
|
await dispatch.seedItem({ phase });
|
|
134
158
|
|