@fusionkit/sdk 0.1.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.
@@ -0,0 +1,67 @@
1
+ import type { ActorRef, ChainedEvent, ClaimResult, DisclosureReport, Policy, Receipt, ReceiptBundle, RunnerSummary, RunRequestInput, RunStatus, RunSummary, RunView } from "@fusionkit/protocol";
2
+ export declare class PlaneClientError extends Error {
3
+ readonly status: number;
4
+ readonly body: unknown;
5
+ constructor(status: number, body: unknown);
6
+ }
7
+ /** Thin HTTP client over the plane API, shared by the CLI, SDKs, and runner. */
8
+ export declare class PlaneClient {
9
+ readonly baseUrl: string;
10
+ private readonly adminToken?;
11
+ constructor(baseUrl: string, adminToken?: string);
12
+ private json;
13
+ enroll(input: {
14
+ enrollToken: string;
15
+ publicKeyPem: string;
16
+ pool: string;
17
+ }): Promise<{
18
+ runnerId: string;
19
+ runnerToken: string;
20
+ }>;
21
+ putBlob(content: Buffer, token?: string): Promise<string>;
22
+ getBlob(hash: string): Promise<Buffer>;
23
+ requestRun(request: RunRequestInput): Promise<{
24
+ runId: string;
25
+ status: RunStatus;
26
+ consentRequirements: string[];
27
+ }>;
28
+ dryRun(request: RunRequestInput): Promise<DisclosureReport>;
29
+ approve(runId: string, actor: ActorRef): Promise<{
30
+ runId: string;
31
+ status: RunStatus;
32
+ }>;
33
+ cancel(runId: string, actor: ActorRef): Promise<{
34
+ runId: string;
35
+ status: RunStatus;
36
+ }>;
37
+ getRun(runId: string): Promise<RunView>;
38
+ listRuns(): Promise<{
39
+ runs: RunSummary[];
40
+ }>;
41
+ listRunners(): Promise<{
42
+ runners: RunnerSummary[];
43
+ }>;
44
+ getPolicy(): Promise<{
45
+ policy: Policy;
46
+ policyHash: string;
47
+ }>;
48
+ claim(input: {
49
+ runnerToken: string;
50
+ pool: string;
51
+ }): Promise<ClaimResult | {
52
+ empty: true;
53
+ }>;
54
+ postEvents(runId: string, claimToken: string, events: ChainedEvent[]): Promise<{
55
+ ok: boolean;
56
+ }>;
57
+ complete(runId: string, claimToken: string, receipt: Receipt): Promise<{
58
+ receipt: Receipt;
59
+ }>;
60
+ getBundle(runId: string): Promise<ReceiptBundle>;
61
+ private runBundlePath;
62
+ /** Canonical download URL for a run's signed receipt bundle. */
63
+ runBundleUrl(runId: string): string;
64
+ /** Canonical control-panel deep link for a run. */
65
+ runUiUrl(runId: string): string;
66
+ exportJsonl(since?: string): Promise<string>;
67
+ }
package/dist/client.js ADDED
@@ -0,0 +1,168 @@
1
+ export class PlaneClientError extends Error {
2
+ status;
3
+ body;
4
+ constructor(status, body) {
5
+ const message = typeof body === "object" && body !== null && "error" in body
6
+ ? String(body.error)
7
+ : `plane request failed with status ${status}`;
8
+ super(message);
9
+ this.name = "PlaneClientError";
10
+ this.status = status;
11
+ this.body = body;
12
+ }
13
+ }
14
+ /** Default transport-retry policy for idempotent requests. */
15
+ const DEFAULT_RETRY_ATTEMPTS = 3;
16
+ const RETRY_BACKOFF_STEP_MS = 100;
17
+ /**
18
+ * Retry idempotent requests on transport-level failures: a keep-alive
19
+ * socket the server closed while idle surfaces as a TypeError from fetch,
20
+ * not as an HTTP error. GETs are idempotent by construction in this API;
21
+ * blob uploads are content-addressed, so retrying them is also safe. Other
22
+ * POSTs (run requests, claims, events, completion) are never retried here.
23
+ */
24
+ async function fetchIdempotent(url, init, attempts = DEFAULT_RETRY_ATTEMPTS) {
25
+ const retryable = init.idempotent ?? init.method === "GET";
26
+ for (let attempt = 1;; attempt++) {
27
+ try {
28
+ return await fetch(url, init);
29
+ }
30
+ catch (error) {
31
+ if (!(error instanceof TypeError) || !retryable || attempt >= attempts) {
32
+ throw error;
33
+ }
34
+ // Linear backoff: brief, since this only covers idle-socket races.
35
+ await new Promise((resolve) => setTimeout(resolve, RETRY_BACKOFF_STEP_MS * attempt));
36
+ }
37
+ }
38
+ }
39
+ /** Parse a response body as JSON, tolerating empty or non-JSON bodies. */
40
+ async function parseJsonResponse(response) {
41
+ const text = await response.text();
42
+ if (text.length === 0)
43
+ return undefined;
44
+ try {
45
+ return JSON.parse(text);
46
+ }
47
+ catch {
48
+ return { error: text };
49
+ }
50
+ }
51
+ /** Thin HTTP client over the plane API, shared by the CLI, SDKs, and runner. */
52
+ export class PlaneClient {
53
+ baseUrl;
54
+ adminToken;
55
+ constructor(baseUrl, adminToken) {
56
+ this.baseUrl = baseUrl.replace(/\/$/, "");
57
+ this.adminToken = adminToken;
58
+ }
59
+ async json(method, path, body, token) {
60
+ const headers = {};
61
+ const auth = token ?? this.adminToken;
62
+ if (auth)
63
+ headers.authorization = `Bearer ${auth}`;
64
+ if (body !== undefined)
65
+ headers["content-type"] = "application/json";
66
+ const response = await fetchIdempotent(`${this.baseUrl}${path}`, {
67
+ method,
68
+ headers,
69
+ body: body === undefined ? undefined : JSON.stringify(body)
70
+ });
71
+ const payload = await parseJsonResponse(response);
72
+ if (!response.ok)
73
+ throw new PlaneClientError(response.status, payload);
74
+ return payload;
75
+ }
76
+ enroll(input) {
77
+ return this.json("POST", "/v1/runners/enroll", input);
78
+ }
79
+ async putBlob(content, token) {
80
+ const headers = {
81
+ "content-type": "application/octet-stream"
82
+ };
83
+ const auth = token ?? this.adminToken;
84
+ if (auth)
85
+ headers.authorization = `Bearer ${auth}`;
86
+ const response = await fetchIdempotent(`${this.baseUrl}/v1/blobs`, {
87
+ method: "POST",
88
+ headers,
89
+ body: new Uint8Array(content),
90
+ idempotent: true
91
+ });
92
+ const payload = (await response.json());
93
+ if (!response.ok || !payload.hash) {
94
+ throw new PlaneClientError(response.status, payload);
95
+ }
96
+ return payload.hash;
97
+ }
98
+ async getBlob(hash) {
99
+ const response = await fetchIdempotent(`${this.baseUrl}/v1/blobs/${hash}`, {
100
+ method: "GET"
101
+ });
102
+ if (!response.ok) {
103
+ throw new PlaneClientError(response.status, await response.json());
104
+ }
105
+ return Buffer.from(await response.arrayBuffer());
106
+ }
107
+ requestRun(request) {
108
+ return this.json("POST", "/v1/runs", { request });
109
+ }
110
+ dryRun(request) {
111
+ return this.json("POST", "/v1/runs", { request, dryRun: true });
112
+ }
113
+ approve(runId, actor) {
114
+ return this.json("POST", `/v1/runs/${runId}/approve`, { actor });
115
+ }
116
+ cancel(runId, actor) {
117
+ return this.json("POST", `/v1/runs/${runId}/cancel`, { actor });
118
+ }
119
+ getRun(runId) {
120
+ return this.json("GET", `/v1/runs/${runId}`);
121
+ }
122
+ listRuns() {
123
+ return this.json("GET", "/v1/runs");
124
+ }
125
+ listRunners() {
126
+ return this.json("GET", "/v1/runners");
127
+ }
128
+ getPolicy() {
129
+ return this.json("GET", "/v1/policy");
130
+ }
131
+ claim(input) {
132
+ return this.json("POST", "/v1/claims", input);
133
+ }
134
+ postEvents(runId, claimToken, events) {
135
+ return this.json("POST", `/v1/runs/${runId}/events`, { claimToken, events });
136
+ }
137
+ complete(runId, claimToken, receipt) {
138
+ return this.json("POST", `/v1/runs/${runId}/complete`, { claimToken, receipt });
139
+ }
140
+ getBundle(runId) {
141
+ return this.json("GET", this.runBundlePath(runId));
142
+ }
143
+ runBundlePath(runId) {
144
+ return `/v1/runs/${runId}/bundle`;
145
+ }
146
+ /** Canonical download URL for a run's signed receipt bundle. */
147
+ runBundleUrl(runId) {
148
+ return `${this.baseUrl}${this.runBundlePath(runId)}`;
149
+ }
150
+ /** Canonical control-panel deep link for a run. */
151
+ runUiUrl(runId) {
152
+ return `${this.baseUrl}/ui/#/runs/${runId}`;
153
+ }
154
+ async exportJsonl(since) {
155
+ const query = since ? `?since=${encodeURIComponent(since)}` : "";
156
+ const headers = {};
157
+ if (this.adminToken)
158
+ headers.authorization = `Bearer ${this.adminToken}`;
159
+ const response = await fetchIdempotent(`${this.baseUrl}/v1/export${query}`, {
160
+ method: "GET",
161
+ headers
162
+ });
163
+ if (!response.ok) {
164
+ throw new PlaneClientError(response.status, await response.json());
165
+ }
166
+ return response.text();
167
+ }
168
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fusionkit/sdk — a thin client over the plane API. Protocol primitives
3
+ * (verification, hashing, wire types) live in @fusionkit/protocol; consumers
4
+ * import them from there rather than through this package.
5
+ */
6
+ export { PlaneClient, PlaneClientError } from "./client.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fusionkit/sdk — a thin client over the plane API. Protocol primitives
3
+ * (verification, hashing, wire types) live in @fusionkit/protocol; consumers
4
+ * import them from there rather than through this package.
5
+ */
6
+ export { PlaneClient, PlaneClientError } from "./client.js";
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@fusionkit/sdk",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/velum-labs/handoffkit.git",
8
+ "directory": "packages/sdk"
9
+ },
10
+ "description": "Warrant SDK: a thin client over the plane API plus offline receipt verification.",
11
+ "license": "UNLICENSED",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "registry": "https://registry.npmjs.org",
24
+ "access": "public",
25
+ "provenance": true
26
+ },
27
+ "dependencies": {
28
+ "@fusionkit/protocol": "0.1.0"
29
+ }
30
+ }