@layr-labs/ecloud-sdk 0.5.0-dev.3 → 1.0.0-dev.1
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/VERSION +2 -2
- package/dist/attest.cjs +31 -9
- package/dist/attest.cjs.map +1 -1
- package/dist/attest.d.cts +3 -2
- package/dist/attest.d.ts +3 -2
- package/dist/attest.js +31 -9
- package/dist/attest.js.map +1 -1
- package/dist/billing.cjs +46 -6
- package/dist/billing.cjs.map +1 -1
- package/dist/billing.d.cts +3 -1
- package/dist/billing.d.ts +3 -1
- package/dist/billing.js +46 -6
- package/dist/billing.js.map +1 -1
- package/dist/browser.cjs +40 -6
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +40 -6
- package/dist/browser.js.map +1 -1
- package/dist/{compute-Ckyn8ils.d.cts → compute-Bn6KcW3x.d.cts} +1 -1
- package/dist/{compute-Cnw6rwSB.d.ts → compute-Do2t0Fo8.d.ts} +1 -1
- package/dist/compute.cjs +8 -2
- package/dist/compute.cjs.map +1 -1
- package/dist/compute.d.cts +2 -2
- package/dist/compute.d.ts +2 -2
- package/dist/compute.js +8 -2
- package/dist/compute.js.map +1 -1
- package/dist/{helpers-DZL2bg9p.d.cts → helpers-BRamdfGn.d.ts} +5 -1
- package/dist/{helpers-Dj2ME5rp.d.ts → helpers-CfsfcGJO.d.cts} +5 -1
- package/dist/{index-U2vKBrry.d.cts → index-BbH7ZCIu.d.cts} +15 -1
- package/dist/{index-U2vKBrry.d.ts → index-BbH7ZCIu.d.ts} +15 -1
- package/dist/index.cjs +78 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +78 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
version=0.
|
|
2
|
-
commit=
|
|
1
|
+
version=1.0.0-dev.1
|
|
2
|
+
commit=2f1f9686f28371adcd7521bfd335c15cd928f61d
|
package/dist/attest.cjs
CHANGED
|
@@ -47,7 +47,10 @@ var AttestClient = class {
|
|
|
47
47
|
constructor(config) {
|
|
48
48
|
this.config = config;
|
|
49
49
|
}
|
|
50
|
-
async attest() {
|
|
50
|
+
async attest(extraData) {
|
|
51
|
+
if (extraData && extraData.length > 1048576) {
|
|
52
|
+
throw new Error(`extraData exceeds 1MB limit (${extraData.length} bytes)`);
|
|
53
|
+
}
|
|
51
54
|
const { publicKey, privateKey } = (0, import_node_crypto.generateKeyPairSync)("rsa", {
|
|
52
55
|
modulusLength: 4096,
|
|
53
56
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
@@ -55,8 +58,8 @@ var AttestClient = class {
|
|
|
55
58
|
});
|
|
56
59
|
const challengeHash = (0, import_node_crypto.createHash)("sha256").update(CHALLENGE_PREFIX).update(NULL_BYTE).update(publicKey).digest();
|
|
57
60
|
const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;
|
|
58
|
-
const attestationBytes = await this.getAttestation(socketPath, challengeHash);
|
|
59
|
-
const attestResponse = await this.postAttest(attestationBytes, publicKey);
|
|
61
|
+
const attestationBytes = await this.getAttestation(socketPath, challengeHash, extraData);
|
|
62
|
+
const attestResponse = await this.postAttest(attestationBytes, publicKey, extraData);
|
|
60
63
|
this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);
|
|
61
64
|
const rsaPrivateKey = await crypto.subtle.importKey(
|
|
62
65
|
"pkcs8",
|
|
@@ -88,9 +91,13 @@ var AttestClient = class {
|
|
|
88
91
|
throw new Error("KMS response signature verification failed");
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
|
-
getAttestation(socketPath, challenge) {
|
|
94
|
+
getAttestation(socketPath, challenge, extraData) {
|
|
92
95
|
return new Promise((resolve, reject) => {
|
|
93
|
-
const
|
|
96
|
+
const requestBody = { challenge: challenge.toString("base64") };
|
|
97
|
+
if (extraData && extraData.length > 0) {
|
|
98
|
+
requestBody.extra_data = extraData.toString("base64");
|
|
99
|
+
}
|
|
100
|
+
const body = JSON.stringify(requestBody);
|
|
94
101
|
const req = import_node_http.default.request(
|
|
95
102
|
{
|
|
96
103
|
socketPath,
|
|
@@ -118,14 +125,18 @@ var AttestClient = class {
|
|
|
118
125
|
req.end();
|
|
119
126
|
});
|
|
120
127
|
}
|
|
121
|
-
async postAttest(attestationBytes, rsaPublicKey) {
|
|
128
|
+
async postAttest(attestationBytes, rsaPublicKey, extraData) {
|
|
122
129
|
const url = `${this.config.kmsServerURL}/auth/attest`;
|
|
123
|
-
const
|
|
130
|
+
const requestBody = {
|
|
124
131
|
version: 3,
|
|
125
132
|
attestation: attestationBytes.toString("base64"),
|
|
126
133
|
rsaKey: rsaPublicKey,
|
|
127
134
|
audience: this.config.audience
|
|
128
|
-
}
|
|
135
|
+
};
|
|
136
|
+
if (extraData && extraData.length > 0) {
|
|
137
|
+
requestBody.extra_data = extraData.toString("base64");
|
|
138
|
+
}
|
|
139
|
+
const body = JSON.stringify(requestBody);
|
|
129
140
|
const response = await fetch(url, {
|
|
130
141
|
method: "POST",
|
|
131
142
|
headers: { "Content-Type": "application/json" },
|
|
@@ -147,10 +158,21 @@ function pemToBuffer(pem) {
|
|
|
147
158
|
// src/client/modules/attest/jwt-provider.ts
|
|
148
159
|
var JwtProvider = class {
|
|
149
160
|
constructor(attestClient, bufferSeconds = 30) {
|
|
161
|
+
this.pendingExtraData = /* @__PURE__ */ new Map();
|
|
150
162
|
this.attestClient = attestClient;
|
|
151
163
|
this.bufferSeconds = bufferSeconds;
|
|
152
164
|
}
|
|
153
|
-
async getToken() {
|
|
165
|
+
async getToken(extraData) {
|
|
166
|
+
if (extraData && extraData.length > 0) {
|
|
167
|
+
const key = extraData.toString("hex");
|
|
168
|
+
const existing = this.pendingExtraData.get(key);
|
|
169
|
+
if (existing) return existing;
|
|
170
|
+
const promise = this.attestClient.attest(extraData).finally(() => {
|
|
171
|
+
this.pendingExtraData.delete(key);
|
|
172
|
+
});
|
|
173
|
+
this.pendingExtraData.set(key, promise);
|
|
174
|
+
return promise;
|
|
175
|
+
}
|
|
154
176
|
if (this.cachedToken && !this.isExpiringSoon()) {
|
|
155
177
|
return this.cachedToken;
|
|
156
178
|
}
|
package/dist/attest.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/attest.ts","../src/client/modules/attest/attest-client.ts","../src/client/modules/attest/jwt-provider.ts"],"sourcesContent":["/**\n * Attestation module entry point\n *\n * Import from \"@layr-labs/ecloud-sdk/attest\" for direct access to attest module\n */\n\nexport * from \"./client/modules/attest\";\n","import { generateKeyPairSync, createHash, verify } from 'node:crypto';\nimport http from 'node:http';\nimport { compactDecrypt } from 'jose';\n\nexport interface AttestClientConfig {\n kmsServerURL: string;\n kmsPublicKey: string;\n audience: string;\n socketPath?: string;\n}\n\nconst DEFAULT_SOCKET_PATH = '/run/container_launcher/teeserver.sock';\nconst CHALLENGE_PREFIX = 'COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1';\nconst SIGNATURE_PREFIX = 'COMPUTE_APP_KMS_SIGNATURE_V1';\nconst NULL_BYTE = Buffer.from([0x00]);\n\nexport class AttestClient {\n private config: AttestClientConfig;\n\n constructor(config: AttestClientConfig) {\n this.config = config;\n }\n\n async attest(): Promise<string> {\n const { publicKey, privateKey } = generateKeyPairSync('rsa', {\n modulusLength: 4096,\n publicKeyEncoding: { type: 'spki', format: 'pem' } as const,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' } as const,\n });\n\n const challengeHash = createHash('sha256')\n .update(CHALLENGE_PREFIX)\n .update(NULL_BYTE)\n .update(publicKey)\n .digest();\n\n const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;\n const attestationBytes = await this.getAttestation(socketPath, challengeHash);\n const attestResponse = await this.postAttest(attestationBytes, publicKey);\n\n this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);\n\n const rsaPrivateKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToBuffer(privateKey),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['decrypt'],\n );\n\n const { plaintext } = await compactDecrypt(\n attestResponse.data.encryptedToken,\n rsaPrivateKey,\n );\n\n const decrypted = JSON.parse(new TextDecoder().decode(plaintext));\n return decrypted.token;\n }\n\n private verifySignature(\n dataJson: string,\n signature: string,\n ): void {\n const message = Buffer.concat([\n Buffer.from(SIGNATURE_PREFIX),\n NULL_BYTE,\n Buffer.from(dataJson),\n ]);\n\n const valid = verify(\n 'sha256',\n message,\n this.config.kmsPublicKey,\n Buffer.from(signature, 'base64'),\n );\n\n if (!valid) {\n throw new Error('KMS response signature verification failed');\n }\n }\n\n private getAttestation(socketPath: string, challenge: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ challenge: challenge.toString('base64') });\n\n const req = http.request(\n {\n socketPath,\n path: '/v1/bound_evidence',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n if (res.statusCode !== 200) {\n reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));\n return;\n }\n resolve(Buffer.concat(chunks));\n });\n },\n );\n\n req.on('error', (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));\n req.write(body);\n req.end();\n });\n }\n\n private async postAttest(\n attestationBytes: Buffer,\n rsaPublicKey: string,\n ): Promise<{ data: { encryptedToken: string }; signature: string }> {\n const url = `${this.config.kmsServerURL}/auth/attest`;\n const body = JSON.stringify({\n version: 3,\n attestation: attestationBytes.toString('base64'),\n rsaKey: rsaPublicKey,\n audience: this.config.audience,\n });\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`KMS attest failed (${response.status}): ${text}`);\n }\n\n return response.json() as Promise<{ data: { encryptedToken: string }; signature: string }>;\n }\n}\n\nfunction pemToBuffer(pem: string): ArrayBuffer {\n const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s/g, '');\n const buf = Buffer.from(b64, 'base64');\n\n // Copy to a fresh ArrayBuffer to avoid offset issues with Buffer's shared pool\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n}\n","import type { AttestClient } from './attest-client';\n\nexport class JwtProvider {\n private attestClient: AttestClient;\n private bufferSeconds: number;\n private cachedToken?: string;\n private expiresAt?: number;\n private pending?: Promise<string>;\n\n constructor(attestClient: AttestClient, bufferSeconds: number = 30) {\n this.attestClient = attestClient;\n this.bufferSeconds = bufferSeconds;\n }\n\n async getToken(): Promise<string> {\n if (this.cachedToken && !this.isExpiringSoon()) {\n return this.cachedToken;\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.attestClient\n .attest()\n .then((token) => {\n this.cachedToken = token;\n this.expiresAt = this.decodeJwtExp(token);\n return token;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n private isExpiringSoon(): boolean {\n if (!this.expiresAt) return true;\n return Date.now() / 1000 >= this.expiresAt - this.bufferSeconds;\n }\n\n private decodeJwtExp(jwt: string): number | undefined {\n const payload = jwt.split('.')[1];\n if (!payload) return undefined;\n\n const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());\n return decoded.exp;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAAwD;AACxD,uBAAiB;AACjB,kBAA+B;AAS/B,IAAM,sBAAsB;AAC5B,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,YAAY,OAAO,KAAK,CAAC,CAAI,CAAC;AAE7B,IAAM,eAAN,MAAmB;AAAA,EAGxB,YAAY,QAA4B;AACtC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,SAA0B;AAC9B,UAAM,EAAE,WAAW,WAAW,QAAI,wCAAoB,OAAO;AAAA,MAC3D,eAAe;AAAA,MACf,mBAAmB,EAAE,MAAM,QAAQ,QAAQ,MAAM;AAAA,MACjD,oBAAoB,EAAE,MAAM,SAAS,QAAQ,MAAM;AAAA,IACrD,CAAC;AAED,UAAM,oBAAgB,+BAAW,QAAQ,EACtC,OAAO,gBAAgB,EACvB,OAAO,SAAS,EAChB,OAAO,SAAS,EAChB,OAAO;AAEV,UAAM,aAAa,KAAK,OAAO,cAAc;AAC7C,UAAM,mBAAmB,MAAM,KAAK,eAAe,YAAY,aAAa;AAC5E,UAAM,iBAAiB,MAAM,KAAK,WAAW,kBAAkB,SAAS;AAExE,SAAK,gBAAgB,KAAK,UAAU,eAAe,IAAI,GAAG,eAAe,SAAS;AAElF,UAAM,gBAAgB,MAAM,OAAO,OAAO;AAAA,MACxC;AAAA,MACA,YAAY,UAAU;AAAA,MACtB,EAAE,MAAM,YAAY,MAAM,UAAU;AAAA,MACpC;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAEA,UAAM,EAAE,UAAU,IAAI,UAAM;AAAA,MAC1B,eAAe,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,YAAY,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AAChE,WAAO,UAAU;AAAA,EACnB;AAAA,EAEQ,gBACN,UACA,WACM;AACN,UAAM,UAAU,OAAO,OAAO;AAAA,MAC5B,OAAO,KAAK,gBAAgB;AAAA,MAC5B;AAAA,MACA,OAAO,KAAK,QAAQ;AAAA,IACtB,CAAC;AAED,UAAM,YAAQ;AAAA,MACZ;AAAA,MACA;AAAA,MACA,KAAK,OAAO;AAAA,MACZ,OAAO,KAAK,WAAW,QAAQ;AAAA,IACjC;AAEA,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AAAA,EACF;AAAA,EAEQ,eAAe,YAAoB,WAAoC;AAC7E,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,OAAO,KAAK,UAAU,EAAE,WAAW,UAAU,SAAS,QAAQ,EAAE,CAAC;AAEvE,YAAM,MAAM,iBAAAA,QAAK;AAAA,QACf;AAAA,UACE;AAAA,UACA,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,kBAAkB,OAAO,WAAW,IAAI;AAAA,UAC1C;AAAA,QACF;AAAA,QACA,CAAC,QAAQ;AACP,gBAAM,SAAmB,CAAC;AAC1B,cAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,cAAI,GAAG,OAAO,MAAM;AAClB,gBAAI,IAAI,eAAe,KAAK;AAC1B,qBAAO,IAAI,MAAM,2BAA2B,IAAI,UAAU,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;AACnG;AAAA,YACF;AACA,oBAAQ,OAAO,OAAO,MAAM,CAAC;AAAA,UAC/B,CAAC;AAAA,QACH;AAAA,MACF;AAEA,UAAI,GAAG,SAAS,CAAC,QAAQ,OAAO,IAAI,MAAM,mCAAmC,IAAI,OAAO,EAAE,CAAC,CAAC;AAC5F,UAAI,MAAM,IAAI;AACd,UAAI,IAAI;AAAA,IACV,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,WACZ,kBACA,cACkE;AAClE,UAAM,MAAM,GAAG,KAAK,OAAO,YAAY;AACvC,UAAM,OAAO,KAAK,UAAU;AAAA,MAC1B,SAAS;AAAA,MACT,aAAa,iBAAiB,SAAS,QAAQ;AAAA,MAC/C,QAAQ;AAAA,MACR,UAAU,KAAK,OAAO;AAAA,IACxB,CAAC;AAED,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,IACnE;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;AAEA,SAAS,YAAY,KAA0B;AAC7C,QAAM,MAAM,IAAI,QAAQ,sBAAsB,EAAE,EAAE,QAAQ,OAAO,EAAE;AACnE,QAAM,MAAM,OAAO,KAAK,KAAK,QAAQ;AAGrC,SAAO,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,UAAU;AACzE;;;ACjJO,IAAM,cAAN,MAAkB;AAAA,EAOvB,YAAY,cAA4B,gBAAwB,IAAI;AAClE,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,MAAM,WAA4B;AAChC,QAAI,KAAK,eAAe,CAAC,KAAK,eAAe,GAAG;AAC9C,aAAO,KAAK;AAAA,IACd;AAEA,QAAI,KAAK,SAAS;AAChB,aAAO,KAAK;AAAA,IACd;AAEA,SAAK,UAAU,KAAK,aACjB,OAAO,EACP,KAAK,CAAC,UAAU;AACf,WAAK,cAAc;AACnB,WAAK,YAAY,KAAK,aAAa,KAAK;AACxC,aAAO;AAAA,IACT,CAAC,EACA,QAAQ,MAAM;AACb,WAAK,UAAU;AAAA,IACjB,CAAC;AAEH,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,iBAA0B;AAChC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,WAAO,KAAK,IAAI,IAAI,OAAQ,KAAK,YAAY,KAAK;AAAA,EACpD;AAAA,EAEQ,aAAa,KAAiC;AACpD,UAAM,UAAU,IAAI,MAAM,GAAG,EAAE,CAAC;AAChC,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,UAAU,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,EAAE,SAAS,CAAC;AACvE,WAAO,QAAQ;AAAA,EACjB;AACF;","names":["http"]}
|
|
1
|
+
{"version":3,"sources":["../src/attest.ts","../src/client/modules/attest/attest-client.ts","../src/client/modules/attest/jwt-provider.ts"],"sourcesContent":["/**\n * Attestation module entry point\n *\n * Import from \"@layr-labs/ecloud-sdk/attest\" for direct access to attest module\n */\n\nexport * from \"./client/modules/attest\";\n","import { generateKeyPairSync, createHash, verify } from 'node:crypto';\nimport http from 'node:http';\nimport { compactDecrypt } from 'jose';\n\nexport interface AttestClientConfig {\n kmsServerURL: string;\n kmsPublicKey: string;\n audience: string;\n socketPath?: string;\n}\n\nconst DEFAULT_SOCKET_PATH = '/run/container_launcher/teeserver.sock';\nconst CHALLENGE_PREFIX = 'COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1';\nconst SIGNATURE_PREFIX = 'COMPUTE_APP_KMS_SIGNATURE_V1';\nconst NULL_BYTE = Buffer.from([0x00]);\n\nexport class AttestClient {\n private config: AttestClientConfig;\n\n constructor(config: AttestClientConfig) {\n this.config = config;\n }\n\n async attest(extraData?: Buffer): Promise<string> {\n // go-tpm-tools hashes extraData (SHA-256/SHA-512) before binding it into the\n // hardware nonce, so callers can pass arbitrary data up to 1MB.\n if (extraData && extraData.length > 1_048_576) {\n throw new Error(`extraData exceeds 1MB limit (${extraData.length} bytes)`);\n }\n\n const { publicKey, privateKey } = generateKeyPairSync('rsa', {\n modulusLength: 4096,\n publicKeyEncoding: { type: 'spki', format: 'pem' } as const,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' } as const,\n });\n\n const challengeHash = createHash('sha256')\n .update(CHALLENGE_PREFIX)\n .update(NULL_BYTE)\n .update(publicKey)\n .digest();\n\n const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;\n const attestationBytes = await this.getAttestation(socketPath, challengeHash, extraData);\n const attestResponse = await this.postAttest(attestationBytes, publicKey, extraData);\n\n this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);\n\n const rsaPrivateKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToBuffer(privateKey),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['decrypt'],\n );\n\n const { plaintext } = await compactDecrypt(\n attestResponse.data.encryptedToken,\n rsaPrivateKey,\n );\n\n const decrypted = JSON.parse(new TextDecoder().decode(plaintext));\n return decrypted.token;\n }\n\n private verifySignature(\n dataJson: string,\n signature: string,\n ): void {\n const message = Buffer.concat([\n Buffer.from(SIGNATURE_PREFIX),\n NULL_BYTE,\n Buffer.from(dataJson),\n ]);\n\n const valid = verify(\n 'sha256',\n message,\n this.config.kmsPublicKey,\n Buffer.from(signature, 'base64'),\n );\n\n if (!valid) {\n throw new Error('KMS response signature verification failed');\n }\n }\n\n private getAttestation(socketPath: string, challenge: Buffer, extraData?: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const requestBody: Record<string, string> = { challenge: challenge.toString('base64') };\n if (extraData && extraData.length > 0) {\n requestBody.extra_data = extraData.toString('base64');\n }\n const body = JSON.stringify(requestBody);\n\n const req = http.request(\n {\n socketPath,\n path: '/v1/bound_evidence',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n if (res.statusCode !== 200) {\n reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));\n return;\n }\n resolve(Buffer.concat(chunks));\n });\n },\n );\n\n req.on('error', (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));\n req.write(body);\n req.end();\n });\n }\n\n private async postAttest(\n attestationBytes: Buffer,\n rsaPublicKey: string,\n extraData?: Buffer,\n ): Promise<{ data: { encryptedToken: string }; signature: string }> {\n const url = `${this.config.kmsServerURL}/auth/attest`;\n const requestBody: Record<string, unknown> = {\n version: 3,\n attestation: attestationBytes.toString('base64'),\n rsaKey: rsaPublicKey,\n audience: this.config.audience,\n };\n if (extraData && extraData.length > 0) {\n requestBody.extra_data = extraData.toString('base64');\n }\n const body = JSON.stringify(requestBody);\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`KMS attest failed (${response.status}): ${text}`);\n }\n\n return response.json() as Promise<{ data: { encryptedToken: string }; signature: string }>;\n }\n}\n\nfunction pemToBuffer(pem: string): ArrayBuffer {\n const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s/g, '');\n const buf = Buffer.from(b64, 'base64');\n\n // Copy to a fresh ArrayBuffer to avoid offset issues with Buffer's shared pool\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n}\n","import type { AttestClient } from './attest-client';\n\nexport class JwtProvider {\n private attestClient: AttestClient;\n private bufferSeconds: number;\n private cachedToken?: string;\n private expiresAt?: number;\n private pending?: Promise<string>;\n private pendingExtraData = new Map<string, Promise<string>>();\n\n constructor(attestClient: AttestClient, bufferSeconds: number = 30) {\n this.attestClient = attestClient;\n this.bufferSeconds = bufferSeconds;\n }\n\n async getToken(extraData?: Buffer): Promise<string> {\n // When extraData is provided, bypass long-lived cache but deduplicate\n // concurrent requests for the same extraData to avoid thundering herd\n // on TEE hardware calls.\n if (extraData && extraData.length > 0) {\n const key = extraData.toString('hex');\n const existing = this.pendingExtraData.get(key);\n if (existing) return existing;\n const promise = this.attestClient.attest(extraData).finally(() => {\n this.pendingExtraData.delete(key);\n });\n this.pendingExtraData.set(key, promise);\n return promise;\n }\n\n if (this.cachedToken && !this.isExpiringSoon()) {\n return this.cachedToken;\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.attestClient\n .attest()\n .then((token) => {\n this.cachedToken = token;\n this.expiresAt = this.decodeJwtExp(token);\n return token;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n private isExpiringSoon(): boolean {\n if (!this.expiresAt) return true;\n return Date.now() / 1000 >= this.expiresAt - this.bufferSeconds;\n }\n\n private decodeJwtExp(jwt: string): number | undefined {\n const payload = jwt.split('.')[1];\n if (!payload) return undefined;\n\n const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());\n return decoded.exp;\n }\n}\n"],"mappings":"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","names":["http"]}
|
package/dist/attest.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ interface AttestClientConfig {
|
|
|
7
7
|
declare class AttestClient {
|
|
8
8
|
private config;
|
|
9
9
|
constructor(config: AttestClientConfig);
|
|
10
|
-
attest(): Promise<string>;
|
|
10
|
+
attest(extraData?: Buffer): Promise<string>;
|
|
11
11
|
private verifySignature;
|
|
12
12
|
private getAttestation;
|
|
13
13
|
private postAttest;
|
|
@@ -19,8 +19,9 @@ declare class JwtProvider {
|
|
|
19
19
|
private cachedToken?;
|
|
20
20
|
private expiresAt?;
|
|
21
21
|
private pending?;
|
|
22
|
+
private pendingExtraData;
|
|
22
23
|
constructor(attestClient: AttestClient, bufferSeconds?: number);
|
|
23
|
-
getToken(): Promise<string>;
|
|
24
|
+
getToken(extraData?: Buffer): Promise<string>;
|
|
24
25
|
private isExpiringSoon;
|
|
25
26
|
private decodeJwtExp;
|
|
26
27
|
}
|
package/dist/attest.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ interface AttestClientConfig {
|
|
|
7
7
|
declare class AttestClient {
|
|
8
8
|
private config;
|
|
9
9
|
constructor(config: AttestClientConfig);
|
|
10
|
-
attest(): Promise<string>;
|
|
10
|
+
attest(extraData?: Buffer): Promise<string>;
|
|
11
11
|
private verifySignature;
|
|
12
12
|
private getAttestation;
|
|
13
13
|
private postAttest;
|
|
@@ -19,8 +19,9 @@ declare class JwtProvider {
|
|
|
19
19
|
private cachedToken?;
|
|
20
20
|
private expiresAt?;
|
|
21
21
|
private pending?;
|
|
22
|
+
private pendingExtraData;
|
|
22
23
|
constructor(attestClient: AttestClient, bufferSeconds?: number);
|
|
23
|
-
getToken(): Promise<string>;
|
|
24
|
+
getToken(extraData?: Buffer): Promise<string>;
|
|
24
25
|
private isExpiringSoon;
|
|
25
26
|
private decodeJwtExp;
|
|
26
27
|
}
|
package/dist/attest.js
CHANGED
|
@@ -10,7 +10,10 @@ var AttestClient = class {
|
|
|
10
10
|
constructor(config) {
|
|
11
11
|
this.config = config;
|
|
12
12
|
}
|
|
13
|
-
async attest() {
|
|
13
|
+
async attest(extraData) {
|
|
14
|
+
if (extraData && extraData.length > 1048576) {
|
|
15
|
+
throw new Error(`extraData exceeds 1MB limit (${extraData.length} bytes)`);
|
|
16
|
+
}
|
|
14
17
|
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
15
18
|
modulusLength: 4096,
|
|
16
19
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
@@ -18,8 +21,8 @@ var AttestClient = class {
|
|
|
18
21
|
});
|
|
19
22
|
const challengeHash = createHash("sha256").update(CHALLENGE_PREFIX).update(NULL_BYTE).update(publicKey).digest();
|
|
20
23
|
const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;
|
|
21
|
-
const attestationBytes = await this.getAttestation(socketPath, challengeHash);
|
|
22
|
-
const attestResponse = await this.postAttest(attestationBytes, publicKey);
|
|
24
|
+
const attestationBytes = await this.getAttestation(socketPath, challengeHash, extraData);
|
|
25
|
+
const attestResponse = await this.postAttest(attestationBytes, publicKey, extraData);
|
|
23
26
|
this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);
|
|
24
27
|
const rsaPrivateKey = await crypto.subtle.importKey(
|
|
25
28
|
"pkcs8",
|
|
@@ -51,9 +54,13 @@ var AttestClient = class {
|
|
|
51
54
|
throw new Error("KMS response signature verification failed");
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
getAttestation(socketPath, challenge) {
|
|
57
|
+
getAttestation(socketPath, challenge, extraData) {
|
|
55
58
|
return new Promise((resolve, reject) => {
|
|
56
|
-
const
|
|
59
|
+
const requestBody = { challenge: challenge.toString("base64") };
|
|
60
|
+
if (extraData && extraData.length > 0) {
|
|
61
|
+
requestBody.extra_data = extraData.toString("base64");
|
|
62
|
+
}
|
|
63
|
+
const body = JSON.stringify(requestBody);
|
|
57
64
|
const req = http.request(
|
|
58
65
|
{
|
|
59
66
|
socketPath,
|
|
@@ -81,14 +88,18 @@ var AttestClient = class {
|
|
|
81
88
|
req.end();
|
|
82
89
|
});
|
|
83
90
|
}
|
|
84
|
-
async postAttest(attestationBytes, rsaPublicKey) {
|
|
91
|
+
async postAttest(attestationBytes, rsaPublicKey, extraData) {
|
|
85
92
|
const url = `${this.config.kmsServerURL}/auth/attest`;
|
|
86
|
-
const
|
|
93
|
+
const requestBody = {
|
|
87
94
|
version: 3,
|
|
88
95
|
attestation: attestationBytes.toString("base64"),
|
|
89
96
|
rsaKey: rsaPublicKey,
|
|
90
97
|
audience: this.config.audience
|
|
91
|
-
}
|
|
98
|
+
};
|
|
99
|
+
if (extraData && extraData.length > 0) {
|
|
100
|
+
requestBody.extra_data = extraData.toString("base64");
|
|
101
|
+
}
|
|
102
|
+
const body = JSON.stringify(requestBody);
|
|
92
103
|
const response = await fetch(url, {
|
|
93
104
|
method: "POST",
|
|
94
105
|
headers: { "Content-Type": "application/json" },
|
|
@@ -110,10 +121,21 @@ function pemToBuffer(pem) {
|
|
|
110
121
|
// src/client/modules/attest/jwt-provider.ts
|
|
111
122
|
var JwtProvider = class {
|
|
112
123
|
constructor(attestClient, bufferSeconds = 30) {
|
|
124
|
+
this.pendingExtraData = /* @__PURE__ */ new Map();
|
|
113
125
|
this.attestClient = attestClient;
|
|
114
126
|
this.bufferSeconds = bufferSeconds;
|
|
115
127
|
}
|
|
116
|
-
async getToken() {
|
|
128
|
+
async getToken(extraData) {
|
|
129
|
+
if (extraData && extraData.length > 0) {
|
|
130
|
+
const key = extraData.toString("hex");
|
|
131
|
+
const existing = this.pendingExtraData.get(key);
|
|
132
|
+
if (existing) return existing;
|
|
133
|
+
const promise = this.attestClient.attest(extraData).finally(() => {
|
|
134
|
+
this.pendingExtraData.delete(key);
|
|
135
|
+
});
|
|
136
|
+
this.pendingExtraData.set(key, promise);
|
|
137
|
+
return promise;
|
|
138
|
+
}
|
|
117
139
|
if (this.cachedToken && !this.isExpiringSoon()) {
|
|
118
140
|
return this.cachedToken;
|
|
119
141
|
}
|
package/dist/attest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/modules/attest/attest-client.ts","../src/client/modules/attest/jwt-provider.ts"],"sourcesContent":["import { generateKeyPairSync, createHash, verify } from 'node:crypto';\nimport http from 'node:http';\nimport { compactDecrypt } from 'jose';\n\nexport interface AttestClientConfig {\n kmsServerURL: string;\n kmsPublicKey: string;\n audience: string;\n socketPath?: string;\n}\n\nconst DEFAULT_SOCKET_PATH = '/run/container_launcher/teeserver.sock';\nconst CHALLENGE_PREFIX = 'COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1';\nconst SIGNATURE_PREFIX = 'COMPUTE_APP_KMS_SIGNATURE_V1';\nconst NULL_BYTE = Buffer.from([0x00]);\n\nexport class AttestClient {\n private config: AttestClientConfig;\n\n constructor(config: AttestClientConfig) {\n this.config = config;\n }\n\n async attest(): Promise<string> {\n const { publicKey, privateKey } = generateKeyPairSync('rsa', {\n modulusLength: 4096,\n publicKeyEncoding: { type: 'spki', format: 'pem' } as const,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' } as const,\n });\n\n const challengeHash = createHash('sha256')\n .update(CHALLENGE_PREFIX)\n .update(NULL_BYTE)\n .update(publicKey)\n .digest();\n\n const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;\n const attestationBytes = await this.getAttestation(socketPath, challengeHash);\n const attestResponse = await this.postAttest(attestationBytes, publicKey);\n\n this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);\n\n const rsaPrivateKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToBuffer(privateKey),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['decrypt'],\n );\n\n const { plaintext } = await compactDecrypt(\n attestResponse.data.encryptedToken,\n rsaPrivateKey,\n );\n\n const decrypted = JSON.parse(new TextDecoder().decode(plaintext));\n return decrypted.token;\n }\n\n private verifySignature(\n dataJson: string,\n signature: string,\n ): void {\n const message = Buffer.concat([\n Buffer.from(SIGNATURE_PREFIX),\n NULL_BYTE,\n Buffer.from(dataJson),\n ]);\n\n const valid = verify(\n 'sha256',\n message,\n this.config.kmsPublicKey,\n Buffer.from(signature, 'base64'),\n );\n\n if (!valid) {\n throw new Error('KMS response signature verification failed');\n }\n }\n\n private getAttestation(socketPath: string, challenge: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ challenge: challenge.toString('base64') });\n\n const req = http.request(\n {\n socketPath,\n path: '/v1/bound_evidence',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n if (res.statusCode !== 200) {\n reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));\n return;\n }\n resolve(Buffer.concat(chunks));\n });\n },\n );\n\n req.on('error', (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));\n req.write(body);\n req.end();\n });\n }\n\n private async postAttest(\n attestationBytes: Buffer,\n rsaPublicKey: string,\n ): Promise<{ data: { encryptedToken: string }; signature: string }> {\n const url = `${this.config.kmsServerURL}/auth/attest`;\n const body = JSON.stringify({\n version: 3,\n attestation: attestationBytes.toString('base64'),\n rsaKey: rsaPublicKey,\n audience: this.config.audience,\n });\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`KMS attest failed (${response.status}): ${text}`);\n }\n\n return response.json() as Promise<{ data: { encryptedToken: string }; signature: string }>;\n }\n}\n\nfunction pemToBuffer(pem: string): ArrayBuffer {\n const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s/g, '');\n const buf = Buffer.from(b64, 'base64');\n\n // Copy to a fresh ArrayBuffer to avoid offset issues with Buffer's shared pool\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n}\n","import type { AttestClient } from './attest-client';\n\nexport class JwtProvider {\n private attestClient: AttestClient;\n private bufferSeconds: number;\n private cachedToken?: string;\n private expiresAt?: number;\n private pending?: Promise<string>;\n\n constructor(attestClient: AttestClient, bufferSeconds: number = 30) {\n this.attestClient = attestClient;\n this.bufferSeconds = bufferSeconds;\n }\n\n async getToken(): Promise<string> {\n if (this.cachedToken && !this.isExpiringSoon()) {\n return this.cachedToken;\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.attestClient\n .attest()\n .then((token) => {\n this.cachedToken = token;\n this.expiresAt = this.decodeJwtExp(token);\n return token;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n private isExpiringSoon(): boolean {\n if (!this.expiresAt) return true;\n return Date.now() / 1000 >= this.expiresAt - this.bufferSeconds;\n }\n\n private decodeJwtExp(jwt: string): number | undefined {\n const payload = jwt.split('.')[1];\n if (!payload) return undefined;\n\n const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());\n return decoded.exp;\n }\n}\n"],"mappings":";AAAA,SAAS,qBAAqB,YAAY,cAAc;AACxD,OAAO,UAAU;AACjB,SAAS,sBAAsB;AAS/B,IAAM,sBAAsB;AAC5B,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,YAAY,OAAO,KAAK,CAAC,CAAI,CAAC;AAE7B,IAAM,eAAN,MAAmB;AAAA,EAGxB,YAAY,QAA4B;AACtC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,SAA0B;AAC9B,UAAM,EAAE,WAAW,WAAW,IAAI,oBAAoB,OAAO;AAAA,MAC3D,eAAe;AAAA,MACf,mBAAmB,EAAE,MAAM,QAAQ,QAAQ,MAAM;AAAA,MACjD,oBAAoB,EAAE,MAAM,SAAS,QAAQ,MAAM;AAAA,IACrD,CAAC;AAED,UAAM,gBAAgB,WAAW,QAAQ,EACtC,OAAO,gBAAgB,EACvB,OAAO,SAAS,EAChB,OAAO,SAAS,EAChB,OAAO;AAEV,UAAM,aAAa,KAAK,OAAO,cAAc;AAC7C,UAAM,mBAAmB,MAAM,KAAK,eAAe,YAAY,aAAa;AAC5E,UAAM,iBAAiB,MAAM,KAAK,WAAW,kBAAkB,SAAS;AAExE,SAAK,gBAAgB,KAAK,UAAU,eAAe,IAAI,GAAG,eAAe,SAAS;AAElF,UAAM,gBAAgB,MAAM,OAAO,OAAO;AAAA,MACxC;AAAA,MACA,YAAY,UAAU;AAAA,MACtB,EAAE,MAAM,YAAY,MAAM,UAAU;AAAA,MACpC;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAEA,UAAM,EAAE,UAAU,IAAI,MAAM;AAAA,MAC1B,eAAe,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,YAAY,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AAChE,WAAO,UAAU;AAAA,EACnB;AAAA,EAEQ,gBACN,UACA,WACM;AACN,UAAM,UAAU,OAAO,OAAO;AAAA,MAC5B,OAAO,KAAK,gBAAgB;AAAA,MAC5B;AAAA,MACA,OAAO,KAAK,QAAQ;AAAA,IACtB,CAAC;AAED,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA;AAAA,MACA,KAAK,OAAO;AAAA,MACZ,OAAO,KAAK,WAAW,QAAQ;AAAA,IACjC;AAEA,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AAAA,EACF;AAAA,EAEQ,eAAe,YAAoB,WAAoC;AAC7E,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,OAAO,KAAK,UAAU,EAAE,WAAW,UAAU,SAAS,QAAQ,EAAE,CAAC;AAEvE,YAAM,MAAM,KAAK;AAAA,QACf;AAAA,UACE;AAAA,UACA,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,kBAAkB,OAAO,WAAW,IAAI;AAAA,UAC1C;AAAA,QACF;AAAA,QACA,CAAC,QAAQ;AACP,gBAAM,SAAmB,CAAC;AAC1B,cAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,cAAI,GAAG,OAAO,MAAM;AAClB,gBAAI,IAAI,eAAe,KAAK;AAC1B,qBAAO,IAAI,MAAM,2BAA2B,IAAI,UAAU,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;AACnG;AAAA,YACF;AACA,oBAAQ,OAAO,OAAO,MAAM,CAAC;AAAA,UAC/B,CAAC;AAAA,QACH;AAAA,MACF;AAEA,UAAI,GAAG,SAAS,CAAC,QAAQ,OAAO,IAAI,MAAM,mCAAmC,IAAI,OAAO,EAAE,CAAC,CAAC;AAC5F,UAAI,MAAM,IAAI;AACd,UAAI,IAAI;AAAA,IACV,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,WACZ,kBACA,cACkE;AAClE,UAAM,MAAM,GAAG,KAAK,OAAO,YAAY;AACvC,UAAM,OAAO,KAAK,UAAU;AAAA,MAC1B,SAAS;AAAA,MACT,aAAa,iBAAiB,SAAS,QAAQ;AAAA,MAC/C,QAAQ;AAAA,MACR,UAAU,KAAK,OAAO;AAAA,IACxB,CAAC;AAED,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,IACnE;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;AAEA,SAAS,YAAY,KAA0B;AAC7C,QAAM,MAAM,IAAI,QAAQ,sBAAsB,EAAE,EAAE,QAAQ,OAAO,EAAE;AACnE,QAAM,MAAM,OAAO,KAAK,KAAK,QAAQ;AAGrC,SAAO,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,UAAU;AACzE;;;ACjJO,IAAM,cAAN,MAAkB;AAAA,EAOvB,YAAY,cAA4B,gBAAwB,IAAI;AAClE,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,MAAM,WAA4B;AAChC,QAAI,KAAK,eAAe,CAAC,KAAK,eAAe,GAAG;AAC9C,aAAO,KAAK;AAAA,IACd;AAEA,QAAI,KAAK,SAAS;AAChB,aAAO,KAAK;AAAA,IACd;AAEA,SAAK,UAAU,KAAK,aACjB,OAAO,EACP,KAAK,CAAC,UAAU;AACf,WAAK,cAAc;AACnB,WAAK,YAAY,KAAK,aAAa,KAAK;AACxC,aAAO;AAAA,IACT,CAAC,EACA,QAAQ,MAAM;AACb,WAAK,UAAU;AAAA,IACjB,CAAC;AAEH,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,iBAA0B;AAChC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,WAAO,KAAK,IAAI,IAAI,OAAQ,KAAK,YAAY,KAAK;AAAA,EACpD;AAAA,EAEQ,aAAa,KAAiC;AACpD,UAAM,UAAU,IAAI,MAAM,GAAG,EAAE,CAAC;AAChC,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,UAAU,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,EAAE,SAAS,CAAC;AACvE,WAAO,QAAQ;AAAA,EACjB;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client/modules/attest/attest-client.ts","../src/client/modules/attest/jwt-provider.ts"],"sourcesContent":["import { generateKeyPairSync, createHash, verify } from 'node:crypto';\nimport http from 'node:http';\nimport { compactDecrypt } from 'jose';\n\nexport interface AttestClientConfig {\n kmsServerURL: string;\n kmsPublicKey: string;\n audience: string;\n socketPath?: string;\n}\n\nconst DEFAULT_SOCKET_PATH = '/run/container_launcher/teeserver.sock';\nconst CHALLENGE_PREFIX = 'COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1';\nconst SIGNATURE_PREFIX = 'COMPUTE_APP_KMS_SIGNATURE_V1';\nconst NULL_BYTE = Buffer.from([0x00]);\n\nexport class AttestClient {\n private config: AttestClientConfig;\n\n constructor(config: AttestClientConfig) {\n this.config = config;\n }\n\n async attest(extraData?: Buffer): Promise<string> {\n // go-tpm-tools hashes extraData (SHA-256/SHA-512) before binding it into the\n // hardware nonce, so callers can pass arbitrary data up to 1MB.\n if (extraData && extraData.length > 1_048_576) {\n throw new Error(`extraData exceeds 1MB limit (${extraData.length} bytes)`);\n }\n\n const { publicKey, privateKey } = generateKeyPairSync('rsa', {\n modulusLength: 4096,\n publicKeyEncoding: { type: 'spki', format: 'pem' } as const,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' } as const,\n });\n\n const challengeHash = createHash('sha256')\n .update(CHALLENGE_PREFIX)\n .update(NULL_BYTE)\n .update(publicKey)\n .digest();\n\n const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;\n const attestationBytes = await this.getAttestation(socketPath, challengeHash, extraData);\n const attestResponse = await this.postAttest(attestationBytes, publicKey, extraData);\n\n this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);\n\n const rsaPrivateKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToBuffer(privateKey),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['decrypt'],\n );\n\n const { plaintext } = await compactDecrypt(\n attestResponse.data.encryptedToken,\n rsaPrivateKey,\n );\n\n const decrypted = JSON.parse(new TextDecoder().decode(plaintext));\n return decrypted.token;\n }\n\n private verifySignature(\n dataJson: string,\n signature: string,\n ): void {\n const message = Buffer.concat([\n Buffer.from(SIGNATURE_PREFIX),\n NULL_BYTE,\n Buffer.from(dataJson),\n ]);\n\n const valid = verify(\n 'sha256',\n message,\n this.config.kmsPublicKey,\n Buffer.from(signature, 'base64'),\n );\n\n if (!valid) {\n throw new Error('KMS response signature verification failed');\n }\n }\n\n private getAttestation(socketPath: string, challenge: Buffer, extraData?: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const requestBody: Record<string, string> = { challenge: challenge.toString('base64') };\n if (extraData && extraData.length > 0) {\n requestBody.extra_data = extraData.toString('base64');\n }\n const body = JSON.stringify(requestBody);\n\n const req = http.request(\n {\n socketPath,\n path: '/v1/bound_evidence',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n if (res.statusCode !== 200) {\n reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));\n return;\n }\n resolve(Buffer.concat(chunks));\n });\n },\n );\n\n req.on('error', (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));\n req.write(body);\n req.end();\n });\n }\n\n private async postAttest(\n attestationBytes: Buffer,\n rsaPublicKey: string,\n extraData?: Buffer,\n ): Promise<{ data: { encryptedToken: string }; signature: string }> {\n const url = `${this.config.kmsServerURL}/auth/attest`;\n const requestBody: Record<string, unknown> = {\n version: 3,\n attestation: attestationBytes.toString('base64'),\n rsaKey: rsaPublicKey,\n audience: this.config.audience,\n };\n if (extraData && extraData.length > 0) {\n requestBody.extra_data = extraData.toString('base64');\n }\n const body = JSON.stringify(requestBody);\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`KMS attest failed (${response.status}): ${text}`);\n }\n\n return response.json() as Promise<{ data: { encryptedToken: string }; signature: string }>;\n }\n}\n\nfunction pemToBuffer(pem: string): ArrayBuffer {\n const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s/g, '');\n const buf = Buffer.from(b64, 'base64');\n\n // Copy to a fresh ArrayBuffer to avoid offset issues with Buffer's shared pool\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n}\n","import type { AttestClient } from './attest-client';\n\nexport class JwtProvider {\n private attestClient: AttestClient;\n private bufferSeconds: number;\n private cachedToken?: string;\n private expiresAt?: number;\n private pending?: Promise<string>;\n private pendingExtraData = new Map<string, Promise<string>>();\n\n constructor(attestClient: AttestClient, bufferSeconds: number = 30) {\n this.attestClient = attestClient;\n this.bufferSeconds = bufferSeconds;\n }\n\n async getToken(extraData?: Buffer): Promise<string> {\n // When extraData is provided, bypass long-lived cache but deduplicate\n // concurrent requests for the same extraData to avoid thundering herd\n // on TEE hardware calls.\n if (extraData && extraData.length > 0) {\n const key = extraData.toString('hex');\n const existing = this.pendingExtraData.get(key);\n if (existing) return existing;\n const promise = this.attestClient.attest(extraData).finally(() => {\n this.pendingExtraData.delete(key);\n });\n this.pendingExtraData.set(key, promise);\n return promise;\n }\n\n if (this.cachedToken && !this.isExpiringSoon()) {\n return this.cachedToken;\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.attestClient\n .attest()\n .then((token) => {\n this.cachedToken = token;\n this.expiresAt = this.decodeJwtExp(token);\n return token;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n private isExpiringSoon(): boolean {\n if (!this.expiresAt) return true;\n return Date.now() / 1000 >= this.expiresAt - this.bufferSeconds;\n }\n\n private decodeJwtExp(jwt: string): number | undefined {\n const payload = jwt.split('.')[1];\n if (!payload) return undefined;\n\n const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());\n return decoded.exp;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/billing.cjs
CHANGED
|
@@ -317,6 +317,20 @@ var BillingApiClient = class {
|
|
|
317
317
|
const endpoint = `${this.config.billingApiServerURL}/products/${productId}/subscription`;
|
|
318
318
|
await this.makeAuthenticatedRequest(endpoint, "DELETE", productId);
|
|
319
319
|
}
|
|
320
|
+
async getPaymentMethods() {
|
|
321
|
+
const endpoint = `${this.config.billingApiServerURL}/v1/payment-methods`;
|
|
322
|
+
const resp = await this.makeAuthenticatedRequest(endpoint, "GET", "compute");
|
|
323
|
+
return resp.json();
|
|
324
|
+
}
|
|
325
|
+
async purchaseCredits(amountCents, paymentMethodId) {
|
|
326
|
+
const endpoint = `${this.config.billingApiServerURL}/v1/credits/purchase`;
|
|
327
|
+
const body = { amountCents };
|
|
328
|
+
if (paymentMethodId) {
|
|
329
|
+
body.paymentMethodId = paymentMethodId;
|
|
330
|
+
}
|
|
331
|
+
const resp = await this.makeAuthenticatedRequest(endpoint, "POST", "compute", body);
|
|
332
|
+
return resp.json();
|
|
333
|
+
}
|
|
320
334
|
// ==========================================================================
|
|
321
335
|
// Internal Methods
|
|
322
336
|
// ==========================================================================
|
|
@@ -326,10 +340,19 @@ var BillingApiClient = class {
|
|
|
326
340
|
* Uses session auth if useSession is true, otherwise uses EIP-712 signature auth.
|
|
327
341
|
*/
|
|
328
342
|
async makeAuthenticatedRequest(url, method, productId, body) {
|
|
329
|
-
if (this.
|
|
330
|
-
|
|
343
|
+
if (this.options.verbose) {
|
|
344
|
+
console.debug(`[BillingAPI] ${method} ${url}`);
|
|
345
|
+
}
|
|
346
|
+
const resp = this.useSession ? await this.makeSessionAuthenticatedRequest(url, method, body) : await this.makeSignatureAuthenticatedRequest(url, method, productId, body);
|
|
347
|
+
if (this.options.verbose) {
|
|
348
|
+
const data = await resp.json();
|
|
349
|
+
console.debug(`[BillingAPI] Response:`, JSON.stringify(data, null, 2));
|
|
350
|
+
return {
|
|
351
|
+
json: async () => data,
|
|
352
|
+
text: async () => JSON.stringify(data)
|
|
353
|
+
};
|
|
331
354
|
}
|
|
332
|
-
return
|
|
355
|
+
return resp;
|
|
333
356
|
}
|
|
334
357
|
/**
|
|
335
358
|
* Make a request using session-based authentication (cookies)
|
|
@@ -515,7 +538,13 @@ function getEnvironmentConfig(environment, chainID) {
|
|
|
515
538
|
const resolvedChainID = chainID || (environment === "sepolia" || environment === "sepolia-dev" ? SEPOLIA_CHAIN_ID : MAINNET_CHAIN_ID);
|
|
516
539
|
return {
|
|
517
540
|
...env,
|
|
518
|
-
chainID: BigInt(resolvedChainID)
|
|
541
|
+
chainID: BigInt(resolvedChainID),
|
|
542
|
+
...process.env.ECLOUD_USER_API_URL && {
|
|
543
|
+
userApiServerURL: process.env.ECLOUD_USER_API_URL
|
|
544
|
+
},
|
|
545
|
+
...process.env.ECLOUD_RPC_URL && {
|
|
546
|
+
defaultRPCURL: process.env.ECLOUD_RPC_URL
|
|
547
|
+
}
|
|
519
548
|
};
|
|
520
549
|
}
|
|
521
550
|
function getBillingEnvironmentConfig(build) {
|
|
@@ -523,7 +552,12 @@ function getBillingEnvironmentConfig(build) {
|
|
|
523
552
|
if (!config) {
|
|
524
553
|
throw new Error(`Unknown billing environment: ${build}`);
|
|
525
554
|
}
|
|
526
|
-
return
|
|
555
|
+
return {
|
|
556
|
+
...config,
|
|
557
|
+
...process.env.ECLOUD_BILLING_API_URL && {
|
|
558
|
+
billingApiServerURL: process.env.ECLOUD_BILLING_API_URL
|
|
559
|
+
}
|
|
560
|
+
};
|
|
527
561
|
}
|
|
528
562
|
function getBuildType() {
|
|
529
563
|
const buildTimeType = true ? "dev"?.toLowerCase() : void 0;
|
|
@@ -2066,7 +2100,7 @@ function createBillingModule(config) {
|
|
|
2066
2100
|
const address = walletClient.account.address;
|
|
2067
2101
|
const logger = getLogger(verbose);
|
|
2068
2102
|
const billingEnvConfig = getBillingEnvironmentConfig(getBuildType());
|
|
2069
|
-
const billingApi = new BillingApiClient(billingEnvConfig, walletClient);
|
|
2103
|
+
const billingApi = new BillingApiClient(billingEnvConfig, walletClient, { verbose });
|
|
2070
2104
|
const environmentConfig = getEnvironmentConfig(environment);
|
|
2071
2105
|
const usdcCreditsAddress = environmentConfig.usdcCreditsAddress;
|
|
2072
2106
|
if (!usdcCreditsAddress) {
|
|
@@ -2230,6 +2264,12 @@ function createBillingModule(config) {
|
|
|
2230
2264
|
};
|
|
2231
2265
|
}
|
|
2232
2266
|
);
|
|
2267
|
+
},
|
|
2268
|
+
async getPaymentMethods() {
|
|
2269
|
+
return billingApi.getPaymentMethods();
|
|
2270
|
+
},
|
|
2271
|
+
async purchaseCredits(amountCents, paymentMethodId) {
|
|
2272
|
+
return billingApi.purchaseCredits(amountCents, paymentMethodId);
|
|
2233
2273
|
}
|
|
2234
2274
|
};
|
|
2235
2275
|
return module2;
|