privateer-agent 0.7.0 → 0.8.0
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/package.json +4 -2
- package/src/channels/run.ts +11 -2
- package/src/cli/chat.ts +10 -2
- package/src/harbor/index.ts +45 -8
- package/src/harbor/ipc.ts +82 -34
- package/src/mcp/catalog.ts +106 -0
- package/src/providers/account.ts +57 -10
- package/src/providers/catalog.ts +8 -6
- package/src/providers/phala/aci-verifier/VENDORED.md +23 -0
- package/src/providers/phala/aci-verifier/crypto.ts +95 -0
- package/src/providers/phala/aci-verifier/digest.ts +116 -0
- package/src/providers/phala/aci-verifier/e2ee-channel.ts +242 -0
- package/src/providers/phala/aci-verifier/e2ee.ts +73 -0
- package/src/providers/phala/aci-verifier/errors.ts +41 -0
- package/src/providers/phala/aci-verifier/index.ts +87 -0
- package/src/providers/phala/aci-verifier/jcs.ts +69 -0
- package/src/providers/phala/aci-verifier/receipt.ts +139 -0
- package/src/providers/phala/aci-verifier/report.ts +126 -0
- package/src/providers/phala/aci-verifier/types.ts +139 -0
- package/src/providers/phala/sse.ts +43 -0
- package/src/providers/phala/webcrypto-globals.d.ts +17 -0
- package/src/providers/phalaSeal.ts +200 -0
- package/src/providers/sealedShim.ts +295 -0
- package/src/remote/liveTaskSession.ts +11 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Canonicalization Scheme (RFC 8785) for the ACI subset.
|
|
3
|
+
*
|
|
4
|
+
* ACI restricts JSON numbers to integers (§3), so this omits ECMAScript number
|
|
5
|
+
* formatting and instead *rejects* non-integer numbers — a conformant ACI object
|
|
6
|
+
* never contains one, and rejecting is safer than silently mis-serializing. For
|
|
7
|
+
* strings we reuse `JSON.stringify`, whose escaping is exactly RFC 8785's
|
|
8
|
+
* (minimal escapes, `\uXXXX` for other controls, lone surrogates escaped) since
|
|
9
|
+
* ES2019. Object members are ordered by their UTF-16 code units, which is what
|
|
10
|
+
* JavaScript's `<` on strings compares, and `undefined`-valued members are
|
|
11
|
+
* dropped (standard JSON behaviour) — build with explicit `null` where a field
|
|
12
|
+
* must appear.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { AciFormatError } from './errors';
|
|
16
|
+
|
|
17
|
+
/** A value canonicalizable under the ACI JCS subset. */
|
|
18
|
+
export type JcsValue =
|
|
19
|
+
| string
|
|
20
|
+
| number
|
|
21
|
+
| boolean
|
|
22
|
+
| null
|
|
23
|
+
| JcsValue[]
|
|
24
|
+
| { [key: string]: JcsValue | undefined };
|
|
25
|
+
|
|
26
|
+
/** Canonicalize a value to its RFC 8785 string form. */
|
|
27
|
+
export function canonicalize(value: JcsValue): string {
|
|
28
|
+
if (value === null) return 'null';
|
|
29
|
+
switch (typeof value) {
|
|
30
|
+
case 'boolean':
|
|
31
|
+
return value ? 'true' : 'false';
|
|
32
|
+
case 'number':
|
|
33
|
+
if (!Number.isInteger(value)) {
|
|
34
|
+
throw new AciFormatError(
|
|
35
|
+
`JCS: ACI restricts numbers to integers, got ${value} (§3)`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
// -0 canonicalizes to "0".
|
|
39
|
+
return Object.is(value, -0) ? '0' : String(value);
|
|
40
|
+
case 'string':
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
case 'object':
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return '[' + value.map(canonicalize).join(',') + ']';
|
|
45
|
+
}
|
|
46
|
+
return serializeObject(value);
|
|
47
|
+
default:
|
|
48
|
+
throw new AciFormatError(`JCS: unsupported type ${typeof value}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function serializeObject(obj: { [key: string]: JcsValue | undefined }): string {
|
|
53
|
+
const keys = Object.keys(obj)
|
|
54
|
+
.filter((k) => obj[k] !== undefined)
|
|
55
|
+
// RFC 8785 orders by UTF-16 code units; JS `<` on strings does exactly that.
|
|
56
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
57
|
+
let out = '{';
|
|
58
|
+
for (let i = 0; i < keys.length; i++) {
|
|
59
|
+
const k = keys[i]!;
|
|
60
|
+
if (i > 0) out += ',';
|
|
61
|
+
out += JSON.stringify(k) + ':' + canonicalize(obj[k] as JcsValue);
|
|
62
|
+
}
|
|
63
|
+
return out + '}';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Canonicalize and encode to UTF-8 bytes — the form fed to SHA-256 and signatures. */
|
|
67
|
+
export function jcsBytes(value: JcsValue): Uint8Array {
|
|
68
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
69
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 1 receipt verification (§10.2 checks 1–2) and helpers for the body-hash
|
|
3
|
+
* checks (§10.2 checks 3–4). "Established identity and keyset" means a keyset the
|
|
4
|
+
* caller already trusts — from a Level 2 report verification, or published by a
|
|
5
|
+
* party the client trusts. The recomputed `workload_id` and keyset digest of
|
|
6
|
+
* that keyset are the values the receipt must match.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { receiptSigningBytes, computeWorkloadId, computeKeysetDigest } from './digest';
|
|
10
|
+
import { verifySignature, sha256Prefixed } from './crypto';
|
|
11
|
+
import { fromHex } from './crypto';
|
|
12
|
+
import type { Receipt, ReceiptEvent, WorkloadKeyset, Check, ReceiptVerification } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Verify a receipt against an established keyset — §10.2 checks 1 and 2:
|
|
16
|
+
*
|
|
17
|
+
* 1. `signature.key_id` names a key in the keyset's `receipt_signing_keys`,
|
|
18
|
+
* `signature.algo` matches that key, and the signature verifies over the
|
|
19
|
+
* §8.5 canonical bytes under that key.
|
|
20
|
+
* 2. The receipt's `workload_id` and `workload_keyset_digest` equal the values
|
|
21
|
+
* recomputed from the established keyset (§4.1, §4.2).
|
|
22
|
+
*
|
|
23
|
+
* Returns a per-check result — a failed check is `ok: false`, never thrown.
|
|
24
|
+
* Throws {@link UnsupportedAlgorithmError} only when the signing algorithm is
|
|
25
|
+
* outside Web Crypto scope (e.g. `ecdsa-secp256k1`).
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyReceipt(
|
|
28
|
+
receipt: Receipt,
|
|
29
|
+
keyset: WorkloadKeyset,
|
|
30
|
+
): Promise<ReceiptVerification> {
|
|
31
|
+
const checks: Check[] = [];
|
|
32
|
+
|
|
33
|
+
const establishedWorkloadId = await computeWorkloadId(keyset.workload_identity.public_key);
|
|
34
|
+
const establishedDigest = await computeKeysetDigest(keyset);
|
|
35
|
+
|
|
36
|
+
// Check 2: self-described identity matches the established keyset.
|
|
37
|
+
checks.push({
|
|
38
|
+
name: 'workload_id',
|
|
39
|
+
ok: receipt.workload_id === establishedWorkloadId,
|
|
40
|
+
...(receipt.workload_id === establishedWorkloadId
|
|
41
|
+
? {}
|
|
42
|
+
: { detail: `receipt ${receipt.workload_id} != established ${establishedWorkloadId}` }),
|
|
43
|
+
});
|
|
44
|
+
checks.push({
|
|
45
|
+
name: 'workload_keyset_digest',
|
|
46
|
+
ok: receipt.workload_keyset_digest === establishedDigest,
|
|
47
|
+
...(receipt.workload_keyset_digest === establishedDigest
|
|
48
|
+
? {}
|
|
49
|
+
: { detail: `receipt ${receipt.workload_keyset_digest} != established ${establishedDigest}` }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Check 1: signature under a named receipt signing key.
|
|
53
|
+
const keyEntry = keyset.receipt_signing_keys.find((k) => k.key_id === receipt.signature.key_id);
|
|
54
|
+
if (!keyEntry) {
|
|
55
|
+
checks.push({
|
|
56
|
+
name: 'signature',
|
|
57
|
+
ok: false,
|
|
58
|
+
detail: `signature.key_id "${receipt.signature.key_id}" not in receipt_signing_keys`,
|
|
59
|
+
});
|
|
60
|
+
} else if (receipt.signature.algo !== keyEntry.algo) {
|
|
61
|
+
// §3.1: the attested key decides the algorithm; the receipt may not override it.
|
|
62
|
+
checks.push({
|
|
63
|
+
name: 'signature',
|
|
64
|
+
ok: false,
|
|
65
|
+
detail: `signature.algo "${receipt.signature.algo}" != keyset entry algo "${keyEntry.algo}"`,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
const message = receiptSigningBytes(receipt);
|
|
69
|
+
const ok = await verifySignature(
|
|
70
|
+
keyEntry.algo,
|
|
71
|
+
fromHex(keyEntry.public_key),
|
|
72
|
+
fromHex(receipt.signature.value),
|
|
73
|
+
message,
|
|
74
|
+
'receipt signature (§8.5)',
|
|
75
|
+
);
|
|
76
|
+
checks.push({ name: 'signature', ok, ...(ok ? {} : { detail: 'Ed25519 verification failed' }) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Find the first event of a given type in a receipt's event log. */
|
|
83
|
+
export function findEvent(receipt: Receipt, type: string): ReceiptEvent | undefined {
|
|
84
|
+
return receipt.event_log.find((e) => e.type === type);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `sha256:<hex>` of raw body bytes — the form ACI body hashes use (§3). Accepts a
|
|
89
|
+
* string (UTF-8 encoded) or raw bytes.
|
|
90
|
+
*/
|
|
91
|
+
export async function hashBody(body: Uint8Array | string): Promise<string> {
|
|
92
|
+
const bytes = typeof body === 'string' ? new TextEncoder().encode(body) : body;
|
|
93
|
+
return sha256Prefixed(bytes);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* §10.2 check 3: the request bytes the client sent match `request.received.body_hash`.
|
|
98
|
+
* For E2EE requests, pass the decrypted body as the service observed it (§8.3, §12).
|
|
99
|
+
* Returns false when the event or its hash is absent.
|
|
100
|
+
*/
|
|
101
|
+
export async function checkRequestBodyHash(
|
|
102
|
+
receipt: Receipt,
|
|
103
|
+
requestBody: Uint8Array | string,
|
|
104
|
+
): Promise<boolean> {
|
|
105
|
+
const event = findEvent(receipt, 'request.received');
|
|
106
|
+
const expected = event?.body_hash;
|
|
107
|
+
if (typeof expected !== 'string') return false;
|
|
108
|
+
return (await hashBody(requestBody)) === expected;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* §10.2 check 4: the response bytes the client received match
|
|
113
|
+
* `response.returned.wire_hash` — for a stream, the in-order raw SSE bytes.
|
|
114
|
+
* Returns false when the event or its hash is absent.
|
|
115
|
+
*/
|
|
116
|
+
export async function checkResponseWireHash(
|
|
117
|
+
receipt: Receipt,
|
|
118
|
+
responseBody: Uint8Array | string,
|
|
119
|
+
): Promise<boolean> {
|
|
120
|
+
const event = findEvent(receipt, 'response.returned');
|
|
121
|
+
const expected = event?.wire_hash;
|
|
122
|
+
if (typeof expected !== 'string') return false;
|
|
123
|
+
return (await hashBody(responseBody)) === expected;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* For E2EE responses, check the decrypted response bytes match
|
|
128
|
+
* `response.returned.cleartext_hash` (§10.2 check 4, §12). Only meaningful when
|
|
129
|
+
* the client can reproduce the service's pre-encryption serialization.
|
|
130
|
+
*/
|
|
131
|
+
export async function checkResponseCleartextHash(
|
|
132
|
+
receipt: Receipt,
|
|
133
|
+
cleartextBody: Uint8Array | string,
|
|
134
|
+
): Promise<boolean> {
|
|
135
|
+
const event = findEvent(receipt, 'response.returned');
|
|
136
|
+
const expected = event?.cleartext_hash;
|
|
137
|
+
if (typeof expected !== 'string') return false;
|
|
138
|
+
return (await hashBody(cleartextBody)) === expected;
|
|
139
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 2 report-binding checks (§10.1 checks 2–6), minus the hardware root of
|
|
3
|
+
* trust. This verifies the *cryptographic binding* of the report — that its
|
|
4
|
+
* `workload_id`, keyset digest, `report_data`, and endorsement are internally
|
|
5
|
+
* consistent and endorsed by the identity key — for the nonce the client
|
|
6
|
+
* supplied. It does NOT do §10.1 check 1 (the TEE quote verifies to the vendor
|
|
7
|
+
* root) or the "hardware evidence binds `report_data`" half of check 4: parsing
|
|
8
|
+
* and checking a TDX/SEV-SNP quote is verifier-profile territory and needs
|
|
9
|
+
* primitives outside the Web Crypto API. Compose this with a quote verifier and
|
|
10
|
+
* the custody/provenance/channel checks (§10.1 checks 1, 7–10) for full Level 2.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
computeWorkloadId,
|
|
15
|
+
computeKeysetDigest,
|
|
16
|
+
computeReportData,
|
|
17
|
+
keysetEndorsementPayload,
|
|
18
|
+
} from './digest';
|
|
19
|
+
import { verifySignature, fromHex } from './crypto';
|
|
20
|
+
import type { AttestationReport, Check, ReportVerification } from './types';
|
|
21
|
+
|
|
22
|
+
/** Options for {@link verifyReportBinding}. */
|
|
23
|
+
export interface ReportBindingOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Current time in Unix seconds for the freshness check (§10.1 check 6).
|
|
26
|
+
* Defaults to the local clock. Pass an explicit value for deterministic tests.
|
|
27
|
+
*/
|
|
28
|
+
now?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the profile trusts the platform's declared validity window
|
|
31
|
+
* (`freshness.fetched_at`/`stale_after`, §5.1). Off by default — recency comes
|
|
32
|
+
* from the nonce binding, and `fetched_at`/`stale_after` need a securely
|
|
33
|
+
* synced TEE clock to mean anything.
|
|
34
|
+
*/
|
|
35
|
+
trustPlatformClock?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Verify the report's cryptographic bindings for `nonce` (§10.1 checks 2–6).
|
|
40
|
+
* `nonce` is the value the verifier supplied to `GET /v1/aci/attestation`, or
|
|
41
|
+
* `null`/`undefined` when it requested no nonce (§4.4).
|
|
42
|
+
*
|
|
43
|
+
* Returns a per-check result and the identity recomputed from the report's
|
|
44
|
+
* keyset; a failed check is `ok: false`, never thrown. Throws
|
|
45
|
+
* {@link UnsupportedAlgorithmError} when the identity key algorithm is outside
|
|
46
|
+
* Web Crypto scope (e.g. `ecdsa-secp256k1`).
|
|
47
|
+
*/
|
|
48
|
+
export async function verifyReportBinding(
|
|
49
|
+
report: AttestationReport,
|
|
50
|
+
nonce: string | null | undefined,
|
|
51
|
+
options: ReportBindingOptions = {},
|
|
52
|
+
): Promise<ReportVerification> {
|
|
53
|
+
const now = options.now ?? Math.floor(Date.now() / 1000);
|
|
54
|
+
const checks: Check[] = [];
|
|
55
|
+
|
|
56
|
+
const keyset = report.attestation.workload_keyset;
|
|
57
|
+
const identityKey = keyset.workload_identity.public_key;
|
|
58
|
+
|
|
59
|
+
// Check 2: workload_id == digest of the identity public key in the report's keyset.
|
|
60
|
+
const workloadId = await computeWorkloadId(identityKey);
|
|
61
|
+
pushEqual(checks, 'workload_id', report.workload_id, workloadId);
|
|
62
|
+
|
|
63
|
+
// Check 3: workload_keyset_digest == digest of the report's keyset.
|
|
64
|
+
const workloadKeysetDigest = await computeKeysetDigest(keyset);
|
|
65
|
+
pushEqual(checks, 'workload_keyset_digest', report.workload_keyset_digest, workloadKeysetDigest);
|
|
66
|
+
|
|
67
|
+
// Check 4 (binding half): report_data == the §4.4 statement digest for this nonce.
|
|
68
|
+
// The hardware-evidence-binds-report_data half is out of scope (see file header).
|
|
69
|
+
const expectedReportData = await computeReportData(workloadId, workloadKeysetDigest, nonce);
|
|
70
|
+
pushEqual(checks, 'report_data', report.attestation.report_data, expectedReportData);
|
|
71
|
+
|
|
72
|
+
// Check 5: keyset endorsement verifies under the identity key, algo matching.
|
|
73
|
+
const endorsement = report.attestation.keyset_endorsement;
|
|
74
|
+
if (endorsement.algo !== identityKey.algo) {
|
|
75
|
+
checks.push({
|
|
76
|
+
name: 'keyset_endorsement',
|
|
77
|
+
ok: false,
|
|
78
|
+
detail: `endorsement.algo "${endorsement.algo}" != identity key algo "${identityKey.algo}"`,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const ok = await verifySignature(
|
|
82
|
+
identityKey.algo,
|
|
83
|
+
fromHex(identityKey.public_key),
|
|
84
|
+
fromHex(endorsement.value),
|
|
85
|
+
keysetEndorsementPayload(workloadKeysetDigest),
|
|
86
|
+
'keyset endorsement (§4.3)',
|
|
87
|
+
);
|
|
88
|
+
checks.push({
|
|
89
|
+
name: 'keyset_endorsement',
|
|
90
|
+
ok,
|
|
91
|
+
...(ok ? {} : { detail: 'endorsement signature failed under identity key' }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check 6: freshness. Nonce binding is check 4; here bound the epoch and,
|
|
96
|
+
// when trusted, the declared validity window.
|
|
97
|
+
const notAfter = keyset.keyset_epoch.not_after;
|
|
98
|
+
const epochOk = now < notAfter;
|
|
99
|
+
checks.push({
|
|
100
|
+
name: 'keyset_epoch.not_after',
|
|
101
|
+
ok: epochOk,
|
|
102
|
+
...(epochOk ? {} : { detail: `now ${now} >= not_after ${notAfter}` }),
|
|
103
|
+
});
|
|
104
|
+
if (options.trustPlatformClock) {
|
|
105
|
+
const freshness = report.attestation.freshness;
|
|
106
|
+
const fetchedAt = freshness?.fetched_at;
|
|
107
|
+
const staleAfter = freshness?.stale_after;
|
|
108
|
+
const windowOk =
|
|
109
|
+
typeof fetchedAt === 'number' &&
|
|
110
|
+
typeof staleAfter === 'number' &&
|
|
111
|
+
fetchedAt <= now &&
|
|
112
|
+
now < staleAfter;
|
|
113
|
+
checks.push({
|
|
114
|
+
name: 'freshness_window',
|
|
115
|
+
ok: windowOk,
|
|
116
|
+
...(windowOk ? {} : { detail: `now ${now} outside [${fetchedAt}, ${staleAfter})` }),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { ok: checks.every((c) => c.ok), checks, workloadId, workloadKeysetDigest };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pushEqual(checks: Check[], name: string, actual: string, expected: string): void {
|
|
124
|
+
const ok = actual === expected;
|
|
125
|
+
checks.push({ name, ok, ...(ok ? {} : { detail: `report ${actual} != recomputed ${expected}` }) });
|
|
126
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire shapes for the ACI artifacts this verifier reads, plus the result types
|
|
3
|
+
* it returns. These mirror spec/aci.md §4, §5, §8, §9; only the fields the
|
|
4
|
+
* verifier touches are typed precisely, with an index signature left open so
|
|
5
|
+
* unknown extension fields (§3.2) survive canonicalization untouched.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { JcsValue } from './jcs';
|
|
9
|
+
|
|
10
|
+
/** A public key object: `{ algo, public_key }` (§4.1). */
|
|
11
|
+
export interface PublicKey {
|
|
12
|
+
algo: string;
|
|
13
|
+
public_key: string;
|
|
14
|
+
[key: string]: JcsValue | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The workload identity (§4.2): the identity public key plus an optional subject. */
|
|
18
|
+
export interface WorkloadIdentity {
|
|
19
|
+
public_key: PublicKey;
|
|
20
|
+
subject?: string | null;
|
|
21
|
+
[key: string]: JcsValue | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A receipt signing key entry (§4.2). */
|
|
25
|
+
export interface ReceiptSigningKey {
|
|
26
|
+
key_id: string;
|
|
27
|
+
algo: string;
|
|
28
|
+
public_key: string;
|
|
29
|
+
[key: string]: JcsValue | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The workload keyset (§4.2). Digested with JCS to yield `workload_keyset_digest`. */
|
|
33
|
+
export interface WorkloadKeyset {
|
|
34
|
+
workload_identity: WorkloadIdentity;
|
|
35
|
+
keyset_epoch: { version: number; not_after: number; [key: string]: JcsValue | undefined };
|
|
36
|
+
receipt_signing_keys: ReceiptSigningKey[];
|
|
37
|
+
e2ee_public_keys?: JcsValue[];
|
|
38
|
+
tls_public_keys?: JcsValue[];
|
|
39
|
+
[key: string]: JcsValue | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A receipt signature block (§8.2). `value` is dropped for canonical signing bytes (§8.5). */
|
|
43
|
+
export interface ReceiptSignature {
|
|
44
|
+
algo: string;
|
|
45
|
+
key_id: string;
|
|
46
|
+
value: string;
|
|
47
|
+
[key: string]: JcsValue | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A single receipt event (§8.3). Only `seq`/`type` are fixed; other fields are type-specific. */
|
|
51
|
+
export interface ReceiptEvent {
|
|
52
|
+
seq: number;
|
|
53
|
+
type: string;
|
|
54
|
+
[key: string]: JcsValue | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An inference receipt (§8.2). */
|
|
58
|
+
export interface Receipt {
|
|
59
|
+
api_version: string;
|
|
60
|
+
receipt_id: string;
|
|
61
|
+
workload_id: string;
|
|
62
|
+
workload_keyset_digest: string;
|
|
63
|
+
event_log: ReceiptEvent[];
|
|
64
|
+
signature: ReceiptSignature;
|
|
65
|
+
[key: string]: JcsValue | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The keyset endorsement / revocation signature block (§4.3, §5.1). */
|
|
69
|
+
export interface Endorsement {
|
|
70
|
+
algo: string;
|
|
71
|
+
value: string;
|
|
72
|
+
[key: string]: JcsValue | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The `attestation` object of a report (§5.1); only the fields Level 1 reads are typed. */
|
|
76
|
+
export interface Attestation {
|
|
77
|
+
workload_keyset: WorkloadKeyset;
|
|
78
|
+
report_data: string;
|
|
79
|
+
keyset_endorsement: Endorsement;
|
|
80
|
+
freshness?: { fetched_at?: number; stale_after?: number; [key: string]: JcsValue | undefined };
|
|
81
|
+
[key: string]: JcsValue | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** An attestation report (§5.1). */
|
|
85
|
+
export interface AttestationReport {
|
|
86
|
+
api_version: string;
|
|
87
|
+
workload_id: string;
|
|
88
|
+
workload_keyset_digest: string;
|
|
89
|
+
attestation: Attestation;
|
|
90
|
+
[key: string]: JcsValue | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A verifier-provided evidence block on a session record (§9.2). */
|
|
94
|
+
export interface SessionEvidence {
|
|
95
|
+
digest?: string | null;
|
|
96
|
+
data?: string;
|
|
97
|
+
[key: string]: JcsValue | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* An attested session record (§9.2). The `session_id` is recomputed from the
|
|
102
|
+
* named fields; absent optional fields (`endpoint`, `identity`, `evidence.digest`)
|
|
103
|
+
* are restored as JSON `null` in the content-addressing material.
|
|
104
|
+
*/
|
|
105
|
+
export interface SessionRecord {
|
|
106
|
+
upstream_name: string;
|
|
107
|
+
endpoint?: string | null;
|
|
108
|
+
verifier_id: string;
|
|
109
|
+
identity?: JcsValue;
|
|
110
|
+
channel_binding: JcsValue[];
|
|
111
|
+
claims: JcsValue;
|
|
112
|
+
evidence?: SessionEvidence | null;
|
|
113
|
+
[key: string]: JcsValue | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Outcome of one named verification check. */
|
|
117
|
+
export interface Check {
|
|
118
|
+
/** Stable machine-readable id, e.g. `signature`, `workload_id`. */
|
|
119
|
+
name: string;
|
|
120
|
+
ok: boolean;
|
|
121
|
+
/** Human-readable detail, present when the check fails. */
|
|
122
|
+
detail?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Result of {@link verifyReceipt}: overall pass plus the individual §10.2 checks. */
|
|
126
|
+
export interface ReceiptVerification {
|
|
127
|
+
ok: boolean;
|
|
128
|
+
checks: Check[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Result of {@link verifyReportBinding}: overall pass, the §10.1 checks, and the derived identity. */
|
|
132
|
+
export interface ReportVerification {
|
|
133
|
+
ok: boolean;
|
|
134
|
+
checks: Check[];
|
|
135
|
+
/** `workload_id` recomputed from the report's keyset (§4.1). */
|
|
136
|
+
workloadId: string;
|
|
137
|
+
/** `workload_keyset_digest` recomputed from the report's keyset (§4.2). */
|
|
138
|
+
workloadKeysetDigest: string;
|
|
139
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SSE (Server-Sent Events) parsing for streamed chat completions.
|
|
3
|
+
* Yields the parsed JSON object from each `data:` line; skips `[DONE]` and
|
|
4
|
+
* unparseable/partial lines. Used by the streaming provider transports.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Stream parse: iterate a ReadableStream of bytes, yielding parsed SSE data objects. */
|
|
8
|
+
export async function* iterateSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<any> {
|
|
9
|
+
const reader = body.getReader();
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
let buf = '';
|
|
12
|
+
try {
|
|
13
|
+
// eslint-disable-next-line no-constant-condition
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done) break;
|
|
17
|
+
buf += decoder.decode(value, { stream: true });
|
|
18
|
+
const lines = buf.split('\n');
|
|
19
|
+
buf = lines.pop() ?? '';
|
|
20
|
+
for (const obj of parseDataLines(lines)) yield obj;
|
|
21
|
+
}
|
|
22
|
+
// Flush any trailing buffered line.
|
|
23
|
+
if (buf.trim()) for (const obj of parseDataLines([buf])) yield obj;
|
|
24
|
+
} finally {
|
|
25
|
+
try { await reader.cancel(); } catch { /* ignore */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Sync parse: extract SSE data objects from a fully-buffered body (RN fallback). */
|
|
30
|
+
export function parseSSEText(text: string): any[] {
|
|
31
|
+
return parseDataLines(text.split('\n'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseDataLines(lines: string[]): any[] {
|
|
35
|
+
const out: any[] = [];
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
if (!line.startsWith('data:')) continue;
|
|
38
|
+
const t = line.slice(5).trim();
|
|
39
|
+
if (!t || t === '[DONE]') continue;
|
|
40
|
+
try { out.push(JSON.parse(t)); } catch { /* partial / non-JSON — skip */ }
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The vendored @dstack/aci-verifier (and our phalaSeal) use the WebCrypto DOM
|
|
2
|
+
// global type names — CryptoKey, CryptoKeyPair, KeyUsage, BufferSource. This
|
|
3
|
+
// project's tsconfig `lib` is ES2023 (no DOM), so rather than pull the entire DOM
|
|
4
|
+
// lib into a Node CLI (which would add a pile of browser globals and ambiguate
|
|
5
|
+
// things like `fetch`), surface just those four names as globals, aliased to
|
|
6
|
+
// Node's own `webcrypto` types. Node ≥ 22 provides the runtime (X25519/HKDF/
|
|
7
|
+
// AES-GCM/Ed25519 on globalThis.crypto.subtle) — see the VENDORED.md note.
|
|
8
|
+
import type { webcrypto } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
type CryptoKey = webcrypto.CryptoKey;
|
|
12
|
+
type CryptoKeyPair = webcrypto.CryptoKeyPair;
|
|
13
|
+
type KeyUsage = webcrypto.KeyUsage;
|
|
14
|
+
type BufferSource = ArrayBufferView | ArrayBuffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {};
|