Files
toolbox-sdk/packages/contract/test/transfer.test.ts
T
zemion bc91659a42
Verify / verify (push) Canceled after 0s
Release Toolbox SDK 0.3.0
2026-09-02 00:58:50 +02:00

203 lines
6.1 KiB
TypeScript

import {
consumeToolboxTransfer,
createToolboxTransfer,
createToolboxTransferUrl,
readToolboxTransferToken,
type ToolboxTransfer,
type ToolboxTransferStore,
} from "../src/index.js";
import { describe, expect, it } from "vitest";
class MemoryStore implements ToolboxTransferStore {
readonly values = new Map<string, ToolboxTransfer>();
async put(transfer: ToolboxTransfer): Promise<void> {
this.values.set(transfer.token, transfer);
}
async take(
token: string,
expectedTargetAppId: string,
now: number,
): Promise<ToolboxTransfer | undefined> {
const value = this.values.get(token);
if (value !== undefined && value.targetAppId !== expectedTargetAppId) {
throw new Error("Artifact transfer was addressed to another application");
}
this.values.delete(token);
return value === undefined || value.expiresAt <= now ? undefined : value;
}
async deleteExpired(now: number): Promise<number> {
let count = 0;
for (const [token, value] of this.values) {
if (value.expiresAt <= now) {
this.values.delete(token);
count += 1;
}
}
return count;
}
}
const deterministicCrypto = {
getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (array instanceof Uint8Array)
array.forEach((_value, index) => (array[index] = index));
return array;
},
};
describe("one-time artifact transfers", () => {
it("stores, addresses and consumes an artifact exactly once", async () => {
const store = new MemoryStore();
const blob = new Blob(["hello"], { type: "text/plain" });
const transfer = await createToolboxTransfer(
{
source: { appId: "de.add-ideas.file-tools", appVersion: "1.0.0" },
targetAppId: "de.add-ideas.text-tools",
files: [
{ blob, name: "hello.txt", mediaType: blob.type, size: blob.size },
],
},
{ store, crypto: deterministicCrypto, now: () => 1_000 },
);
expect(transfer.token).toHaveLength(22);
const url = createToolboxTransferUrl(
"https://tools.test/apps/text/",
transfer.token,
{ location: "https://tools.test/apps/file/" },
);
expect(readToolboxTransferToken(url)).toBe(transfer.token);
const consumed = await consumeToolboxTransfer(
transfer.token,
"de.add-ideas.text-tools",
{ store, now: () => 2_000 },
);
expect(await consumed?.files[0]?.blob.text()).toBe("hello");
expect(
await consumeToolboxTransfer(transfer.token, "de.add-ideas.text-tools", {
store,
now: () => 2_000,
}),
).toBeUndefined();
});
it("rejects expired, oversized and wrongly addressed transfers", async () => {
const store = new MemoryStore();
const blob = new Blob(["hello"]);
const transfer = await createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
ttlMs: 10,
files: [{ blob, name: "a.bin", mediaType: "", size: blob.size }],
},
{ store, crypto: deterministicCrypto, now: () => 10 },
);
await expect(
consumeToolboxTransfer(transfer.token, "target", {
store,
now: () => 20,
}),
).resolves.toBeUndefined();
await expect(
createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
maxBytes: 4,
files: [{ blob, name: "a.bin", mediaType: "", size: blob.size }],
},
{ store, crypto: deterministicCrypto },
),
).rejects.toThrow(/exceeds/u);
await expect(
createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
maxBytes: 512 * 1024 * 1024 + 1,
files: [{ blob, name: "a.bin", mediaType: "", size: blob.size }],
},
{ store, crypto: deterministicCrypto },
),
).rejects.toThrow(/invalid/iu);
await expect(
createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
files: [{ blob, name: "a.bin", mediaType: "", size: blob.size }],
},
{ store, crypto: deterministicCrypto, now: () => Number.NaN },
),
).rejects.toThrow(/creation time/iu);
const addressed = await createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
files: [{ blob, name: "a.bin", mediaType: "", size: blob.size }],
},
{ store, crypto: deterministicCrypto },
);
await expect(
consumeToolboxTransfer(addressed.token, "another-target", { store }),
).rejects.toThrow(/another application/u);
});
it("keeps opaque handoff tokens on-origin and rejects unsafe descriptors", async () => {
const store = new MemoryStore();
const blob = new Blob(["hello"]);
await expect(
createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
files: [
{
blob,
name: "../hello.txt",
mediaType: "text/plain",
size: blob.size,
},
],
},
{ store, crypto: deterministicCrypto },
),
).rejects.toThrow(/name is invalid/u);
expect(() =>
createToolboxTransferUrl(
"https://other.test/apps/text/",
"AAECAwQFBgcICQoLDA0ODw",
{ location: "https://tools.test/apps/file/" },
),
).toThrow(/current origin/u);
});
it("revalidates same-origin storage records at the consuming trust boundary", async () => {
const store = new MemoryStore();
const blob = new Blob(["hello"]);
const transfer = await createToolboxTransfer(
{
source: { appId: "source" },
targetAppId: "target",
files: [{ blob, name: "hello.txt", mediaType: "text/plain", size: 5 }],
},
{ store, crypto: deterministicCrypto, now: () => 100 },
);
store.values.set(transfer.token, {
...transfer,
artifactVersion: 99 as 1,
});
await expect(
consumeToolboxTransfer(transfer.token, "target", {
store,
now: () => 200,
}),
).rejects.toThrow(/identity is invalid/u);
expect(store.values.has(transfer.token)).toBe(false);
});
});