@averyyy/pi-server 0.82.1-piclient.6 → 0.82.1-piclient.7
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 +0 -12
- package/README.md +0 -25
- package/bin/update.js +23 -156
- 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 +0 -16
- package/dist/pi-server-protocol.d.ts.map +1 -1
- package/dist/pi-server-protocol.js +1 -30
- package/dist/pi-server-protocol.js.map +1 -1
- package/dist/receive-upload.d.ts +0 -5
- package/dist/receive-upload.d.ts.map +1 -1
- package/dist/receive-upload.js +6 -88
- package/dist/receive-upload.js.map +1 -1
- package/dist/request-chunks.d.ts +0 -33
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +48 -540
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +3 -40
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +372 -3061
- package/dist/server.js.map +1 -1
- package/dist/session-persistence.d.ts +1 -39
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +116 -1132
- package/dist/session-persistence.js.map +1 -1
- package/dist/session-store.d.ts +6 -112
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +120 -695
- package/dist/session-store.js.map +1 -1
- package/package.json +3 -3
- package/dist/stream-run-persistence.d.ts +0 -163
- package/dist/stream-run-persistence.d.ts.map +0 -1
- package/dist/stream-run-persistence.js +0 -1824
- package/dist/stream-run-persistence.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -20,20 +20,8 @@
|
|
|
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.
|
|
33
|
-
- Removed provider-execution fingerprint admission checks so established custom-provider pi-client sessions are accepted again.
|
|
34
23
|
|
|
35
24
|
### Changed
|
|
36
25
|
|
|
37
26
|
- Updated the upstream Pi base through commit `0e6909f0`, including the latest provider and compaction fixes.
|
|
38
27
|
- Rebased the server on upstream Pi `0.80.6`, including GPT-5.6 model metadata and `max` thinking support.
|
|
39
|
-
- Added structured, secret-free provider-stream failure logs with response metadata preserved across the proxy.
|
package/README.md
CHANGED
|
@@ -36,31 +36,6 @@ 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
|
-
|
|
64
39
|
## Connect A Client
|
|
65
40
|
|
|
66
41
|
```bash
|
package/bin/update.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readFileSync
|
|
2
|
+
import { readFileSync } 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
|
-
|
|
11
6
|
function defaultPackageRoot() {
|
|
12
7
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
13
8
|
}
|
|
@@ -20,126 +15,28 @@ function readPackageMetadata(packageRoot) {
|
|
|
20
15
|
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8"));
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
function
|
|
24
|
-
|
|
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;
|
|
18
|
+
function runStep(runner, command, args, cwd, stdio = "inherit") {
|
|
19
|
+
return runner(command, args, { cwd, stdio, encoding: "utf-8" });
|
|
107
20
|
}
|
|
108
21
|
|
|
109
22
|
function isMissingGitCheckout(result) {
|
|
110
23
|
const text = `${String(result.stdout ?? "")}\n${String(result.stderr ?? "")}`;
|
|
111
|
-
return
|
|
24
|
+
return result.status !== 0 && text.includes("not a git repository");
|
|
112
25
|
}
|
|
113
26
|
|
|
114
|
-
function
|
|
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) {
|
|
27
|
+
async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr) {
|
|
125
28
|
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
|
-
];
|
|
134
29
|
const result = runStep(
|
|
135
30
|
runner,
|
|
136
|
-
|
|
137
|
-
[
|
|
31
|
+
"npm",
|
|
32
|
+
["install", "-g", "--ignore-scripts", "--legacy-peer-deps", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
|
|
138
33
|
repoRoot,
|
|
139
|
-
NPM_UPDATE_TIMEOUT_MS,
|
|
140
34
|
);
|
|
141
35
|
if (result.status !== 0) {
|
|
142
|
-
|
|
36
|
+
stderr.write(
|
|
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;
|
|
143
40
|
}
|
|
144
41
|
stdout.write("pi-server update complete\n");
|
|
145
42
|
return 0;
|
|
@@ -151,66 +48,36 @@ export async function runPiServerUpdate(_args = [], options = {}) {
|
|
|
151
48
|
const runner = options.runner ?? spawnSync;
|
|
152
49
|
const stdout = options.stdout ?? process.stdout;
|
|
153
50
|
const stderr = options.stderr ?? process.stderr;
|
|
154
|
-
const platform = options.platform ?? process.platform;
|
|
155
|
-
const nodePath = options.nodePath ?? process.execPath;
|
|
156
51
|
const pkg = readPackageMetadata(packageRoot);
|
|
157
52
|
|
|
158
53
|
stdout.write(`pi-server ${pkg.version}\n`);
|
|
159
54
|
stdout.write(`Updating checkout: ${repoRoot}\n`);
|
|
160
55
|
|
|
161
|
-
const
|
|
162
|
-
const status = runStep(runner, "git", statusArgs, repoRoot, GIT_STATUS_TIMEOUT_MS, "pipe");
|
|
56
|
+
const status = runStep(runner, "git", ["status", "--porcelain"], repoRoot, "pipe");
|
|
163
57
|
if (status.status !== 0) {
|
|
164
58
|
if (isMissingGitCheckout(status)) {
|
|
165
|
-
|
|
166
|
-
if (!npmInvocation) {
|
|
167
|
-
return 1;
|
|
168
|
-
}
|
|
169
|
-
return runNpmGlobalUpdate(runner, npmInvocation, repoRoot, stdout, stderr);
|
|
59
|
+
return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr);
|
|
170
60
|
}
|
|
171
|
-
|
|
61
|
+
stderr.write("pi-server update failed: unable to inspect git status\n");
|
|
62
|
+
return status.status ?? 1;
|
|
172
63
|
}
|
|
173
64
|
if (String(status.stdout ?? "").trim().length > 0) {
|
|
174
65
|
stderr.write("pi-server update failed: working tree has uncommitted changes\n");
|
|
175
66
|
return 1;
|
|
176
67
|
}
|
|
177
68
|
|
|
178
|
-
const npmInvocation = getNpmInvocation(platform, nodePath, stderr);
|
|
179
|
-
if (!npmInvocation) {
|
|
180
|
-
return 1;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
69
|
const steps = [
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
},
|
|
70
|
+
["git", ["pull", "--ff-only"]],
|
|
71
|
+
["npm", ["install", "--ignore-scripts"]],
|
|
72
|
+
["npm", ["run", "install:pi-client"]],
|
|
73
|
+
["npm", ["run", "install:pi-server"]],
|
|
208
74
|
];
|
|
209
75
|
|
|
210
|
-
for (const
|
|
211
|
-
const result = runStep(runner,
|
|
76
|
+
for (const [command, args] of steps) {
|
|
77
|
+
const result = runStep(runner, command, args, repoRoot);
|
|
212
78
|
if (result.status !== 0) {
|
|
213
|
-
|
|
79
|
+
stderr.write(`pi-server update failed: ${command} ${args.join(" ")}\n`);
|
|
80
|
+
return result.status ?? 1;
|
|
214
81
|
}
|
|
215
82
|
}
|
|
216
83
|
|
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,
|
|
4
|
-
export { appendAssistantResponse, appendMessages,
|
|
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";
|
|
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,
|
|
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;AAC1D,OAAO,EACN,uBAAuB,EACvB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,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, startServer } from \"./server.ts\";\nexport {\n\tappendAssistantResponse,\n\tappendMessages,\n\tclearAllSessions,\n\tdeleteSession,\n\texportSessionState,\n\tgetOrCreateSession,\n\tgetSession,\n\thashSessionEntries,\n\tlistSessions,\n\ttype PersistedSessionState,\n\trestoreSessionState,\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,
|
|
4
|
+
export { appendAssistantResponse, appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, restoreSessionState, 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,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EACN,uBAAuB,EACvB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,YAAY,EAEZ,mBAAmB,EAInB,gBAAgB,GAChB,MAAM,oBAAoB,CAAC","sourcesContent":["export { loadConfig, type ServerConfig } from \"./config.ts\";\nexport { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from \"./event-encoding.ts\";\nexport { createPiServer, startServer } from \"./server.ts\";\nexport {\n\tappendAssistantResponse,\n\tappendMessages,\n\tclearAllSessions,\n\tdeleteSession,\n\texportSessionState,\n\tgetOrCreateSession,\n\tgetSession,\n\thashSessionEntries,\n\tlistSessions,\n\ttype PersistedSessionState,\n\trestoreSessionState,\n\ttype SessionState,\n\ttype SessionStaticContext,\n\ttype SessionSummary,\n\tsetStaticContext,\n} from \"./session-store.ts\";\n"]}
|
|
@@ -4,22 +4,6 @@ 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;
|
|
23
7
|
export declare function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string;
|
|
24
8
|
export declare const PI_SERVER_EMPTY_TREE_HASH: string;
|
|
25
9
|
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,
|
|
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,CAW5F;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 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})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).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,28 +1,4 @@
|
|
|
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
|
-
}
|
|
26
2
|
export function hashPiServerStaticContext(context) {
|
|
27
3
|
if (!context)
|
|
28
4
|
return "";
|
|
@@ -32,14 +8,9 @@ export function hashPiServerStaticContext(context) {
|
|
|
32
8
|
name: tool.name,
|
|
33
9
|
description: tool.description,
|
|
34
10
|
parameters: tool.parameters,
|
|
35
|
-
constrainedSampling: tool.constrainedSampling,
|
|
36
11
|
})),
|
|
37
12
|
};
|
|
38
|
-
|
|
39
|
-
if (serialized === undefined) {
|
|
40
|
-
throw new Error("Failed to serialize pi-server static context");
|
|
41
|
-
}
|
|
42
|
-
return createHash("sha256").update(serialized).digest("hex");
|
|
13
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
43
14
|
}
|
|
44
15
|
export const PI_SERVER_EMPTY_TREE_HASH = createHash("sha256").update("pi-tree-v1").digest("hex");
|
|
45
16
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,MAAM,UAAU,yBAAyB,CAAC,OAA0C,EAAU;IAC7F,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,SAAS,GAAG;QACjB,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;SAC3B,CAAC,CAAC;KACH,CAAC;IACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEjG,MAAM,UAAU,qBAAqB,CAAC,KAAuB,EAAU;IACtE,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CACxE;AAED,MAAM,UAAU,sBAAsB,CAAC,YAAoB,EAAE,SAAiB,EAAU;IACvF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,YAAY,IAAI,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CACjF;AAED,MAAM,UAAU,6BAA6B,CAAC,OAA2B,EAAY;IACpF,MAAM,MAAM,GAAG,CAAC,yBAAyB,CAAC,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd;AAED,MAAM,UAAU,0BAA0B,CAAC,OAA2B,EAAU;IAC/E,OAAO,6BAA6B,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAAA,CAC9D","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 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})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).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,8 +1,3 @@
|
|
|
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;
|
|
6
1
|
export declare class ReceiveUploadError extends Error {
|
|
7
2
|
status: number;
|
|
8
3
|
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":"AASA,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;IAEf,YAAY,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAG1C;CACD;AA8DD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAqB/F","sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\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.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\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) {\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\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\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\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\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\tif (existsSync(destination)) throw new ReceiveUploadError(409, `destination already exists: ${destination}`);\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: upload.entries.filter((entry) => entry.type === \"file\").length };\n}\n"]}
|
package/dist/receive-upload.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join
|
|
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;
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
8
3
|
export class ReceiveUploadError extends Error {
|
|
9
4
|
status;
|
|
10
5
|
constructor(status, message) {
|
|
@@ -18,17 +13,11 @@ function invalid(message) {
|
|
|
18
13
|
function validatePath(path) {
|
|
19
14
|
if (typeof path !== "string")
|
|
20
15
|
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
|
-
}
|
|
24
16
|
if (path.includes("\\") || path.includes("\0"))
|
|
25
17
|
invalid(`invalid entry path: ${path}`);
|
|
26
18
|
if (path !== "" && path.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
27
19
|
invalid(`invalid entry path: ${path}`);
|
|
28
20
|
}
|
|
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
|
-
}
|
|
32
21
|
return path;
|
|
33
22
|
}
|
|
34
23
|
function validateBody(body) {
|
|
@@ -41,18 +30,13 @@ function validateBody(body) {
|
|
|
41
30
|
upload.name === ".." ||
|
|
42
31
|
upload.name.includes("/") ||
|
|
43
32
|
upload.name.includes("\\") ||
|
|
44
|
-
upload.name.includes("\0")
|
|
45
|
-
upload.name.length > RECEIVE_UPLOAD_MAX_NAME_CHARS) {
|
|
33
|
+
upload.name.includes("\0")) {
|
|
46
34
|
invalid("invalid upload name");
|
|
47
35
|
}
|
|
48
36
|
if (!Array.isArray(upload.entries) || upload.entries.length === 0)
|
|
49
37
|
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
|
-
}
|
|
53
38
|
const entries = [];
|
|
54
39
|
const entryTypes = new Map();
|
|
55
|
-
let decodedBytes = 0;
|
|
56
40
|
for (const value of upload.entries) {
|
|
57
41
|
if (typeof value !== "object" || value === null)
|
|
58
42
|
invalid("each entry must be an object");
|
|
@@ -74,10 +58,6 @@ function validateBody(body) {
|
|
|
74
58
|
const data = Buffer.from(entry.data, "base64");
|
|
75
59
|
if (data.toString("base64") !== entry.data)
|
|
76
60
|
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
|
-
}
|
|
81
61
|
entries.push({ path, type: "file", data });
|
|
82
62
|
}
|
|
83
63
|
const rootType = entryTypes.get("");
|
|
@@ -94,74 +74,12 @@ function validateBody(body) {
|
|
|
94
74
|
}
|
|
95
75
|
return { name: upload.name, entries };
|
|
96
76
|
}
|
|
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
|
-
}
|
|
154
77
|
export function receiveUpload(uploadDir, body) {
|
|
155
78
|
const upload = validateBody(body);
|
|
156
79
|
mkdirSync(uploadDir, { recursive: true });
|
|
157
80
|
const destination = join(uploadDir, upload.name);
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
}
|
|
81
|
+
if (existsSync(destination))
|
|
82
|
+
throw new ReceiveUploadError(409, `destination already exists: ${destination}`);
|
|
165
83
|
const temporaryDir = mkdtempSync(join(uploadDir, ".upload-"));
|
|
166
84
|
const temporaryRoot = join(temporaryDir, upload.name);
|
|
167
85
|
try {
|
|
@@ -178,6 +96,6 @@ export function receiveUpload(uploadDir, body) {
|
|
|
178
96
|
finally {
|
|
179
97
|
rmSync(temporaryDir, { recursive: true, force: true });
|
|
180
98
|
}
|
|
181
|
-
return { path: destination, files:
|
|
99
|
+
return { path: destination, files: upload.entries.filter((entry) => entry.type === "file").length };
|
|
182
100
|
}
|
|
183
101
|
//# sourceMappingURL=receive-upload.js.map
|