@zeke-02/tinfoil 0.0.2
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/LICENSE +661 -0
- package/README.md +169 -0
- package/dist/__tests__/test-utils.d.ts +1 -0
- package/dist/__tests__/test-utils.js +44 -0
- package/dist/ai-sdk-provider.d.ts +7 -0
- package/dist/ai-sdk-provider.js +23 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +20 -0
- package/dist/encrypted-body-fetch.d.ts +8 -0
- package/dist/encrypted-body-fetch.js +93 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +20 -0
- package/dist/esm/__tests__/test-utils.d.ts +1 -0
- package/dist/esm/__tests__/test-utils.js +38 -0
- package/dist/esm/ai-sdk-provider.d.ts +7 -0
- package/dist/esm/ai-sdk-provider.js +20 -0
- package/dist/esm/config.d.ts +17 -0
- package/dist/esm/config.js +17 -0
- package/dist/esm/encrypted-body-fetch.d.ts +8 -0
- package/dist/esm/encrypted-body-fetch.js +86 -0
- package/dist/esm/env.d.ts +5 -0
- package/dist/esm/env.js +17 -0
- package/dist/esm/fetch-adapter.d.ts +21 -0
- package/dist/esm/fetch-adapter.js +23 -0
- package/dist/esm/index.browser.d.ts +7 -0
- package/dist/esm/index.browser.js +8 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/pinned-tls-fetch.d.ts +1 -0
- package/dist/esm/pinned-tls-fetch.js +110 -0
- package/dist/esm/secure-client.d.ts +20 -0
- package/dist/esm/secure-client.js +123 -0
- package/dist/esm/secure-fetch.browser.d.ts +1 -0
- package/dist/esm/secure-fetch.browser.js +10 -0
- package/dist/esm/secure-fetch.d.ts +1 -0
- package/dist/esm/secure-fetch.js +22 -0
- package/dist/esm/tinfoilai.d.ts +54 -0
- package/dist/esm/tinfoilai.js +134 -0
- package/dist/esm/unverified-client.d.ts +18 -0
- package/dist/esm/unverified-client.js +33 -0
- package/dist/esm/verifier.d.ts +141 -0
- package/dist/esm/verifier.js +741 -0
- package/dist/esm/wasm-exec.js +668 -0
- package/dist/fetch-adapter.d.ts +21 -0
- package/dist/fetch-adapter.js +27 -0
- package/dist/index.browser.d.ts +7 -0
- package/dist/index.browser.js +29 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +49 -0
- package/dist/pinned-tls-fetch.d.ts +1 -0
- package/dist/pinned-tls-fetch.js +116 -0
- package/dist/secure-client.d.ts +20 -0
- package/dist/secure-client.js +127 -0
- package/dist/secure-fetch.browser.d.ts +1 -0
- package/dist/secure-fetch.browser.js +13 -0
- package/dist/secure-fetch.d.ts +1 -0
- package/dist/secure-fetch.js +25 -0
- package/dist/tinfoilai.d.ts +54 -0
- package/dist/tinfoilai.js +141 -0
- package/dist/unverified-client.d.ts +18 -0
- package/dist/unverified-client.js +37 -0
- package/dist/verifier.d.ts +141 -0
- package/dist/verifier.js +781 -0
- package/dist/wasm-exec.js +668 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Tinfoil Node Client
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tinfoilsh/tinfoil-node/actions)
|
|
4
|
+
[](https://npmjs.org/package/tinfoil)
|
|
5
|
+
|
|
6
|
+
This client library provides secure and convenient access to the Tinfoil Priavate Inference endpoints from TypeScript or JavaScript.
|
|
7
|
+
|
|
8
|
+
It is a wrapper around the OpenAI client that verifies enclave attestation and routes traffic to the Tinfoil Private Inference endpoints through an [EHBP](https://github.com/tinfoilsh/encrypted-http-body-protocol)-secured transport. EHBP encrypts all payloads directly to an attested enclave using [HPKE (RFC 9180)](https://www.rfc-editor.org/rfc/rfc9180.html).
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install tinfoil
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
Node 20+.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { TinfoilAI } from "tinfoil";
|
|
24
|
+
|
|
25
|
+
const client = new TinfoilAI({
|
|
26
|
+
apiKey: "<YOUR_API_KEY>", // or use TINFOIL_API_KEY env var
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Uses identical method calls as the OpenAI client
|
|
30
|
+
const completion = await client.chat.completions.create({
|
|
31
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
32
|
+
model: "llama3-3-70b",
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Browser Support
|
|
37
|
+
|
|
38
|
+
The SDK supports browser environments. This allows you to use the secure enclave-backed OpenAI API directly from web applications.
|
|
39
|
+
|
|
40
|
+
### ⚠️ Security Warning
|
|
41
|
+
|
|
42
|
+
Using API keys directly in the browser exposes them to anyone who can view your page source.
|
|
43
|
+
For production applications, always use a backend server to handle API keys.
|
|
44
|
+
|
|
45
|
+
### Browser Usage
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { TinfoilAI } from 'tinfoil';
|
|
49
|
+
|
|
50
|
+
const client = new TinfoilAI({
|
|
51
|
+
apiKey: 'your-api-key',
|
|
52
|
+
dangerouslyAllowBrowser: true // Required for browser usage
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Optional: pre-initialize; you can also call APIs directly
|
|
56
|
+
await client.ready();
|
|
57
|
+
|
|
58
|
+
const completion = await client.chat.completions.create({
|
|
59
|
+
model: 'llama3-3-70b',
|
|
60
|
+
messages: [{ role: 'user', content: 'Hello!' }]
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Browser Requirements
|
|
65
|
+
|
|
66
|
+
- Modern browsers with ES2020 support
|
|
67
|
+
- WebAssembly support for enclave verification
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## Verification helpers
|
|
71
|
+
|
|
72
|
+
This package exposes verification helpers that load the Go-based WebAssembly verifier once per process and provide structured, stepwise attestation results you can use in applications (e.g., to show progress, log transitions, or gate features).
|
|
73
|
+
|
|
74
|
+
The verification functionality is contained in `verifier.ts`.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
### Core Verifier API
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Verifier } from "tinfoil";
|
|
81
|
+
|
|
82
|
+
const verifier = new Verifier();
|
|
83
|
+
|
|
84
|
+
// Perform runtime attestation
|
|
85
|
+
const runtime = await verifier.verifyEnclave("enclave.host.com");
|
|
86
|
+
// Returns: { measurement: AttestationMeasurement, tlsPublicKeyFingerprint: string, hpkePublicKey: string }
|
|
87
|
+
|
|
88
|
+
// Perform code attestation
|
|
89
|
+
const code = await verifier.verifyCode("tinfoilsh/repo", "digest-hash");
|
|
90
|
+
// Returns: { measurement: AttestationMeasurement }
|
|
91
|
+
|
|
92
|
+
// Fetch latest digest from GitHub releases
|
|
93
|
+
const digest = await verifier.fetchLatestDigest("tinfoilsh/repo");
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Verification Document
|
|
97
|
+
|
|
98
|
+
The `SecureClient` provides access to a comprehensive verification document that tracks all verification steps, including failures:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { SecureClient } from "tinfoil";
|
|
102
|
+
|
|
103
|
+
const client = new SecureClient();
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await client.ready();
|
|
107
|
+
const response = await client.fetch('https://api.example.com/data');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Even on error, you can access the verification document
|
|
110
|
+
const doc = await client.getVerificationDocument();
|
|
111
|
+
|
|
112
|
+
// The document contains detailed step information:
|
|
113
|
+
// - fetchDigest: GitHub release digest retrieval
|
|
114
|
+
// - verifyCode: Code measurement verification
|
|
115
|
+
// - verifyEnclave: Runtime attestation verification
|
|
116
|
+
// - compareMeasurements: Code vs runtime measurement comparison
|
|
117
|
+
// - createTransport: Transport initialization (optional)
|
|
118
|
+
// - verifyHPKEKey: HPKE key verification (optional)
|
|
119
|
+
// - otherError: Catch-all for unexpected errors (optional)
|
|
120
|
+
|
|
121
|
+
console.log('Security verified:', doc.securityVerified);
|
|
122
|
+
|
|
123
|
+
// Check individual steps
|
|
124
|
+
if (doc.steps.verifyEnclave.status === 'failed') {
|
|
125
|
+
console.log('Enclave verification failed:', doc.steps.verifyEnclave.error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Testing
|
|
131
|
+
|
|
132
|
+
The project includes both unit tests and integration tests:
|
|
133
|
+
|
|
134
|
+
### Running Unit Tests
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm test
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Running Integration Tests
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
RUN_TINFOIL_INTEGRATION=true npm test
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This runs the full test suite including integration tests that:
|
|
147
|
+
- Make actual network requests to Tinfoil services
|
|
148
|
+
- Perform real enclave attestation verification
|
|
149
|
+
- Test end-to-end functionality with live services
|
|
150
|
+
|
|
151
|
+
Integration tests are skipped by default to keep the test suite fast and avoid network dependencies during development.
|
|
152
|
+
|
|
153
|
+
## Running examples
|
|
154
|
+
|
|
155
|
+
See [examples/README.md](https://github.com/tinfoilsh/tinfoil-node/blob/main/examples/README.md).
|
|
156
|
+
|
|
157
|
+
## API Documentation
|
|
158
|
+
|
|
159
|
+
This library mirrors the official OpenAI Node.js client for common endpoints (e.g., chat, images, embeddings) and types, and is designed to feel familiar. Some less commonly used surfaces may not be fully covered. See the [OpenAI client](https://github.com/openai/openai-node) for complete API usage and documentation.
|
|
160
|
+
|
|
161
|
+
## Reporting Vulnerabilities
|
|
162
|
+
|
|
163
|
+
Please report security vulnerabilities by either:
|
|
164
|
+
|
|
165
|
+
- Emailing [security@tinfoil.sh](mailto:security@tinfoil.sh)
|
|
166
|
+
|
|
167
|
+
- Opening an issue on GitHub on this repository
|
|
168
|
+
|
|
169
|
+
We aim to respond to security reports within 24 hours and will keep you updated on our progress.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function withMockedModules(mocks: Record<string, unknown>, modulesToReload: string[], run: () => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.withMockedModules = withMockedModules;
|
|
7
|
+
const module_1 = __importDefault(require("module"));
|
|
8
|
+
const testRequire = module_1.default.createRequire(__filename);
|
|
9
|
+
async function withMockedModules(mocks, modulesToReload, run) {
|
|
10
|
+
const moduleAny = module_1.default;
|
|
11
|
+
const originalLoad = moduleAny._load;
|
|
12
|
+
moduleAny._load = function (request, parent, isMain) {
|
|
13
|
+
if (Object.prototype.hasOwnProperty.call(mocks, request)) {
|
|
14
|
+
return mocks[request];
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line prefer-rest-params
|
|
17
|
+
return originalLoad.apply(this, arguments);
|
|
18
|
+
};
|
|
19
|
+
const restoredCache = [];
|
|
20
|
+
for (const specifier of modulesToReload) {
|
|
21
|
+
try {
|
|
22
|
+
const resolved = testRequire.resolve(specifier);
|
|
23
|
+
restoredCache.push({ path: resolved, cached: require.cache[resolved] });
|
|
24
|
+
delete require.cache[resolved];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Module not yet cached; nothing to remove.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await run();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
moduleAny._load = originalLoad;
|
|
35
|
+
for (const entry of restoredCache) {
|
|
36
|
+
if (entry.cached) {
|
|
37
|
+
require.cache[entry.path] = entry.cached;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
delete require.cache[entry.path];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface CreateTinfoilAIOptions {
|
|
2
|
+
baseURL?: string;
|
|
3
|
+
enclaveURL?: string;
|
|
4
|
+
configRepo?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createTinfoilAI(apiKey: string, options?: CreateTinfoilAIOptions): Promise<import("@ai-sdk/openai-compatible").OpenAICompatibleProvider<string, string, string, string>>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTinfoilAI = createTinfoilAI;
|
|
4
|
+
const openai_compatible_1 = require("@ai-sdk/openai-compatible");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const secure_client_1 = require("./secure-client");
|
|
7
|
+
async function createTinfoilAI(apiKey, options = {}) {
|
|
8
|
+
const baseURL = options.baseURL || config_1.TINFOIL_CONFIG.INFERENCE_BASE_URL;
|
|
9
|
+
const enclaveURL = options.enclaveURL || config_1.TINFOIL_CONFIG.ENCLAVE_URL;
|
|
10
|
+
const configRepo = options.configRepo || config_1.TINFOIL_CONFIG.INFERENCE_PROXY_REPO;
|
|
11
|
+
const secureClient = new secure_client_1.SecureClient({
|
|
12
|
+
baseURL,
|
|
13
|
+
enclaveURL,
|
|
14
|
+
configRepo,
|
|
15
|
+
});
|
|
16
|
+
await secureClient.ready();
|
|
17
|
+
return (0, openai_compatible_1.createOpenAICompatible)({
|
|
18
|
+
name: "tinfoil",
|
|
19
|
+
baseURL: baseURL.replace(/\/$/, ""),
|
|
20
|
+
apiKey: apiKey,
|
|
21
|
+
fetch: secureClient.fetch,
|
|
22
|
+
});
|
|
23
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for the Tinfoil Node SDK
|
|
3
|
+
*/
|
|
4
|
+
export declare const TINFOIL_CONFIG: {
|
|
5
|
+
/**
|
|
6
|
+
* The base URL for the Tinfoil router API
|
|
7
|
+
*/
|
|
8
|
+
readonly INFERENCE_BASE_URL: "https://router.inf6.tinfoil.sh/v1/";
|
|
9
|
+
/**
|
|
10
|
+
* The URL for enclave key discovery and attestation endpoints
|
|
11
|
+
*/
|
|
12
|
+
readonly ENCLAVE_URL: "https://router.inf6.tinfoil.sh";
|
|
13
|
+
/**
|
|
14
|
+
* The GitHub repository for code attestation verification
|
|
15
|
+
*/
|
|
16
|
+
readonly INFERENCE_PROXY_REPO: "tinfoilsh/confidential-model-router";
|
|
17
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TINFOIL_CONFIG = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Configuration constants for the Tinfoil Node SDK
|
|
6
|
+
*/
|
|
7
|
+
exports.TINFOIL_CONFIG = {
|
|
8
|
+
/**
|
|
9
|
+
* The base URL for the Tinfoil router API
|
|
10
|
+
*/
|
|
11
|
+
INFERENCE_BASE_URL: "https://router.inf6.tinfoil.sh/v1/",
|
|
12
|
+
/**
|
|
13
|
+
* The URL for enclave key discovery and attestation endpoints
|
|
14
|
+
*/
|
|
15
|
+
ENCLAVE_URL: "https://router.inf6.tinfoil.sh",
|
|
16
|
+
/**
|
|
17
|
+
* The GitHub repository for code attestation verification
|
|
18
|
+
*/
|
|
19
|
+
INFERENCE_PROXY_REPO: "tinfoilsh/confidential-model-router",
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function getHPKEKey(enclaveURL: string): Promise<CryptoKey>;
|
|
2
|
+
export declare function normalizeEncryptedBodyRequestArgs(input: RequestInfo | URL, init?: RequestInit): {
|
|
3
|
+
url: string;
|
|
4
|
+
init?: RequestInit;
|
|
5
|
+
};
|
|
6
|
+
export declare function encryptedBodyRequest(input: RequestInfo | URL, hpkePublicKey?: string, init?: RequestInit, enclaveURL?: string): Promise<Response>;
|
|
7
|
+
export declare function createEncryptedBodyFetch(baseURL: string, hpkePublicKey?: string, enclaveURL?: string): typeof fetch;
|
|
8
|
+
export declare function resetTransport(): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getHPKEKey = getHPKEKey;
|
|
4
|
+
exports.normalizeEncryptedBodyRequestArgs = normalizeEncryptedBodyRequestArgs;
|
|
5
|
+
exports.encryptedBodyRequest = encryptedBodyRequest;
|
|
6
|
+
exports.createEncryptedBodyFetch = createEncryptedBodyFetch;
|
|
7
|
+
exports.resetTransport = resetTransport;
|
|
8
|
+
const ehbp_1 = require("@zeke-02/ehbp");
|
|
9
|
+
const fetch_adapter_1 = require("./fetch-adapter");
|
|
10
|
+
let transport = null;
|
|
11
|
+
// Public API
|
|
12
|
+
async function getHPKEKey(enclaveURL) {
|
|
13
|
+
const keysURL = new URL(ehbp_1.PROTOCOL.KEYS_PATH, enclaveURL);
|
|
14
|
+
if (keysURL.protocol !== "https:") {
|
|
15
|
+
throw new Error(`HTTPS is required for remote key retrieval. Invalid protocol: ${keysURL.protocol}`);
|
|
16
|
+
}
|
|
17
|
+
const fetchFn = (0, fetch_adapter_1.getFetch)();
|
|
18
|
+
const response = await fetchFn(keysURL.toString());
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`Failed to get server public key: ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
const contentType = response.headers.get("content-type");
|
|
23
|
+
if (contentType !== ehbp_1.PROTOCOL.KEYS_MEDIA_TYPE) {
|
|
24
|
+
throw new Error(`Invalid content type: ${contentType}`);
|
|
25
|
+
}
|
|
26
|
+
const keysData = new Uint8Array(await response.arrayBuffer());
|
|
27
|
+
const serverIdentity = await ehbp_1.Identity.unmarshalPublicConfig(keysData);
|
|
28
|
+
return serverIdentity.getPublicKey();
|
|
29
|
+
}
|
|
30
|
+
function normalizeEncryptedBodyRequestArgs(input, init) {
|
|
31
|
+
if (typeof input === "string") {
|
|
32
|
+
return { url: input, init };
|
|
33
|
+
}
|
|
34
|
+
if (input instanceof URL) {
|
|
35
|
+
return { url: input.toString(), init };
|
|
36
|
+
}
|
|
37
|
+
const request = input;
|
|
38
|
+
const cloned = request.clone();
|
|
39
|
+
const derivedInit = {
|
|
40
|
+
method: cloned.method,
|
|
41
|
+
headers: new Headers(cloned.headers),
|
|
42
|
+
body: cloned.body ?? undefined,
|
|
43
|
+
signal: cloned.signal,
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
url: cloned.url,
|
|
47
|
+
init: { ...derivedInit, ...init },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function encryptedBodyRequest(input, hpkePublicKey, init, enclaveURL) {
|
|
51
|
+
const { url: requestUrl, init: requestInit } = normalizeEncryptedBodyRequestArgs(input, init);
|
|
52
|
+
const u = new URL(requestUrl);
|
|
53
|
+
const { origin } = u;
|
|
54
|
+
const keyOrigin = enclaveURL ? new URL(enclaveURL).origin : origin;
|
|
55
|
+
if (!transport) {
|
|
56
|
+
transport = getTransportForOrigin(origin, keyOrigin);
|
|
57
|
+
}
|
|
58
|
+
const transportInstance = await transport;
|
|
59
|
+
if (hpkePublicKey) {
|
|
60
|
+
const transportKeyHash = await transportInstance.getServerPublicKeyHex();
|
|
61
|
+
if (transportKeyHash !== hpkePublicKey) {
|
|
62
|
+
transport = null;
|
|
63
|
+
throw new Error(`HPKE public key mismatch. Expected: ${hpkePublicKey}, Got: ${transportKeyHash}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return transportInstance.request(requestUrl, requestInit);
|
|
67
|
+
}
|
|
68
|
+
function createEncryptedBodyFetch(baseURL, hpkePublicKey, enclaveURL) {
|
|
69
|
+
return (async (input, init) => {
|
|
70
|
+
const normalized = normalizeEncryptedBodyRequestArgs(input, init);
|
|
71
|
+
const targetUrl = new URL(normalized.url, baseURL);
|
|
72
|
+
return encryptedBodyRequest(targetUrl.toString(), hpkePublicKey, normalized.init, enclaveURL);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function resetTransport() {
|
|
76
|
+
transport = null;
|
|
77
|
+
}
|
|
78
|
+
async function getTransportForOrigin(origin, keyOrigin) {
|
|
79
|
+
if (typeof globalThis !== "undefined") {
|
|
80
|
+
const isSecure = globalThis.isSecureContext !== false;
|
|
81
|
+
const hasSubtle = !!(globalThis.crypto && globalThis.crypto.subtle);
|
|
82
|
+
if (!isSecure || !hasSubtle) {
|
|
83
|
+
const reason = !isSecure
|
|
84
|
+
? "insecure context (use HTTPS or localhost)"
|
|
85
|
+
: "missing WebCrypto SubtleCrypto";
|
|
86
|
+
throw new Error(`EHBP requires a secure browser context: ${reason}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const clientIdentity = await ehbp_1.Identity.generate();
|
|
90
|
+
const serverPublicKey = await getHPKEKey(keyOrigin);
|
|
91
|
+
const requestHost = new URL(origin).host;
|
|
92
|
+
return new ehbp_1.Transport(clientIdentity, requestHost, serverPublicKey);
|
|
93
|
+
}
|
package/dist/env.d.ts
ADDED
package/dist/env.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRealBrowser = isRealBrowser;
|
|
4
|
+
/**
|
|
5
|
+
* Detects if the code is running in a real browser environment.
|
|
6
|
+
* Returns false for Node.js environments, even with WASM loaded.
|
|
7
|
+
*/
|
|
8
|
+
function isRealBrowser() {
|
|
9
|
+
if (typeof process !== "undefined" &&
|
|
10
|
+
process.versions &&
|
|
11
|
+
process.versions.node) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
15
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function withMockedModules(mocks: Record<string, unknown>, modulesToReload: string[], run: () => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Module from "module";
|
|
2
|
+
const testRequire = Module.createRequire(__filename);
|
|
3
|
+
export async function withMockedModules(mocks, modulesToReload, run) {
|
|
4
|
+
const moduleAny = Module;
|
|
5
|
+
const originalLoad = moduleAny._load;
|
|
6
|
+
moduleAny._load = function (request, parent, isMain) {
|
|
7
|
+
if (Object.prototype.hasOwnProperty.call(mocks, request)) {
|
|
8
|
+
return mocks[request];
|
|
9
|
+
}
|
|
10
|
+
// eslint-disable-next-line prefer-rest-params
|
|
11
|
+
return originalLoad.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
const restoredCache = [];
|
|
14
|
+
for (const specifier of modulesToReload) {
|
|
15
|
+
try {
|
|
16
|
+
const resolved = testRequire.resolve(specifier);
|
|
17
|
+
restoredCache.push({ path: resolved, cached: require.cache[resolved] });
|
|
18
|
+
delete require.cache[resolved];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Module not yet cached; nothing to remove.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await run();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
moduleAny._load = originalLoad;
|
|
29
|
+
for (const entry of restoredCache) {
|
|
30
|
+
if (entry.cached) {
|
|
31
|
+
require.cache[entry.path] = entry.cached;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
delete require.cache[entry.path];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface CreateTinfoilAIOptions {
|
|
2
|
+
baseURL?: string;
|
|
3
|
+
enclaveURL?: string;
|
|
4
|
+
configRepo?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createTinfoilAI(apiKey: string, options?: CreateTinfoilAIOptions): Promise<import("@ai-sdk/openai-compatible").OpenAICompatibleProvider<string, string, string, string>>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
2
|
+
import { TINFOIL_CONFIG } from "./config";
|
|
3
|
+
import { SecureClient } from "./secure-client";
|
|
4
|
+
export async function createTinfoilAI(apiKey, options = {}) {
|
|
5
|
+
const baseURL = options.baseURL || TINFOIL_CONFIG.INFERENCE_BASE_URL;
|
|
6
|
+
const enclaveURL = options.enclaveURL || TINFOIL_CONFIG.ENCLAVE_URL;
|
|
7
|
+
const configRepo = options.configRepo || TINFOIL_CONFIG.INFERENCE_PROXY_REPO;
|
|
8
|
+
const secureClient = new SecureClient({
|
|
9
|
+
baseURL,
|
|
10
|
+
enclaveURL,
|
|
11
|
+
configRepo,
|
|
12
|
+
});
|
|
13
|
+
await secureClient.ready();
|
|
14
|
+
return createOpenAICompatible({
|
|
15
|
+
name: "tinfoil",
|
|
16
|
+
baseURL: baseURL.replace(/\/$/, ""),
|
|
17
|
+
apiKey: apiKey,
|
|
18
|
+
fetch: secureClient.fetch,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for the Tinfoil Node SDK
|
|
3
|
+
*/
|
|
4
|
+
export declare const TINFOIL_CONFIG: {
|
|
5
|
+
/**
|
|
6
|
+
* The base URL for the Tinfoil router API
|
|
7
|
+
*/
|
|
8
|
+
readonly INFERENCE_BASE_URL: "https://router.inf6.tinfoil.sh/v1/";
|
|
9
|
+
/**
|
|
10
|
+
* The URL for enclave key discovery and attestation endpoints
|
|
11
|
+
*/
|
|
12
|
+
readonly ENCLAVE_URL: "https://router.inf6.tinfoil.sh";
|
|
13
|
+
/**
|
|
14
|
+
* The GitHub repository for code attestation verification
|
|
15
|
+
*/
|
|
16
|
+
readonly INFERENCE_PROXY_REPO: "tinfoilsh/confidential-model-router";
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for the Tinfoil Node SDK
|
|
3
|
+
*/
|
|
4
|
+
export const TINFOIL_CONFIG = {
|
|
5
|
+
/**
|
|
6
|
+
* The base URL for the Tinfoil router API
|
|
7
|
+
*/
|
|
8
|
+
INFERENCE_BASE_URL: "https://router.inf6.tinfoil.sh/v1/",
|
|
9
|
+
/**
|
|
10
|
+
* The URL for enclave key discovery and attestation endpoints
|
|
11
|
+
*/
|
|
12
|
+
ENCLAVE_URL: "https://router.inf6.tinfoil.sh",
|
|
13
|
+
/**
|
|
14
|
+
* The GitHub repository for code attestation verification
|
|
15
|
+
*/
|
|
16
|
+
INFERENCE_PROXY_REPO: "tinfoilsh/confidential-model-router",
|
|
17
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function getHPKEKey(enclaveURL: string): Promise<CryptoKey>;
|
|
2
|
+
export declare function normalizeEncryptedBodyRequestArgs(input: RequestInfo | URL, init?: RequestInit): {
|
|
3
|
+
url: string;
|
|
4
|
+
init?: RequestInit;
|
|
5
|
+
};
|
|
6
|
+
export declare function encryptedBodyRequest(input: RequestInfo | URL, hpkePublicKey?: string, init?: RequestInit, enclaveURL?: string): Promise<Response>;
|
|
7
|
+
export declare function createEncryptedBodyFetch(baseURL: string, hpkePublicKey?: string, enclaveURL?: string): typeof fetch;
|
|
8
|
+
export declare function resetTransport(): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Identity, Transport, PROTOCOL } from "@zeke-02/ehbp";
|
|
2
|
+
import { getFetch } from "./fetch-adapter";
|
|
3
|
+
let transport = null;
|
|
4
|
+
// Public API
|
|
5
|
+
export async function getHPKEKey(enclaveURL) {
|
|
6
|
+
const keysURL = new URL(PROTOCOL.KEYS_PATH, enclaveURL);
|
|
7
|
+
if (keysURL.protocol !== "https:") {
|
|
8
|
+
throw new Error(`HTTPS is required for remote key retrieval. Invalid protocol: ${keysURL.protocol}`);
|
|
9
|
+
}
|
|
10
|
+
const fetchFn = getFetch();
|
|
11
|
+
const response = await fetchFn(keysURL.toString());
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`Failed to get server public key: ${response.status}`);
|
|
14
|
+
}
|
|
15
|
+
const contentType = response.headers.get("content-type");
|
|
16
|
+
if (contentType !== PROTOCOL.KEYS_MEDIA_TYPE) {
|
|
17
|
+
throw new Error(`Invalid content type: ${contentType}`);
|
|
18
|
+
}
|
|
19
|
+
const keysData = new Uint8Array(await response.arrayBuffer());
|
|
20
|
+
const serverIdentity = await Identity.unmarshalPublicConfig(keysData);
|
|
21
|
+
return serverIdentity.getPublicKey();
|
|
22
|
+
}
|
|
23
|
+
export function normalizeEncryptedBodyRequestArgs(input, init) {
|
|
24
|
+
if (typeof input === "string") {
|
|
25
|
+
return { url: input, init };
|
|
26
|
+
}
|
|
27
|
+
if (input instanceof URL) {
|
|
28
|
+
return { url: input.toString(), init };
|
|
29
|
+
}
|
|
30
|
+
const request = input;
|
|
31
|
+
const cloned = request.clone();
|
|
32
|
+
const derivedInit = {
|
|
33
|
+
method: cloned.method,
|
|
34
|
+
headers: new Headers(cloned.headers),
|
|
35
|
+
body: cloned.body ?? undefined,
|
|
36
|
+
signal: cloned.signal,
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
url: cloned.url,
|
|
40
|
+
init: { ...derivedInit, ...init },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function encryptedBodyRequest(input, hpkePublicKey, init, enclaveURL) {
|
|
44
|
+
const { url: requestUrl, init: requestInit } = normalizeEncryptedBodyRequestArgs(input, init);
|
|
45
|
+
const u = new URL(requestUrl);
|
|
46
|
+
const { origin } = u;
|
|
47
|
+
const keyOrigin = enclaveURL ? new URL(enclaveURL).origin : origin;
|
|
48
|
+
if (!transport) {
|
|
49
|
+
transport = getTransportForOrigin(origin, keyOrigin);
|
|
50
|
+
}
|
|
51
|
+
const transportInstance = await transport;
|
|
52
|
+
if (hpkePublicKey) {
|
|
53
|
+
const transportKeyHash = await transportInstance.getServerPublicKeyHex();
|
|
54
|
+
if (transportKeyHash !== hpkePublicKey) {
|
|
55
|
+
transport = null;
|
|
56
|
+
throw new Error(`HPKE public key mismatch. Expected: ${hpkePublicKey}, Got: ${transportKeyHash}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return transportInstance.request(requestUrl, requestInit);
|
|
60
|
+
}
|
|
61
|
+
export function createEncryptedBodyFetch(baseURL, hpkePublicKey, enclaveURL) {
|
|
62
|
+
return (async (input, init) => {
|
|
63
|
+
const normalized = normalizeEncryptedBodyRequestArgs(input, init);
|
|
64
|
+
const targetUrl = new URL(normalized.url, baseURL);
|
|
65
|
+
return encryptedBodyRequest(targetUrl.toString(), hpkePublicKey, normalized.init, enclaveURL);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function resetTransport() {
|
|
69
|
+
transport = null;
|
|
70
|
+
}
|
|
71
|
+
async function getTransportForOrigin(origin, keyOrigin) {
|
|
72
|
+
if (typeof globalThis !== "undefined") {
|
|
73
|
+
const isSecure = globalThis.isSecureContext !== false;
|
|
74
|
+
const hasSubtle = !!(globalThis.crypto && globalThis.crypto.subtle);
|
|
75
|
+
if (!isSecure || !hasSubtle) {
|
|
76
|
+
const reason = !isSecure
|
|
77
|
+
? "insecure context (use HTTPS or localhost)"
|
|
78
|
+
: "missing WebCrypto SubtleCrypto";
|
|
79
|
+
throw new Error(`EHBP requires a secure browser context: ${reason}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const clientIdentity = await Identity.generate();
|
|
83
|
+
const serverPublicKey = await getHPKEKey(keyOrigin);
|
|
84
|
+
const requestHost = new URL(origin).host;
|
|
85
|
+
return new Transport(clientIdentity, requestHost, serverPublicKey);
|
|
86
|
+
}
|
package/dist/esm/env.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if the code is running in a real browser environment.
|
|
3
|
+
* Returns false for Node.js environments, even with WASM loaded.
|
|
4
|
+
*/
|
|
5
|
+
export function isRealBrowser() {
|
|
6
|
+
if (typeof process !== "undefined" &&
|
|
7
|
+
process.versions &&
|
|
8
|
+
process.versions.node) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
12
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch adapter for Tauri v2
|
|
3
|
+
*
|
|
4
|
+
* This module provides a centralized fetch implementation for Tauri v2.
|
|
5
|
+
* For testing purposes, the fetch function can be overridden by setting
|
|
6
|
+
* the global __TINFOIL_TEST_FETCH__ property.
|
|
7
|
+
*/
|
|
8
|
+
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
|
9
|
+
declare global {
|
|
10
|
+
var __TINFOIL_TEST_FETCH__: typeof fetch | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get the fetch implementation to use.
|
|
14
|
+
* In tests, this can be overridden by setting globalThis.__TINFOIL_TEST_FETCH__
|
|
15
|
+
*/
|
|
16
|
+
export declare function getFetch(): typeof tauriFetch;
|
|
17
|
+
/**
|
|
18
|
+
* The fetch function to use throughout the application.
|
|
19
|
+
* Uses Tauri's fetch by default, but can be mocked for testing.
|
|
20
|
+
*/
|
|
21
|
+
export declare const fetch: typeof tauriFetch;
|