@gjermundgaraba/clankerbox-sdk 0.4.1 → 0.12.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/index.mjs ADDED
@@ -0,0 +1,106 @@
1
+ import { A as Capacity, B as hostErrors, C as HostId, D as isId, E as formatId, F as Internal, I as Invalid, L as NotFound, M as ConflictKind, N as ErrorTag, O as parseId, P as HostErrorTag, R as Precondition, S as version, T as Name, _ as Profile, a as HostGroup, b as SshEndpoint, c as readTimeout, d as ActionRecord, f as Checkpoint, g as MachineSpec, h as Machine, i as CheckpointGroup, j as Conflict, k as parseName, l as versionHeader, m as Host, n as client_exports, o as Http, p as CreateRequest, s as MachineGroup, u as ActionName, v as Runtime, w as Id, x as release, y as Setup, z as Unavailable } from "./client-jXYWrnuZ.mjs";
2
+ import { Effect, FileSystem, Path, Predicate, Schema, Stream } from "effect";
3
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
4
+ //#region src/profile.ts
5
+ /** Reads a profile file for `create`. Hosts never see a profile. */
6
+ /** Profile files are JSON, decoded with closed input like every call: unknown keys are refused. */
7
+ const decodeProfile = Schema.decodeUnknownEffect(Schema.fromJsonString(Profile));
8
+ /**
9
+ * Reads the profile file at `file`. Its name, the file name without `.json`, is the machine's
10
+ * `profile` label, and its `setup.path` is resolved against the file's directory. The setup
11
+ * itself is read by whoever sends it, with `readSetup`.
12
+ */
13
+ const loadProfile = (file) => Effect.gen(function* () {
14
+ const fs = yield* FileSystem.FileSystem;
15
+ const path = yield* Path.Path;
16
+ const text = yield* fs.readFileString(file).pipe(Effect.mapError((error) => new Invalid({ message: `couldn't read profile ${file}: ${error.message}` })));
17
+ const profile = yield* decodeProfile(text, { onExcessProperty: "error" }).pipe(Effect.mapError((error) => new Invalid({ message: `profile ${file}: ${error.message}` })));
18
+ const label = path.basename(file, ".json");
19
+ return profile.setup === void 0 ? {
20
+ ...profile,
21
+ label
22
+ } : {
23
+ ...profile,
24
+ setup: {
25
+ ...profile.setup,
26
+ path: path.resolve(path.dirname(file), profile.setup.path)
27
+ },
28
+ label
29
+ };
30
+ });
31
+ //#endregion
32
+ //#region src/setup.ts
33
+ /**
34
+ * Setup scripts for create. A host receives one script: a file is sent as its text, and a
35
+ * recipe directory (a `setup.sh` and the files it needs) is packed into one self-extracting
36
+ * script. Nothing here logs a script or a packed recipe: a recipe can carry secrets.
37
+ */
38
+ const lineLength = 76;
39
+ /** base64 has no `_`, so no line of the payload can end the here-document early. */
40
+ const terminator = "CLANKERBOX_RECIPE";
41
+ const wrap = (base64) => {
42
+ const lines = [];
43
+ for (let at = 0; at < base64.length; at += lineLength) lines.push(base64.slice(at, at + lineLength));
44
+ return lines.join("\n");
45
+ };
46
+ /**
47
+ * Pipes the archive into `tar -x` in a fresh directory, so no file of the recipe's can
48
+ * collide with the archive, then runs `./setup.sh` from there: its `#!` line picks the
49
+ * interpreter, as for a single-file setup, and the unpacker marks it executable whatever mode
50
+ * it was packed with. The directory is under `/var/tmp`, as a single-file setup is, which
51
+ * nothing mounts `noexec`, and is removed however setup ends. Setup runs as a child
52
+ * rather than through `exec`, so the cleanup trap still runs after it. `--no-same-owner`
53
+ * keeps the packer's uid off the files when root unpacks them in the guest.
54
+ */
55
+ const selfExtracting = (archive) => [
56
+ "#!/bin/sh",
57
+ "set -eu",
58
+ "recipe=$(mktemp -d /var/tmp/clankerbox-recipe.XXXXXX)",
59
+ `trap 'rm -rf "$recipe"' EXIT`,
60
+ "trap 'exit 1' HUP INT TERM",
61
+ `base64 -d <<'${terminator}' | tar -x -f - --no-same-owner -C "$recipe"`,
62
+ wrap(archive),
63
+ terminator,
64
+ "cd \"$recipe\"",
65
+ "chmod +x setup.sh",
66
+ "./setup.sh",
67
+ ""
68
+ ].join("\n");
69
+ /**
70
+ * Packs a recipe directory into one self-extracting script: a base64 tar that unpacks into a
71
+ * temporary directory, then runs `setup.sh` from there. `COPYFILE_DISABLE=1` and
72
+ * `--no-xattrs` keep macOS `tar` from adding `._*` files and provenance headers.
73
+ */
74
+ const packRecipe = (directory) => Effect.gen(function* () {
75
+ const fs = yield* FileSystem.FileSystem;
76
+ const noSetup = new Invalid({ message: `recipe ${directory} has no setup.sh` });
77
+ if ((yield* fs.stat(`${directory}/setup.sh`).pipe(Effect.mapError((error) => Predicate.isTagged(error.reason, "NotFound") ? noSetup : error))).type !== "File") return yield* noSetup;
78
+ const tar = yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(ChildProcess.make("tar", [
79
+ "--no-xattrs",
80
+ "-c",
81
+ "-f",
82
+ "-",
83
+ "-C",
84
+ directory,
85
+ "."
86
+ ], {
87
+ env: { COPYFILE_DISABLE: "1" },
88
+ extendEnv: true,
89
+ stdin: "ignore"
90
+ }));
91
+ const [archive, errors, code] = yield* Effect.all([
92
+ Stream.mkUint8Array(tar.stdout),
93
+ Stream.mkString(Stream.decodeText(tar.stderr)),
94
+ tar.exitCode
95
+ ], { concurrency: "unbounded" });
96
+ if (code !== 0) return yield* new Invalid({ message: `tar couldn't pack recipe ${directory} (exit ${code}): ${errors.trim()}` });
97
+ return selfExtracting(Buffer.from(archive.buffer, archive.byteOffset, archive.byteLength).toString("base64"));
98
+ }).pipe(Effect.scoped, Effect.catchTag("PlatformError", (error) => Effect.fail(new Invalid({ message: `couldn't pack recipe ${directory}: ${error.message}` }))));
99
+ /** Reads a setup: a script file's text, or a recipe directory packed into one script. */
100
+ const readSetup = (path) => Effect.gen(function* () {
101
+ const fs = yield* FileSystem.FileSystem;
102
+ if ((yield* fs.stat(path)).type === "Directory") return yield* packRecipe(path);
103
+ return yield* fs.readFileString(path);
104
+ }).pipe(Effect.catchTag("PlatformError", (error) => Effect.fail(new Invalid({ message: `couldn't read setup ${path}: ${error.message}` }))));
105
+ //#endregion
106
+ export { ActionName, ActionRecord, Capacity, Checkpoint, CheckpointGroup, client_exports as Client, Conflict, ConflictKind, CreateRequest, ErrorTag, Host, HostErrorTag, HostGroup, HostId, Http, Id, Internal, Invalid, Machine, MachineGroup, MachineSpec, Name, NotFound, Precondition, Profile, Runtime, Setup, SshEndpoint, Unavailable, formatId, hostErrors, isId, loadProfile, packRecipe, parseId, parseName, readSetup, readTimeout, release, version, versionHeader };
@@ -0,0 +1,6 @@
1
+ import { E as Invalid, n as HostEntry, r as Options, t as Client } from "./client-BVzrg2pq.mjs";
2
+ import { Layer } from "effect";
3
+ //#region src/node.d.ts
4
+ /** Node's `http` module sets no timeout of its own: a mutation can run for hours. */
5
+ export declare const layer: (hosts: ReadonlyArray<HostEntry>, options?: Options) => Layer.Layer<Client, Invalid>;
6
+ //#endregion
package/dist/node.mjs ADDED
@@ -0,0 +1,12 @@
1
+ import { r as make, t as Client } from "./client-jXYWrnuZ.mjs";
2
+ import { Layer } from "effect";
3
+ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
4
+ //#region src/node.ts
5
+ /**
6
+ * The client over Node's `http` module, the SDK's `/node` entry. It stands apart so that an
7
+ * app that brings its own `HttpClient` to `Client.make` never loads `@effect/platform-node`.
8
+ */
9
+ /** Node's `http` module sets no timeout of its own: a mutation can run for hours. */
10
+ const layer = (hosts, options) => Layer.effect(Client, make(hosts, options)).pipe(Layer.provide(NodeHttpClient.layerNodeHttp));
11
+ //#endregion
12
+ export { layer };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/package.json CHANGED
@@ -1,65 +1,54 @@
1
1
  {
2
2
  "name": "@gjermundgaraba/clankerbox-sdk",
3
- "version": "0.4.1",
4
- "description": "Generated Protobuf messages and Connect RPC service descriptors for clankerbox",
3
+ "version": "0.12.0",
4
+ "description": "clankerbox's API contract and client library, for Effect",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/gjermundgaraba/clankerbox.git",
9
- "directory": "protocol"
10
- },
11
- "homepage": "https://github.com/gjermundgaraba/clankerbox/tree/main/protocol#readme",
12
- "bugs": "https://github.com/gjermundgaraba/clankerbox/issues",
13
- "publishConfig": {
14
- "access": "public",
15
- "registry": "https://registry.npmjs.org/"
9
+ "directory": "packages/sdk"
16
10
  },
11
+ "files": [
12
+ "dist"
13
+ ],
17
14
  "type": "module",
15
+ "sideEffects": false,
18
16
  "exports": {
19
17
  ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js"
18
+ "types": "./dist/index.d.mts",
19
+ "import": "./dist/index.mjs"
22
20
  },
23
- "./resources": {
24
- "types": "./dist/gen/clankerbox/v1/resources_pb.d.ts",
25
- "import": "./dist/gen/clankerbox/v1/resources_pb.js"
21
+ "./node": {
22
+ "types": "./dist/node.d.mts",
23
+ "import": "./dist/node.mjs"
26
24
  },
27
- "./machine": {
28
- "types": "./dist/gen/clankerbox/v1/machine_pb.d.ts",
29
- "import": "./dist/gen/clankerbox/v1/machine_pb.js"
30
- },
31
- "./session": {
32
- "types": "./dist/gen/clankerbox/v1/session_pb.d.ts",
33
- "import": "./dist/gen/clankerbox/v1/session_pb.js"
34
- },
35
- "./host": {
36
- "types": "./dist/gen/clankerbox/v1/host_pb.d.ts",
37
- "import": "./dist/gen/clankerbox/v1/host_pb.js"
38
- },
39
- "./profile": {
40
- "types": "./dist/gen/clankerbox/v1/profile_pb.d.ts",
41
- "import": "./dist/gen/clankerbox/v1/profile_pb.js"
42
- }
25
+ "./package.json": "./package.json"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
43
29
  },
44
- "files": [
45
- "dist",
46
- "README.md",
47
- "LICENSE"
48
- ],
49
30
  "dependencies": {
50
- "@bufbuild/protobuf": "2.15.0"
31
+ "@gjermundgaraba/effect-actions": "0.9.0"
51
32
  },
52
33
  "devDependencies": {
53
- "@bufbuild/protoc-gen-es": "2.15.0",
34
+ "@effect/platform-node": "4.0.0",
35
+ "@types/node": "26.6.4",
36
+ "effect": "4.0.0",
54
37
  "typescript": "7.0.2",
55
- "@connectrpc/connect": "2.2.0",
56
- "@connectrpc/connect-node": "2.2.0"
38
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
39
+ "vite-plus": "1.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@effect/platform-node": "^4.0.0",
43
+ "effect": "^4.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@effect/platform-node": {
47
+ "optional": true
48
+ }
57
49
  },
58
50
  "scripts": {
59
- "generate": "./scripts/generate.sh",
60
- "build": "tsc -p tsconfig.json",
61
- "check": "tsc -p tsconfig.json --noEmit",
62
- "test": "node --test test/*.test.mjs",
63
- "test:package": "node scripts/test-package.mjs"
51
+ "build": "vp pack",
52
+ "test": "vp test"
64
53
  }
65
54
  }
package/README.md DELETED
@@ -1,100 +0,0 @@
1
- # @gjermundgaraba/clankerbox-sdk
2
-
3
- Generated Protobuf messages, TypeScript types and Connect RPC service descriptors
4
- for clankerbox. This ESM package includes compiled JavaScript and declarations;
5
- consumers do not need protoc, a Go toolchain, or a clankerbox source checkout.
6
-
7
- ```sh
8
- npm install @gjermundgaraba/clankerbox-sdk @connectrpc/connect @connectrpc/connect-node
9
- ```
10
-
11
- ```ts
12
- import { MachineService, SessionService } from "@gjermundgaraba/clankerbox-sdk";
13
- import { createClient } from "@connectrpc/connect";
14
- import { createConnectTransport } from "@connectrpc/connect-node";
15
- ```
16
-
17
- The package supplies descriptors, not a transport or lifecycle coordinator.
18
- Node consumers supply Connect 2.x and use
19
- `createConnectTransport({httpVersion: "2", ...})`; browser fetch cannot carry
20
- the bidirectional attachment stream. `@bufbuild/protobuf` is included as a
21
- runtime dependency. All descriptors are exported at the root and through
22
- `/resources`, `/machine`, `/session` and `/host` subpaths.
23
-
24
- SDK **0.4.1** targets clankerbox **0.9.0** and its attachment-owned session
25
- contract: sessions are created by `Open.create` inside `AttachSession`, and each
26
- `Input` is sent at the `input_offset` the guest last acknowledged. Use this pair
27
- together; SDK and controller version numbers are independent. The wire contract
28
- is unchanged from SDK 0.4.0 and clankerbox 0.8.0. Older installations and guest
29
- images are not migrated.
30
-
31
- ## RPC contract
32
-
33
- The wire package is `clankerbox.v1`. The Protobuf sources under
34
- `clankerbox/v1/` generate Go messages in `gen/clankerbox/v1`, Go Connect
35
- handlers and clients in `gen/clankerbox/v1/clankerboxv1connect`, and TypeScript
36
- in `src/gen/clankerbox/v1`. Generated code is checked in.
37
-
38
- The services:
39
-
40
- - `MachineService`: discovery, resource reads, lifecycle mutations, operation
41
- inspection and label replacement. Lifecycle mutations carry idempotency keys
42
- and return durable Operations; clients poll `GetOperation` and never resubmit
43
- a mutation because a wait timed out. `SetLabels` is the exception: it takes
44
- no key and returns the updated `Machine` synchronously.
45
- - `SessionService`: terminal sessions, mounted on controller, host and guest
46
- with endpoint-specific authorization.
47
- - `HostService`: private operation submission and status plus host and machine
48
- inspection. Each action is a `oneof` payload.
49
-
50
- Public `Profile`, `Host`, `Machine` and `Checkpoint` messages omit image paths,
51
- private endpoints, engine store paths and credentials. `internal/rpcmodel`
52
- converts between these messages and the services' persisted records.
53
-
54
- Errors combine a Connect status code with a typed `ErrorDetail` reason.
55
- `retryable` describes the operation, not terminal input: input is addressed by
56
- offset, so repeating it on the same attachment is safe and carrying it to another
57
- is not. The streaming semantics of
58
- `AttachSession` are specified in
59
- [terminal sessions](https://github.com/gjermundgaraba/clankerbox/blob/main/docs/terminal-sessions.md).
60
-
61
- ## Generation and packaging
62
-
63
- ```sh
64
- cd protocol
65
- pnpm install --frozen-lockfile
66
- pnpm generate
67
- pnpm build
68
- pnpm check
69
- pnpm test
70
- pnpm test:package
71
- pnpm pack
72
- ```
73
-
74
- `generate` requires protoc 36.1 and installs pinned protoc-gen-go,
75
- protoc-gen-connect-go and protoc-gen-es into `.tools/`. Go dependency versions
76
- are in the root `go.mod`; JavaScript versions are in `package.json` and the
77
- lockfile. Regeneration must leave the checked-in output unchanged.
78
-
79
- `pnpm pack` clean-builds JavaScript and declarations and copies the repository's
80
- MIT license into the package. `pnpm test:package` packs and installs the artifact
81
- in a temporary consumer with lifecycle scripts disabled, then checks runtime
82
- imports, TypeScript declarations, package contents and license inclusion.
83
-
84
- Publishing uses dedicated `sdk-v<VERSION>` tags, not controller release tags.
85
- See [SDK publishing](https://github.com/gjermundgaraba/clankerbox/blob/main/docs/sdk-publishing.md)
86
- for first-publish setup and the tokenless GitHub Actions release workflow.
87
-
88
- ## Tests
89
-
90
- ```sh
91
- go test -race ./internal/rpcmodel ./gen/...
92
- cd protocol && pnpm test
93
- ```
94
-
95
- The Go tests cover conversion round trips, private-field redaction, every
96
- operation action and state, and typed errors over a real connection. The SDK
97
- tests cover bigint and JSON precision, oneofs and service shapes.
98
- `test/real-vm.mjs` and `test/public-session.mjs` drive a running machine and are
99
- not part of the default test glob; see
100
- [tests](https://github.com/gjermundgaraba/clankerbox/blob/main/tests/README.md).