@tangle-network/tcloud 0.4.2 → 0.4.4
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/attestation.cjs +46 -0
- package/dist/attestation.d.cts +1 -0
- package/dist/attestation.d.ts +1 -0
- package/dist/attestation.js +23 -0
- package/dist/{chunk-A7AEPV2G.js → chunk-4ZUVGKVH.js} +1 -1
- package/dist/chunk-DBIT227N.js +324 -0
- package/dist/{chunk-B22AH4JH.js → chunk-M5K3EFNP.js} +87 -0
- package/dist/{chunk-577KIKFA.js → chunk-MB4VK4MM.js} +38 -3
- package/dist/cli.cjs +370 -1
- package/dist/cli.js +73 -4
- package/dist/{client-D7_hFedn.d.ts → client-DkQugNHH.d.cts} +67 -1
- package/dist/{client-D7_hFedn.d.cts → client-DkQugNHH.d.ts} +67 -1
- package/dist/index.cjs +444 -2
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +33 -5
- package/dist/instance.cjs +87 -0
- package/dist/instance.d.cts +1 -1
- package/dist/instance.d.ts +1 -1
- package/dist/instance.js +1 -1
- package/dist/sandbox.cjs +350 -0
- package/dist/sandbox.d.cts +90 -0
- package/dist/sandbox.d.ts +90 -0
- package/dist/sandbox.js +16 -0
- package/dist/shielded.cjs +87 -0
- package/dist/shielded.d.cts +1 -1
- package/dist/shielded.d.ts +1 -1
- package/dist/shielded.js +2 -2
- package/package.json +32 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ShieldedWallet, generateWallet } from './shielded.js';
|
|
2
2
|
export { createShieldedClient, estimateCost, signSpendAuth } from './shielded.js';
|
|
3
|
-
import { T as TCloudClient, a as TCloudConfig } from './client-
|
|
4
|
-
export { A as ApiKeyInfo, b as AvatarGenerateRequest, c as AvatarGenerateResponse, d as AvatarJobStatus, e as AvatarResult, B as BatchJobResponse, f as BatchRequest, g as BridgeOptions, h as BridgeSession, C as ChatCompletion, i as ChatCompletionChunk, j as ChatMessage, k as ChatOptions, l as CompletionOptions, m as CompletionResponse, n as CreateKeyOptions, o as CreatedKey, p as CreditBalance, E as EmbeddingOptions, q as EmbeddingResponse, F as FineTuningJob, r as FineTuningJobOptions, G as GatewayOptions, I as ImageGenerateOptions, s as ImageResponse, J as JobEvent, M as Model, O as Operator, t as OperatorInfo, P as PricingTier, u as PrivacyConfig, v as PrivateRouter, w as PrivateRouterConfig,
|
|
3
|
+
import { T as TCloudClient, a as TCloudConfig, R as RotatingClientConfig } from './client-DkQugNHH.js';
|
|
4
|
+
export { A as ApiKeyInfo, b as AvatarGenerateRequest, c as AvatarGenerateResponse, d as AvatarJobStatus, e as AvatarResult, B as BatchJobResponse, f as BatchRequest, g as BridgeOptions, h as BridgeSession, C as ChatCompletion, i as ChatCompletionChunk, j as ChatMessage, k as ChatOptions, l as CompletionOptions, m as CompletionResponse, n as CreateKeyOptions, o as CreatedKey, p as CreditBalance, E as EmbeddingOptions, q as EmbeddingResponse, F as FineTuningJob, r as FineTuningJobOptions, G as GatewayOptions, I as ImageGenerateOptions, s as ImageResponse, J as JobEvent, M as Model, O as Operator, t as OperatorInfo, P as PricingTier, u as PrivacyConfig, v as PrivateRouter, w as PrivateRouterConfig, x as RerankOptions, y as RerankResponse, z as RetryConfig, D as RotatingRoutingConfig, H as RotationStats, K as RoutingConfig, L as RoutingStrategy, S as ShieldedConfig, N as SpendAuth, Q as SpendingLimits, U as TCloudError, V as TierConfig, W as TranscriptionResponse, X as UpdateKeyOptions, Y as VideoGenerateOptions, Z as VideoResponse, _ as WatchJobOptions } from './client-DkQugNHH.js';
|
|
5
|
+
export { TCloudSandbox, TCloudSandboxAttestationStatus, TCloudSandboxConfig, TCloudSandboxCreateOptions, TCloudSandboxCreateResult, TCloudSandboxTee, TCloudTeeAttestationChallenge, TCloudTeeAttestationHeartbeat, TCloudTeeAttestationHeartbeatOptions, TCloudTeeAttestationHeartbeatSample, createTeeAttestationChallenge, generateAttestationNonce, startTeeAttestationHeartbeat } from './sandbox.js';
|
|
6
|
+
export { AsyncAttestationPolicy, AsyncHardwareVerifier, AttestationPolicy, AttestationVerificationResult, HardwareVerifier, HardwareVerifierResult, ParsedAttestation, SevSnpReport, SevSnpVerifierOptions, TeeType, assertAttestation, createSevSnpHardwareVerifier, createTdxHardwareVerifier, normalizeTeeType, parseAttestation, parseSevSnpReport, toHex, verifyAttestation, verifyAttestationAsync } from '@tangle-network/tcloud-attestation';
|
|
5
7
|
export { AgentProfile, AgentProfileCapabilities, AgentProfileMcpServer, AgentProfileModelHints, AgentProfilePermissionValue, AgentProfilePrompt, AgentProfileResources } from '@tangle-network/sandbox';
|
|
6
8
|
import 'viem';
|
|
7
9
|
|
|
@@ -36,6 +38,19 @@ declare class TCloud extends TCloudClient {
|
|
|
36
38
|
wallet: ShieldedWallet;
|
|
37
39
|
stopAutoReplenish: () => void;
|
|
38
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Create a client that rotates operators per call.
|
|
43
|
+
* See {@link TCloudClient.rotating} for semantics.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const client = TCloud.rotating({
|
|
47
|
+
* apiKey: process.env.TCLOUD_API_KEY,
|
|
48
|
+
* routing: { strategy: 'min-exposure' },
|
|
49
|
+
* })
|
|
50
|
+
* const stats = client.getRotationStats()
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
static rotating(config?: RotatingClientConfig): TCloudClient;
|
|
39
54
|
/**
|
|
40
55
|
* Generate a new ephemeral wallet (without creating a client).
|
|
41
56
|
*
|
|
@@ -47,4 +62,4 @@ declare class TCloud extends TCloudClient {
|
|
|
47
62
|
static generateWallet: typeof generateWallet;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
export { ShieldedWallet, TCloud, TCloudClient, TCloudConfig, generateWallet };
|
|
65
|
+
export { RotatingClientConfig, ShieldedWallet, TCloud, TCloudClient, TCloudConfig, generateWallet };
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
import {
|
|
2
|
-
TCloud
|
|
3
|
-
|
|
2
|
+
TCloud,
|
|
3
|
+
assertAttestation,
|
|
4
|
+
createSevSnpHardwareVerifier,
|
|
5
|
+
createTdxHardwareVerifier,
|
|
6
|
+
normalizeTeeType,
|
|
7
|
+
parseAttestation,
|
|
8
|
+
parseSevSnpReport,
|
|
9
|
+
toHex,
|
|
10
|
+
verifyAttestation,
|
|
11
|
+
verifyAttestationAsync
|
|
12
|
+
} from "./chunk-MB4VK4MM.js";
|
|
13
|
+
import {
|
|
14
|
+
TCloudSandbox,
|
|
15
|
+
createTeeAttestationChallenge,
|
|
16
|
+
generateAttestationNonce,
|
|
17
|
+
startTeeAttestationHeartbeat
|
|
18
|
+
} from "./chunk-DBIT227N.js";
|
|
4
19
|
import {
|
|
5
20
|
createShieldedClient,
|
|
6
21
|
estimateCost,
|
|
7
22
|
generateWallet,
|
|
8
23
|
signSpendAuth
|
|
9
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-4ZUVGKVH.js";
|
|
10
25
|
import {
|
|
11
26
|
BridgeSession,
|
|
12
27
|
PrivateRouter,
|
|
13
28
|
TCloudClient,
|
|
14
29
|
TCloudError
|
|
15
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-M5K3EFNP.js";
|
|
16
31
|
export {
|
|
17
32
|
BridgeSession,
|
|
18
33
|
PrivateRouter,
|
|
19
34
|
TCloud,
|
|
20
35
|
TCloudClient,
|
|
21
36
|
TCloudError,
|
|
37
|
+
TCloudSandbox,
|
|
38
|
+
assertAttestation,
|
|
39
|
+
createSevSnpHardwareVerifier,
|
|
22
40
|
createShieldedClient,
|
|
41
|
+
createTdxHardwareVerifier,
|
|
42
|
+
createTeeAttestationChallenge,
|
|
23
43
|
estimateCost,
|
|
44
|
+
generateAttestationNonce,
|
|
24
45
|
generateWallet,
|
|
25
|
-
|
|
46
|
+
normalizeTeeType,
|
|
47
|
+
parseAttestation,
|
|
48
|
+
parseSevSnpReport,
|
|
49
|
+
signSpendAuth,
|
|
50
|
+
startTeeAttestationHeartbeat,
|
|
51
|
+
toHex,
|
|
52
|
+
verifyAttestation,
|
|
53
|
+
verifyAttestationAsync
|
|
26
54
|
};
|
package/dist/instance.cjs
CHANGED
|
@@ -53,6 +53,8 @@ var PrivateRouter = class {
|
|
|
53
53
|
usage = /* @__PURE__ */ new Map();
|
|
54
54
|
currentIndex = 0;
|
|
55
55
|
totalRequests = 0;
|
|
56
|
+
/** Slug of the most-recently-selected operator. Populated on each selectOperator() hit. */
|
|
57
|
+
_lastSelectedSlug = null;
|
|
56
58
|
constructor(config = {}) {
|
|
57
59
|
this.config = {
|
|
58
60
|
strategy: config.strategy || "round-robin",
|
|
@@ -198,6 +200,11 @@ var PrivateRouter = class {
|
|
|
198
200
|
requestCount: (existing?.requestCount || 0) + 1,
|
|
199
201
|
lastUsedAt: Date.now()
|
|
200
202
|
});
|
|
203
|
+
this._lastSelectedSlug = op.slug;
|
|
204
|
+
}
|
|
205
|
+
/** Slug of the most-recently-selected operator (null before the first call). */
|
|
206
|
+
get lastSelectedSlug() {
|
|
207
|
+
return this._lastSelectedSlug;
|
|
201
208
|
}
|
|
202
209
|
getLastUsedOperator() {
|
|
203
210
|
let latest = null;
|
|
@@ -255,6 +262,7 @@ var PrivateRouter = class {
|
|
|
255
262
|
};
|
|
256
263
|
|
|
257
264
|
// src/client.ts
|
|
265
|
+
var ROTATING_MARKER = "__tcloudRotating";
|
|
258
266
|
var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
|
|
259
267
|
var SDK_VERSION = "0.4.0";
|
|
260
268
|
async function proxiedFetch(privacy, url, init, streaming) {
|
|
@@ -352,6 +360,54 @@ var TCloudClient = class _TCloudClient {
|
|
|
352
360
|
const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
|
|
353
361
|
return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
|
|
354
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Build a client that rotates which operator serves each call. Mirrors
|
|
365
|
+
* {@link TCloudClient.shielded} in shape: returns a standard `TCloudClient`
|
|
366
|
+
* that behaves identically for the OpenAI-compatible surface but
|
|
367
|
+
* dispatches each chat/completions/embeddings request through a
|
|
368
|
+
* {@link PrivateRouter} — different operator per call per the chosen
|
|
369
|
+
* strategy.
|
|
370
|
+
*
|
|
371
|
+
* ```ts
|
|
372
|
+
* const tcloud = TCloudClient.rotating({
|
|
373
|
+
* apiKey: process.env.TCLOUD_API_KEY,
|
|
374
|
+
* routing: { strategy: 'min-exposure' },
|
|
375
|
+
* })
|
|
376
|
+
* await tcloud.ask('hello')
|
|
377
|
+
* tcloud.getRotationStats() // { callsByOperator: { ... }, currentOperator: '…' }
|
|
378
|
+
* ```
|
|
379
|
+
*
|
|
380
|
+
* Rotation is meaningful only for stateless calls. Sandbox-harness
|
|
381
|
+
* sessions bind to a single operator for the lifetime of the session;
|
|
382
|
+
* `rotating()` clients refuse to dispatch them — see {@link bridge}.
|
|
383
|
+
*/
|
|
384
|
+
static rotating(config = {}) {
|
|
385
|
+
const routing = config.routing ?? {};
|
|
386
|
+
const strategy = routing.strategy ?? "min-exposure";
|
|
387
|
+
const { routing: _omit, ...base } = config;
|
|
388
|
+
const client = new _TCloudClient(base);
|
|
389
|
+
const router = new PrivateRouter({
|
|
390
|
+
strategy,
|
|
391
|
+
minOperators: routing.minOperators ?? 1,
|
|
392
|
+
maxRequestsPerOperator: routing.maxRequestsPerOperator,
|
|
393
|
+
excludeOperators: routing.excludeOperators,
|
|
394
|
+
preferRegions: routing.preferRegions
|
|
395
|
+
});
|
|
396
|
+
if (routing.pool && routing.pool.length > 0) {
|
|
397
|
+
router.setOperators(routing.pool);
|
|
398
|
+
client._cachedOperators = routing.pool;
|
|
399
|
+
client._operatorsCachedAt = Date.now();
|
|
400
|
+
}
|
|
401
|
+
;
|
|
402
|
+
client.privateRouter = router;
|
|
403
|
+
Object.defineProperty(client, ROTATING_MARKER, {
|
|
404
|
+
value: true,
|
|
405
|
+
enumerable: false,
|
|
406
|
+
configurable: false,
|
|
407
|
+
writable: false
|
|
408
|
+
});
|
|
409
|
+
return client;
|
|
410
|
+
}
|
|
355
411
|
constructor(config = {}) {
|
|
356
412
|
this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
357
413
|
this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
|
|
@@ -667,10 +723,38 @@ var TCloudClient = class _TCloudClient {
|
|
|
667
723
|
*
|
|
668
724
|
* Sessions persist across process restarts — use the same `resume` id
|
|
669
725
|
* to land on the same CLI conversation (context intact, no replay tax).
|
|
726
|
+
*
|
|
727
|
+
* Guard: clients built via {@link TCloudClient.rotating} cannot dispatch
|
|
728
|
+
* sandbox-harness sessions (rotation rotates per call; a sandbox session
|
|
729
|
+
* binds to one operator). Attempting `bridge({ harness: 'sandbox' })` on
|
|
730
|
+
* a rotating client throws.
|
|
670
731
|
*/
|
|
671
732
|
bridge(cfg) {
|
|
733
|
+
if (cfg.harness === "sandbox" && this[ROTATING_MARKER] === true) {
|
|
734
|
+
throw new Error(
|
|
735
|
+
"TCloudClient.rotating() cannot dispatch sandbox-harness sessions.\nSandbox sessions bind to a single operator; rotation is meaningful only for\nstateless calls. Use TCloudClient.shielded() + AgentProfile.confidential.tee\nfor privacy-preserving sandbox execution instead."
|
|
736
|
+
);
|
|
737
|
+
}
|
|
672
738
|
return new BridgeSession(this, cfg);
|
|
673
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Rotation stats — populated only on clients created via
|
|
742
|
+
* {@link TCloudClient.rotating}. Non-rotating clients return an empty
|
|
743
|
+
* counter and `currentOperator: null`.
|
|
744
|
+
*/
|
|
745
|
+
getRotationStats() {
|
|
746
|
+
if (!this.privateRouter) {
|
|
747
|
+
return { callsByOperator: {}, currentOperator: null };
|
|
748
|
+
}
|
|
749
|
+
const callsByOperator = {};
|
|
750
|
+
for (const row of this.privateRouter.getStats().operatorBreakdown) {
|
|
751
|
+
callsByOperator[row.slug] = row.requests;
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
callsByOperator,
|
|
755
|
+
currentOperator: this.privateRouter.lastSelectedSlug
|
|
756
|
+
};
|
|
757
|
+
}
|
|
674
758
|
/** Convenience: send a single message and get the text response */
|
|
675
759
|
async ask(message, modelOrOptions) {
|
|
676
760
|
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
@@ -1172,6 +1256,8 @@ var BridgeSession = class _BridgeSession {
|
|
|
1172
1256
|
this.client = client;
|
|
1173
1257
|
this.cfg = cfg;
|
|
1174
1258
|
}
|
|
1259
|
+
client;
|
|
1260
|
+
cfg;
|
|
1175
1261
|
/** Full chat completion (non-streaming). */
|
|
1176
1262
|
async chat(options) {
|
|
1177
1263
|
return this.client.chat({ ...options, bridge: this.cfg });
|
|
@@ -1241,6 +1327,7 @@ var TCloudError = class extends Error {
|
|
|
1241
1327
|
this.status = status;
|
|
1242
1328
|
this.name = "TCloudError";
|
|
1243
1329
|
}
|
|
1330
|
+
status;
|
|
1244
1331
|
};
|
|
1245
1332
|
|
|
1246
1333
|
// src/instance.ts
|
package/dist/instance.d.cts
CHANGED
package/dist/instance.d.ts
CHANGED
package/dist/instance.js
CHANGED
package/dist/sandbox.cjs
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/sandbox.ts
|
|
21
|
+
var sandbox_exports = {};
|
|
22
|
+
__export(sandbox_exports, {
|
|
23
|
+
TCloudSandbox: () => TCloudSandbox,
|
|
24
|
+
buildSandboxCreateOptions: () => buildSandboxCreateOptions,
|
|
25
|
+
createTeeAttestationChallenge: () => createTeeAttestationChallenge,
|
|
26
|
+
generateAttestationNonce: () => generateAttestationNonce,
|
|
27
|
+
shouldVerifyAttestation: () => shouldVerifyAttestation,
|
|
28
|
+
startTeeAttestationHeartbeat: () => startTeeAttestationHeartbeat
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(sandbox_exports);
|
|
31
|
+
var import_sandbox = require("@tangle-network/sandbox");
|
|
32
|
+
var import_node_crypto = require("crypto");
|
|
33
|
+
var import_tcloud_attestation = require("@tangle-network/tcloud-attestation");
|
|
34
|
+
var DEFAULT_SANDBOX_URL = "https://sandbox.tangle.tools";
|
|
35
|
+
var TCloudSandbox = class {
|
|
36
|
+
client;
|
|
37
|
+
apiKey;
|
|
38
|
+
baseUrl;
|
|
39
|
+
timeoutMs;
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.apiKey = config.apiKey;
|
|
42
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_SANDBOX_URL).replace(/\/+$/, "");
|
|
43
|
+
this.timeoutMs = config.timeoutMs;
|
|
44
|
+
this.client = new import_sandbox.Sandbox({
|
|
45
|
+
apiKey: this.apiKey,
|
|
46
|
+
baseUrl: this.baseUrl,
|
|
47
|
+
timeoutMs: config.timeoutMs
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async create(options) {
|
|
51
|
+
const effectiveVerify = shouldVerifyAttestation(options);
|
|
52
|
+
const attestationPolicy = buildAttestationPolicy(options);
|
|
53
|
+
const createOptions = buildSandboxCreateOptions({
|
|
54
|
+
...options,
|
|
55
|
+
verify: effectiveVerify
|
|
56
|
+
});
|
|
57
|
+
const sandbox = await this.client.create(createOptions);
|
|
58
|
+
this.attachTeeApiFallbacks(sandbox);
|
|
59
|
+
let attestation = attestationFromMetadata(sandbox.metadata);
|
|
60
|
+
if ((effectiveVerify || createOptions.confidential?.attestationNonce) && !attestation) {
|
|
61
|
+
const getTeeAttestation = sandbox.getTeeAttestation;
|
|
62
|
+
if (typeof getTeeAttestation !== "function") {
|
|
63
|
+
throw new Error("Installed @tangle-network/sandbox does not expose TEE attestation fetching");
|
|
64
|
+
}
|
|
65
|
+
attestation = (await getTeeAttestation.call(
|
|
66
|
+
sandbox,
|
|
67
|
+
createOptions.confidential?.attestationNonce ? { attestationNonce: createOptions.confidential.attestationNonce } : void 0
|
|
68
|
+
)).attestation;
|
|
69
|
+
}
|
|
70
|
+
if (effectiveVerify && !attestation) {
|
|
71
|
+
throw new Error("TEE attestation verification requested but no evidence was returned");
|
|
72
|
+
}
|
|
73
|
+
const verification = effectiveVerify ? await (0, import_tcloud_attestation.verifyAttestationAsync)(attestation, {
|
|
74
|
+
...attestationPolicy,
|
|
75
|
+
expectedNonce: createOptions.confidential?.attestationNonce
|
|
76
|
+
}) : void 0;
|
|
77
|
+
if (verification && !verification.valid) {
|
|
78
|
+
throw new Error(`TEE attestation verification failed: ${verification.errors.join("; ")}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
sandbox,
|
|
82
|
+
attestation,
|
|
83
|
+
verification,
|
|
84
|
+
attestationNonce: createOptions.confidential?.attestationNonce,
|
|
85
|
+
attestationStatus: {
|
|
86
|
+
requested: Boolean(options.tee),
|
|
87
|
+
evidenceReturned: Boolean(attestation),
|
|
88
|
+
verified: Boolean(verification?.valid),
|
|
89
|
+
nonceBound: Boolean(createOptions.confidential?.attestationNonce && verification?.valid),
|
|
90
|
+
errors: verification?.errors ?? []
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
attachTeeApiFallbacks(sandbox) {
|
|
95
|
+
if (typeof sandbox !== "object" || sandbox === null) return;
|
|
96
|
+
const target = sandbox;
|
|
97
|
+
if (typeof target.id !== "string" || target.id.length === 0) return;
|
|
98
|
+
if (typeof target.getTeeAttestation !== "function") {
|
|
99
|
+
target.getTeeAttestation = (options) => this.fetchTeeAttestation(target.id, options?.attestationNonce);
|
|
100
|
+
}
|
|
101
|
+
if (typeof target.getTeePublicKey !== "function") {
|
|
102
|
+
target.getTeePublicKey = () => this.fetchTeePublicKey(target.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async fetchTeeAttestation(sandboxId, attestationNonce) {
|
|
106
|
+
const response = await this.fetchSandboxApi(
|
|
107
|
+
`/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/attestation`,
|
|
108
|
+
{
|
|
109
|
+
method: attestationNonce ? "POST" : "GET",
|
|
110
|
+
body: attestationNonce ? JSON.stringify({ attestation_nonce: attestationNonce }) : void 0
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
if (attestationNonce && !data.attestationNonce) {
|
|
115
|
+
data.attestationNonce = attestationNonce;
|
|
116
|
+
}
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
async fetchTeePublicKey(sandboxId) {
|
|
120
|
+
const response = await this.fetchSandboxApi(
|
|
121
|
+
`/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/public-key`,
|
|
122
|
+
{ method: "GET" }
|
|
123
|
+
);
|
|
124
|
+
return response.json();
|
|
125
|
+
}
|
|
126
|
+
async fetchSandboxApi(path, options) {
|
|
127
|
+
const headers = new Headers(options.headers);
|
|
128
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
129
|
+
if (options.body && !headers.has("Content-Type")) {
|
|
130
|
+
headers.set("Content-Type", "application/json");
|
|
131
|
+
}
|
|
132
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
133
|
+
...options,
|
|
134
|
+
headers,
|
|
135
|
+
signal: options.signal ?? (this.timeoutMs ? AbortSignal.timeout(this.timeoutMs) : void 0)
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const body = await response.text();
|
|
139
|
+
throw new Error(`Sandbox API ${path} failed with HTTP ${response.status}: ${body}`);
|
|
140
|
+
}
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
function createTeeAttestationChallenge(context) {
|
|
145
|
+
const randomHex = generateAttestationNonce(32);
|
|
146
|
+
if (context == null) {
|
|
147
|
+
return { nonce: randomHex, randomHex };
|
|
148
|
+
}
|
|
149
|
+
const contextHashHex = (0, import_node_crypto.createHash)("sha256").update(typeof context === "string" ? Buffer.from(context) : Buffer.from(context)).digest("hex");
|
|
150
|
+
return {
|
|
151
|
+
nonce: `${randomHex}${contextHashHex}`,
|
|
152
|
+
randomHex,
|
|
153
|
+
contextHashHex
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function startTeeAttestationHeartbeat(sandbox, options = {}) {
|
|
157
|
+
const getTeeAttestation = sandbox?.getTeeAttestation;
|
|
158
|
+
if (typeof getTeeAttestation !== "function") {
|
|
159
|
+
throw new Error("Sandbox does not expose TEE attestation fetching");
|
|
160
|
+
}
|
|
161
|
+
const intervalMs = options.intervalMs ?? 6e4;
|
|
162
|
+
const sessionId = options.sessionId ?? generateAttestationNonce(16);
|
|
163
|
+
const policy = options.tee ? buildAttestationPolicy({ tee: options.tee, attestationPolicy: options.attestationPolicy }) : options.attestationPolicy ?? {};
|
|
164
|
+
let stopped = false;
|
|
165
|
+
let failures = 0;
|
|
166
|
+
let sequence = 0;
|
|
167
|
+
let latest;
|
|
168
|
+
let timer;
|
|
169
|
+
let resolveDone;
|
|
170
|
+
const done = new Promise((resolve) => {
|
|
171
|
+
resolveDone = resolve;
|
|
172
|
+
});
|
|
173
|
+
const stop = () => {
|
|
174
|
+
if (stopped) return;
|
|
175
|
+
stopped = true;
|
|
176
|
+
if (timer) clearTimeout(timer);
|
|
177
|
+
resolveDone?.();
|
|
178
|
+
};
|
|
179
|
+
const ping = async (context) => {
|
|
180
|
+
if (stopped || options.signal?.aborted) {
|
|
181
|
+
throw new Error("TEE attestation heartbeat is stopped");
|
|
182
|
+
}
|
|
183
|
+
const nextSequence = sequence + 1;
|
|
184
|
+
const boundContext = context ?? `${sessionId}:${nextSequence}`;
|
|
185
|
+
const challenge = createTeeAttestationChallenge(boundContext);
|
|
186
|
+
const response = await getTeeAttestation.call(sandbox, {
|
|
187
|
+
attestationNonce: challenge.nonce
|
|
188
|
+
});
|
|
189
|
+
const attestation = response?.attestation;
|
|
190
|
+
if (!attestation) {
|
|
191
|
+
throw new Error("TEE attestation heartbeat returned no evidence");
|
|
192
|
+
}
|
|
193
|
+
const verification = await (0, import_tcloud_attestation.verifyAttestationAsync)(attestation, {
|
|
194
|
+
...policy,
|
|
195
|
+
expectedNonce: challenge.nonce
|
|
196
|
+
});
|
|
197
|
+
if (!verification.valid) {
|
|
198
|
+
throw new Error(`TEE attestation heartbeat verification failed: ${verification.errors.join("; ")}`);
|
|
199
|
+
}
|
|
200
|
+
const sample = {
|
|
201
|
+
sequence: nextSequence,
|
|
202
|
+
nonce: challenge.nonce,
|
|
203
|
+
context: boundContext,
|
|
204
|
+
attestation,
|
|
205
|
+
verification,
|
|
206
|
+
checkedAt: /* @__PURE__ */ new Date()
|
|
207
|
+
};
|
|
208
|
+
sequence = nextSequence;
|
|
209
|
+
latest = sample;
|
|
210
|
+
options.onSuccess?.(sample);
|
|
211
|
+
return sample;
|
|
212
|
+
};
|
|
213
|
+
const schedule = () => {
|
|
214
|
+
if (stopped || options.signal?.aborted) {
|
|
215
|
+
stop();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
timer = setTimeout(() => {
|
|
219
|
+
void loop();
|
|
220
|
+
}, intervalMs);
|
|
221
|
+
};
|
|
222
|
+
const loop = async () => {
|
|
223
|
+
try {
|
|
224
|
+
await ping();
|
|
225
|
+
schedule();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
failures += 1;
|
|
228
|
+
options.onFailure?.(error);
|
|
229
|
+
if (options.continueOnFailure) {
|
|
230
|
+
schedule();
|
|
231
|
+
} else {
|
|
232
|
+
stop();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
237
|
+
if (options.immediate === false) {
|
|
238
|
+
schedule();
|
|
239
|
+
} else {
|
|
240
|
+
void loop();
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
stop,
|
|
244
|
+
ping,
|
|
245
|
+
get stopped() {
|
|
246
|
+
return stopped;
|
|
247
|
+
},
|
|
248
|
+
get failures() {
|
|
249
|
+
return failures;
|
|
250
|
+
},
|
|
251
|
+
get latest() {
|
|
252
|
+
return latest;
|
|
253
|
+
},
|
|
254
|
+
done
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function buildSandboxCreateOptions(options) {
|
|
258
|
+
if (!options.tee && (options.sealed || options.attestationNonce || options.verify)) {
|
|
259
|
+
throw new Error("TEE options require a tee value");
|
|
260
|
+
}
|
|
261
|
+
const shouldGenerateNonce = options.attestationNonce === "auto" || shouldVerifyAttestation(options) && !options.attestationNonce;
|
|
262
|
+
const attestationNonce = shouldGenerateNonce ? generateAttestationNonce() : options.attestationNonce;
|
|
263
|
+
if (attestationNonce) {
|
|
264
|
+
validateAttestationNonce(attestationNonce);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
name: options.name,
|
|
268
|
+
environment: options.environment ?? options.image,
|
|
269
|
+
sshEnabled: options.ssh || void 0,
|
|
270
|
+
git: options.gitUrl ? {
|
|
271
|
+
url: options.gitUrl,
|
|
272
|
+
ref: options.gitRef
|
|
273
|
+
} : void 0,
|
|
274
|
+
resources: options.cpu || options.memoryMb || options.diskGb ? {
|
|
275
|
+
cpuCores: options.cpu,
|
|
276
|
+
memoryMB: options.memoryMb,
|
|
277
|
+
diskGB: options.diskGb
|
|
278
|
+
} : void 0,
|
|
279
|
+
backend: options.backend ? { type: options.backend } : void 0,
|
|
280
|
+
confidential: options.tee ? {
|
|
281
|
+
tee: options.tee,
|
|
282
|
+
sealed: options.sealed || void 0,
|
|
283
|
+
attestationNonce,
|
|
284
|
+
attestationRefresh: Boolean(attestationNonce)
|
|
285
|
+
} : void 0
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function shouldVerifyAttestation(options) {
|
|
289
|
+
return Boolean(options.tee || options.verify);
|
|
290
|
+
}
|
|
291
|
+
function buildAttestationPolicy(options) {
|
|
292
|
+
if (!options.tee || options.tee === "any") return options.attestationPolicy ?? {};
|
|
293
|
+
const requestedTypes = acceptedAttestationTypesForTee(options.tee);
|
|
294
|
+
const acceptedTeeTypes = options.attestationPolicy?.acceptedTeeTypes;
|
|
295
|
+
if (acceptedTeeTypes?.length && !acceptedTeeTypes.some((type) => requestedTypes.includes(type))) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`TEE attestation policy does not accept requested TEE type ${options.tee}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...options.attestationPolicy,
|
|
302
|
+
acceptedTeeTypes: acceptedTeeTypes?.length ? acceptedTeeTypes.filter((type) => requestedTypes.includes(type)) : requestedTypes
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function acceptedAttestationTypesForTee(tee) {
|
|
306
|
+
switch (tee) {
|
|
307
|
+
case "phala-dstack":
|
|
308
|
+
return ["tdx", "phala-dstack"];
|
|
309
|
+
case "gcp":
|
|
310
|
+
return ["tdx", "sev-snp", "gcp"];
|
|
311
|
+
case "azure":
|
|
312
|
+
return ["sev-snp", "azure"];
|
|
313
|
+
default:
|
|
314
|
+
return [(0, import_tcloud_attestation.normalizeTeeType)(tee)];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function validateAttestationNonce(value) {
|
|
318
|
+
const normalized = value.trim().toLowerCase().replace(/^0x/, "");
|
|
319
|
+
if (!/^[0-9a-f]+$/.test(normalized)) {
|
|
320
|
+
throw new Error("attestation nonce must be hex");
|
|
321
|
+
}
|
|
322
|
+
if (normalized.length % 2 !== 0) {
|
|
323
|
+
throw new Error("attestation nonce must have even hex length");
|
|
324
|
+
}
|
|
325
|
+
const bytes = normalized.length / 2;
|
|
326
|
+
if (bytes < 32 || bytes > 64) {
|
|
327
|
+
throw new Error(`attestation nonce must be 32-64 bytes, got ${bytes}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function generateAttestationNonce(bytes = 32) {
|
|
331
|
+
return Array.from((0, import_node_crypto.randomBytes)(bytes)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
332
|
+
}
|
|
333
|
+
function attestationFromMetadata(metadata) {
|
|
334
|
+
const raw = metadata?.teeAttestationJson;
|
|
335
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(raw);
|
|
338
|
+
} catch {
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
343
|
+
0 && (module.exports = {
|
|
344
|
+
TCloudSandbox,
|
|
345
|
+
buildSandboxCreateOptions,
|
|
346
|
+
createTeeAttestationChallenge,
|
|
347
|
+
generateAttestationNonce,
|
|
348
|
+
shouldVerifyAttestation,
|
|
349
|
+
startTeeAttestationHeartbeat
|
|
350
|
+
});
|