@nylorun/runtime 0.15.2-beta → 0.17.0-beta
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 +134 -0
- package/README.md +68 -29
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +341 -7
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +15 -13
- package/dist/api/components.d.ts +2667 -711
- package/dist/api/components.js +24 -11
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +6 -0
- package/dist/api/http/define.d.ts +19 -12
- package/dist/api/http/define.js +13 -27
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +12 -172
- package/dist/api/http/routes/actions.js +0 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +516 -0
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +45 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +150 -0
- package/dist/api/http/routes/sessions.js +9 -13
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/api/http/routes/vaults.d.ts +12 -3
- package/dist/api/http/routes/vaults.js +122 -54
- package/dist/api/openapi.js +4 -10
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/admin-api.d.ts +12 -5
- package/dist/host/admin-api.js +72 -6
- package/dist/host/app.d.ts +0 -1
- package/dist/host/app.js +35 -24
- package/dist/host/config-for.d.ts +4 -0
- package/dist/host/config-for.js +2 -0
- package/dist/host/config.d.ts +0 -6
- package/dist/host/create-host.d.ts +3 -10
- package/dist/host/create-host.js +4 -1
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +52 -2
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +167 -37
- package/dist/host/stack-config.d.ts +112 -9
- package/dist/host/stack-config.js +222 -20
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +8 -0
- package/dist/mcp/connect.js +2 -1
- package/dist/mcp/pool.d.ts +1 -0
- package/dist/mcp/pool.js +1 -0
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +14084 -9426
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +1168 -250
- package/dist/store/postgres/schema.js +174 -28
- package/dist/store/postgres/store.js +339 -103
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +163 -32
- package/dist/streams/control.d.ts +2 -5
- package/dist/streams/control.js +5 -8
- package/dist/streams/types.d.ts +8 -8
- package/dist/streams/types.js +1 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +31 -10
- package/dist/tenant/context.d.ts +83 -26
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +24 -14
- package/dist/tenant/effects.js +10 -398
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/operator-keys.d.ts +18 -0
- package/dist/tenant/operator-keys.js +55 -0
- package/dist/tenant/outbound.d.ts +30 -0
- package/dist/tenant/outbound.js +142 -10
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +42 -0
- package/dist/tenant/runtime.js +134 -33
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +6 -1
- package/dist/tenant/sessions.js +29 -2
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +8 -2
- package/dist/tenant/streams.js +25 -6
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/types.d.ts +39 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +54 -0
- package/dist/vault/sources.js +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -180
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `WorkspaceReader` seam (F8.2): how core reads a session's sandbox workspace at turn end, to
|
|
3
|
+
* export its outputs as a folder artifact. Two calls, a listing and one file's bytes, both the
|
|
4
|
+
* claim of whoever holds the workspace: the Runtime stores what it is given without observing it.
|
|
5
|
+
*
|
|
6
|
+
* One implementation, `sandboxWorkspaceReader`, over the workspace capability (F6.2,
|
|
7
|
+
* `harness-api/workspace.ts`): the in-process `SandboxManager`, or the harness that serves the
|
|
8
|
+
* Tenant's workspaces (`workspace.read`). F6.2 reimplements it over the Harness API's `workspace.read`, and F7.2's pod
|
|
9
|
+
* sandboxes are read through the same seam, so the export never knows which kind of sandbox it
|
|
10
|
+
* reads.
|
|
11
|
+
*/
|
|
12
|
+
import { type AgentManifest } from "@nylorun/core/define";
|
|
13
|
+
import { type SandboxManager } from "../sandbox/manager.js";
|
|
14
|
+
/** What the reader needs of a workspace: a `SandboxManager`, or the workspace capability. */
|
|
15
|
+
export type WorkspaceFiles = Pick<SandboxManager, "listFiles" | "readBytes">;
|
|
16
|
+
/** Whose workspace to read: the session whose turn ended, resolved to the workspace it uses. */
|
|
17
|
+
export interface WorkspaceSession {
|
|
18
|
+
/** The session whose turn ended. */
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
/** The turn that ended. */
|
|
21
|
+
readonly turnId: string | null;
|
|
22
|
+
/** The session that owns the workspace (one hop through `sandboxOwnerId`). */
|
|
23
|
+
readonly ownerId: string;
|
|
24
|
+
/** The sandbox resource the workspace belongs to, when the owner is attached to one. */
|
|
25
|
+
readonly sandboxId?: string;
|
|
26
|
+
/** The session's pinned manifest: its sandbox capability carries the sandbox's spec. */
|
|
27
|
+
readonly manifest: AgentManifest;
|
|
28
|
+
}
|
|
29
|
+
/** One regular file under the listed directory. */
|
|
30
|
+
export interface WorkspaceEntry {
|
|
31
|
+
/** Relative to the directory listed, `/`-separated, never `.` or `..` segments. */
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly size: number;
|
|
34
|
+
}
|
|
35
|
+
export interface WorkspaceListing {
|
|
36
|
+
/** The files in path order, symbolic links left out. */
|
|
37
|
+
readonly entries: readonly WorkspaceEntry[];
|
|
38
|
+
/** More than `maxEntries` files: `entries` holds `maxEntries + 1` of them. */
|
|
39
|
+
readonly truncated: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface WorkspaceReader {
|
|
42
|
+
/**
|
|
43
|
+
* The regular files under `dir` (absolute, e.g. `/workspace/outputs`), recursively. Undefined
|
|
44
|
+
* when the session has no sandbox, its sandbox was never created, or `dir` is not a directory.
|
|
45
|
+
*/
|
|
46
|
+
list(session: WorkspaceSession, dir: string, options: {
|
|
47
|
+
readonly maxEntries: number;
|
|
48
|
+
readonly signal: AbortSignal;
|
|
49
|
+
}): Promise<WorkspaceListing | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* One file's bytes (absolute path), or undefined when it is gone. A file larger than
|
|
52
|
+
* `maxBytes` throws `SandboxFileTooLargeError`; a sandbox that fails throws.
|
|
53
|
+
*/
|
|
54
|
+
readBytes(session: WorkspaceSession, path: string, options: {
|
|
55
|
+
readonly maxBytes: number;
|
|
56
|
+
readonly signal: AbortSignal;
|
|
57
|
+
}): Promise<Uint8Array | undefined>;
|
|
58
|
+
}
|
|
59
|
+
/** The reader over the workspaces, in each sandbox's queue of tool calls. */
|
|
60
|
+
export declare function sandboxWorkspaceReader(sandbox: WorkspaceFiles): WorkspaceReader;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `WorkspaceReader` seam (F8.2): how core reads a session's sandbox workspace at turn end, to
|
|
3
|
+
* export its outputs as a folder artifact. Two calls, a listing and one file's bytes, both the
|
|
4
|
+
* claim of whoever holds the workspace: the Runtime stores what it is given without observing it.
|
|
5
|
+
*
|
|
6
|
+
* One implementation, `sandboxWorkspaceReader`, over the workspace capability (F6.2,
|
|
7
|
+
* `harness-api/workspace.ts`): the in-process `SandboxManager`, or the harness that serves the
|
|
8
|
+
* Tenant's workspaces (`workspace.read`). F6.2 reimplements it over the Harness API's `workspace.read`, and F7.2's pod
|
|
9
|
+
* sandboxes are read through the same seam, so the export never knows which kind of sandbox it
|
|
10
|
+
* reads.
|
|
11
|
+
*/
|
|
12
|
+
import { SANDBOX_CAPABILITY_ID, } from "@nylorun/core/define";
|
|
13
|
+
import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
14
|
+
import { SandboxFileTooLargeError } from "../sandbox/types.js";
|
|
15
|
+
/** The reader over the workspaces, in each sandbox's queue of tool calls. */
|
|
16
|
+
export function sandboxWorkspaceReader(sandbox) {
|
|
17
|
+
const target = (session) => {
|
|
18
|
+
const capability = sandboxCapabilityOf(session.manifest, SANDBOX_CAPABILITY_ID, "read");
|
|
19
|
+
if (!capability)
|
|
20
|
+
return undefined;
|
|
21
|
+
return {
|
|
22
|
+
capability,
|
|
23
|
+
ref: {
|
|
24
|
+
id: session.ownerId,
|
|
25
|
+
activeTurnId: session.turnId,
|
|
26
|
+
manifest: session.manifest,
|
|
27
|
+
...(session.sandboxId === undefined ? {} : { sandboxId: session.sandboxId }),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
async list(session, dir, options) {
|
|
33
|
+
const found = target(session);
|
|
34
|
+
if (!found)
|
|
35
|
+
return undefined;
|
|
36
|
+
const listed = await sandbox.listFiles(found.ref, found.capability, dir, options.maxEntries, options.signal);
|
|
37
|
+
if (listed.kind === "missing")
|
|
38
|
+
return undefined;
|
|
39
|
+
if (listed.kind === "failed")
|
|
40
|
+
throw new Error(listed.message);
|
|
41
|
+
return listed.listing;
|
|
42
|
+
},
|
|
43
|
+
async readBytes(session, path, options) {
|
|
44
|
+
const found = target(session);
|
|
45
|
+
if (!found)
|
|
46
|
+
return undefined;
|
|
47
|
+
const read = await sandbox.readBytes(found.ref, found.capability, path, options.maxBytes, options.signal);
|
|
48
|
+
if (read.kind === "read")
|
|
49
|
+
return read.bytes;
|
|
50
|
+
if (read.kind === "missing")
|
|
51
|
+
return undefined;
|
|
52
|
+
if (read.code === "artifact.too_large")
|
|
53
|
+
throw new SandboxFileTooLargeError(path, options.maxBytes + 1, options.maxBytes);
|
|
54
|
+
throw new Error(read.message);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ZipEntry {
|
|
2
|
+
/** The path in the archive, `/`-separated. */
|
|
3
|
+
readonly name: string;
|
|
4
|
+
/** The file's bytes, opened only when the writer reaches it. */
|
|
5
|
+
readonly open: () => Promise<AsyncIterable<Uint8Array>>;
|
|
6
|
+
}
|
|
7
|
+
/** Why a folder cannot be zipped without ZIP64, or undefined when it can. */
|
|
8
|
+
export declare function zipTooLarge(files: number, bytes: number): string | undefined;
|
|
9
|
+
/** The archive's bytes, as the writer produces them. */
|
|
10
|
+
export declare function zipStream(entries: readonly ZipEntry[], modified?: Date): AsyncGenerator<Uint8Array>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A streaming zip writer (PKWARE APPNOTE 6.3), enough for a folder artifact's download: each
|
|
3
|
+
* file deflated as it streams, its CRC-32 and sizes in a data descriptor after it (general
|
|
4
|
+
* purpose bit 3) and again in the central directory, names in UTF-8 (bit 11). Nothing is
|
|
5
|
+
* buffered beyond one chunk. No ZIP64: a folder this serves stays under 4 GiB and 65,535 files
|
|
6
|
+
* (the export's limits), and `zipTooLarge` says when one would not.
|
|
7
|
+
*/
|
|
8
|
+
import { constants, crc32, createDeflateRaw } from "node:zlib";
|
|
9
|
+
const LOCAL = 0x04034b50;
|
|
10
|
+
const DESCRIPTOR = 0x08074b50;
|
|
11
|
+
const CENTRAL = 0x02014b50;
|
|
12
|
+
const END = 0x06054b50;
|
|
13
|
+
const FLAGS = 0x0808; // bit 3: sizes in a data descriptor; bit 11: UTF-8 names
|
|
14
|
+
const DEFLATE = 8;
|
|
15
|
+
const VERSION = 20;
|
|
16
|
+
/** Made by Unix (3), so the external attributes below are a Unix mode. */
|
|
17
|
+
const MADE_BY_UNIX = (3 << 8) | VERSION;
|
|
18
|
+
/** A regular file, `rw-r--r--`, in the high 16 bits. */
|
|
19
|
+
const REGULAR_FILE = (0o100644 << 16) >>> 0;
|
|
20
|
+
const MAX_32 = 0xffffffff;
|
|
21
|
+
/** Why a folder cannot be zipped without ZIP64, or undefined when it can. */
|
|
22
|
+
export function zipTooLarge(files, bytes) {
|
|
23
|
+
if (files > 0xffff)
|
|
24
|
+
return `A zip holds at most 65535 files; this folder has ${files}`;
|
|
25
|
+
// Deflate adds at most 5 bytes per 16 KiB block; keep well clear of 4 GiB.
|
|
26
|
+
if (bytes > 3.5 * 1024 * 1024 * 1024)
|
|
27
|
+
return `A zip holds at most 3.5 GiB; this folder has ${bytes} bytes`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** MS-DOS date and time, as zip headers store them (local time is not known: UTC). */
|
|
31
|
+
function dosDateTime(date) {
|
|
32
|
+
const year = Math.max(1980, date.getUTCFullYear());
|
|
33
|
+
return {
|
|
34
|
+
time: (date.getUTCHours() << 11) | (date.getUTCMinutes() << 5) | (date.getUTCSeconds() >> 1),
|
|
35
|
+
date: ((year - 1980) << 9) | ((date.getUTCMonth() + 1) << 5) | date.getUTCDate(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** The archive's bytes, as the writer produces them. */
|
|
39
|
+
export async function* zipStream(entries, modified = new Date()) {
|
|
40
|
+
const stamp = dosDateTime(modified);
|
|
41
|
+
const central = [];
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
45
|
+
const local = Buffer.alloc(30);
|
|
46
|
+
local.writeUInt32LE(LOCAL, 0);
|
|
47
|
+
local.writeUInt16LE(VERSION, 4);
|
|
48
|
+
local.writeUInt16LE(FLAGS, 6);
|
|
49
|
+
local.writeUInt16LE(DEFLATE, 8);
|
|
50
|
+
local.writeUInt16LE(stamp.time, 10);
|
|
51
|
+
local.writeUInt16LE(stamp.date, 12);
|
|
52
|
+
// CRC and sizes (14–25) are zero: they follow in the data descriptor.
|
|
53
|
+
local.writeUInt16LE(name.length, 26);
|
|
54
|
+
local.writeUInt16LE(0, 28);
|
|
55
|
+
const headerAt = offset;
|
|
56
|
+
yield local;
|
|
57
|
+
yield name;
|
|
58
|
+
offset += local.length + name.length;
|
|
59
|
+
let crc = 0;
|
|
60
|
+
let size = 0;
|
|
61
|
+
let compressed = 0;
|
|
62
|
+
const deflate = createDeflateRaw({ level: constants.Z_DEFAULT_COMPRESSION });
|
|
63
|
+
const source = await entry.open();
|
|
64
|
+
const feed = (async () => {
|
|
65
|
+
try {
|
|
66
|
+
for await (const chunk of source) {
|
|
67
|
+
// The reader went away (a cancelled download): stop reading the file.
|
|
68
|
+
if (deflate.destroyed)
|
|
69
|
+
break;
|
|
70
|
+
crc = crc32(chunk, crc);
|
|
71
|
+
size += chunk.length;
|
|
72
|
+
if (!deflate.write(chunk))
|
|
73
|
+
await new Promise((resolve) => {
|
|
74
|
+
deflate.once("drain", resolve);
|
|
75
|
+
deflate.once("close", resolve);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!deflate.destroyed)
|
|
79
|
+
deflate.end();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
deflate.destroy(error);
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
for await (const chunk of deflate) {
|
|
86
|
+
compressed += chunk.length;
|
|
87
|
+
yield chunk;
|
|
88
|
+
}
|
|
89
|
+
await feed;
|
|
90
|
+
offset += compressed;
|
|
91
|
+
if (offset > MAX_32 || size > MAX_32)
|
|
92
|
+
throw new Error("The folder is too large to zip without ZIP64");
|
|
93
|
+
const descriptor = Buffer.alloc(16);
|
|
94
|
+
descriptor.writeUInt32LE(DESCRIPTOR, 0);
|
|
95
|
+
descriptor.writeUInt32LE(crc >>> 0, 4);
|
|
96
|
+
descriptor.writeUInt32LE(compressed, 8);
|
|
97
|
+
descriptor.writeUInt32LE(size, 12);
|
|
98
|
+
yield descriptor;
|
|
99
|
+
offset += descriptor.length;
|
|
100
|
+
const record = Buffer.alloc(46);
|
|
101
|
+
record.writeUInt32LE(CENTRAL, 0);
|
|
102
|
+
record.writeUInt16LE(MADE_BY_UNIX, 4);
|
|
103
|
+
record.writeUInt16LE(VERSION, 6);
|
|
104
|
+
record.writeUInt16LE(FLAGS, 8);
|
|
105
|
+
record.writeUInt16LE(DEFLATE, 10);
|
|
106
|
+
record.writeUInt16LE(stamp.time, 12);
|
|
107
|
+
record.writeUInt16LE(stamp.date, 14);
|
|
108
|
+
record.writeUInt32LE(crc >>> 0, 16);
|
|
109
|
+
record.writeUInt32LE(compressed, 20);
|
|
110
|
+
record.writeUInt32LE(size, 24);
|
|
111
|
+
record.writeUInt16LE(name.length, 28);
|
|
112
|
+
// Extra, comment, disk and internal attributes (30–37) stay zero.
|
|
113
|
+
record.writeUInt32LE(REGULAR_FILE, 38);
|
|
114
|
+
record.writeUInt32LE(headerAt, 42);
|
|
115
|
+
central.push(record, name);
|
|
116
|
+
}
|
|
117
|
+
const directory = Buffer.concat(central);
|
|
118
|
+
yield directory;
|
|
119
|
+
const end = Buffer.alloc(22);
|
|
120
|
+
end.writeUInt32LE(END, 0);
|
|
121
|
+
end.writeUInt16LE(entries.length, 8);
|
|
122
|
+
end.writeUInt16LE(entries.length, 10);
|
|
123
|
+
end.writeUInt32LE(directory.length, 12);
|
|
124
|
+
end.writeUInt32LE(offset, 16);
|
|
125
|
+
yield end;
|
|
126
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type BlobStore } from "./types.js";
|
|
2
|
+
export interface FsBlobStoreOptions {
|
|
3
|
+
/** Absolute directory the store owns; created on the first write. */
|
|
4
|
+
root: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The `fs` adapter: objects as files under `root`, for embedding (`startEphemeralRuntime`, a
|
|
8
|
+
* Host without an Object store) and tests. `objects/<key>` holds the bytes, `meta/<key>.json`
|
|
9
|
+
* the content type; a write goes to `tmp/` first and is renamed into place, so a reader sees
|
|
10
|
+
* the old object or the new one, never part of one. A key cannot also be the directory of
|
|
11
|
+
* another key (`a` and `a/b`); the Runtime's keys never are.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createFsBlobStore(options: FsBlobStoreOptions): BlobStore;
|
package/dist/blob/fs.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { BlobRangeError, BlobTooLargeError, DEFAULT_CONTENT_TYPE, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
|
|
6
|
+
function isMissing(error) {
|
|
7
|
+
const code = error?.code;
|
|
8
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `fs` adapter: objects as files under `root`, for embedding (`startEphemeralRuntime`, a
|
|
12
|
+
* Host without an Object store) and tests. `objects/<key>` holds the bytes, `meta/<key>.json`
|
|
13
|
+
* the content type; a write goes to `tmp/` first and is renamed into place, so a reader sees
|
|
14
|
+
* the old object or the new one, never part of one. A key cannot also be the directory of
|
|
15
|
+
* another key (`a` and `a/b`); the Runtime's keys never are.
|
|
16
|
+
*/
|
|
17
|
+
export function createFsBlobStore(options) {
|
|
18
|
+
const objects = join(options.root, "objects");
|
|
19
|
+
const metadata = join(options.root, "meta");
|
|
20
|
+
const tmp = join(options.root, "tmp");
|
|
21
|
+
const objectPath = (key) => join(objects, ...key.split("/"));
|
|
22
|
+
const metaPath = (key) => `${join(metadata, ...key.split("/"))}.json`;
|
|
23
|
+
async function readMeta(key) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(await readFile(metaPath(key), "utf8"));
|
|
26
|
+
return { contentType: parsed.contentType ?? DEFAULT_CONTENT_TYPE };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { contentType: DEFAULT_CONTENT_TYPE };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function head(key) {
|
|
33
|
+
assertBlobKey(key);
|
|
34
|
+
let stats;
|
|
35
|
+
try {
|
|
36
|
+
stats = await stat(objectPath(key));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isMissing(error))
|
|
40
|
+
return undefined;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (!stats.isFile())
|
|
44
|
+
return undefined;
|
|
45
|
+
const meta = await readMeta(key);
|
|
46
|
+
return { key, size: stats.size, contentType: meta.contentType, lastModified: stats.mtime };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: "fs",
|
|
50
|
+
async put(key, body, put = {}) {
|
|
51
|
+
assertBlobKey(key);
|
|
52
|
+
assertMaxBytes(put.maxBytes);
|
|
53
|
+
put.signal?.throwIfAborted();
|
|
54
|
+
await mkdir(tmp, { recursive: true });
|
|
55
|
+
const id = randomUUID();
|
|
56
|
+
const dataTmp = join(tmp, id);
|
|
57
|
+
const metaTmp = join(tmp, `${id}.json`);
|
|
58
|
+
const hash = createHash("sha256");
|
|
59
|
+
let size = 0;
|
|
60
|
+
const file = await open(dataTmp, "wx", 0o600);
|
|
61
|
+
try {
|
|
62
|
+
try {
|
|
63
|
+
for await (const chunk of chunksOf(body)) {
|
|
64
|
+
put.signal?.throwIfAborted();
|
|
65
|
+
size += chunk.byteLength;
|
|
66
|
+
if (put.maxBytes !== undefined && size > put.maxBytes)
|
|
67
|
+
throw new BlobTooLargeError(put.maxBytes);
|
|
68
|
+
hash.update(chunk);
|
|
69
|
+
await file.write(chunk);
|
|
70
|
+
}
|
|
71
|
+
await file.sync();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await file.close();
|
|
75
|
+
}
|
|
76
|
+
const stored = { contentType: put.contentType ?? DEFAULT_CONTENT_TYPE };
|
|
77
|
+
await writeTmp(metaTmp, JSON.stringify(stored));
|
|
78
|
+
await mkdir(dirname(objectPath(key)), { recursive: true });
|
|
79
|
+
await mkdir(dirname(metaPath(key)), { recursive: true });
|
|
80
|
+
await rename(metaTmp, metaPath(key));
|
|
81
|
+
await rename(dataTmp, objectPath(key));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
await rm(dataTmp, { force: true });
|
|
85
|
+
await rm(metaTmp, { force: true });
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return { key, size, sha256: hash.digest("hex") };
|
|
89
|
+
},
|
|
90
|
+
async get(key, get = {}) {
|
|
91
|
+
assertBlobKey(key);
|
|
92
|
+
if (get.range)
|
|
93
|
+
assertRange(get.range);
|
|
94
|
+
get.signal?.throwIfAborted();
|
|
95
|
+
let file;
|
|
96
|
+
try {
|
|
97
|
+
file = await open(objectPath(key), "r");
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (isMissing(error) || error?.code === "EISDIR")
|
|
101
|
+
return undefined;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const stats = await file.stat();
|
|
106
|
+
if (!stats.isFile()) {
|
|
107
|
+
await file.close();
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const meta = await readMeta(key);
|
|
111
|
+
const base = {
|
|
112
|
+
key,
|
|
113
|
+
size: stats.size,
|
|
114
|
+
contentType: meta.contentType,
|
|
115
|
+
lastModified: stats.mtime,
|
|
116
|
+
};
|
|
117
|
+
if (!get.range) {
|
|
118
|
+
const stream = file.createReadStream({ autoClose: true });
|
|
119
|
+
return { ...base, body: Readable.toWeb(stream) };
|
|
120
|
+
}
|
|
121
|
+
if (get.range.start >= stats.size)
|
|
122
|
+
throw new BlobRangeError(get.range, stats.size);
|
|
123
|
+
const end = Math.min(get.range.end ?? stats.size - 1, stats.size - 1);
|
|
124
|
+
const stream = file.createReadStream({ start: get.range.start, end, autoClose: true });
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
body: Readable.toWeb(stream),
|
|
128
|
+
range: { start: get.range.start, end },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await file.close().catch(() => undefined);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
head,
|
|
137
|
+
async delete(key) {
|
|
138
|
+
assertBlobKey(key);
|
|
139
|
+
for (const path of [objectPath(key), metaPath(key)])
|
|
140
|
+
try {
|
|
141
|
+
await unlink(path);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const code = error?.code;
|
|
145
|
+
if (!isMissing(error) && code !== "EISDIR" && code !== "EPERM")
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
async *list(prefix) {
|
|
150
|
+
assertBlobPrefix(prefix);
|
|
151
|
+
// Walk the deepest directory the prefix names, then filter and sort: `a-b` sorts before
|
|
152
|
+
// `a/c`, so directory order is not key order.
|
|
153
|
+
const slash = prefix.lastIndexOf("/");
|
|
154
|
+
const start = slash < 0 ? objects : join(objects, ...prefix.slice(0, slash).split("/"));
|
|
155
|
+
const found = [];
|
|
156
|
+
const walk = async (directory) => {
|
|
157
|
+
let entries;
|
|
158
|
+
try {
|
|
159
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (isMissing(error))
|
|
163
|
+
return;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const path = join(directory, entry.name);
|
|
168
|
+
if (entry.isDirectory())
|
|
169
|
+
await walk(path);
|
|
170
|
+
else if (entry.isFile()) {
|
|
171
|
+
const key = relative(objects, path).split(sep).join("/");
|
|
172
|
+
if (!key.startsWith(prefix))
|
|
173
|
+
continue;
|
|
174
|
+
const stats = await stat(path).catch(() => undefined);
|
|
175
|
+
if (stats)
|
|
176
|
+
found.push({ key, size: stats.size, lastModified: stats.mtime });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
await walk(start);
|
|
181
|
+
found.sort((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0));
|
|
182
|
+
yield* found;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function writeTmp(path, text) {
|
|
187
|
+
const file = await open(path, "wx", 0o600);
|
|
188
|
+
try {
|
|
189
|
+
await file.writeFile(text, "utf8");
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await file.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `BlobStore` seam (`./types.ts`) and its adapters. A Host with an Object store
|
|
3
|
+
* (`StackConfig.objectStore`, from `NYLORUN_OBJECT_STORE_*`) passes an `s3` store to its Tenant
|
|
4
|
+
* (`TenantOpenHooks.blobs`); without one the Tenant keeps blobs on disk (`fs`, under
|
|
5
|
+
* `TenantPaths.blobs`). Tenant code reaches it as `TenantContext.blobs`.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export { createFsBlobStore, type FsBlobStoreOptions } from "./fs.js";
|
|
9
|
+
export { createS3BlobStore, type S3BlobStore, type S3BlobStoreOptions } from "./s3.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `BlobStore` seam (`./types.ts`) and its adapters. A Host with an Object store
|
|
3
|
+
* (`StackConfig.objectStore`, from `NYLORUN_OBJECT_STORE_*`) passes an `s3` store to its Tenant
|
|
4
|
+
* (`TenantOpenHooks.blobs`); without one the Tenant keeps blobs on disk (`fs`, under
|
|
5
|
+
* `TenantPaths.blobs`). Tenant code reaches it as `TenantContext.blobs`.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export { createFsBlobStore } from "./fs.js";
|
|
9
|
+
export { createS3BlobStore } from "./s3.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type BlobStore } from "./types.js";
|
|
2
|
+
export interface S3BlobStoreOptions {
|
|
3
|
+
/** The S3 endpoint, e.g. `http://rustfs:9000` or `https://s3.eu-west-1.amazonaws.com`. */
|
|
4
|
+
endpoint: string;
|
|
5
|
+
bucket: string;
|
|
6
|
+
/** The signing region. Default `us-east-1`, which RustFS accepts. */
|
|
7
|
+
region?: string;
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
/**
|
|
11
|
+
* Path-style URLs (`<endpoint>/<bucket>/<key>`), which RustFS and most S3 servers expect.
|
|
12
|
+
* `false` uses virtual-hosted URLs (`<bucket>.<endpoint host>/<key>`). Default `true`.
|
|
13
|
+
*/
|
|
14
|
+
forcePathStyle?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* A body longer than this is uploaded in parts of about this size (multipart upload); a
|
|
17
|
+
* shorter one in one request. At least 5 MiB, the S3 minimum. Default 8 MiB.
|
|
18
|
+
*/
|
|
19
|
+
partSize?: number;
|
|
20
|
+
/** Attempts for a request that fails with a network error, 429 or 5xx. Default 3. */
|
|
21
|
+
attempts?: number;
|
|
22
|
+
/** Replaces the global `fetch` (tests). */
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
/** The `s3` adapter, plus the one bucket operation the Runtime needs at boot. */
|
|
26
|
+
export interface S3BlobStore extends BlobStore {
|
|
27
|
+
readonly kind: "s3";
|
|
28
|
+
/** Create the bucket unless it exists. The local stack's runtime calls it at boot. */
|
|
29
|
+
ensureBucket(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The `s3` adapter over the plain S3 API: PutObject, multipart upload, GetObject with Range,
|
|
33
|
+
* HeadObject, DeleteObject and ListObjectsV2, signed with SigV4 by aws4fetch (MIT, no
|
|
34
|
+
* dependencies, built on `fetch` and web streams). It uses no server's admin, KMS or lifecycle
|
|
35
|
+
* API (D35), so RustFS, S3, R2 or any S3 server sits behind it.
|
|
36
|
+
*
|
|
37
|
+
* Uploads stream: the body is counted and hashed as it arrives and sent in parts of
|
|
38
|
+
* `partSize`, so memory stays at about one part whatever the size, and a body that passes
|
|
39
|
+
* `maxBytes` aborts its multipart upload. Each request carries the SHA-256 of its payload,
|
|
40
|
+
* which the server checks. Downloads hand back the response's stream.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createS3BlobStore(options: S3BlobStoreOptions): S3BlobStore;
|