@calimero-network/mero-js 26.11.0 → 27.0.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/README.md +73 -288
- package/dist/cloud-v2/attestation.d.ts +91 -0
- package/dist/cloud-v2/attestation.js +135 -0
- package/dist/cloud-v2/capabilities.d.ts +30 -0
- package/dist/cloud-v2/capabilities.js +31 -0
- package/dist/cloud-v2/client.d.ts +426 -0
- package/dist/cloud-v2/client.js +646 -0
- package/dist/cloud-v2/errors.d.ts +98 -0
- package/dist/cloud-v2/errors.js +165 -0
- package/dist/cloud-v2/index.d.ts +10 -0
- package/dist/cloud-v2/index.js +5 -0
- package/dist/cloud-v2/passkey.d.ts +27 -0
- package/dist/cloud-v2/passkey.js +73 -0
- package/dist/crypto/internal.d.ts +3 -37
- package/dist/crypto/internal.js +3 -72
- package/dist/index.browser.mjs +4 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +1750 -12772
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +3 -56
- package/dist/index.js +14 -75
- package/dist/index.mjs +1750 -12772
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +4 -4
- package/dist/sealed/index.js +2 -2
- package/dist/sealed/release.d.ts +21 -76
- package/dist/sealed/release.js +24 -108
- package/dist/sealed/sealed.d.ts +8 -56
- package/dist/sealed/sealed.js +11 -89
- package/dist/sealed/verify.d.ts +7 -13
- package/dist/sealed/verify.js +3 -7
- package/package.json +8 -13
- package/dist/account/account-admin.d.ts +0 -80
- package/dist/account/account-admin.js +0 -346
- package/dist/account/account.d.ts +0 -196
- package/dist/account/account.js +0 -269
- package/dist/account/application-id.d.ts +0 -50
- package/dist/account/application-id.js +0 -83
- package/dist/account/bootstrap-from-invitation.d.ts +0 -110
- package/dist/account/bootstrap-from-invitation.js +0 -80
- package/dist/account/create-context.d.ts +0 -103
- package/dist/account/create-context.js +0 -267
- package/dist/account/govern.d.ts +0 -24
- package/dist/account/govern.js +0 -50
- package/dist/account/index.d.ts +0 -26
- package/dist/account/index.js +0 -15
- package/dist/account/join-as-account.d.ts +0 -23
- package/dist/account/join-as-account.js +0 -34
- package/dist/account/join-with-node.d.ts +0 -115
- package/dist/account/join-with-node.js +0 -161
- package/dist/account/node-admin.d.ts +0 -20
- package/dist/account/node-admin.js +0 -33
- package/dist/account/relay-from-invitation.d.ts +0 -137
- package/dist/account/relay-from-invitation.js +0 -180
- package/dist/account/relay-login-error.d.ts +0 -32
- package/dist/account/relay-login-error.js +0 -34
- package/dist/account/session.d.ts +0 -266
- package/dist/account/session.js +0 -915
- package/dist/account/wordlist.d.ts +0 -16
- package/dist/account/wordlist.js +0 -273
- package/dist/admin-api/admin-client.d.ts +0 -688
- package/dist/admin-api/admin-client.js +0 -1230
- package/dist/admin-api/admin-factory.d.ts +0 -14
- package/dist/admin-api/admin-factory.js +0 -22
- package/dist/admin-api/admin-types.d.ts +0 -2046
- package/dist/admin-api/admin-types.js +0 -12
- package/dist/admin-api/index.d.ts +0 -4
- package/dist/admin-api/index.js +0 -5
- package/dist/admin-api/redeem-invitation.d.ts +0 -101
- package/dist/admin-api/redeem-invitation.js +0 -200
- package/dist/auth/index.d.ts +0 -34
- package/dist/auth/index.js +0 -67
- package/dist/auth-api/auth-client.d.ts +0 -31
- package/dist/auth-api/auth-client.js +0 -117
- package/dist/auth-api/auth-factory.d.ts +0 -7
- package/dist/auth-api/auth-factory.js +0 -41
- package/dist/auth-api/auth-types.d.ts +0 -130
- package/dist/auth-api/auth-types.js +0 -2
- package/dist/auth-api/index.d.ts +0 -3
- package/dist/auth-api/index.js +0 -4
- package/dist/capabilities.d.ts +0 -84
- package/dist/capabilities.js +0 -117
- package/dist/cloud/account-ownership.d.ts +0 -63
- package/dist/cloud/account-ownership.js +0 -87
- package/dist/cloud/cloud-client.d.ts +0 -949
- package/dist/cloud/cloud-client.js +0 -1068
- package/dist/cloud/connect.d.ts +0 -213
- package/dist/cloud/connect.js +0 -260
- package/dist/cloud/enrol-redirect.d.ts +0 -181
- package/dist/cloud/enrol-redirect.js +0 -244
- package/dist/cloud/index.d.ts +0 -12
- package/dist/cloud/index.js +0 -10
- package/dist/cloud/link-redirect.d.ts +0 -95
- package/dist/cloud/link-redirect.js +0 -121
- package/dist/cloud/link-state.d.ts +0 -36
- package/dist/cloud/link-state.js +0 -104
- package/dist/cloud/routing-proof.d.ts +0 -113
- package/dist/cloud/routing-proof.js +0 -96
- package/dist/device-cert/device-cert.d.ts +0 -189
- package/dist/device-cert/device-cert.js +0 -239
- package/dist/device-cert/device-scope.d.ts +0 -58
- package/dist/device-cert/device-scope.js +0 -76
- package/dist/device-cert/index.d.ts +0 -4
- package/dist/device-cert/index.js +0 -3
- package/dist/ephemeral/codec.d.ts +0 -4
- package/dist/ephemeral/codec.js +0 -12
- package/dist/ephemeral/index.d.ts +0 -57
- package/dist/ephemeral/index.js +0 -101
- package/dist/ephemeral/types.d.ts +0 -27
- package/dist/ephemeral/types.js +0 -1
- package/dist/errors/classify.d.ts +0 -65
- package/dist/errors/classify.js +0 -117
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -1
- package/dist/events/group.d.ts +0 -95
- package/dist/events/group.js +0 -10
- package/dist/events/index.d.ts +0 -5
- package/dist/events/index.js +0 -2
- package/dist/events/sse.d.ts +0 -127
- package/dist/events/sse.js +0 -469
- package/dist/events/ws.d.ts +0 -57
- package/dist/events/ws.js +0 -212
- package/dist/http-client/api-response.d.ts +0 -15
- package/dist/http-client/api-response.js +0 -1
- package/dist/http-client/http-factory.d.ts +0 -71
- package/dist/http-client/http-factory.js +0 -68
- package/dist/http-client/http-types.d.ts +0 -87
- package/dist/http-client/http-types.js +0 -1
- package/dist/http-client/index.d.ts +0 -7
- package/dist/http-client/index.js +0 -11
- package/dist/http-client/path-segment.d.ts +0 -1
- package/dist/http-client/path-segment.js +0 -8
- package/dist/http-client/retry.d.ts +0 -5
- package/dist/http-client/retry.js +0 -82
- package/dist/http-client/signal-utils.d.ts +0 -2
- package/dist/http-client/signal-utils.js +0 -41
- package/dist/http-client/web-client.d.ts +0 -80
- package/dist/http-client/web-client.js +0 -557
- package/dist/invitation/index.d.ts +0 -2
- package/dist/invitation/index.js +0 -2
- package/dist/invitation/invitation.d.ts +0 -106
- package/dist/invitation/invitation.js +0 -104
- package/dist/login/index.d.ts +0 -4
- package/dist/login/index.js +0 -3
- package/dist/login/login.d.ts +0 -111
- package/dist/login/login.js +0 -110
- package/dist/login/session.d.ts +0 -124
- package/dist/login/session.js +0 -141
- package/dist/mero-js.d.ts +0 -231
- package/dist/mero-js.js +0 -469
- package/dist/namespace-op/index.d.ts +0 -2
- package/dist/namespace-op/index.js +0 -3
- package/dist/namespace-op/namespace-op.d.ts +0 -150
- package/dist/namespace-op/namespace-op.js +0 -191
- package/dist/nodeDiscovery.d.ts +0 -47
- package/dist/nodeDiscovery.js +0 -101
- package/dist/owner-op/index.d.ts +0 -2
- package/dist/owner-op/index.js +0 -3
- package/dist/owner-op/owner-op.d.ts +0 -119
- package/dist/owner-op/owner-op.js +0 -214
- package/dist/presence/relay-presence.d.ts +0 -56
- package/dist/presence/relay-presence.js +0 -107
- package/dist/presence/statement.d.ts +0 -12
- package/dist/presence/statement.js +0 -17
- package/dist/relay/index.d.ts +0 -4
- package/dist/relay/index.js +0 -2
- package/dist/relay/nonce-source.d.ts +0 -72
- package/dist/relay/nonce-source.js +0 -111
- package/dist/relay/relay-client.d.ts +0 -472
- package/dist/relay/relay-client.js +0 -518
- package/dist/relay-attestation/attest-node-key.d.ts +0 -66
- package/dist/relay-attestation/attest-node-key.js +0 -110
- package/dist/relay-attestation/index.d.ts +0 -1
- package/dist/relay-attestation/index.js +0 -1
- package/dist/request/caller-proof.d.ts +0 -43
- package/dist/request/caller-proof.js +0 -36
- package/dist/request/index.d.ts +0 -3
- package/dist/request/index.js +0 -3
- package/dist/request/proof-signer.d.ts +0 -49
- package/dist/request/proof-signer.js +0 -59
- package/dist/request/request-sig.d.ts +0 -99
- package/dist/request/request-sig.js +0 -95
- package/dist/request-proof/authorizer.d.ts +0 -41
- package/dist/request-proof/authorizer.js +0 -60
- package/dist/request-proof/index.d.ts +0 -4
- package/dist/request-proof/index.js +0 -2
- package/dist/request-proof/request-proof.d.ts +0 -53
- package/dist/request-proof/request-proof.js +0 -95
- package/dist/rpc/index.d.ts +0 -64
- package/dist/rpc/index.js +0 -78
- package/dist/signer/index.d.ts +0 -2
- package/dist/signer/index.js +0 -3
- package/dist/signer/signer.d.ts +0 -92
- package/dist/signer/signer.js +0 -147
- package/dist/token-store/index.d.ts +0 -27
- package/dist/token-store/index.js +0 -62
- package/dist/transport/index.d.ts +0 -6
- package/dist/transport/index.js +0 -3
- package/dist/transport/mero-client.d.ts +0 -294
- package/dist/transport/mero-client.js +0 -386
- package/dist/transport/relay-observer.d.ts +0 -152
- package/dist/transport/relay-observer.js +0 -173
- package/dist/transport/relay-transport.d.ts +0 -73
- package/dist/transport/relay-transport.js +0 -94
- package/dist/transport/types.d.ts +0 -100
- package/dist/transport/types.js +0 -32
- package/dist/types/api-response.d.ts +0 -15
- package/dist/types/api-response.js +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/warrant/creation-warrant.d.ts +0 -163
- package/dist/warrant/creation-warrant.js +0 -194
- package/dist/warrant/governance-op.d.ts +0 -202
- package/dist/warrant/governance-op.js +0 -355
- package/dist/warrant/governance-warrant.d.ts +0 -124
- package/dist/warrant/governance-warrant.js +0 -164
- package/dist/warrant/index.d.ts +0 -8
- package/dist/warrant/index.js +0 -5
- package/dist/warrant/warrant.d.ts +0 -188
- package/dist/warrant/warrant.js +0 -202
package/README.md
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
# Mero.js —
|
|
1
|
+
# Mero.js — the JavaScript client for Calimero 1.0
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The JavaScript/TypeScript client for [Calimero](https://calimero-network.github.io/core/)
|
|
4
|
+
1.0, the confidential cloud. `CalimeroClient` verifies a node's TDX attestation,
|
|
5
|
+
seals every request to it end to end (Noise NK, `calimero/sealed-http/v2`), signs
|
|
6
|
+
people in with WebAuthn passkeys and machines with API keys, and calls apps that
|
|
7
|
+
each run on their own replicated log. It runs in the browser and Node 18+, and is
|
|
8
|
+
byte-compatible with core's `calimero-sealed` and `calimero-client`.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
> **Version 27 is the Calimero 1.0 client only.** The v1 SDK for merod 0.x nodes
|
|
11
|
+
> (`MeroJs`, the admin and auth API clients, JSON-RPC, contexts and namespaces)
|
|
12
|
+
> is in 26.x. See [Migrating from v1](https://calimero-network.github.io/mero-js/reference/migrating-from-v1/).
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
**<https://calimero-network.github.io/mero-js/>** — quickstart, guides, and the
|
|
13
|
-
complete API reference (`MeroJs`, admin API, auth API, RPC, events, cloud,
|
|
14
|
-
capabilities, and the error model). The source lives under [`docs/`](docs/).
|
|
14
|
+
## Documentation
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
> package.
|
|
16
|
+
**<https://calimero-network.github.io/mero-js/>**: quickstart, guides, and the
|
|
17
|
+
reference for every method and the route it calls. The source is under [`docs/`](docs/).
|
|
19
18
|
|
|
20
19
|
## Installation
|
|
21
20
|
|
|
@@ -23,309 +22,95 @@ capabilities, and the error model). The source lives under [`docs/`](docs/).
|
|
|
23
22
|
npm install @calimero-network/mero-js
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
and
|
|
25
|
+
The package ships ESM, CommonJS and a minified browser build with its own
|
|
26
|
+
TypeScript declarations. Its one dependency, `@phala/dcap-qvl`, verifies TDX
|
|
27
|
+
quotes and is loaded on first use.
|
|
29
28
|
|
|
30
29
|
## Quick start
|
|
31
30
|
|
|
32
|
-
```
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
const sdk = createMeroJs({
|
|
36
|
-
baseUrl: 'http://localhost:2528',
|
|
37
|
-
// timeoutMs defaults to 10000; pass a tokenStore to persist auth
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Authenticate (username/password against the node's auth service)
|
|
41
|
-
await sdk.authenticate({ username: 'dev', password: 'dev' });
|
|
42
|
-
|
|
43
|
-
// Admin API — list this node's contexts
|
|
44
|
-
const { contexts } = await sdk.admin.getContexts(); // ContextWithGroup[]
|
|
45
|
-
console.log(`found ${contexts.length} contexts`);
|
|
46
|
-
|
|
47
|
-
// Execute a context method over JSON-RPC
|
|
48
|
-
try {
|
|
49
|
-
const value = await sdk.rpc.execute<string>({
|
|
50
|
-
contextId: contexts[0].id,
|
|
51
|
-
method: 'get_value',
|
|
52
|
-
argsJson: { key: 'greeting' },
|
|
53
|
-
});
|
|
54
|
-
console.log(value);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
if (err instanceof RpcError) console.error(`contract error ${err.code}: ${err.message}`);
|
|
57
|
-
else if (err instanceof HTTPError) console.error(`HTTP ${err.status}: ${err.statusText}`);
|
|
58
|
-
else throw err;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Stream real-time context events (SSE)
|
|
62
|
-
sdk.events.on('event', (e) => console.log(e.contextId, e.type, e.data));
|
|
63
|
-
await sdk.events.connect();
|
|
64
|
-
await sdk.events.subscribe([contexts[0].id]);
|
|
65
|
-
|
|
66
|
-
// Tear down open connections when done
|
|
67
|
-
sdk.close();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`sdk.auth`, `sdk.admin`, `sdk.rpc`, `sdk.events` (SSE) and `sdk.ws` (experimental
|
|
71
|
-
WebSocket) are all reachable from the one `MeroJs` instance. See the
|
|
72
|
-
[quickstart](https://calimero-network.github.io/mero-js/get-started/quickstart/)
|
|
73
|
-
and [authentication guide](https://calimero-network.github.io/mero-js/get-started/authentication/)
|
|
74
|
-
for token persistence and the refresh lifecycle.
|
|
75
|
-
|
|
76
|
-
## Error model
|
|
77
|
-
|
|
78
|
-
Every failure surfaces as a typed error:
|
|
79
|
-
|
|
80
|
-
- **`HTTPError`** — a non-2xx response. Network failures, timeouts, and
|
|
81
|
-
cancellations are also wrapped as `HTTPError` with **`status: 0`** (the
|
|
82
|
-
underlying `AbortError`/`TimeoutError` is not propagated).
|
|
83
|
-
- **`AuthRevokedError`** (extends `HTTPError`) — the refresh token was reused or
|
|
84
|
-
revoked; the session is over. The SDK has already cleared its tokens.
|
|
85
|
-
- **`RpcError`** — the WASM contract returned a JSON-RPC error (`code`, `message`,
|
|
86
|
-
optional `type`/`data`).
|
|
87
|
-
|
|
88
|
-
`classifyError(err)` turns any of these into `{ kind, retryable, message }`.
|
|
89
|
-
`kind` is read off the status (`'conflict'` for a 409, `'unavailable'` for a
|
|
90
|
-
503, `'unreachable'` for status 0, and so on). `retryable` says whether the same
|
|
91
|
-
request could work later. `message` is the node's own words. To follow an
|
|
92
|
-
invitation, use `sdk.admin.redeemInvitation`: a join that failed but landed
|
|
93
|
-
comes back as `already-member` rather than as an error.
|
|
94
|
-
|
|
95
|
-
`401`s are handled internally: the SDK refreshes the token and retries once, so
|
|
96
|
-
you only see a `401` if the refresh itself fails. Full details in the
|
|
97
|
-
[error model reference](https://calimero-network.github.io/mero-js/reference/error-model/).
|
|
98
|
-
|
|
99
|
-
## Writing without a node
|
|
100
|
-
|
|
101
|
-
The quickstart above assumes you have a node and a credential on it. A browser
|
|
102
|
-
tab, a phone, or an agent has neither — the runtime cannot compile the app's WASM
|
|
103
|
-
against materialized state, and never received the scope key that seals the
|
|
104
|
-
deltas. So there is a second connection shape: sign in to **Calimero Cloud**, and
|
|
105
|
-
write through an attested TEE **relay** using
|
|
106
|
-
[delegated execution](https://calimero-network.github.io/core/protocol/delegated-authorship/).
|
|
107
|
-
|
|
108
|
-
You sign a *warrant* locally — your consent for one specific intent, once — the
|
|
109
|
-
relay runs the method as **your** principal, and the resulting change is
|
|
110
|
-
attributed to your account and device. Your signing key never leaves the
|
|
111
|
-
process; only signatures do.
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
import { connectCloud } from '@calimero-network/mero-js';
|
|
115
|
-
|
|
116
|
-
const connection = await connectCloud({
|
|
117
|
-
googleIdToken, // or a stored sessionToken
|
|
118
|
-
authorAccount, // your account, hex
|
|
119
|
-
authorProof, // your AccountProof<DeviceCert>, hex borsh
|
|
120
|
-
deviceSecret, // your device's ed25519 seed, hex — never transmitted
|
|
121
|
-
});
|
|
31
|
+
```ts
|
|
32
|
+
import { CalimeroClient, browserPasskey } from '@calimero-network/mero-js';
|
|
122
33
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
34
|
+
const client = new CalimeroClient({
|
|
35
|
+
baseUrl: 'https://eu1.cloud.calimero.network',
|
|
36
|
+
// Trust the signed mero-tee release the node names, from this one up.
|
|
37
|
+
policy: { release: { minReleaseVersion: '2.3.109' } },
|
|
126
38
|
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
`connectCloud` replaces the **node-URL prompt**, not the authentication: it asks
|
|
130
|
-
the cloud which namespaces the account owns and which relay serves them, then
|
|
131
|
-
builds the relay client. `sdk.cloud` exposes the same cloud API from an existing
|
|
132
|
-
`MeroJs` instance for apps that have both a node and a cloud account, and
|
|
133
|
-
`RelayClient` is available directly for a relay you were told about out of band.
|
|
134
|
-
|
|
135
|
-
One precondition is not yours to satisfy: the node must relay. A fleet node
|
|
136
|
-
admitted as a `RelayTee` (the namespace's TEE admission policy set with
|
|
137
|
-
`mode: 'relay'`) relays by role; a `ReadOnlyTee` is a replica and never relays;
|
|
138
|
-
and a node too old to report a role needs an admin of the namespace to grant it
|
|
139
|
-
`CAN_AUTHOR_ON_BEHALF`. `connectCloud` picks a relay on the cloud's `canExecute`,
|
|
140
|
-
which folds that rule in, and otherwise throws saying which of those is missing —
|
|
141
|
-
naming the account that needs the grant when that is the fix.
|
|
142
|
-
|
|
143
|
-
A keyholder can also **create** a context through a relay:
|
|
144
|
-
`relay.createContext({ groupId, applicationId, initArgs })` signs a creation
|
|
145
|
-
warrant (checked against *your* `CAN_CREATE_CONTEXT`, not the relay's) and
|
|
146
|
-
returns the new `contextId`.
|
|
147
|
-
|
|
148
|
-
And it can **govern** through one: `relay.govern({ groupId, op })` signs a
|
|
149
|
-
governance warrant over one op, such as `memberAddedOp(account, 'Member')` or
|
|
150
|
-
`groupCreatedOp({ ... })`, and every peer applies it under *your* rights.
|
|
151
|
-
|
|
152
|
-
See the [cloud client](https://calimero-network.github.io/mero-js/reference/cloud/),
|
|
153
|
-
[relay client](https://calimero-network.github.io/mero-js/reference/relay/) and
|
|
154
|
-
[connectCloud](https://calimero-network.github.io/mero-js/reference/connect-cloud/)
|
|
155
|
-
references — including why a warrant nonce must never restart.
|
|
156
|
-
|
|
157
|
-
## Account (delegated) layer
|
|
158
|
-
|
|
159
|
-
`connectCloud` is the shape for an account that already belongs somewhere. The
|
|
160
|
-
account layer is everything around it for an account that owns nothing yet: it
|
|
161
|
-
has a device the wallet certified and no relay, and it has to found a namespace
|
|
162
|
-
or be invited into one before it can write. It is framework-free — plain TS over
|
|
163
|
-
`sessionStorage`/`localStorage` — and `@calimero-network/mero-react` re-exports
|
|
164
|
-
every name below unchanged (its hooks are built on it), so an app may import
|
|
165
|
-
them from either package.
|
|
166
|
-
|
|
167
|
-
- **Session** — `DelegatedCredential` (what the wallet certified: account,
|
|
168
|
-
certificate, device secret) and `DelegatedAccountSession` (that plus the relay
|
|
169
|
-
to write through, `null` until the account is a member of something).
|
|
170
|
-
`readDelegatedSession` / `saveDelegatedSession` / `clearDelegatedSession`,
|
|
171
|
-
`readDelegatedCredential` / `saveDelegatedCredential` /
|
|
172
|
-
`clearDelegatedCredential`; `readRelayMap`, `rememberRelay`,
|
|
173
|
-
`relayForContext`, `knownRelays`, `listDelegatedContexts`,
|
|
174
|
-
`listDelegatedNamespaces`, `carryExecutorAccount`.
|
|
175
|
-
- **The relay's node key** — `learnRelayNodeKey` (attests the relay: its signed
|
|
176
|
-
mero-tee release from the cloud mirror, Intel's chain, every register; retries
|
|
177
|
-
the transport, refuses a bad quote), `attemptRelayNodeKey`,
|
|
178
|
-
`resolveRelayNodeKey`, `pinRelayNodeKey`, `readPinnedRelayNodeKey`,
|
|
179
|
-
`RELAY_NODE_KEY_RETRY_MS`. `@phala/dcap-qvl` is loaded on the first real quote
|
|
180
|
-
and is external to the bundles: a consumer that never meets a real relay never
|
|
181
|
-
downloads the verifier.
|
|
182
|
-
- **Client** — `buildDelegatedClient(session, contextId)`: a `MeroClient` over
|
|
183
|
-
the relay transport, or `null` while there is no relay (a logged-in state, not
|
|
184
|
-
a broken one). `forgetMethodKinds`, `persistedNonces`, `markContextNonceSpent`.
|
|
185
|
-
- **Admin, one shape on both transports** — `createAccountAdmin({ session,
|
|
186
|
-
read, app })` returns an `AdminApiClient` whose writes are governance and
|
|
187
|
-
creation warrants signed by the account and whose reads go through the relay;
|
|
188
|
-
`createNodeAdmin({ admin, app })` is the same `AdminApiClient` for a node, with
|
|
189
|
-
`createNamespace` installing the package first. Refusals: `NoRelayError`,
|
|
190
|
-
`NotForAccountError`, `InvitationNotClaimableError`.
|
|
191
|
-
- **Founding and creating** — `foundDelegatedNamespace`,
|
|
192
|
-
`createDelegatedContext`, `createDelegatedPrivateContext`,
|
|
193
|
-
`delegatedGovernance`, `governGroup`, `governRoot`, `namespaceOfGroup`,
|
|
194
|
-
`rememberGroupNamespace`; `HA_REFUSAL_MESSAGES`,
|
|
195
|
-
`HA_ACCOUNT_NOT_LINKED_MESSAGE` for the cloud's HA answers.
|
|
196
|
-
- **Which application** — `applicationIdForBundle`, `selectLatestBundle`,
|
|
197
|
-
`fetchRegistryBundles`, `resolveApplicationIdFromRegistry`,
|
|
198
|
-
`latestPublishedVersion` (the registry's answer for a package).
|
|
199
|
-
- **Joining from an invitation** — `resolveRelayFromInvitation` (which relay
|
|
200
|
-
admits, from the invitation's signed admitters and the cloud), `joinWithNode`,
|
|
201
|
-
`bootstrapFromInvitation` (both steps, as a result rather than a throw),
|
|
202
|
-
`joinAsAccount` (the same, switching the session onto the relay that admitted),
|
|
203
|
-
`normaliseAccount`.
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
import {
|
|
207
|
-
beginDeviceEnrolment, readEnrolmentCallback, completeDeviceEnrolment,
|
|
208
|
-
saveDelegatedSession, learnRelayNodeKey, pinRelayNodeKey, buildDelegatedClient,
|
|
209
|
-
createAccountAdmin, joinAsAccount, resolveApplicationIdFromRegistry, type DelegatedAccountSession,
|
|
210
|
-
} from '@calimero-network/mero-js';
|
|
211
|
-
|
|
212
|
-
// 1. Enrol: the wallet certifies a device key this app generated and keeps.
|
|
213
|
-
// (first visit) beginDeviceEnrolment({ walletUrl, devicePublicKey, kemPublicKey, returnTo });
|
|
214
|
-
// (on return)
|
|
215
|
-
const callback = readEnrolmentCallback(); // null on an ordinary load
|
|
216
|
-
const enrolled = await completeDeviceEnrolment({ ...callback, devicePublicKey, kemPublicKey });
|
|
217
|
-
let session: DelegatedAccountSession = {
|
|
218
|
-
account: enrolled.account, credential: enrolled.credential, deviceSecret, relayUrl: null,
|
|
219
|
-
};
|
|
220
|
-
saveDelegatedSession(session);
|
|
221
|
-
|
|
222
|
-
// 2. Found a namespace, or join one. A new account has no relay; either gives it one.
|
|
223
|
-
const registryUrl = 'https://apps.calimero.network';
|
|
224
|
-
const app = await resolveApplicationIdFromRegistry(registryUrl, 'com.example.kv');
|
|
225
|
-
const admin = createAccountAdmin(
|
|
226
|
-
{ session, read: null, app: { packageName: 'com.example.kv', packageVersion: app.version, registryUrl } },
|
|
227
|
-
{ join: (namespaceId, invitation) => joinAsAccount(session, namespaceId, invitation, {
|
|
228
|
-
onJoined: (next) => { session = next; saveDelegatedSession(next); },
|
|
229
|
-
}) },
|
|
230
|
-
);
|
|
231
|
-
const { namespaceId } = await admin.createNamespace({ applicationId: app.applicationId, name: 'my-notes' });
|
|
232
|
-
// or: await admin.joinNamespace(namespaceId, { invitation });
|
|
233
39
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const client = buildDelegatedClient(session, null)!;
|
|
238
|
-
const { contextId } = await admin.createContext({ groupId: namespaceId, applicationId: app.applicationId, serviceName: 'kv' });
|
|
239
|
-
await client.execute(contextId, 'set', { key: 'greeting', value: 'hello' });
|
|
40
|
+
await client.signUp(browserPasskey(), 'alice'); // or signIn(browserPasskey())
|
|
41
|
+
await client.installPackage('com.example.kv', '1.0.0'); // signed .mpk from the registry
|
|
42
|
+
const app = await client.createApp('kv', { package: 'com.example.kv', version: '1.0.0' }, { confirmMethods: ['clear'] });
|
|
240
43
|
|
|
241
|
-
|
|
242
|
-
const {
|
|
44
|
+
await client.call(app, 'set', { key: 'a', value: '1' });
|
|
45
|
+
const { result } = await client.view(app, 'get', { key: 'a' });
|
|
46
|
+
await client.callConfirmed(app, 'clear', {}, browserPasskey()); // passkey consent
|
|
47
|
+
for await (const event of client.events(app)) console.log(event);
|
|
48
|
+
await client.upgradeApp(app, { version: '1.1.0' }, browserPasskey()); // the owner approves
|
|
243
49
|
```
|
|
244
50
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
with no node of their own can be admitted by one of them. The `read` client is
|
|
249
|
-
`null` until the relay's node key is attested, which is why the session is
|
|
250
|
-
built in that order.
|
|
51
|
+
A development cluster (`merod local`, `merod run --dev`) sends mock quotes,
|
|
52
|
+
which pass only with `policy: { allowMock: true }`. Nothing is read from the
|
|
53
|
+
environment.
|
|
251
54
|
|
|
252
|
-
##
|
|
55
|
+
## What the client checks
|
|
253
56
|
|
|
254
|
-
|
|
57
|
+
Before it sends anything to a node, as core's Rust client does (`Measurements::Release`):
|
|
255
58
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const policy = await verifySignedAgentPolicy(policyJson, bundleJson, { profile: 'locked-read-only' });
|
|
264
|
-
const verifier = agentQuoteVerifier(policy, { dcapVerify, fetchCollateral: (quote) => getCollateral(PCCS_URL, quote) });
|
|
265
|
-
|
|
266
|
-
const owner = await generateOwnerKey(); // keep owner.secret: only it can change or release the agent
|
|
267
|
-
const agent = await attestAgent({ gateUrl, agentId: 'a-1f2e', verifier });
|
|
268
|
-
await provisionAgent({ gateUrl, agent, ownerSecret: owner.secret, verifier, secrets: { NEAR_AI_API_KEY: key } });
|
|
269
|
-
// later: await releaseAgent({ gateUrl, agent: await attestAgent({ gateUrl, agentId, verifier }), ownerSecret, verifier });
|
|
270
|
-
```
|
|
59
|
+
1. The node names the mero-tee release it runs. A release below `minReleaseVersion` is refused before anything is fetched.
|
|
60
|
+
2. The release's `published-mrtds.json` is fetched and its Sigstore signature is checked against the mero-tee node release workflow.
|
|
61
|
+
By default it comes from the `cloud.calimero.network` mirror; `releaseSource: 'github'` reads the GitHub release instead (Node only: GitHub sends no CORS headers).
|
|
62
|
+
3. The quote is checked against Intel's collateral with `@phala/dcap-qvl` (Intel's PCS unless you give `fetchCollateral`): the signature chain and the TCB status.
|
|
63
|
+
4. A debug TD is refused.
|
|
64
|
+
5. The registers must be the release's `locked-read-only` image; a debug profile is trusted only with `allowDebugProfile`.
|
|
65
|
+
6. The quote's report data binds the client's nonce and the transport key the sealed channel opens to.
|
|
271
66
|
|
|
272
|
-
|
|
273
|
-
- `provisionAgent` refuses an agent another key claimed before sending anything. The first bundle for an unclaimed agent claims it for your key, and a fresh quote must then name your key as owner.
|
|
274
|
-
- Secrets are sealed with RFC 9180 HPKE in Auth mode (X25519, HKDF-SHA256, ChaCha20-Poly1305) to the gate's in-memory key, for that agent id only.
|
|
275
|
-
- `releaseAgent` stops the agent and erases its slot; a fresh quote must name no owner.
|
|
67
|
+
Pinned measurements go through `policy.verifyQuote` (`createQuoteVerifier`).
|
|
276
68
|
|
|
277
|
-
##
|
|
69
|
+
## The API
|
|
278
70
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
`
|
|
282
|
-
`
|
|
71
|
+
- **Credentials**: `signUp`, `signIn`, `refresh`, `signOut`, `signOutEverywhere`, `useSession`, `useApiKey`, `me`, `usage`, `addPasskey`, `labelPasskey`, `removePasskey`, `createApiKey`, `listApiKeys`, `revokeApiKey`.
|
|
72
|
+
- **Packages**: `installPackage`, `uploadPackage`, `packages`, `package`, `uninstallPackage`; `uploadBundle` (raw WASM, development clusters only).
|
|
73
|
+
- **Apps**: `createApp`, `listApps`, `getApp`, `deleteApp`, `setAccess`, `setAppMetadata`, `appStorage`, `appAbi`, `upgradeApp`, `transferApp`.
|
|
74
|
+
- **Calls**: `call`, `callConfirmed`, `view`, `events`. A call always carries an idempotency key, and is sent again with it while the node answers `503`.
|
|
75
|
+
- **Blobs and presence**: `uploadBlob`, `downloadBlob`, `listBlobs`, `blobInfo`, `deleteBlob`; `publishPresence`, `presence`, `presenceStream`.
|
|
76
|
+
- **Organizations**: `createOrg`, `getOrg`, `deleteOrg`, `setOrgMetadata`, `setMember`, `removeMember`, `setMemberMetadata`, `setCapabilities` (`ORG_CAPABILITIES`), teams, `invite`, `redeemInvite`.
|
|
77
|
+
- **Owner decisions** (passkey approval): `upgradeOrg`, `upgradeRun`, `retryUpgradeRun`, `approveRelease`, `approveOutbound`.
|
|
78
|
+
- **Any route**: `get`, `post`, `put`, `delete`; `postConfirmed` and `deleteConfirmed` answer a `412 CONFIRM_REQUIRED` with a passkey.
|
|
283
79
|
|
|
284
|
-
|
|
285
|
-
|
|
80
|
+
Refusals are typed (`LoginRequired`, `NotFound`, `ConfirmRequired`, `ConfirmCancelled`,
|
|
81
|
+
`ArgsTooLarge`, `IdempotencyMismatch`, `AppError`, `TrapError`, `UnavailableError`),
|
|
82
|
+
all extending `CalimeroError` with the node's `status` and `code`. See the
|
|
83
|
+
[error model](https://calimero-network.github.io/mero-js/reference/error-model/).
|
|
286
84
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const data = await http.get<{ message: string }>('/api/hello');
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Methods (`get`/`post`/`put`/`delete`/`patch`/`head`/`request`) return parsed data
|
|
296
|
-
directly and support per-call `{ signal, timeoutMs, parse }`. See the
|
|
297
|
-
[HTTP transport guide](https://calimero-network.github.io/mero-js/guides/http-transport/)
|
|
298
|
-
for options, parsing rules, retries, and cancellation.
|
|
85
|
+
The pieces it is built from are exported too: the sealed transport
|
|
86
|
+
(`createSealedFetch`), quote and signed-release verification
|
|
87
|
+
(`createQuoteVerifier`, `createSignedReleaseVerifier`, `trustSignedRelease`), and
|
|
88
|
+
the client for mero-tee's private agent gate (`attestAgent`, `provisionAgent`,
|
|
89
|
+
`releaseAgent`).
|
|
299
90
|
|
|
300
91
|
## Development
|
|
301
92
|
|
|
302
93
|
```bash
|
|
303
94
|
pnpm install
|
|
304
95
|
pnpm build
|
|
305
|
-
pnpm test
|
|
96
|
+
pnpm test:unit # unit tests, against a stand-in node over a real sealed channel
|
|
306
97
|
pnpm lint
|
|
307
98
|
pnpm typecheck
|
|
99
|
+
pnpm typecheck:consumer
|
|
308
100
|
```
|
|
309
101
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
The SDK mirrors core's HTTP wire types by hand, so two layers guard against drift:
|
|
102
|
+
The cluster tests (`tests/cloud-v2`) start three `merod run --dev` nodes and
|
|
103
|
+
run the client against them:
|
|
313
104
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
NODE_URL=http://localhost:2528 MERO_E2E_USER=dev MERO_E2E_PASS=dev pnpm test:e2e
|
|
319
|
-
```
|
|
320
|
-
In CI this runs both against a released `merod` (this repo) and against a
|
|
321
|
-
PR-built `merod` (core's `sdk-e2e`).
|
|
105
|
+
```bash
|
|
106
|
+
MEROD_BINARY=/path/to/merod KV_STORE_WASM=/path/to/kv_store.wasm pnpm test:cloud-v2
|
|
107
|
+
```
|
|
322
108
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
`
|
|
327
|
-
|
|
328
|
-
coverage stays complete.
|
|
109
|
+
Without the variables they use `../core/target/debug/merod` and
|
|
110
|
+
`../core/target/wasm32-unknown-unknown/app-release/kv_store.wasm` from a sibling
|
|
111
|
+
core checkout. `scripts/download-released-merod.sh` fetches the newest released
|
|
112
|
+
1.0 `merod` and its kv-store fixture, which is what CI runs them against; core's
|
|
113
|
+
`e2e.yml` runs the same suite against core's own tree.
|
|
329
114
|
|
|
330
115
|
## License
|
|
331
116
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check every cloud client runs before it sends anything: the node's quote
|
|
3
|
+
* must be genuine (or, in development, an explicitly allowed mock), from an
|
|
4
|
+
* image this client trusts, and its report data must bind this nonce, the
|
|
5
|
+
* expected app and the transport key the sealed channel will be opened to.
|
|
6
|
+
*
|
|
7
|
+
* Report data, as core's `calimero-cloud-client::attest` builds it:
|
|
8
|
+
* `nonce(32) || SHA-256("calimero.tee-attest.transport-key.v1" | app_hash | transport_key)`.
|
|
9
|
+
*
|
|
10
|
+
* A real quote is judged as the Rust client judges it (`attest::verify` with
|
|
11
|
+
* `Measurements::Release`): the node names the mero-tee release it runs; that
|
|
12
|
+
* release must be at least `minReleaseVersion`; its `published-mrtds.json` is
|
|
13
|
+
* fetched and its Sigstore signature checked against the node release workflow;
|
|
14
|
+
* the quote's signature chain and TCB status are checked against Intel's
|
|
15
|
+
* collateral (fetched here: the node sends none, and cannot forge it); the TD
|
|
16
|
+
* must not be a debug TD; and its registers must be the release's image for the
|
|
17
|
+
* trusted profile, `locked-read-only` unless a debug profile is explicitly
|
|
18
|
+
* allowed. Nothing here reads the environment: mock quotes pass only with
|
|
19
|
+
* `allowMock`.
|
|
20
|
+
*/
|
|
21
|
+
import { type SignedNodeRelease } from '../sealed/release.js';
|
|
22
|
+
import { type VerifyTransportQuote } from '../sealed/sealed.js';
|
|
23
|
+
import { type DcapCollateral, type DcapVerify } from '../sealed/verify.js';
|
|
24
|
+
/** The mock quote format development clusters use. */
|
|
25
|
+
export declare const MOCK_QUOTE_HEADER = "MOCK_TDX_QUOTE_V1";
|
|
26
|
+
/** How the signed release a node names is trusted (core's `Measurements::Release`). */
|
|
27
|
+
export interface CloudReleasePolicy {
|
|
28
|
+
/**
|
|
29
|
+
* The oldest mero-tee release trusted, e.g. `2.3.109`. Required: every
|
|
30
|
+
* release ever signed stays validly signed, so without a floor a node could
|
|
31
|
+
* name an old release with a flaw since fixed.
|
|
32
|
+
*/
|
|
33
|
+
minReleaseVersion: string;
|
|
34
|
+
/** The image profile trusted; defaults to `locked-read-only`. */
|
|
35
|
+
profile?: string;
|
|
36
|
+
/** Trust a debug profile. Its image is readable by the operator: testing only. */
|
|
37
|
+
allowDebugProfile?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Where the release's signed measurements come from: `'github'` (the release
|
|
40
|
+
* assets, what core's Rust client reads; no CORS, so not in browsers), the
|
|
41
|
+
* base URL of a mirror (default {@link DEFAULT_RELEASE_MIRROR}), or a function.
|
|
42
|
+
* Untrusted either way: the Sigstore signature is checked here.
|
|
43
|
+
*/
|
|
44
|
+
releaseSource?: 'github' | string | ((version: string) => Promise<SignedNodeRelease>);
|
|
45
|
+
/** DCAP verification; defaults to `verify` from `@phala/dcap-qvl`, loaded on first use. */
|
|
46
|
+
dcapVerify?: DcapVerify;
|
|
47
|
+
/**
|
|
48
|
+
* Intel's collateral for a quote; defaults to `@phala/dcap-qvl`'s
|
|
49
|
+
* `getCollateralFromPcs` (Intel's PCS). Point it at a PCCS a browser can reach
|
|
50
|
+
* if Intel's does not answer the page.
|
|
51
|
+
*/
|
|
52
|
+
fetchCollateral?: (quote: Uint8Array) => Promise<DcapCollateral>;
|
|
53
|
+
/** Override the release signer (defaults to the mero-tee node release workflow). */
|
|
54
|
+
identity?: string;
|
|
55
|
+
issuer?: string;
|
|
56
|
+
/** The current time in milliseconds; defaults to `Date.now`. For tests. */
|
|
57
|
+
now?: () => number;
|
|
58
|
+
}
|
|
59
|
+
export interface CloudPolicy {
|
|
60
|
+
/** Accept mock quotes. Development clusters only; never set from the environment. */
|
|
61
|
+
allowMock?: boolean;
|
|
62
|
+
/** Trust real quotes from the signed release the node runs. */
|
|
63
|
+
release?: CloudReleasePolicy;
|
|
64
|
+
/**
|
|
65
|
+
* Verify a real quote yourself instead (e.g. `createQuoteVerifier` with
|
|
66
|
+
* pinned measurements). Takes precedence over `release`.
|
|
67
|
+
*/
|
|
68
|
+
verifyQuote?: VerifyTransportQuote;
|
|
69
|
+
/** The app bundle hash (hex) the quote must bind, when talking to one app. */
|
|
70
|
+
appHash?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface AttestationDoc {
|
|
73
|
+
quote: string;
|
|
74
|
+
transportKey: string;
|
|
75
|
+
appHash: string;
|
|
76
|
+
nonce: string;
|
|
77
|
+
release: {
|
|
78
|
+
version: string;
|
|
79
|
+
profile: string;
|
|
80
|
+
};
|
|
81
|
+
cluster: string;
|
|
82
|
+
node: number;
|
|
83
|
+
mock: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The release a node names, checked against the floor before anything is
|
|
87
|
+
* fetched. Accepts `2.3.109` and the tag form `mero-tee-v2.3.109`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function claimedRelease(doc: Pick<AttestationDoc, 'release'>, minReleaseVersion: string): string;
|
|
90
|
+
/** Fetch the node's attestation and return its transport key once it checks out. */
|
|
91
|
+
export declare function fetchAttestedKey(baseUrl: string, policy: CloudPolicy, fetchImpl?: typeof fetch): Promise<Uint8Array>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check every cloud client runs before it sends anything: the node's quote
|
|
3
|
+
* must be genuine (or, in development, an explicitly allowed mock), from an
|
|
4
|
+
* image this client trusts, and its report data must bind this nonce, the
|
|
5
|
+
* expected app and the transport key the sealed channel will be opened to.
|
|
6
|
+
*
|
|
7
|
+
* Report data, as core's `calimero-cloud-client::attest` builds it:
|
|
8
|
+
* `nonce(32) || SHA-256("calimero.tee-attest.transport-key.v1" | app_hash | transport_key)`.
|
|
9
|
+
*
|
|
10
|
+
* A real quote is judged as the Rust client judges it (`attest::verify` with
|
|
11
|
+
* `Measurements::Release`): the node names the mero-tee release it runs; that
|
|
12
|
+
* release must be at least `minReleaseVersion`; its `published-mrtds.json` is
|
|
13
|
+
* fetched and its Sigstore signature checked against the node release workflow;
|
|
14
|
+
* the quote's signature chain and TCB status are checked against Intel's
|
|
15
|
+
* collateral (fetched here: the node sends none, and cannot forge it); the TD
|
|
16
|
+
* must not be a debug TD; and its registers must be the release's image for the
|
|
17
|
+
* trusted profile, `locked-read-only` unless a debug profile is explicitly
|
|
18
|
+
* allowed. Nothing here reads the environment: mock quotes pass only with
|
|
19
|
+
* `allowMock`.
|
|
20
|
+
*/
|
|
21
|
+
import { fromHex, hex } from '../crypto/internal.js';
|
|
22
|
+
import { DEFAULT_RELEASE_MIRROR, cloudNodeReleaseUrl, compareReleaseVersions, fetchGithubNodeRelease, fetchNodeRelease, releaseVersion, trustSignedRelease, } from '../sealed/release.js';
|
|
23
|
+
import { transportKeyBinding } from '../sealed/sealed.js';
|
|
24
|
+
import { PRODUCTION_PROFILE, createQuoteVerifier } from '../sealed/verify.js';
|
|
25
|
+
/** The mock quote format development clusters use. */
|
|
26
|
+
export const MOCK_QUOTE_HEADER = 'MOCK_TDX_QUOTE_V1';
|
|
27
|
+
function base64ToBytes(b64) {
|
|
28
|
+
const bin = atob(b64);
|
|
29
|
+
const out = new Uint8Array(bin.length);
|
|
30
|
+
for (let i = 0; i < bin.length; i++)
|
|
31
|
+
out[i] = bin.charCodeAt(i);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function equal(a, b) {
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
let diff = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++)
|
|
39
|
+
diff |= a[i] ^ b[i];
|
|
40
|
+
return diff === 0;
|
|
41
|
+
}
|
|
42
|
+
let dcapModule;
|
|
43
|
+
/** A function out of `@phala/dcap-qvl`, whatever shape the bundler gave the (CommonJS) module. */
|
|
44
|
+
async function dcap(name) {
|
|
45
|
+
dcapModule ?? (dcapModule = import('@phala/dcap-qvl'));
|
|
46
|
+
const ns = await dcapModule;
|
|
47
|
+
const f = typeof ns[name] === 'function' ? ns[name] : ns.default?.[name];
|
|
48
|
+
if (typeof f !== 'function')
|
|
49
|
+
throw new TypeError(`@phala/dcap-qvl loaded without ${name}`);
|
|
50
|
+
return f;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The release a node names, checked against the floor before anything is
|
|
54
|
+
* fetched. Accepts `2.3.109` and the tag form `mero-tee-v2.3.109`.
|
|
55
|
+
*/
|
|
56
|
+
export function claimedRelease(doc, minReleaseVersion) {
|
|
57
|
+
const floor = releaseVersion(minReleaseVersion, 'minReleaseVersion');
|
|
58
|
+
const raw = String(doc.release?.version ?? '').trim().replace(/^mero-tee-v/, '').replace(/^v/, '');
|
|
59
|
+
const version = releaseVersion(raw, 'The release the node names');
|
|
60
|
+
if (compareReleaseVersions(version, floor) < 0) {
|
|
61
|
+
throw new Error(`mero-tee release ${version} is older than the minimum ${floor}`);
|
|
62
|
+
}
|
|
63
|
+
return version;
|
|
64
|
+
}
|
|
65
|
+
async function releaseFor(version, policy, fetchImpl) {
|
|
66
|
+
const source = policy.releaseSource;
|
|
67
|
+
if (typeof source === 'function')
|
|
68
|
+
return source(version);
|
|
69
|
+
if (source === 'github')
|
|
70
|
+
return fetchGithubNodeRelease(version, { fetch: fetchImpl });
|
|
71
|
+
return fetchNodeRelease(cloudNodeReleaseUrl(source ?? DEFAULT_RELEASE_MIRROR, version), { fetch: fetchImpl });
|
|
72
|
+
}
|
|
73
|
+
/** Judge a real quote as core's `Measurements::Release` does. */
|
|
74
|
+
async function verifyRelease(doc, args, policy, fetchImpl) {
|
|
75
|
+
// Refuse what no release could admit before reaching for the network.
|
|
76
|
+
const version = claimedRelease(doc, policy.minReleaseVersion);
|
|
77
|
+
const trusted = await trustSignedRelease({
|
|
78
|
+
release: await releaseFor(version, policy, fetchImpl),
|
|
79
|
+
profile: policy.profile ?? PRODUCTION_PROFILE,
|
|
80
|
+
allowDebugProfile: policy.allowDebugProfile,
|
|
81
|
+
minReleaseVersion: policy.minReleaseVersion,
|
|
82
|
+
identity: policy.identity,
|
|
83
|
+
issuer: policy.issuer,
|
|
84
|
+
});
|
|
85
|
+
const fetchCollateral = policy.fetchCollateral ??
|
|
86
|
+
(async (quote) => (await dcap('getCollateralFromPcs'))(quote));
|
|
87
|
+
const verifier = createQuoteVerifier({
|
|
88
|
+
dcapVerify: policy.dcapVerify ?? (await dcap('verify')),
|
|
89
|
+
fetchCollateral,
|
|
90
|
+
now: policy.now,
|
|
91
|
+
...trusted,
|
|
92
|
+
});
|
|
93
|
+
return verifier(args);
|
|
94
|
+
}
|
|
95
|
+
/** Fetch the node's attestation and return its transport key once it checks out. */
|
|
96
|
+
export async function fetchAttestedKey(baseUrl, policy, fetchImpl = (input, init) => fetch(input, init)) {
|
|
97
|
+
const nonce = hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
98
|
+
const url = new URL(`${baseUrl.replace(/\/+$/, '')}/attestation`);
|
|
99
|
+
url.searchParams.set('nonce', nonce);
|
|
100
|
+
if (policy.appHash)
|
|
101
|
+
url.searchParams.set('appHash', policy.appHash);
|
|
102
|
+
const response = await fetchImpl(url.toString());
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw new Error(`The node refused to attest: HTTP ${response.status}`);
|
|
105
|
+
const doc = (await response.json());
|
|
106
|
+
const transportKey = fromHex(doc.transportKey, 'transportKey', 32);
|
|
107
|
+
const inner = policy.appHash ? fromHex(policy.appHash, 'appHash', 32) : new Uint8Array(32);
|
|
108
|
+
const suffix = await transportKeyBinding(inner, transportKey);
|
|
109
|
+
const quote = base64ToBytes(doc.quote);
|
|
110
|
+
const header = new TextEncoder().encode(MOCK_QUOTE_HEADER);
|
|
111
|
+
if (equal(quote.slice(0, header.length), header)) {
|
|
112
|
+
if (!policy.allowMock) {
|
|
113
|
+
throw new Error('mock quote refused: this client only trusts real TEEs');
|
|
114
|
+
}
|
|
115
|
+
const report = quote.slice(header.length, header.length + 64);
|
|
116
|
+
if (!equal(report.slice(0, 32), fromHex(nonce, 'nonce', 32)) || !equal(report.slice(32), suffix)) {
|
|
117
|
+
throw new Error('the quote does not bind this transport key, app and nonce');
|
|
118
|
+
}
|
|
119
|
+
return transportKey;
|
|
120
|
+
}
|
|
121
|
+
const args = { quoteB64: doc.quote, nonce, reportDataSuffix: hex(suffix) };
|
|
122
|
+
let ok;
|
|
123
|
+
if (policy.verifyQuote) {
|
|
124
|
+
ok = await policy.verifyQuote(args);
|
|
125
|
+
}
|
|
126
|
+
else if (policy.release) {
|
|
127
|
+
ok = await verifyRelease(doc, args, policy.release, fetchImpl);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw new Error('real quote refused: this client trusts no TD image (set policy.release)');
|
|
131
|
+
}
|
|
132
|
+
if (!ok)
|
|
133
|
+
throw new Error('The attestation did not verify, so its transport key is not trusted');
|
|
134
|
+
return transportKey;
|
|
135
|
+
}
|