@tangle-network/sandbox 0.9.4-develop.20260701122931.9698e96 → 0.9.4-develop.20260702094234.bf6d602
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/auth/index.js +66 -25
- package/package.json +2 -2
package/dist/auth/index.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
65
|
+
* Base64URL-encoded JWT header `{"alg":"HS256","typ":"JWT"}`. Computed on
|
|
66
|
+
* first use (not at module load) and memoized.
|
|
32
67
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
123
|
-
|
|
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 =
|
|
159
|
-
const expectedRaw =
|
|
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.
|
|
3
|
+
"version": "0.9.4-develop.20260702094234.bf6d602",
|
|
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.
|
|
85
|
+
"@tangle-network/agent-core": "^0.4.0",
|
|
86
86
|
"@tangle-network/agent-interface": "^0.13.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|