@stigmer/server 3.22.1 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifactstorage/artifact-storage.d.ts +29 -31
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +5 -7
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/artifactstorage/r2-storage.d.ts +12 -7
- package/dist/artifactstorage/r2-storage.d.ts.map +1 -1
- package/dist/artifactstorage/r2-storage.js +6 -14
- package/dist/artifactstorage/r2-storage.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +49 -24
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +4 -3
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js.map +1 -1
- package/dist/boot/skill-transfer-origin.d.ts +52 -0
- package/dist/boot/skill-transfer-origin.d.ts.map +1 -0
- package/dist/boot/skill-transfer-origin.js +54 -0
- package/dist/boot/skill-transfer-origin.js.map +1 -0
- package/dist/domain/plugin/controller.d.ts +3 -7
- package/dist/domain/plugin/controller.d.ts.map +1 -1
- package/dist/domain/plugin/controller.js +8 -8
- package/dist/domain/plugin/controller.js.map +1 -1
- package/dist/domain/skill/constants.d.ts +26 -3
- package/dist/domain/skill/constants.d.ts.map +1 -1
- package/dist/domain/skill/constants.js +26 -3
- package/dist/domain/skill/constants.js.map +1 -1
- package/dist/domain/skill/controller.d.ts +6 -11
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +33 -22
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/skill/push.d.ts +2 -2
- package/dist/domain/skill/push.d.ts.map +1 -1
- package/dist/domain/skill/push.js +2 -2
- package/dist/domain/skill/push.js.map +1 -1
- package/dist/domain/skill/transfer/handler.d.ts +17 -9
- package/dist/domain/skill/transfer/handler.d.ts.map +1 -1
- package/dist/domain/skill/transfer/handler.js +19 -8
- package/dist/domain/skill/transfer/handler.js.map +1 -1
- package/dist/domain/skill/transfer/slots.d.ts +26 -41
- package/dist/domain/skill/transfer/slots.d.ts.map +1 -1
- package/dist/domain/skill/transfer/slots.js +80 -87
- package/dist/domain/skill/transfer/slots.js.map +1 -1
- package/dist/domain/skill/transfer/staging.d.ts +48 -0
- package/dist/domain/skill/transfer/staging.d.ts.map +1 -0
- package/dist/domain/skill/transfer/staging.js +127 -0
- package/dist/domain/skill/transfer/staging.js.map +1 -0
- package/dist/pipeline/steps/resolve-artifact-source.d.ts +11 -8
- package/dist/pipeline/steps/resolve-artifact-source.d.ts.map +1 -1
- package/dist/pipeline/steps/resolve-artifact-source.js +8 -4
- package/dist/pipeline/steps/resolve-artifact-source.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +36 -33
- package/src/artifactstorage/__tests__/r2-storage.test.ts +11 -13
- package/src/artifactstorage/artifact-storage.ts +37 -33
- package/src/artifactstorage/r2-storage.ts +6 -15
- package/src/boot/__tests__/skill-transfer-origin.test.ts +72 -0
- package/src/boot/compose.ts +62 -25
- package/src/boot/config.ts +4 -3
- package/src/boot/skill-transfer-origin.ts +76 -0
- package/src/domain/plugin/controller.ts +15 -17
- package/src/domain/skill/__tests__/skill.test.ts +5 -3
- package/src/domain/skill/__tests__/slots.test.ts +113 -60
- package/src/domain/skill/__tests__/staging.test.ts +304 -0
- package/src/domain/skill/constants.ts +28 -3
- package/src/domain/skill/controller.ts +44 -35
- package/src/domain/skill/push.ts +3 -3
- package/src/domain/skill/transfer/handler.ts +33 -15
- package/src/domain/skill/transfer/slots.ts +89 -100
- package/src/domain/skill/transfer/staging.ts +191 -0
- package/src/pipeline/steps/resolve-artifact-source.ts +20 -13
- package/src/transport/registry/__tests__/lanes.test.ts +1 -1
package/src/boot/config.ts
CHANGED
|
@@ -121,9 +121,10 @@ export interface ServerConfig {
|
|
|
121
121
|
readonly oauthRedirectUri: string;
|
|
122
122
|
/**
|
|
123
123
|
* Skill artifact storage root (STORAGE_PATH; Go defaultStoragePath
|
|
124
|
-
* ~/.stigmer/storage). Artifacts live at {storagePath}/skills
|
|
125
|
-
*
|
|
126
|
-
*
|
|
124
|
+
* ~/.stigmer/storage). Artifacts live at {storagePath}/skills/ —
|
|
125
|
+
* byte-identical to Go's layout, so a Go-written directory is served in
|
|
126
|
+
* place at cutover — and upload staging at {storagePath}/skills/staging/,
|
|
127
|
+
* the prefix the domain names (domain/skill/constants.ts).
|
|
127
128
|
*/
|
|
128
129
|
readonly storagePath: string;
|
|
129
130
|
/**
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the skill transfer lane's origin can be reached by the callers
|
|
3
|
+
* it is minted for.
|
|
4
|
+
*
|
|
5
|
+
* On the local blob driver the server itself carries artifact bytes over
|
|
6
|
+
* HTTP, and every upload and download capability URL it mints is rendered
|
|
7
|
+
* from `SKILL_TRANSFER_BASE_URL`, which defaults to the server's own
|
|
8
|
+
* loopback. That default is right for a single-operator install reached
|
|
9
|
+
* on the machine it runs on, and silently wrong for a hosted one: the
|
|
10
|
+
* server boots, serves, and passes every in-process check while handing
|
|
11
|
+
* the CLI, the SDKs and the sandbox runner a host none of them can reach —
|
|
12
|
+
* every push above the gRPC cap fails on the PUT and every sandbox mount
|
|
13
|
+
* of a skill with supporting files loses them (stigmer#1219). No test
|
|
14
|
+
* inside the process can see it, so the process says it at boot.
|
|
15
|
+
*
|
|
16
|
+
* The hosted signal is the require-authentication posture (boot/compose.ts
|
|
17
|
+
* combines its two sources): a server that requires credentials serves
|
|
18
|
+
* callers it does not share a machine with. A bucket driver signs URLs
|
|
19
|
+
* straight to its bucket and never renders the base URL, so the verdict
|
|
20
|
+
* is moot there and says so rather than warning about a knob nothing
|
|
21
|
+
* reads.
|
|
22
|
+
*
|
|
23
|
+
* Proven by __tests__/skill-transfer-origin.test.ts.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Hosts that name the process's own machine and nothing beyond it. */
|
|
27
|
+
const LOOPBACK_HOSTS = new Set([
|
|
28
|
+
"localhost",
|
|
29
|
+
"127.0.0.1",
|
|
30
|
+
"[::1]",
|
|
31
|
+
"0.0.0.0",
|
|
32
|
+
"[::]",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export type SkillTransferOriginVerdict =
|
|
36
|
+
/** The base URL names a host other than the server's own loopback. */
|
|
37
|
+
| { readonly kind: "reachable" }
|
|
38
|
+
/** Loopback on a single-operator install: the default, and correct. */
|
|
39
|
+
| { readonly kind: "loopback-local" }
|
|
40
|
+
/** Loopback on a server that requires authentication: minted URLs are dead on arrival. */
|
|
41
|
+
| { readonly kind: "loopback-hosted"; readonly baseUrl: string }
|
|
42
|
+
/** The blob driver signs its own URLs; the base URL is never rendered. */
|
|
43
|
+
| { readonly kind: "unused" };
|
|
44
|
+
|
|
45
|
+
export interface SkillTransferOriginInputs {
|
|
46
|
+
/** The configured or defaulted `SKILL_TRANSFER_BASE_URL`. */
|
|
47
|
+
readonly baseUrl: string;
|
|
48
|
+
/** Whether the local driver relays artifact bytes through this server. */
|
|
49
|
+
readonly relaysThroughServer: boolean;
|
|
50
|
+
/** The composed require-authentication posture. */
|
|
51
|
+
readonly requireAuthentication: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assessSkillTransferOrigin(
|
|
55
|
+
inputs: SkillTransferOriginInputs,
|
|
56
|
+
): SkillTransferOriginVerdict {
|
|
57
|
+
if (!inputs.relaysThroughServer) {
|
|
58
|
+
return { kind: "unused" };
|
|
59
|
+
}
|
|
60
|
+
if (!namesLoopback(inputs.baseUrl)) {
|
|
61
|
+
return { kind: "reachable" };
|
|
62
|
+
}
|
|
63
|
+
return inputs.requireAuthentication
|
|
64
|
+
? { kind: "loopback-hosted", baseUrl: inputs.baseUrl }
|
|
65
|
+
: { kind: "loopback-local" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function namesLoopback(baseUrl: string): boolean {
|
|
69
|
+
try {
|
|
70
|
+
return LOOPBACK_HOSTS.has(new URL(baseUrl).hostname);
|
|
71
|
+
} catch {
|
|
72
|
+
// An unparseable base URL renders into every capability URL as-is; it
|
|
73
|
+
// is not loopback, and the failure it causes is loud on the first mint.
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -106,8 +106,10 @@ import type { VersionHistoryBinding } from "../../pipeline/steps/version-history
|
|
|
106
106
|
import { newAuthorizeVisibilityTransitionStep } from "../../pipeline/steps/visibility-gates.js";
|
|
107
107
|
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
108
108
|
import type { Store } from "../../store/interface.js";
|
|
109
|
-
import {
|
|
110
|
-
|
|
109
|
+
import type {
|
|
110
|
+
ArchiveStaging,
|
|
111
|
+
StagedUpload,
|
|
112
|
+
} from "../skill/transfer/staging.js";
|
|
111
113
|
import { TRANSFER_LANE_NOT_CONFIGURED } from "./constants.js";
|
|
112
114
|
import type { PluginMaterializerProvider } from "./materialize/ports.js";
|
|
113
115
|
import { findMembers } from "./members.js";
|
|
@@ -140,12 +142,6 @@ import {
|
|
|
140
142
|
} from "./push.js";
|
|
141
143
|
import { pluginSearchExtractor } from "./search-extractor.js";
|
|
142
144
|
|
|
143
|
-
/** The transfer lane's controller-side pair; the slots are the skill lane's (one upload surface). */
|
|
144
|
-
export interface PluginTransferLaneDeps {
|
|
145
|
-
readonly slots: UploadSlots;
|
|
146
|
-
readonly baseUrl: string;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
145
|
export interface PluginControllerDeps {
|
|
150
146
|
readonly store: Store;
|
|
151
147
|
readonly logger: Logger;
|
|
@@ -153,7 +149,8 @@ export interface PluginControllerDeps {
|
|
|
153
149
|
readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
|
|
154
150
|
readonly artifactStorage: ContentAddressedArchiveStore;
|
|
155
151
|
readonly materializerProvider: PluginMaterializerProvider;
|
|
156
|
-
|
|
152
|
+
/** The skill lane's staging port (one upload surface for every archive); absent, the lane answers FailedPrecondition. */
|
|
153
|
+
readonly staging?: ArchiveStaging;
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
/** Registers both plugin services on the router (routes stage). */
|
|
@@ -208,7 +205,7 @@ async function push(
|
|
|
208
205
|
.addStep(newValidateProtoStep())
|
|
209
206
|
.addStep(
|
|
210
207
|
newResolveArtifactSourceStep<typeof PushPluginRequestSchema>(
|
|
211
|
-
deps.
|
|
208
|
+
deps.staging,
|
|
212
209
|
{
|
|
213
210
|
source: (input) => ({
|
|
214
211
|
artifact: input.artifact,
|
|
@@ -274,16 +271,17 @@ async function push(
|
|
|
274
271
|
// ─── createArtifactUploadUrl ─────────────────────────────────────────────
|
|
275
272
|
|
|
276
273
|
/**
|
|
277
|
-
* Mints a short-lived, single-use upload URL
|
|
278
|
-
* over-limit declaration is refused with the limit BEFORE any
|
|
274
|
+
* Mints a short-lived, single-use upload URL through the shared staging
|
|
275
|
+
* port; an over-limit declaration is refused with the limit BEFORE any
|
|
276
|
+
* bytes move.
|
|
279
277
|
*/
|
|
280
278
|
async function createArtifactUploadUrl(
|
|
281
279
|
deps: PluginControllerDeps,
|
|
282
280
|
req: CreatePluginArtifactUploadUrlRequest,
|
|
283
281
|
ctx: HandlerContext,
|
|
284
282
|
): Promise<PluginArtifactUploadUrl> {
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
283
|
+
const staging = deps.staging;
|
|
284
|
+
if (staging === undefined) {
|
|
287
285
|
throw failedPreconditionError(TRANSFER_LANE_NOT_CONFIGURED);
|
|
288
286
|
}
|
|
289
287
|
const reqCtx = new RequestContext(
|
|
@@ -310,14 +308,14 @@ async function createArtifactUploadUrl(
|
|
|
310
308
|
`plugin archive size ${req.sizeBytes} bytes exceeds the ${MAX_ZIP_SIZE}-byte (100MB) archive limit`,
|
|
311
309
|
);
|
|
312
310
|
}
|
|
313
|
-
let minted:
|
|
311
|
+
let minted: StagedUpload;
|
|
314
312
|
try {
|
|
315
|
-
minted =
|
|
313
|
+
minted = await staging.mint(Number(req.sizeBytes));
|
|
316
314
|
} catch (error) {
|
|
317
315
|
throw internalError(error, "failed to mint upload reference");
|
|
318
316
|
}
|
|
319
317
|
return create(PluginArtifactUploadUrlSchema, {
|
|
320
|
-
url:
|
|
318
|
+
url: minted.url,
|
|
321
319
|
artifactUploadRef: minted.ref,
|
|
322
320
|
ttlSeconds: Math.trunc(minted.ttlMs / 1000),
|
|
323
321
|
});
|
|
@@ -434,7 +434,7 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
|
|
|
434
434
|
);
|
|
435
435
|
});
|
|
436
436
|
|
|
437
|
-
it("a ref whose slot was minted but never uploaded is rejected at push", async () => {
|
|
437
|
+
it("a ref whose slot was minted but never uploaded is rejected at push — the one condition every driver can answer", async () => {
|
|
438
438
|
const minted = await command.createArtifactUploadUrl({ org: ORG, sizeBytes: 1024n });
|
|
439
439
|
const err = await expectCode(
|
|
440
440
|
command.push({ org: ORG, artifactUploadRef: minted.artifactUploadRef }),
|
|
@@ -442,7 +442,7 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
|
|
|
442
442
|
"empty slot",
|
|
443
443
|
);
|
|
444
444
|
expect(err.rawMessage).toBe(
|
|
445
|
-
"artifact_upload_ref not usable: upload reference
|
|
445
|
+
"artifact_upload_ref not usable: upload reference unknown or expired — request a new upload URL via createArtifactUploadUrl",
|
|
446
446
|
);
|
|
447
447
|
});
|
|
448
448
|
|
|
@@ -499,7 +499,9 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
|
|
|
499
499
|
const minted = await query.getArtifactDownloadUrl({
|
|
500
500
|
artifactStorageKey: pushed.status!.artifactStorageKey,
|
|
501
501
|
});
|
|
502
|
-
|
|
502
|
+
// The floor of the URL's validity, the same on every driver; the local
|
|
503
|
+
// lane's content-hash URL simply outlives it.
|
|
504
|
+
expect(minted.ttlSeconds).toBe(3600);
|
|
503
505
|
expect(minted.sizeBytes).toBe(BigInt(artifact.length));
|
|
504
506
|
|
|
505
507
|
const resp = await fetch(minted.url);
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pins the upload-slot registry against Go's slots_test.go
|
|
3
|
-
*
|
|
4
|
-
* (
|
|
5
|
-
*
|
|
2
|
+
* Pins the upload-slot registry against Go's slots_test.go, keyed by the
|
|
3
|
+
* domain's staging key: reservation bounds and key shape, TTL expiry
|
|
4
|
+
* (injected clock), exact-declared-size enforcement (short AND long
|
|
5
|
+
* bodies), single upload per key, staged-file placement under the driver
|
|
6
|
+
* root, expiry sweep on reservation, the boot-time staging wipe, and the
|
|
7
|
+
* staging directory's recreation after the driver's delete pruned it.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
8
18
|
import { tmpdir } from "node:os";
|
|
9
19
|
import path from "node:path";
|
|
10
20
|
import { Readable } from "node:stream";
|
|
11
21
|
|
|
12
22
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
23
|
|
|
24
|
+
import { STAGING_KEY_PREFIX } from "../constants.js";
|
|
14
25
|
import {
|
|
15
26
|
SizeMismatchError,
|
|
16
27
|
SlotConsumedError,
|
|
17
|
-
SlotEmptyError,
|
|
18
28
|
SlotUnknownError,
|
|
19
29
|
UploadSlots,
|
|
20
30
|
} from "../transfer/slots.js";
|
|
31
|
+
import { newUploadRef, stagingKeyOf } from "../transfer/staging.js";
|
|
21
32
|
|
|
22
33
|
const TTL_MS = 15 * 60 * 1000;
|
|
23
34
|
const MAX_SIZE = 100 * 1024 * 1024;
|
|
24
35
|
|
|
25
|
-
let
|
|
36
|
+
let root: string;
|
|
26
37
|
let stagingDir: string;
|
|
27
38
|
let nowMs: number;
|
|
28
39
|
let slots: UploadSlots;
|
|
@@ -31,101 +42,143 @@ function body(text: string): Readable {
|
|
|
31
42
|
return Readable.from([Buffer.from(text)]);
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
/** A fresh, well-formed staging key — what the domain's mint hands the registry. */
|
|
46
|
+
function freshKey(): string {
|
|
47
|
+
const key = stagingKeyOf(newUploadRef());
|
|
48
|
+
if (key === undefined) throw new Error("a minted ref always has a key");
|
|
49
|
+
return key;
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
beforeEach(() => {
|
|
35
|
-
|
|
36
|
-
stagingDir = path.join(
|
|
53
|
+
root = mkdtempSync(path.join(tmpdir(), "skill-slots-test-"));
|
|
54
|
+
stagingDir = path.join(root, STAGING_KEY_PREFIX);
|
|
37
55
|
nowMs = 1_000_000;
|
|
38
|
-
slots = new UploadSlots(
|
|
56
|
+
slots = new UploadSlots(
|
|
57
|
+
root,
|
|
58
|
+
STAGING_KEY_PREFIX,
|
|
59
|
+
TTL_MS,
|
|
60
|
+
MAX_SIZE,
|
|
61
|
+
() => nowMs,
|
|
62
|
+
);
|
|
39
63
|
});
|
|
40
64
|
|
|
41
65
|
afterEach(() => {
|
|
42
|
-
rmSync(
|
|
66
|
+
rmSync(root, { recursive: true, force: true });
|
|
43
67
|
});
|
|
44
68
|
|
|
45
69
|
describe("UploadSlots", () => {
|
|
46
|
-
it("
|
|
47
|
-
|
|
48
|
-
expect(ref).toMatch(/^sau_[0-9a-f]{32}$/);
|
|
49
|
-
expect(ttlMs).toBe(TTL_MS);
|
|
70
|
+
it("reserves a key with the registry TTL", () => {
|
|
71
|
+
expect(slots.reserve(freshKey(), 1024)).toEqual({ ttlMs: TTL_MS });
|
|
50
72
|
});
|
|
51
73
|
|
|
52
|
-
it("rejects out-of-bounds declarations at
|
|
53
|
-
expect(() => slots.
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
it("rejects out-of-bounds declarations at reservation", () => {
|
|
75
|
+
expect(() => slots.reserve(freshKey(), 0)).toThrow(
|
|
76
|
+
"declared size 0 outside (0, 104857600]",
|
|
77
|
+
);
|
|
78
|
+
expect(() => slots.reserve(freshKey(), -1)).toThrow(
|
|
79
|
+
`declared size -1 outside (0, ${MAX_SIZE}]`,
|
|
80
|
+
);
|
|
81
|
+
expect(() => slots.reserve(freshKey(), MAX_SIZE + 1)).toThrow(
|
|
56
82
|
`declared size ${MAX_SIZE + 1} outside (0, ${MAX_SIZE}]`,
|
|
57
83
|
);
|
|
58
84
|
});
|
|
59
85
|
|
|
60
|
-
it("
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
expect(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(
|
|
86
|
+
it("refuses a key outside the staging prefix or not shaped like a staged archive", () => {
|
|
87
|
+
expect(() => slots.reserve("skills/deadbeef.zip", 1)).toThrow(
|
|
88
|
+
`staging key skills/deadbeef.zip is outside ${STAGING_KEY_PREFIX}`,
|
|
89
|
+
);
|
|
90
|
+
expect(() =>
|
|
91
|
+
slots.reserve(`${STAGING_KEY_PREFIX}../escape.zip`, 1),
|
|
92
|
+
).toThrow("does not name a staged archive");
|
|
93
|
+
expect(() => slots.reserve(`${STAGING_KEY_PREFIX}notes.txt`, 1)).toThrow(
|
|
94
|
+
"does not name a staged archive",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("refuses to reserve the same key twice", () => {
|
|
99
|
+
const key = freshKey();
|
|
100
|
+
slots.reserve(key, 1);
|
|
101
|
+
expect(() => slots.reserve(key, 1)).toThrow(
|
|
102
|
+
`staging key ${key} is already reserved`,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stages a received body at the key under the root — where the driver reads it", async () => {
|
|
107
|
+
const key = freshKey();
|
|
108
|
+
slots.reserve(key, 5);
|
|
109
|
+
await slots.receive(key, body("hello"));
|
|
110
|
+
expect(readFileSync(path.join(root, key), "utf8")).toBe("hello");
|
|
68
111
|
});
|
|
69
112
|
|
|
70
|
-
it("rejects receive on an
|
|
71
|
-
await expect(slots.receive(
|
|
113
|
+
it("rejects receive on an unreserved key", async () => {
|
|
114
|
+
await expect(slots.receive(freshKey(), body("x"))).rejects.toThrow(
|
|
115
|
+
SlotUnknownError,
|
|
116
|
+
);
|
|
72
117
|
});
|
|
73
118
|
|
|
74
|
-
it("rejects receive on an expired
|
|
75
|
-
const
|
|
119
|
+
it("rejects receive on an expired key", async () => {
|
|
120
|
+
const key = freshKey();
|
|
121
|
+
slots.reserve(key, 1);
|
|
76
122
|
nowMs += TTL_MS + 1;
|
|
77
|
-
await expect(slots.receive(
|
|
123
|
+
await expect(slots.receive(key, body("x"))).rejects.toThrow(
|
|
124
|
+
SlotUnknownError,
|
|
125
|
+
);
|
|
78
126
|
});
|
|
79
127
|
|
|
80
|
-
it("rejects a second upload to the same
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
await
|
|
128
|
+
it("rejects a second upload to the same key", async () => {
|
|
129
|
+
const key = freshKey();
|
|
130
|
+
slots.reserve(key, 1);
|
|
131
|
+
await slots.receive(key, body("x"));
|
|
132
|
+
await expect(slots.receive(key, body("x"))).rejects.toThrow(
|
|
133
|
+
SlotConsumedError,
|
|
134
|
+
);
|
|
84
135
|
});
|
|
85
136
|
|
|
86
137
|
it("rejects a short body, naming both sizes, and stages nothing", async () => {
|
|
87
|
-
const
|
|
88
|
-
|
|
138
|
+
const key = freshKey();
|
|
139
|
+
slots.reserve(key, 10);
|
|
140
|
+
await expect(slots.receive(key, body("short"))).rejects.toThrow(
|
|
89
141
|
"upload size mismatch: received 5 bytes, declared 10",
|
|
90
142
|
);
|
|
91
|
-
await expect(slots.receive(
|
|
143
|
+
await expect(slots.receive(key, body("short"))).rejects.toBeInstanceOf(
|
|
144
|
+
SizeMismatchError,
|
|
145
|
+
);
|
|
92
146
|
expect(readdirSync(stagingDir)).toEqual([]);
|
|
93
147
|
});
|
|
94
148
|
|
|
95
149
|
it("rejects an over-declaration body without buffering it whole (declared+1 proof)", async () => {
|
|
96
|
-
const
|
|
97
|
-
|
|
150
|
+
const key = freshKey();
|
|
151
|
+
slots.reserve(key, 3);
|
|
152
|
+
await expect(slots.receive(key, body("longer than three"))).rejects.toThrow(
|
|
98
153
|
"upload size mismatch: received 4 bytes, declared 3",
|
|
99
154
|
);
|
|
100
155
|
});
|
|
101
156
|
|
|
102
|
-
it("
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
it("rejects consume on an expired ref", async () => {
|
|
108
|
-
const { ref } = slots.mint(1);
|
|
109
|
-
await slots.receive(ref, body("x"));
|
|
110
|
-
nowMs += TTL_MS + 1;
|
|
111
|
-
await expect(slots.consume(ref)).rejects.toThrow(SlotUnknownError);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("sweeps expired slots and their staged files on mint", async () => {
|
|
115
|
-
const { ref } = slots.mint(1);
|
|
116
|
-
await slots.receive(ref, body("x"));
|
|
157
|
+
it("sweeps expired slots and their staged files on reservation", async () => {
|
|
158
|
+
const key = freshKey();
|
|
159
|
+
slots.reserve(key, 1);
|
|
160
|
+
await slots.receive(key, body("x"));
|
|
117
161
|
expect(readdirSync(stagingDir)).toHaveLength(1);
|
|
118
162
|
nowMs += TTL_MS + 1;
|
|
119
|
-
slots.
|
|
163
|
+
slots.reserve(freshKey(), 1);
|
|
120
164
|
expect(readdirSync(stagingDir)).toEqual([]);
|
|
121
165
|
});
|
|
122
166
|
|
|
167
|
+
it("recreates the staging directory when the driver's delete pruned it between uploads", async () => {
|
|
168
|
+
const key = freshKey();
|
|
169
|
+
slots.reserve(key, 1);
|
|
170
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
171
|
+
await slots.receive(key, body("x"));
|
|
172
|
+
expect(readFileSync(path.join(root, key), "utf8")).toBe("x");
|
|
173
|
+
});
|
|
174
|
+
|
|
123
175
|
it("wipes the staging directory at construction (orphans are unreachable)", () => {
|
|
124
|
-
const
|
|
176
|
+
const orphanRoot = path.join(root, "orphan-root");
|
|
177
|
+
const orphanDir = path.join(orphanRoot, STAGING_KEY_PREFIX);
|
|
125
178
|
mkdirSync(orphanDir, { recursive: true });
|
|
126
|
-
const orphan = path.join(orphanDir, "
|
|
179
|
+
const orphan = path.join(orphanDir, "deadbeef.zip");
|
|
127
180
|
writeFileSync(orphan, "stale bytes");
|
|
128
|
-
new UploadSlots(
|
|
181
|
+
new UploadSlots(orphanRoot, STAGING_KEY_PREFIX, TTL_MS, MAX_SIZE);
|
|
129
182
|
expect(existsSync(orphan)).toBe(false);
|
|
130
183
|
expect(readdirSync(orphanDir)).toEqual([]);
|
|
131
184
|
});
|