@averyyy/pi-server 0.82.1-piclient.2 → 0.82.1-piclient.4
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/CHANGELOG.md +11 -0
- package/README.md +25 -0
- package/bin/update.js +156 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pi-server-protocol.d.ts +16 -0
- package/dist/pi-server-protocol.d.ts.map +1 -1
- package/dist/pi-server-protocol.js +30 -1
- package/dist/pi-server-protocol.js.map +1 -1
- package/dist/receive-upload.d.ts +5 -0
- package/dist/receive-upload.d.ts.map +1 -1
- package/dist/receive-upload.js +88 -6
- package/dist/receive-upload.js.map +1 -1
- package/dist/request-chunks.d.ts +33 -0
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +540 -48
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +41 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3088 -366
- package/dist/server.js.map +1 -1
- package/dist/session-persistence.d.ts +39 -1
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +1132 -116
- package/dist/session-persistence.js.map +1 -1
- package/dist/session-store.d.ts +112 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +695 -120
- package/dist/session-store.js.map +1 -1
- package/dist/stream-run-persistence.d.ts +163 -0
- package/dist/stream-run-persistence.d.ts.map +1 -0
- package/dist/stream-run-persistence.js +1824 -0
- package/dist/stream-run-persistence.js.map +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -20,8 +20,19 @@
|
|
|
20
20
|
### Fixed
|
|
21
21
|
|
|
22
22
|
- Used `--legacy-peer-deps` for npm-global fork updates so existing upstream Pi installs do not trigger peer override warnings for forked prerelease aliases.
|
|
23
|
+
- Fixed provider streams that close without a terminal event and made duplicate `runId` requests single-flight, replayable, and bounded by count and retention time.
|
|
24
|
+
- Fixed static-context hashes to include canonical constrained-sampling tool configuration.
|
|
25
|
+
- Fixed Windows updates to invoke the validated npm CLI through the active Node executable and terminate stalled git or npm steps with actionable errors.
|
|
26
|
+
- Added durable provider-run journals with cursor replay, terminal acknowledgement, restart-uncertainty handling, bounded lossless persistence backpressure, and immediate provider cancellation.
|
|
27
|
+
- Reworked session persistence as checksummed append-only WAL records plus periodic snapshots, with strict revision/digest validation and narrow torn-write recovery.
|
|
28
|
+
- Made server-side compaction authoritative, replayable, heartbeat-streamed, branch-local, and compatible with native Pi compaction hooks and cancellation.
|
|
29
|
+
- Bounded session history, request, response, SSE, subscriber, run-journal, and upload resources with explicit structured failures instead of silent truncation, and reduced chunk reassembly peak memory with independently decoded base64 segments.
|
|
30
|
+
- Made session tree mutation and file receive retries idempotent while rejecting divergent duplicate IDs or destination contents.
|
|
31
|
+
- Added per-filesystem-operation no-progress watchdogs for durable run persistence and a bounded CLI fatal-shutdown grace, without imposing a total provider-run deadline or retrying indeterminate writes.
|
|
32
|
+
- Made compaction capacity admission exact and atomic before provider execution and journal settlement, serialized deletion against queued work, and enforced single-owner fail-stop persistence within and across processes.
|
|
23
33
|
|
|
24
34
|
### Changed
|
|
25
35
|
|
|
26
36
|
- Updated the upstream Pi base through commit `0e6909f0`, including the latest provider and compaction fixes.
|
|
27
37
|
- Rebased the server on upstream Pi `0.80.6`, including GPT-5.6 model metadata and `max` thinking support.
|
|
38
|
+
- Added structured, secret-free provider-stream failure logs with response metadata preserved across the proxy.
|
package/README.md
CHANGED
|
@@ -36,6 +36,31 @@ PI_SERVER_UPLOAD_DIR=/path/to/upload_files
|
|
|
36
36
|
|
|
37
37
|
Received files default to `~/.pi/upload_files`.
|
|
38
38
|
|
|
39
|
+
Long-run safety limits are explicit and configurable:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
PI_SERVER_SESSION_MAX_ENTRIES=250000
|
|
43
|
+
PI_SERVER_SESSION_MAX_LOGICAL_BYTES=268435456
|
|
44
|
+
PI_SERVER_SESSIONS_MAX_ENTRIES=500000
|
|
45
|
+
PI_SERVER_SESSIONS_MAX_LOGICAL_BYTES=536870912
|
|
46
|
+
PI_SERVER_MAX_LOADED_SESSIONS=1024
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The server rejects a mutation with structured HTTP `507` before exceeding these limits. It does not truncate,
|
|
50
|
+
delete, or compact durable history to make room.
|
|
51
|
+
|
|
52
|
+
Transport and persistence watchdogs measure continuous lack of progress, not total operation duration:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
PI_SERVER_REQUEST_BODY_NO_PROGRESS_TIMEOUT_MS=90000
|
|
56
|
+
PI_SERVER_STREAM_DRAIN_IDLE_TIMEOUT_MS=90000
|
|
57
|
+
PI_SERVER_STREAM_RUN_IO_NO_PROGRESS_TIMEOUT_MS=120000
|
|
58
|
+
PI_SERVER_COMPACT_RUN_IO_NO_PROGRESS_TIMEOUT_MS=120000
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An active provider or compaction run has no short total-duration deadline; it continues while making progress and
|
|
62
|
+
remaining within configured capacity.
|
|
63
|
+
|
|
39
64
|
## Connect A Client
|
|
40
65
|
|
|
41
66
|
```bash
|
package/bin/update.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
+
const GIT_STATUS_TIMEOUT_MS = 30_000;
|
|
7
|
+
const GIT_UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
8
|
+
const NPM_UPDATE_TIMEOUT_MS = 20 * 60_000;
|
|
9
|
+
const UPDATE_KILL_SIGNAL = "SIGTERM";
|
|
10
|
+
|
|
6
11
|
function defaultPackageRoot() {
|
|
7
12
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
13
|
}
|
|
@@ -15,28 +20,126 @@ function readPackageMetadata(packageRoot) {
|
|
|
15
20
|
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8"));
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function
|
|
19
|
-
|
|
23
|
+
function isFile(path) {
|
|
24
|
+
try {
|
|
25
|
+
return statSync(path).isFile();
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveNpmInvocation(platform, nodePath) {
|
|
32
|
+
if (platform !== "win32") {
|
|
33
|
+
return { command: "npm", argsPrefix: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!isFile(nodePath)) {
|
|
37
|
+
throw new Error(`Node executable not found: ${nodePath}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const npmPackagePath = resolve(dirname(nodePath), "node_modules", "npm", "package.json");
|
|
41
|
+
if (!isFile(npmPackagePath)) {
|
|
42
|
+
throw new Error(`npm package metadata not found: ${npmPackagePath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let npmPackage;
|
|
46
|
+
try {
|
|
47
|
+
npmPackage = JSON.parse(readFileSync(npmPackagePath, "utf-8"));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new Error(`unable to read npm package metadata ${npmPackagePath}: ${message}`, { cause: error });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const npmBin = typeof npmPackage.bin === "string" ? npmPackage.bin : npmPackage.bin?.npm;
|
|
54
|
+
if (typeof npmBin !== "string" || npmBin.length === 0) {
|
|
55
|
+
throw new Error(`npm CLI entry missing from package metadata: ${npmPackagePath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const npmCliPath = resolve(dirname(npmPackagePath), npmBin);
|
|
59
|
+
if (!isFile(npmCliPath)) {
|
|
60
|
+
throw new Error(`npm CLI not found: ${npmCliPath}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { command: nodePath, argsPrefix: [npmCliPath] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runStep(runner, command, args, cwd, timeout, stdio = "inherit") {
|
|
67
|
+
try {
|
|
68
|
+
return runner(command, args, {
|
|
69
|
+
cwd,
|
|
70
|
+
stdio,
|
|
71
|
+
encoding: "utf-8",
|
|
72
|
+
timeout,
|
|
73
|
+
killSignal: UPDATE_KILL_SIGNAL,
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { status: null, error };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function reportStepFailure(stderr, displayCommand, result, timeout) {
|
|
81
|
+
if (result.error?.code === "ETIMEDOUT") {
|
|
82
|
+
stderr.write(
|
|
83
|
+
`pi-server update failed: ${displayCommand} timed out after ${timeout} ms and was terminated with ${UPDATE_KILL_SIGNAL}\n`,
|
|
84
|
+
);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.error) {
|
|
89
|
+
const errorCode = typeof result.error.code === "string" ? ` (${result.error.code})` : "";
|
|
90
|
+
const message = result.error instanceof Error ? result.error.message : String(result.error);
|
|
91
|
+
stderr.write(`pi-server update failed: unable to start ${displayCommand}${errorCode}: ${message}\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (result.signal) {
|
|
96
|
+
stderr.write(`pi-server update failed: ${displayCommand} was terminated by signal ${result.signal}\n`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof result.status === "number") {
|
|
101
|
+
stderr.write(`pi-server update failed: ${displayCommand} exited with code ${result.status}\n`);
|
|
102
|
+
return result.status === 0 ? 1 : result.status;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stderr.write(`pi-server update failed: ${displayCommand} ended without an exit status\n`);
|
|
106
|
+
return 1;
|
|
20
107
|
}
|
|
21
108
|
|
|
22
109
|
function isMissingGitCheckout(result) {
|
|
23
110
|
const text = `${String(result.stdout ?? "")}\n${String(result.stderr ?? "")}`;
|
|
24
|
-
return result.status !== 0 && text.includes("not a git repository");
|
|
111
|
+
return !result.error && !result.signal && result.status !== 0 && text.includes("not a git repository");
|
|
25
112
|
}
|
|
26
113
|
|
|
27
|
-
|
|
114
|
+
function getNpmInvocation(platform, nodePath, stderr) {
|
|
115
|
+
try {
|
|
116
|
+
return resolveNpmInvocation(platform, nodePath);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
stderr.write(`pi-server update failed: ${message}\n`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function runNpmGlobalUpdate(runner, npmInvocation, repoRoot, stdout, stderr) {
|
|
28
125
|
stdout.write("Updating npm packages: @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
|
|
126
|
+
const npmArgs = [
|
|
127
|
+
"install",
|
|
128
|
+
"-g",
|
|
129
|
+
"--ignore-scripts",
|
|
130
|
+
"--legacy-peer-deps",
|
|
131
|
+
"@averyyy/pi-client@latest",
|
|
132
|
+
"@averyyy/pi-server@latest",
|
|
133
|
+
];
|
|
29
134
|
const result = runStep(
|
|
30
135
|
runner,
|
|
31
|
-
|
|
32
|
-
[
|
|
136
|
+
npmInvocation.command,
|
|
137
|
+
[...npmInvocation.argsPrefix, ...npmArgs],
|
|
33
138
|
repoRoot,
|
|
139
|
+
NPM_UPDATE_TIMEOUT_MS,
|
|
34
140
|
);
|
|
35
141
|
if (result.status !== 0) {
|
|
36
|
-
stderr.
|
|
37
|
-
"pi-server update failed: npm install -g --ignore-scripts --legacy-peer-deps @averyyy/pi-client@latest @averyyy/pi-server@latest\n",
|
|
38
|
-
);
|
|
39
|
-
return result.status ?? 1;
|
|
142
|
+
return reportStepFailure(stderr, `npm ${npmArgs.join(" ")}`, result, NPM_UPDATE_TIMEOUT_MS);
|
|
40
143
|
}
|
|
41
144
|
stdout.write("pi-server update complete\n");
|
|
42
145
|
return 0;
|
|
@@ -48,36 +151,66 @@ export async function runPiServerUpdate(_args = [], options = {}) {
|
|
|
48
151
|
const runner = options.runner ?? spawnSync;
|
|
49
152
|
const stdout = options.stdout ?? process.stdout;
|
|
50
153
|
const stderr = options.stderr ?? process.stderr;
|
|
154
|
+
const platform = options.platform ?? process.platform;
|
|
155
|
+
const nodePath = options.nodePath ?? process.execPath;
|
|
51
156
|
const pkg = readPackageMetadata(packageRoot);
|
|
52
157
|
|
|
53
158
|
stdout.write(`pi-server ${pkg.version}\n`);
|
|
54
159
|
stdout.write(`Updating checkout: ${repoRoot}\n`);
|
|
55
160
|
|
|
56
|
-
const
|
|
161
|
+
const statusArgs = ["status", "--porcelain"];
|
|
162
|
+
const status = runStep(runner, "git", statusArgs, repoRoot, GIT_STATUS_TIMEOUT_MS, "pipe");
|
|
57
163
|
if (status.status !== 0) {
|
|
58
164
|
if (isMissingGitCheckout(status)) {
|
|
59
|
-
|
|
165
|
+
const npmInvocation = getNpmInvocation(platform, nodePath, stderr);
|
|
166
|
+
if (!npmInvocation) {
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
return runNpmGlobalUpdate(runner, npmInvocation, repoRoot, stdout, stderr);
|
|
60
170
|
}
|
|
61
|
-
stderr.
|
|
62
|
-
return status.status ?? 1;
|
|
171
|
+
return reportStepFailure(stderr, `git ${statusArgs.join(" ")}`, status, GIT_STATUS_TIMEOUT_MS);
|
|
63
172
|
}
|
|
64
173
|
if (String(status.stdout ?? "").trim().length > 0) {
|
|
65
174
|
stderr.write("pi-server update failed: working tree has uncommitted changes\n");
|
|
66
175
|
return 1;
|
|
67
176
|
}
|
|
68
177
|
|
|
178
|
+
const npmInvocation = getNpmInvocation(platform, nodePath, stderr);
|
|
179
|
+
if (!npmInvocation) {
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
69
183
|
const steps = [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
184
|
+
{
|
|
185
|
+
command: "git",
|
|
186
|
+
args: ["pull", "--ff-only"],
|
|
187
|
+
displayCommand: "git pull --ff-only",
|
|
188
|
+
timeout: GIT_UPDATE_TIMEOUT_MS,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
command: npmInvocation.command,
|
|
192
|
+
args: [...npmInvocation.argsPrefix, "install", "--ignore-scripts"],
|
|
193
|
+
displayCommand: "npm install --ignore-scripts",
|
|
194
|
+
timeout: NPM_UPDATE_TIMEOUT_MS,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
command: npmInvocation.command,
|
|
198
|
+
args: [...npmInvocation.argsPrefix, "run", "install:pi-client"],
|
|
199
|
+
displayCommand: "npm run install:pi-client",
|
|
200
|
+
timeout: NPM_UPDATE_TIMEOUT_MS,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
command: npmInvocation.command,
|
|
204
|
+
args: [...npmInvocation.argsPrefix, "run", "install:pi-server"],
|
|
205
|
+
displayCommand: "npm run install:pi-server",
|
|
206
|
+
timeout: NPM_UPDATE_TIMEOUT_MS,
|
|
207
|
+
},
|
|
74
208
|
];
|
|
75
209
|
|
|
76
|
-
for (const
|
|
77
|
-
const result = runStep(runner, command, args, repoRoot);
|
|
210
|
+
for (const step of steps) {
|
|
211
|
+
const result = runStep(runner, step.command, step.args, repoRoot, step.timeout);
|
|
78
212
|
if (result.status !== 0) {
|
|
79
|
-
stderr.
|
|
80
|
-
return result.status ?? 1;
|
|
213
|
+
return reportStepFailure(stderr, step.displayCommand, result, step.timeout);
|
|
81
214
|
}
|
|
82
215
|
}
|
|
83
216
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { loadConfig, type ServerConfig } from "./config.ts";
|
|
2
2
|
export { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from "./event-encoding.ts";
|
|
3
|
-
export { createPiServer, startServer } from "./server.ts";
|
|
4
|
-
export { appendAssistantResponse, appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, type PersistedSessionState, restoreSessionState, type SessionState, type SessionStaticContext, type SessionSummary, setStaticContext, } from "./session-store.ts";
|
|
3
|
+
export { createPiServer, type PiServerOptions, startServer } from "./server.ts";
|
|
4
|
+
export { appendAssistantResponse, appendMessages, applySessionMutation, calculateSessionLogicalBytes, clearAllSessions, configureSessionCapacityLimits, DEFAULT_SESSION_CAPACITY_LIMITS, deleteSession, exportSessionState, getOrCreateSession, getSession, getSessionCapacityLimits, getSessionCapacityUsage, hashSessionEntries, listSessions, type PersistedSessionState, preflightSessionCapacityMutation, resetSessionCapacityLimits, restoreSessionState, SessionCapacityError, type SessionCapacityLimits, type SessionCapacityMutation, type SessionCapacityProjection, type SessionCapacityUsage, type SessionState, type SessionStaticContext, type SessionSummary, setStaticContext, } from "./session-store.ts";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACN,uBAAuB,EACvB,cAAc,EACd,oBAAoB,EACpB,4BAA4B,EAC5B,gBAAgB,EAChB,8BAA8B,EAC9B,+BAA+B,EAC/B,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,wBAAwB,EACxB,uBAAuB,EACvB,kBAAkB,EAClB,YAAY,EACZ,KAAK,qBAAqB,EAC1B,gCAAgC,EAChC,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,gBAAgB,GAChB,MAAM,oBAAoB,CAAC","sourcesContent":["export { loadConfig, type ServerConfig } from \"./config.ts\";\nexport { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from \"./event-encoding.ts\";\nexport { createPiServer, type PiServerOptions, startServer } from \"./server.ts\";\nexport {\n\tappendAssistantResponse,\n\tappendMessages,\n\tapplySessionMutation,\n\tcalculateSessionLogicalBytes,\n\tclearAllSessions,\n\tconfigureSessionCapacityLimits,\n\tDEFAULT_SESSION_CAPACITY_LIMITS,\n\tdeleteSession,\n\texportSessionState,\n\tgetOrCreateSession,\n\tgetSession,\n\tgetSessionCapacityLimits,\n\tgetSessionCapacityUsage,\n\thashSessionEntries,\n\tlistSessions,\n\ttype PersistedSessionState,\n\tpreflightSessionCapacityMutation,\n\tresetSessionCapacityLimits,\n\trestoreSessionState,\n\tSessionCapacityError,\n\ttype SessionCapacityLimits,\n\ttype SessionCapacityMutation,\n\ttype SessionCapacityProjection,\n\ttype SessionCapacityUsage,\n\ttype SessionState,\n\ttype SessionStaticContext,\n\ttype SessionSummary,\n\tsetStaticContext,\n} from \"./session-store.ts\";\n"]}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { loadConfig } from "./config.js";
|
|
2
2
|
export { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from "./event-encoding.js";
|
|
3
3
|
export { createPiServer, startServer } from "./server.js";
|
|
4
|
-
export { appendAssistantResponse, appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, restoreSessionState, setStaticContext, } from "./session-store.js";
|
|
4
|
+
export { appendAssistantResponse, appendMessages, applySessionMutation, calculateSessionLogicalBytes, clearAllSessions, configureSessionCapacityLimits, DEFAULT_SESSION_CAPACITY_LIMITS, deleteSession, exportSessionState, getOrCreateSession, getSession, getSessionCapacityLimits, getSessionCapacityUsage, hashSessionEntries, listSessions, preflightSessionCapacityMutation, resetSessionCapacityLimits, restoreSessionState, SessionCapacityError, setStaticContext, } from "./session-store.js";
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAwB,WAAW,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACN,uBAAuB,EACvB,cAAc,EACd,oBAAoB,EACpB,4BAA4B,EAC5B,gBAAgB,EAChB,8BAA8B,EAC9B,+BAA+B,EAC/B,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,wBAAwB,EACxB,uBAAuB,EACvB,kBAAkB,EAClB,YAAY,EAEZ,gCAAgC,EAChC,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EAQpB,gBAAgB,GAChB,MAAM,oBAAoB,CAAC","sourcesContent":["export { loadConfig, type ServerConfig } from \"./config.ts\";\nexport { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from \"./event-encoding.ts\";\nexport { createPiServer, type PiServerOptions, startServer } from \"./server.ts\";\nexport {\n\tappendAssistantResponse,\n\tappendMessages,\n\tapplySessionMutation,\n\tcalculateSessionLogicalBytes,\n\tclearAllSessions,\n\tconfigureSessionCapacityLimits,\n\tDEFAULT_SESSION_CAPACITY_LIMITS,\n\tdeleteSession,\n\texportSessionState,\n\tgetOrCreateSession,\n\tgetSession,\n\tgetSessionCapacityLimits,\n\tgetSessionCapacityUsage,\n\thashSessionEntries,\n\tlistSessions,\n\ttype PersistedSessionState,\n\tpreflightSessionCapacityMutation,\n\tresetSessionCapacityLimits,\n\trestoreSessionState,\n\tSessionCapacityError,\n\ttype SessionCapacityLimits,\n\ttype SessionCapacityMutation,\n\ttype SessionCapacityProjection,\n\ttype SessionCapacityUsage,\n\ttype SessionState,\n\ttype SessionStaticContext,\n\ttype SessionSummary,\n\tsetStaticContext,\n} from \"./session-store.ts\";\n"]}
|
|
@@ -4,6 +4,22 @@ export interface PiServerStaticContext {
|
|
|
4
4
|
systemPrompt?: string;
|
|
5
5
|
tools?: Tool[];
|
|
6
6
|
}
|
|
7
|
+
export interface PiServerSessionIdentity {
|
|
8
|
+
staticContextHash: string;
|
|
9
|
+
treeHash: string;
|
|
10
|
+
entryCount: number;
|
|
11
|
+
leafId: string | null;
|
|
12
|
+
revision: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PiServerStreamBaseIdentity {
|
|
15
|
+
baseStaticContextHash: string;
|
|
16
|
+
baseTreeHash: string;
|
|
17
|
+
baseEntryCount: number;
|
|
18
|
+
baseLeafId: string | null;
|
|
19
|
+
baseRevision: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function matchesPiServerStreamBase(base: PiServerStreamBaseIdentity, session: PiServerSessionIdentity): boolean;
|
|
22
|
+
export declare function canonicalJsonStringify(value: unknown): string | undefined;
|
|
7
23
|
export declare function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string;
|
|
8
24
|
export declare const PI_SERVER_EMPTY_TREE_HASH: string;
|
|
9
25
|
export declare function hashPiServerTreeEntry(entry: SessionTreeEntry): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pi-server-protocol.d.ts","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CACf;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"pi-server-protocol.d.ts","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACvC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IAC1C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAQrH;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgBzE;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,CAgB5F;AAED,eAAO,MAAM,yBAAyB,QAA0D,CAAC;AAEjG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAErE;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAMnF;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAE9E","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport interface PiServerSessionIdentity {\n\tstaticContextHash: string;\n\ttreeHash: string;\n\tentryCount: number;\n\tleafId: string | null;\n\trevision: number;\n}\n\nexport interface PiServerStreamBaseIdentity {\n\tbaseStaticContextHash: string;\n\tbaseTreeHash: string;\n\tbaseEntryCount: number;\n\tbaseLeafId: string | null;\n\tbaseRevision: number;\n}\n\nexport function matchesPiServerStreamBase(base: PiServerStreamBaseIdentity, session: PiServerSessionIdentity): boolean {\n\treturn (\n\t\tbase.baseStaticContextHash === session.staticContextHash &&\n\t\tbase.baseTreeHash === session.treeHash &&\n\t\tbase.baseEntryCount === session.entryCount &&\n\t\tbase.baseLeafId === session.leafId &&\n\t\tbase.baseRevision === session.revision\n\t);\n}\n\nexport function canonicalJsonStringify(value: unknown): string | undefined {\n\tif (Array.isArray(value)) {\n\t\treturn `[${Array.from(value, (item) => canonicalJsonStringify(item) ?? \"null\").join(\",\")}]`;\n\t}\n\tif (value !== null && typeof value === \"object\") {\n\t\tconst record = value as Record<string, unknown>;\n\t\tconst properties: string[] = [];\n\t\tfor (const key of Object.keys(record).sort()) {\n\t\t\tconst serialized = canonicalJsonStringify(record[key]);\n\t\t\tif (serialized !== undefined) {\n\t\t\t\tproperties.push(`${JSON.stringify(key)}:${serialized}`);\n\t\t\t}\n\t\t}\n\t\treturn `{${properties.join(\",\")}}`;\n\t}\n\treturn JSON.stringify(value);\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t\tconstrainedSampling: tool.constrainedSampling,\n\t\t})),\n\t};\n\tconst serialized = canonicalJsonStringify(canonical);\n\tif (serialized === undefined) {\n\t\tthrow new Error(\"Failed to serialize pi-server static context\");\n\t}\n\treturn createHash(\"sha256\").update(serialized).digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
export function matchesPiServerStreamBase(base, session) {
|
|
3
|
+
return (base.baseStaticContextHash === session.staticContextHash &&
|
|
4
|
+
base.baseTreeHash === session.treeHash &&
|
|
5
|
+
base.baseEntryCount === session.entryCount &&
|
|
6
|
+
base.baseLeafId === session.leafId &&
|
|
7
|
+
base.baseRevision === session.revision);
|
|
8
|
+
}
|
|
9
|
+
export function canonicalJsonStringify(value) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return `[${Array.from(value, (item) => canonicalJsonStringify(item) ?? "null").join(",")}]`;
|
|
12
|
+
}
|
|
13
|
+
if (value !== null && typeof value === "object") {
|
|
14
|
+
const record = value;
|
|
15
|
+
const properties = [];
|
|
16
|
+
for (const key of Object.keys(record).sort()) {
|
|
17
|
+
const serialized = canonicalJsonStringify(record[key]);
|
|
18
|
+
if (serialized !== undefined) {
|
|
19
|
+
properties.push(`${JSON.stringify(key)}:${serialized}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return `{${properties.join(",")}}`;
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
2
26
|
export function hashPiServerStaticContext(context) {
|
|
3
27
|
if (!context)
|
|
4
28
|
return "";
|
|
@@ -8,9 +32,14 @@ export function hashPiServerStaticContext(context) {
|
|
|
8
32
|
name: tool.name,
|
|
9
33
|
description: tool.description,
|
|
10
34
|
parameters: tool.parameters,
|
|
35
|
+
constrainedSampling: tool.constrainedSampling,
|
|
11
36
|
})),
|
|
12
37
|
};
|
|
13
|
-
|
|
38
|
+
const serialized = canonicalJsonStringify(canonical);
|
|
39
|
+
if (serialized === undefined) {
|
|
40
|
+
throw new Error("Failed to serialize pi-server static context");
|
|
41
|
+
}
|
|
42
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
14
43
|
}
|
|
15
44
|
export const PI_SERVER_EMPTY_TREE_HASH = createHash("sha256").update("pi-tree-v1").digest("hex");
|
|
16
45
|
export function hashPiServerTreeEntry(entry) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pi-server-protocol.js","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"pi-server-protocol.js","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport interface PiServerSessionIdentity {\n\tstaticContextHash: string;\n\ttreeHash: string;\n\tentryCount: number;\n\tleafId: string | null;\n\trevision: number;\n}\n\nexport interface PiServerStreamBaseIdentity {\n\tbaseStaticContextHash: string;\n\tbaseTreeHash: string;\n\tbaseEntryCount: number;\n\tbaseLeafId: string | null;\n\tbaseRevision: number;\n}\n\nexport function matchesPiServerStreamBase(base: PiServerStreamBaseIdentity, session: PiServerSessionIdentity): boolean {\n\treturn (\n\t\tbase.baseStaticContextHash === session.staticContextHash &&\n\t\tbase.baseTreeHash === session.treeHash &&\n\t\tbase.baseEntryCount === session.entryCount &&\n\t\tbase.baseLeafId === session.leafId &&\n\t\tbase.baseRevision === session.revision\n\t);\n}\n\nexport function canonicalJsonStringify(value: unknown): string | undefined {\n\tif (Array.isArray(value)) {\n\t\treturn `[${Array.from(value, (item) => canonicalJsonStringify(item) ?? \"null\").join(\",\")}]`;\n\t}\n\tif (value !== null && typeof value === \"object\") {\n\t\tconst record = value as Record<string, unknown>;\n\t\tconst properties: string[] = [];\n\t\tfor (const key of Object.keys(record).sort()) {\n\t\t\tconst serialized = canonicalJsonStringify(record[key]);\n\t\t\tif (serialized !== undefined) {\n\t\t\t\tproperties.push(`${JSON.stringify(key)}:${serialized}`);\n\t\t\t}\n\t\t}\n\t\treturn `{${properties.join(\",\")}}`;\n\t}\n\treturn JSON.stringify(value);\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t\tconstrainedSampling: tool.constrainedSampling,\n\t\t})),\n\t};\n\tconst serialized = canonicalJsonStringify(canonical);\n\tif (serialized === undefined) {\n\t\tthrow new Error(\"Failed to serialize pi-server static context\");\n\t}\n\treturn createHash(\"sha256\").update(serialized).digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
|
package/dist/receive-upload.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare const RECEIVE_UPLOAD_MAX_ENTRIES = 10000;
|
|
2
|
+
export declare const RECEIVE_UPLOAD_MAX_NAME_CHARS = 255;
|
|
3
|
+
export declare const RECEIVE_UPLOAD_MAX_PATH_CHARS = 4096;
|
|
4
|
+
export declare const RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS = 255;
|
|
5
|
+
export declare const RECEIVE_UPLOAD_MAX_DECODED_BYTES: number;
|
|
1
6
|
export declare class ReceiveUploadError extends Error {
|
|
2
7
|
status: number;
|
|
3
8
|
constructor(status: number, message: string);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receive-upload.d.ts","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"receive-upload.d.ts","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,0BAA0B,QAAS,CAAC;AACjD,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAClD,eAAO,MAAM,qCAAqC,MAAM,CAAC;AACzD,eAAO,MAAM,gCAAgC,QAAmB,CAAC;AAEjE,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;IAEf,YAAY,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAG1C;CACD;AAuID,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA2B/F","sourcesContent":["import {\n\tcloseSync,\n\texistsSync,\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\topenSync,\n\treaddirSync,\n\treadSync,\n\trenameSync,\n\trmSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join, relative, sep } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\n\nexport const RECEIVE_UPLOAD_MAX_ENTRIES = 10_000;\nexport const RECEIVE_UPLOAD_MAX_NAME_CHARS = 255;\nexport const RECEIVE_UPLOAD_MAX_PATH_CHARS = 4096;\nexport const RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS = 255;\nexport const RECEIVE_UPLOAD_MAX_DECODED_BYTES = 64 * 1024 * 1024;\n\nexport class ReceiveUploadError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t}\n}\n\nfunction invalid(message: string): never {\n\tthrow new ReceiveUploadError(400, message);\n}\n\nfunction validatePath(path: unknown): string {\n\tif (typeof path !== \"string\") invalid(\"entry path must be a string\");\n\tif (path.length > RECEIVE_UPLOAD_MAX_PATH_CHARS) {\n\t\tinvalid(`entry path must not exceed ${RECEIVE_UPLOAD_MAX_PATH_CHARS} characters`);\n\t}\n\tif (path.includes(\"\\\\\") || path.includes(\"\\0\")) invalid(`invalid entry path: ${path}`);\n\tif (path !== \"\" && path.split(\"/\").some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n\t\tinvalid(`invalid entry path: ${path}`);\n\t}\n\tif (path.split(\"/\").some((part) => part.length > RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS)) {\n\t\tinvalid(`entry path segment must not exceed ${RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS} characters`);\n\t}\n\treturn path;\n}\n\nfunction validateBody(body: unknown): { name: string; entries: UploadEntry[] } {\n\tif (typeof body !== \"object\" || body === null) invalid(\"upload body must be an object\");\n\tconst upload = body as { name?: unknown; entries?: unknown };\n\tif (\n\t\ttypeof upload.name !== \"string\" ||\n\t\tupload.name === \"\" ||\n\t\tupload.name === \".\" ||\n\t\tupload.name === \"..\" ||\n\t\tupload.name.includes(\"/\") ||\n\t\tupload.name.includes(\"\\\\\") ||\n\t\tupload.name.includes(\"\\0\") ||\n\t\tupload.name.length > RECEIVE_UPLOAD_MAX_NAME_CHARS\n\t) {\n\t\tinvalid(\"invalid upload name\");\n\t}\n\tif (!Array.isArray(upload.entries) || upload.entries.length === 0) invalid(\"entries must be a non-empty array\");\n\tif (upload.entries.length > RECEIVE_UPLOAD_MAX_ENTRIES) {\n\t\tinvalid(`entries must not exceed ${RECEIVE_UPLOAD_MAX_ENTRIES}`);\n\t}\n\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\n\tlet decodedBytes = 0;\n\tfor (const value of upload.entries) {\n\t\tif (typeof value !== \"object\" || value === null) invalid(\"each entry must be an object\");\n\t\tconst entry = value as { path?: unknown; type?: unknown; data?: unknown };\n\t\tconst path = validatePath(entry.path);\n\t\tif (entry.type !== \"file\" && entry.type !== \"directory\") invalid(`invalid entry type: ${String(entry.type)}`);\n\t\tif (entryTypes.has(path)) invalid(`duplicate entry path: ${path}`);\n\t\tentryTypes.set(path, entry.type);\n\t\tif (entry.type === \"directory\") {\n\t\t\tif (entry.data !== undefined) invalid(`directory entry must not contain data: ${path}`);\n\t\t\tentries.push({ path, type: \"directory\" });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof entry.data !== \"string\") invalid(`file entry data must be base64: ${path}`);\n\t\tconst data = Buffer.from(entry.data, \"base64\");\n\t\tif (data.toString(\"base64\") !== entry.data) invalid(`file entry data must be valid base64: ${path}`);\n\t\tdecodedBytes += data.byteLength;\n\t\tif (decodedBytes > RECEIVE_UPLOAD_MAX_DECODED_BYTES) {\n\t\t\tinvalid(`decoded upload data must not exceed ${RECEIVE_UPLOAD_MAX_DECODED_BYTES} bytes`);\n\t\t}\n\t\tentries.push({ path, type: \"file\", data });\n\t}\n\n\tconst rootType = entryTypes.get(\"\");\n\tif (!rootType) invalid(\"entries must include the root path\");\n\tif (rootType === \"file\" && entries.length !== 1) invalid(\"a file upload cannot contain child entries\");\n\tfor (const entry of entries) {\n\t\tif (entry.path === \"\") continue;\n\t\tconst parent = entry.path.includes(\"/\") ? entry.path.slice(0, entry.path.lastIndexOf(\"/\")) : \"\";\n\t\tif (entryTypes.get(parent) !== \"directory\") invalid(`missing directory entry: ${parent}`);\n\t}\n\treturn { name: upload.name, entries };\n}\n\nfunction fileMatches(path: string, expected: Buffer): boolean {\n\tconst stat = lstatSync(path);\n\tif (!stat.isFile() || stat.size !== expected.byteLength) return false;\n\tconst handle = openSync(path, \"r\");\n\tconst buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, expected.byteLength)));\n\tlet offset = 0;\n\ttry {\n\t\twhile (offset < expected.byteLength) {\n\t\t\tconst bytesRead = readSync(\n\t\t\t\thandle,\n\t\t\t\tbuffer,\n\t\t\t\t0,\n\t\t\t\tMath.min(buffer.byteLength, expected.byteLength - offset),\n\t\t\t\toffset,\n\t\t\t);\n\t\t\tif (bytesRead === 0 || !buffer.subarray(0, bytesRead).equals(expected.subarray(offset, offset + bytesRead))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\toffset += bytesRead;\n\t\t}\n\t\treturn true;\n\t} finally {\n\t\tcloseSync(handle);\n\t}\n}\n\nfunction existingDestinationMatches(destination: string, entries: UploadEntry[]): boolean {\n\tconst expected = new Map(entries.map((entry) => [entry.path, entry]));\n\tconst root = expected.get(\"\");\n\tif (!root) return false;\n\tconst destinationStat = lstatSync(destination);\n\tif (root.type === \"file\") {\n\t\treturn !destinationStat.isSymbolicLink() && fileMatches(destination, root.data!);\n\t}\n\tif (!destinationStat.isDirectory() || destinationStat.isSymbolicLink()) return false;\n\n\tlet visited = 1;\n\tconst visitDirectory = (directory: string): boolean => {\n\t\tfor (const child of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tif (child.isSymbolicLink()) return false;\n\t\t\tconst childPath = join(directory, child.name);\n\t\t\tconst entryPath = relative(destination, childPath).split(sep).join(\"/\");\n\t\t\tconst entry = expected.get(entryPath);\n\t\t\tif (!entry) return false;\n\t\t\tvisited++;\n\t\t\tif (visited > expected.size) return false;\n\t\t\tif (entry.type === \"directory\") {\n\t\t\t\tif (!child.isDirectory() || !visitDirectory(childPath)) return false;\n\t\t\t} else if (!child.isFile() || !fileMatches(childPath, entry.data!)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\treturn visitDirectory(destination) && visited === expected.size;\n}\n\nexport function receiveUpload(uploadDir: string, body: unknown): { path: string; files: number } {\n\tconst upload = validateBody(body);\n\tmkdirSync(uploadDir, { recursive: true });\n\tconst destination = join(uploadDir, upload.name);\n\tconst fileCount = upload.entries.filter((entry) => entry.type === \"file\").length;\n\tif (existsSync(destination)) {\n\t\tif (existingDestinationMatches(destination, upload.entries)) {\n\t\t\treturn { path: destination, files: fileCount };\n\t\t}\n\t\tthrow new ReceiveUploadError(409, `destination already exists with different contents: ${destination}`);\n\t}\n\tconst temporaryDir = mkdtempSync(join(uploadDir, \".upload-\"));\n\tconst temporaryRoot = join(temporaryDir, upload.name);\n\ttry {\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"directory\")) {\n\t\t\tmkdirSync(join(temporaryRoot, entry.path), { recursive: true });\n\t\t}\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"file\")) {\n\t\t\tconst path = join(temporaryRoot, entry.path);\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, entry.data!);\n\t\t}\n\t\trenameSync(temporaryRoot, destination);\n\t} finally {\n\t\trmSync(temporaryDir, { recursive: true, force: true });\n\t}\n\treturn { path: destination, files: fileCount };\n}\n"]}
|
package/dist/receive-upload.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
3
|
+
export const RECEIVE_UPLOAD_MAX_ENTRIES = 10_000;
|
|
4
|
+
export const RECEIVE_UPLOAD_MAX_NAME_CHARS = 255;
|
|
5
|
+
export const RECEIVE_UPLOAD_MAX_PATH_CHARS = 4096;
|
|
6
|
+
export const RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS = 255;
|
|
7
|
+
export const RECEIVE_UPLOAD_MAX_DECODED_BYTES = 64 * 1024 * 1024;
|
|
3
8
|
export class ReceiveUploadError extends Error {
|
|
4
9
|
status;
|
|
5
10
|
constructor(status, message) {
|
|
@@ -13,11 +18,17 @@ function invalid(message) {
|
|
|
13
18
|
function validatePath(path) {
|
|
14
19
|
if (typeof path !== "string")
|
|
15
20
|
invalid("entry path must be a string");
|
|
21
|
+
if (path.length > RECEIVE_UPLOAD_MAX_PATH_CHARS) {
|
|
22
|
+
invalid(`entry path must not exceed ${RECEIVE_UPLOAD_MAX_PATH_CHARS} characters`);
|
|
23
|
+
}
|
|
16
24
|
if (path.includes("\\") || path.includes("\0"))
|
|
17
25
|
invalid(`invalid entry path: ${path}`);
|
|
18
26
|
if (path !== "" && path.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
19
27
|
invalid(`invalid entry path: ${path}`);
|
|
20
28
|
}
|
|
29
|
+
if (path.split("/").some((part) => part.length > RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS)) {
|
|
30
|
+
invalid(`entry path segment must not exceed ${RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS} characters`);
|
|
31
|
+
}
|
|
21
32
|
return path;
|
|
22
33
|
}
|
|
23
34
|
function validateBody(body) {
|
|
@@ -30,13 +41,18 @@ function validateBody(body) {
|
|
|
30
41
|
upload.name === ".." ||
|
|
31
42
|
upload.name.includes("/") ||
|
|
32
43
|
upload.name.includes("\\") ||
|
|
33
|
-
upload.name.includes("\0")
|
|
44
|
+
upload.name.includes("\0") ||
|
|
45
|
+
upload.name.length > RECEIVE_UPLOAD_MAX_NAME_CHARS) {
|
|
34
46
|
invalid("invalid upload name");
|
|
35
47
|
}
|
|
36
48
|
if (!Array.isArray(upload.entries) || upload.entries.length === 0)
|
|
37
49
|
invalid("entries must be a non-empty array");
|
|
50
|
+
if (upload.entries.length > RECEIVE_UPLOAD_MAX_ENTRIES) {
|
|
51
|
+
invalid(`entries must not exceed ${RECEIVE_UPLOAD_MAX_ENTRIES}`);
|
|
52
|
+
}
|
|
38
53
|
const entries = [];
|
|
39
54
|
const entryTypes = new Map();
|
|
55
|
+
let decodedBytes = 0;
|
|
40
56
|
for (const value of upload.entries) {
|
|
41
57
|
if (typeof value !== "object" || value === null)
|
|
42
58
|
invalid("each entry must be an object");
|
|
@@ -58,6 +74,10 @@ function validateBody(body) {
|
|
|
58
74
|
const data = Buffer.from(entry.data, "base64");
|
|
59
75
|
if (data.toString("base64") !== entry.data)
|
|
60
76
|
invalid(`file entry data must be valid base64: ${path}`);
|
|
77
|
+
decodedBytes += data.byteLength;
|
|
78
|
+
if (decodedBytes > RECEIVE_UPLOAD_MAX_DECODED_BYTES) {
|
|
79
|
+
invalid(`decoded upload data must not exceed ${RECEIVE_UPLOAD_MAX_DECODED_BYTES} bytes`);
|
|
80
|
+
}
|
|
61
81
|
entries.push({ path, type: "file", data });
|
|
62
82
|
}
|
|
63
83
|
const rootType = entryTypes.get("");
|
|
@@ -74,12 +94,74 @@ function validateBody(body) {
|
|
|
74
94
|
}
|
|
75
95
|
return { name: upload.name, entries };
|
|
76
96
|
}
|
|
97
|
+
function fileMatches(path, expected) {
|
|
98
|
+
const stat = lstatSync(path);
|
|
99
|
+
if (!stat.isFile() || stat.size !== expected.byteLength)
|
|
100
|
+
return false;
|
|
101
|
+
const handle = openSync(path, "r");
|
|
102
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, expected.byteLength)));
|
|
103
|
+
let offset = 0;
|
|
104
|
+
try {
|
|
105
|
+
while (offset < expected.byteLength) {
|
|
106
|
+
const bytesRead = readSync(handle, buffer, 0, Math.min(buffer.byteLength, expected.byteLength - offset), offset);
|
|
107
|
+
if (bytesRead === 0 || !buffer.subarray(0, bytesRead).equals(expected.subarray(offset, offset + bytesRead))) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
offset += bytesRead;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
closeSync(handle);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function existingDestinationMatches(destination, entries) {
|
|
119
|
+
const expected = new Map(entries.map((entry) => [entry.path, entry]));
|
|
120
|
+
const root = expected.get("");
|
|
121
|
+
if (!root)
|
|
122
|
+
return false;
|
|
123
|
+
const destinationStat = lstatSync(destination);
|
|
124
|
+
if (root.type === "file") {
|
|
125
|
+
return !destinationStat.isSymbolicLink() && fileMatches(destination, root.data);
|
|
126
|
+
}
|
|
127
|
+
if (!destinationStat.isDirectory() || destinationStat.isSymbolicLink())
|
|
128
|
+
return false;
|
|
129
|
+
let visited = 1;
|
|
130
|
+
const visitDirectory = (directory) => {
|
|
131
|
+
for (const child of readdirSync(directory, { withFileTypes: true })) {
|
|
132
|
+
if (child.isSymbolicLink())
|
|
133
|
+
return false;
|
|
134
|
+
const childPath = join(directory, child.name);
|
|
135
|
+
const entryPath = relative(destination, childPath).split(sep).join("/");
|
|
136
|
+
const entry = expected.get(entryPath);
|
|
137
|
+
if (!entry)
|
|
138
|
+
return false;
|
|
139
|
+
visited++;
|
|
140
|
+
if (visited > expected.size)
|
|
141
|
+
return false;
|
|
142
|
+
if (entry.type === "directory") {
|
|
143
|
+
if (!child.isDirectory() || !visitDirectory(childPath))
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
else if (!child.isFile() || !fileMatches(childPath, entry.data)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
return visitDirectory(destination) && visited === expected.size;
|
|
153
|
+
}
|
|
77
154
|
export function receiveUpload(uploadDir, body) {
|
|
78
155
|
const upload = validateBody(body);
|
|
79
156
|
mkdirSync(uploadDir, { recursive: true });
|
|
80
157
|
const destination = join(uploadDir, upload.name);
|
|
81
|
-
|
|
82
|
-
|
|
158
|
+
const fileCount = upload.entries.filter((entry) => entry.type === "file").length;
|
|
159
|
+
if (existsSync(destination)) {
|
|
160
|
+
if (existingDestinationMatches(destination, upload.entries)) {
|
|
161
|
+
return { path: destination, files: fileCount };
|
|
162
|
+
}
|
|
163
|
+
throw new ReceiveUploadError(409, `destination already exists with different contents: ${destination}`);
|
|
164
|
+
}
|
|
83
165
|
const temporaryDir = mkdtempSync(join(uploadDir, ".upload-"));
|
|
84
166
|
const temporaryRoot = join(temporaryDir, upload.name);
|
|
85
167
|
try {
|
|
@@ -96,6 +178,6 @@ export function receiveUpload(uploadDir, body) {
|
|
|
96
178
|
finally {
|
|
97
179
|
rmSync(temporaryDir, { recursive: true, force: true });
|
|
98
180
|
}
|
|
99
|
-
return { path: destination, files:
|
|
181
|
+
return { path: destination, files: fileCount };
|
|
100
182
|
}
|
|
101
183
|
//# sourceMappingURL=receive-upload.js.map
|