@tangle-network/sandbox 0.9.4-develop.20260701061438.a81baba → 0.9.4-develop.20260702071701.d493d16

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/README.md CHANGED
@@ -75,6 +75,7 @@ try {
75
75
  }
76
76
  ```
77
77
 
78
+ The detached lease includes `billing.seconds`, `billing.customerCostUsd`, and provider cost basis fields once billing is finalized.
78
79
  See `examples/gpu-eval-lease.ts` for the full create, run, detach, and sandbox cleanup flow.
79
80
 
80
81
  ## Installation
@@ -1,39 +1,78 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
2
1
  //#region src/auth/tokens.ts
2
+ const textEncoder = new TextEncoder();
3
+ const textDecoder = new TextDecoder();
3
4
  /**
4
- * JWT Token Utilities
5
- *
6
- * Token generation and verification using HMAC-SHA256. Server-only
7
- * (uses Node.js `crypto`).
8
- */
9
- /**
10
- * Base64URL encode (RFC 7515).
5
+ * Base64URL encode (RFC 7515). Isomorphic: `btoa` + `TextEncoder` exist in
6
+ * Node and browsers, so — unlike `Buffer` — this never blanks a browser page
7
+ * that transitively imports the module.
11
8
  */
12
9
  function base64UrlEncode(data) {
13
- return (typeof data === "string" ? Buffer.from(data) : data).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
+ const bytes = typeof data === "string" ? textEncoder.encode(data) : data;
11
+ let binary = "";
12
+ for (const byte of bytes) binary += String.fromCharCode(byte);
13
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
14
  }
15
15
  /**
16
- * Base64URL decode (RFC 7515) to raw bytes. Returns `null` if the
17
- * input contains characters outside the base64url alphabet.
16
+ * Base64URL decode (RFC 7515) to raw bytes. Returns `null` if the input is
17
+ * not valid base64url (characters outside the alphabet, or a length that
18
+ * cannot decode).
18
19
  */
19
- function decodeBase64UrlToBuffer(input) {
20
+ function decodeBase64UrlToBytes(input) {
20
21
  if (!/^[A-Za-z0-9_-]*$/.test(input)) return null;
21
22
  const padded = input + "=".repeat((4 - input.length % 4) % 4);
22
- return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64");
23
+ let binary;
24
+ try {
25
+ binary = atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
26
+ } catch {
27
+ return null;
28
+ }
29
+ const bytes = new Uint8Array(binary.length);
30
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
31
+ return bytes;
32
+ }
33
+ /**
34
+ * Base64URL decode to a UTF-8 string, or `null` if the input is not valid
35
+ * base64url.
36
+ */
37
+ function decodeBase64UrlToString(input) {
38
+ const bytes = decodeBase64UrlToBytes(input);
39
+ return bytes === null ? null : textDecoder.decode(bytes);
40
+ }
41
+ let nodeCrypto;
42
+ /**
43
+ * Resolve Node's `crypto` lazily and synchronously. Signing and verification
44
+ * are server-only (HMAC-SHA256), but this module must stay importable in a
45
+ * browser bundle — a static `import "node:crypto"` forces bundlers to resolve
46
+ * the builtin at load and blanks the page. `process.getBuiltinModule` exists
47
+ * only on a Node runtime (Node >= 22.3); in a browser `globalThis.process` is
48
+ * undefined, so callers get a clear server-only error instead of a crash.
49
+ */
50
+ function getNodeCrypto() {
51
+ if (nodeCrypto) return nodeCrypto;
52
+ const resolved = globalThis.process?.getBuiltinModule?.("node:crypto");
53
+ if (!resolved) throw new Error("@tangle-network/sandbox/auth: token signing and verification are server-only (HMAC-SHA256 via Node.js crypto) and cannot run in a browser.");
54
+ nodeCrypto = resolved;
55
+ return nodeCrypto;
23
56
  }
24
57
  /**
25
58
  * Create HMAC-SHA256 signature.
26
59
  */
27
60
  function createSignature(data, secret) {
61
+ const { createHmac } = getNodeCrypto();
28
62
  return base64UrlEncode(createHmac("sha256", secret).update(data).digest());
29
63
  }
30
64
  /**
31
- * JWT header (always the same for our use case).
65
+ * Base64URL-encoded JWT header `{"alg":"HS256","typ":"JWT"}`. Computed on
66
+ * first use (not at module load) and memoized.
32
67
  */
33
- const JWT_HEADER = base64UrlEncode(JSON.stringify({
34
- alg: "HS256",
35
- typ: "JWT"
36
- }));
68
+ let jwtHeaderCache;
69
+ function jwtHeader() {
70
+ if (jwtHeaderCache === void 0) jwtHeaderCache = base64UrlEncode(JSON.stringify({
71
+ alg: "HS256",
72
+ typ: "JWT"
73
+ }));
74
+ return jwtHeaderCache;
75
+ }
37
76
  function issueToken(signingSecret, payload, ttlMinutes) {
38
77
  const now = Math.floor(Date.now() / 1e3);
39
78
  const fullPayload = {
@@ -41,7 +80,8 @@ function issueToken(signingSecret, payload, ttlMinutes) {
41
80
  iat: now,
42
81
  exp: now + ttlMinutes * 60
43
82
  };
44
- const data = `${JWT_HEADER}.${base64UrlEncode(JSON.stringify(fullPayload))}`;
83
+ const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
84
+ const data = `${jwtHeader()}.${encodedPayload}`;
45
85
  return `${data}.${createSignature(data, signingSecret)}`;
46
86
  }
47
87
  /**
@@ -119,8 +159,8 @@ function unsafeDecodeToken(token) {
119
159
  if (parts.length !== 3) return null;
120
160
  const payload = parts[1];
121
161
  if (payload === void 0) return null;
122
- const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
123
- const decoded = Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString();
162
+ const decoded = decodeBase64UrlToString(payload);
163
+ if (decoded === null) return null;
124
164
  return JSON.parse(decoded);
125
165
  } catch {
126
166
  return null;
@@ -145,20 +185,21 @@ function verifyToken(token, signingSecret, options = {}) {
145
185
  if (parts.length !== 3) return null;
146
186
  const [headerB64, payloadB64, signatureB64] = parts;
147
187
  if (!headerB64 || !payloadB64 || !signatureB64) return null;
188
+ const headerJson = decodeBase64UrlToString(headerB64);
189
+ if (headerJson === null) return null;
148
190
  let header;
149
191
  try {
150
- const headerPadded = headerB64 + "=".repeat((4 - headerB64.length % 4) % 4);
151
- const headerJson = Buffer.from(headerPadded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString();
152
192
  header = JSON.parse(headerJson);
153
193
  } catch {
154
194
  return null;
155
195
  }
156
196
  if (header.alg !== "HS256") return null;
157
197
  const expectedSig = createSignature(`${headerB64}.${payloadB64}`, signingSecret);
158
- const providedRaw = decodeBase64UrlToBuffer(signatureB64);
159
- const expectedRaw = decodeBase64UrlToBuffer(expectedSig);
198
+ const providedRaw = decodeBase64UrlToBytes(signatureB64);
199
+ const expectedRaw = decodeBase64UrlToBytes(expectedSig);
160
200
  if (!providedRaw || !expectedRaw) return null;
161
201
  if (providedRaw.length !== expectedRaw.length) return null;
202
+ const { timingSafeEqual } = getNodeCrypto();
162
203
  if (!timingSafeEqual(providedRaw, expectedRaw)) return null;
163
204
  const payload = unsafeDecodeToken(token);
164
205
  if (!payload) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox",
3
- "version": "0.9.4-develop.20260701061438.a81baba",
3
+ "version": "0.9.4-develop.20260702071701.d493d16",
4
4
  "description": "Client SDK for the Tangle Sandbox platform - build AI agent applications with dev containers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -82,7 +82,7 @@
82
82
  "url": "https://github.com/tangle-network/agent-dev-container/issues"
83
83
  },
84
84
  "dependencies": {
85
- "@tangle-network/agent-core": "^0.3.3",
85
+ "@tangle-network/agent-core": "^0.4.0",
86
86
  "@tangle-network/agent-interface": "^0.13.0"
87
87
  },
88
88
  "peerDependencies": {