@calimero-network/mero-js 26.11.0 → 27.0.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/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/dist/account/index.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Account roots — minting one, restoring it from a phrase, and proving you hold it.
|
|
2
|
-
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, createAccountRootSigner, accountRootSignerFromPhrase, signAccountLink, signAccountLogin, } from './account.js';
|
|
3
|
-
export { resolveRoot } from './account.js';
|
|
4
|
-
export { resolveRootPair } from './account.js';
|
|
5
|
-
export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, RelaysUnavailableError, } from './session.js';
|
|
6
|
-
export { resolveRelayFromInvitation, normaliseAccount } from './relay-from-invitation.js';
|
|
7
|
-
export { joinWithNode } from './join-with-node.js';
|
|
8
|
-
export { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
|
|
9
|
-
export { joinAsAccount } from './join-as-account.js';
|
|
10
|
-
export { createDelegatedContext, createDelegatedPrivateContext, foundDelegatedNamespace, delegatedGovernance, latestPublishedVersion, HA_ACCOUNT_NOT_LINKED_MESSAGE, HA_REFUSAL_MESSAGES, } from './create-context.js';
|
|
11
|
-
export { governGroup, governRoot, namespaceOfGroup, rememberGroupNamespace } from './govern.js';
|
|
12
|
-
export { applicationIdForBundle, fetchRegistryBundles, resolveApplicationIdFromRegistry, selectLatestBundle, } from './application-id.js';
|
|
13
|
-
export { createAccountAdmin, InvitationNotClaimableError, NoRelayError, NotForAccountError, } from './account-admin.js';
|
|
14
|
-
export { RelayLoginError } from './relay-login-error.js';
|
|
15
|
-
export { createNodeAdmin } from './node-admin.js';
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { SignedGroupOpenInvitation } from '../admin-api/index.js';
|
|
2
|
-
import { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
|
|
3
|
-
import { type DelegatedSession } from './session.js';
|
|
4
|
-
/**
|
|
5
|
-
* Redeem an invitation for an account, the way `useDelegatedBootstrap` does:
|
|
6
|
-
* resolve the relay the invitation names, have it admit the account, add that
|
|
7
|
-
* relay to the account's map and hand the session over to `onJoined`.
|
|
8
|
-
*
|
|
9
|
-
* It is what an account's `admin.joinNamespace` runs, so an app that joins with
|
|
10
|
-
* the node call gets the account path with no code of its own. It works with no
|
|
11
|
-
* relay yet: the join is how an account gets its first one.
|
|
12
|
-
*
|
|
13
|
-
* Rejects on a refused join with the refusal's reason, its `step`, and its HTTP
|
|
14
|
-
* `status` when it has one, as the node call would; nothing is remembered or
|
|
15
|
-
* switched then.
|
|
16
|
-
*/
|
|
17
|
-
export declare function joinAsAccount(session: DelegatedSession, namespaceId: string, invitation: SignedGroupOpenInvitation, opts: {
|
|
18
|
-
cloudBaseUrl?: string;
|
|
19
|
-
onJoined: (session: DelegatedSession) => void;
|
|
20
|
-
bootstrap?: typeof bootstrapFromInvitation;
|
|
21
|
-
}): Promise<{
|
|
22
|
-
namespaceId: string;
|
|
23
|
-
}>;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
|
|
2
|
-
import { carryExecutorAccount, rememberRelay } from './session.js';
|
|
3
|
-
/**
|
|
4
|
-
* Redeem an invitation for an account, the way `useDelegatedBootstrap` does:
|
|
5
|
-
* resolve the relay the invitation names, have it admit the account, add that
|
|
6
|
-
* relay to the account's map and hand the session over to `onJoined`.
|
|
7
|
-
*
|
|
8
|
-
* It is what an account's `admin.joinNamespace` runs, so an app that joins with
|
|
9
|
-
* the node call gets the account path with no code of its own. It works with no
|
|
10
|
-
* relay yet: the join is how an account gets its first one.
|
|
11
|
-
*
|
|
12
|
-
* Rejects on a refused join with the refusal's reason, its `step`, and its HTTP
|
|
13
|
-
* `status` when it has one, as the node call would; nothing is remembered or
|
|
14
|
-
* switched then.
|
|
15
|
-
*/
|
|
16
|
-
export async function joinAsAccount(session, namespaceId, invitation, opts) {
|
|
17
|
-
const bootstrap = opts.bootstrap ?? bootstrapFromInvitation;
|
|
18
|
-
const outcome = await bootstrap({
|
|
19
|
-
namespaceId,
|
|
20
|
-
invitation,
|
|
21
|
-
credential: { account: session.account, credential: session.credential, deviceSecret: session.deviceSecret },
|
|
22
|
-
cloudBaseUrl: opts.cloudBaseUrl,
|
|
23
|
-
});
|
|
24
|
-
if (outcome.ok === false) {
|
|
25
|
-
throw Object.assign(new Error(outcome.reason), {
|
|
26
|
-
step: outcome.step,
|
|
27
|
-
...(outcome.status !== undefined ? { status: outcome.status } : {}),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
if (outcome.session.relayUrl)
|
|
31
|
-
rememberRelay(session.account, outcome.session.relayUrl, { namespaceId });
|
|
32
|
-
opts.onJoined(carryExecutorAccount(session, outcome.session));
|
|
33
|
-
return { namespaceId };
|
|
34
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Present a signed join to ONE named node.
|
|
3
|
-
*
|
|
4
|
-
* This is the whole of admission for a caller with no node of its own: sign the
|
|
5
|
-
* membership op with the device key the account certified, and POST it to a node
|
|
6
|
-
* the invitation named as an admitter.
|
|
7
|
-
*
|
|
8
|
-
* POST /admin-api/namespaces/{namespaceId}/admit
|
|
9
|
-
* { "invitation": SignedGroupOpenInvitation, "signedOp": "<hex borsh SignedNamespaceOp>" }
|
|
10
|
-
*
|
|
11
|
-
* Verified against core at rc.43 — `crates/server/src/admin/handlers/namespaces/
|
|
12
|
-
* admit_join.rs`, request type `AdmitJoinApiRequest` in
|
|
13
|
-
* `crates/server/primitives/src/admin/mod.rs`. The handler also refuses a
|
|
14
|
-
* mismatch between the invitation's `group_id` and the namespace in the path, so
|
|
15
|
-
* the two are never allowed to disagree here.
|
|
16
|
-
*
|
|
17
|
-
* ## Why it needs no token and no proof
|
|
18
|
-
*
|
|
19
|
-
* The joiner's signature IS the authorization. The op is signed by the device
|
|
20
|
-
* key named in the credential it carries, and every peer checks
|
|
21
|
-
* `signer == credential.statement.sign_pk` before applying a join — so an
|
|
22
|
-
* admitter can carry this and can never author it: it cannot admit a different
|
|
23
|
-
* account, change the group, or grant a role. It can refuse, and that is all.
|
|
24
|
-
* This is also why core serves the route without a credential (it sits on the
|
|
25
|
-
* public admin router, beside `/health` and the intent routes, on every node
|
|
26
|
-
* whatever its auth mode), why the fleet ingress exempts the one anchored path,
|
|
27
|
-
* and why this posts with `fetch` rather than through `AdminApiClient`, which is
|
|
28
|
-
* built around a node credential this caller does not have. A node that predates
|
|
29
|
-
* that change and runs embedded auth answers 401 here; the failure is reported at
|
|
30
|
-
* the `admit` step like any other refusal.
|
|
31
|
-
*
|
|
32
|
-
* ## Why the op has no parents
|
|
33
|
-
*
|
|
34
|
-
* A keyholder holds no node, so it has no view of the namespace DAG and cannot
|
|
35
|
-
* name its heads. Empty parents is the only thing it *can* sign, and the direct
|
|
36
|
-
* admission path exists precisely for callers in that position.
|
|
37
|
-
*/
|
|
38
|
-
import { type SignedGroupOpenInvitation } from '../admin-api/index.js';
|
|
39
|
-
import type { DelegatedCredential, DelegatedSession } from './session.js';
|
|
40
|
-
/** Which step failed, when one did. */
|
|
41
|
-
export type JoinWithNodeStep =
|
|
42
|
-
/** The op could not be signed. Local, deterministic, and nothing was sent. */
|
|
43
|
-
'sign'
|
|
44
|
-
/** The node refused it, or could not be reached. */
|
|
45
|
-
| 'admit';
|
|
46
|
-
export interface JoinWithNodeSuccess {
|
|
47
|
-
readonly ok: true;
|
|
48
|
-
/**
|
|
49
|
-
* Whether the admitter said it published the op.
|
|
50
|
-
*
|
|
51
|
-
* NOT "you are a member": membership lands when peers apply the op, which the
|
|
52
|
-
* admitter neither performs nor waits for.
|
|
53
|
-
*/
|
|
54
|
-
readonly published: boolean;
|
|
55
|
-
/**
|
|
56
|
-
* The session to connect with — the admitting node, used immediately.
|
|
57
|
-
*
|
|
58
|
-
* Deliberately not waiting for `getAccountRelays` to answer: the fleet writes
|
|
59
|
-
* the account's recovery envelope asynchronously, so the cloud lookup lags the
|
|
60
|
-
* admission by an unpredictable amount. The node that just carried the join is
|
|
61
|
-
* known to serve this namespace right now, which is a better answer than a
|
|
62
|
-
* lookup that will agree later.
|
|
63
|
-
*/
|
|
64
|
-
readonly session: DelegatedSession;
|
|
65
|
-
}
|
|
66
|
-
export interface JoinWithNodeFailure {
|
|
67
|
-
readonly ok: false;
|
|
68
|
-
readonly step: JoinWithNodeStep;
|
|
69
|
-
readonly reason: string;
|
|
70
|
-
/** The HTTP status, when the node answered with one. */
|
|
71
|
-
readonly status?: number;
|
|
72
|
-
}
|
|
73
|
-
export type JoinWithNodeResult = JoinWithNodeSuccess | JoinWithNodeFailure;
|
|
74
|
-
export interface JoinWithNodeInput {
|
|
75
|
-
/**
|
|
76
|
-
* The node to present the join to — its admin API origin.
|
|
77
|
-
*
|
|
78
|
-
* Resolved from the cloud's `/admitters` lookup or supplied by the caller;
|
|
79
|
-
* never read off the invitation, signed or not: the inviter may be the attacker.
|
|
80
|
-
*/
|
|
81
|
-
readonly nodeUrl: string;
|
|
82
|
-
/**
|
|
83
|
-
* The exact URL to POST to, when something already knows it.
|
|
84
|
-
*
|
|
85
|
-
* The cloud hands back a ready-made `admit_url` which need not be a path on
|
|
86
|
-
* `nodeUrl`'s origin, and rebuilding it from a guess is how a working
|
|
87
|
-
* admission becomes a 404. Omitted, the documented route on `nodeUrl` is used.
|
|
88
|
-
*/
|
|
89
|
-
readonly admitUrl?: string;
|
|
90
|
-
/** The namespace being joined, 64 hex. Must match the invitation's group. */
|
|
91
|
-
readonly namespaceId: string;
|
|
92
|
-
/**
|
|
93
|
-
* The invitation, exactly as the inviter's node returned it.
|
|
94
|
-
*
|
|
95
|
-
* Passed through unchanged and never rebuilt field by field: the signature
|
|
96
|
-
* covers the body, and re-modelling it through a local type drops the unsigned
|
|
97
|
-
* bootstrap fields and invalidates the signature with them.
|
|
98
|
-
*/
|
|
99
|
-
readonly invitation: SignedGroupOpenInvitation;
|
|
100
|
-
/** The enrolled account, its certificate, and the device secret that signs. */
|
|
101
|
-
readonly credential: DelegatedCredential;
|
|
102
|
-
/** Injected in tests. */
|
|
103
|
-
readonly deps?: {
|
|
104
|
-
readonly fetch?: typeof fetch;
|
|
105
|
-
readonly nonce?: () => Promise<bigint>;
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Sign the membership op and present it to `nodeUrl`.
|
|
110
|
-
*
|
|
111
|
-
* Resolves rather than throws for every expected refusal, because they are
|
|
112
|
-
* different things for the person to do: a malformed op is final, an unreachable
|
|
113
|
-
* node is a retry, and a 409 means pick another admitter.
|
|
114
|
-
*/
|
|
115
|
-
export declare function joinWithNode(input: JoinWithNodeInput): Promise<JoinWithNodeResult>;
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Present a signed join to ONE named node.
|
|
3
|
-
*
|
|
4
|
-
* This is the whole of admission for a caller with no node of its own: sign the
|
|
5
|
-
* membership op with the device key the account certified, and POST it to a node
|
|
6
|
-
* the invitation named as an admitter.
|
|
7
|
-
*
|
|
8
|
-
* POST /admin-api/namespaces/{namespaceId}/admit
|
|
9
|
-
* { "invitation": SignedGroupOpenInvitation, "signedOp": "<hex borsh SignedNamespaceOp>" }
|
|
10
|
-
*
|
|
11
|
-
* Verified against core at rc.43 — `crates/server/src/admin/handlers/namespaces/
|
|
12
|
-
* admit_join.rs`, request type `AdmitJoinApiRequest` in
|
|
13
|
-
* `crates/server/primitives/src/admin/mod.rs`. The handler also refuses a
|
|
14
|
-
* mismatch between the invitation's `group_id` and the namespace in the path, so
|
|
15
|
-
* the two are never allowed to disagree here.
|
|
16
|
-
*
|
|
17
|
-
* ## Why it needs no token and no proof
|
|
18
|
-
*
|
|
19
|
-
* The joiner's signature IS the authorization. The op is signed by the device
|
|
20
|
-
* key named in the credential it carries, and every peer checks
|
|
21
|
-
* `signer == credential.statement.sign_pk` before applying a join — so an
|
|
22
|
-
* admitter can carry this and can never author it: it cannot admit a different
|
|
23
|
-
* account, change the group, or grant a role. It can refuse, and that is all.
|
|
24
|
-
* This is also why core serves the route without a credential (it sits on the
|
|
25
|
-
* public admin router, beside `/health` and the intent routes, on every node
|
|
26
|
-
* whatever its auth mode), why the fleet ingress exempts the one anchored path,
|
|
27
|
-
* and why this posts with `fetch` rather than through `AdminApiClient`, which is
|
|
28
|
-
* built around a node credential this caller does not have. A node that predates
|
|
29
|
-
* that change and runs embedded auth answers 401 here; the failure is reported at
|
|
30
|
-
* the `admit` step like any other refusal.
|
|
31
|
-
*
|
|
32
|
-
* ## Why the op has no parents
|
|
33
|
-
*
|
|
34
|
-
* A keyholder holds no node, so it has no view of the namespace DAG and cannot
|
|
35
|
-
* name its heads. Empty parents is the only thing it *can* sign, and the direct
|
|
36
|
-
* admission path exists precisely for callers in that position.
|
|
37
|
-
*/
|
|
38
|
-
import { createLocalStorageNonceSource, } from '../relay/index.js';
|
|
39
|
-
import { signMemberJoinOp, } from '../namespace-op/index.js';
|
|
40
|
-
import { pathSegment } from '../http-client/path-segment.js';
|
|
41
|
-
/**
|
|
42
|
-
* Turn an admit refusal into the thing to go and do about it.
|
|
43
|
-
*
|
|
44
|
-
* Each status has one dominant cause and they have nothing to do with each
|
|
45
|
-
* other, so a bare "HTTP 403" sends people to the wrong place — most often to
|
|
46
|
-
* the invitation, when the real answer is which node it was presented to.
|
|
47
|
-
*/
|
|
48
|
-
function explainAdmitFailure(status, body) {
|
|
49
|
-
const detail = body ? `: ${body}` : '';
|
|
50
|
-
switch (status) {
|
|
51
|
-
case 400:
|
|
52
|
-
return (`The node refused the join as malformed (400)${detail}. The signature covers the ` +
|
|
53
|
-
'invitation exactly as sent, so an edited or re-serialised invitation fails here — as ' +
|
|
54
|
-
"does a namespace id that is not the invitation's own group.");
|
|
55
|
-
case 403:
|
|
56
|
-
return (`The node refused to carry this join (403)${detail}. Either it is not in the ` +
|
|
57
|
-
"invitation's signed admitters list — being live and listed by the cloud is not the " +
|
|
58
|
-
'same thing — or the invitation itself was rejected as expired or not the inviter’s ' +
|
|
59
|
-
'to issue.');
|
|
60
|
-
case 409:
|
|
61
|
-
return (`That node holds no device of its own, so it cannot endorse anyone (409)${detail}. ` +
|
|
62
|
-
'Another admitter from the same invitation would work.');
|
|
63
|
-
default:
|
|
64
|
-
return `The join was not published (HTTP ${status})${detail}.`;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* A join-nonce counter for this account, persisted so a reload does not replay.
|
|
69
|
-
*
|
|
70
|
-
* The device secret never appears in the key: storage keys are enumerable by
|
|
71
|
-
* anything running on the origin. One counter per account is right here — a
|
|
72
|
-
* bootstrap is once per account per device, and the node's receiving window
|
|
73
|
-
* accepts gaps, so a shared counter can only ever skip, never reuse.
|
|
74
|
-
*/
|
|
75
|
-
function joinNonce(account) {
|
|
76
|
-
const source = createLocalStorageNonceSource(`calimero.delegated.joinnonce.${account}`);
|
|
77
|
-
return () => source.next();
|
|
78
|
-
}
|
|
79
|
-
/** Strip a trailing slash so a pasted URL and a typed one address one node. */
|
|
80
|
-
function origin(url) {
|
|
81
|
-
return url.replace(/\/+$/, '');
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Sign the membership op and present it to `nodeUrl`.
|
|
85
|
-
*
|
|
86
|
-
* Resolves rather than throws for every expected refusal, because they are
|
|
87
|
-
* different things for the person to do: a malformed op is final, an unreachable
|
|
88
|
-
* node is a retry, and a 409 means pick another admitter.
|
|
89
|
-
*/
|
|
90
|
-
export async function joinWithNode(input) {
|
|
91
|
-
const { account, credential, deviceSecret } = input.credential;
|
|
92
|
-
let signedOp;
|
|
93
|
-
try {
|
|
94
|
-
signedOp = await signMemberJoinOp({
|
|
95
|
-
namespaceId: input.namespaceId,
|
|
96
|
-
member: account,
|
|
97
|
-
invitation: input.invitation,
|
|
98
|
-
credential,
|
|
99
|
-
deviceSecret,
|
|
100
|
-
nonce: await (input.deps?.nonce ?? joinNonce(account))(),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
catch (e) {
|
|
104
|
-
return {
|
|
105
|
-
ok: false,
|
|
106
|
-
step: 'sign',
|
|
107
|
-
reason: 'The join op could not be signed, so nothing was sent: ' +
|
|
108
|
-
`${e instanceof Error ? e.message : String(e)}`,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
// `AdmitJoinRequest` names the shape, so a change to it in mero-js breaks this
|
|
112
|
-
// at the typecheck rather than as a 400 from a node.
|
|
113
|
-
const body = { invitation: input.invitation, signedOp };
|
|
114
|
-
const url = input.admitUrl ??
|
|
115
|
-
`${origin(input.nodeUrl)}/admin-api/namespaces/${pathSegment(input.namespaceId)}/admit`;
|
|
116
|
-
const doFetch = input.deps?.fetch ?? fetch;
|
|
117
|
-
let response;
|
|
118
|
-
try {
|
|
119
|
-
response = await doFetch(url, {
|
|
120
|
-
method: 'POST',
|
|
121
|
-
// No Authorization header and no request proof, deliberately: the route is
|
|
122
|
-
// unauthenticated because the joiner's signature is the authorization.
|
|
123
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
124
|
-
body: JSON.stringify(body),
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
catch (e) {
|
|
128
|
-
return {
|
|
129
|
-
ok: false,
|
|
130
|
-
step: 'admit',
|
|
131
|
-
reason: `The admitter at ${input.nodeUrl} could not be reached: ` +
|
|
132
|
-
`${e instanceof Error ? e.message : String(e)}. The invitation is untouched — this is ` +
|
|
133
|
-
'the node being unreachable, not a refusal.',
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const text = await response.text();
|
|
137
|
-
if (!response.ok) {
|
|
138
|
-
return {
|
|
139
|
-
ok: false,
|
|
140
|
-
step: 'admit',
|
|
141
|
-
status: response.status,
|
|
142
|
-
reason: explainAdmitFailure(response.status, text),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
let published = false;
|
|
146
|
-
try {
|
|
147
|
-
const parsed = text ? JSON.parse(text) : {};
|
|
148
|
-
published = parsed.data?.published === true;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// A 2xx with a body this client cannot read is still a node that took the
|
|
152
|
-
// call. Reported as "not published" rather than as an error, because that is
|
|
153
|
-
// exactly what is not known.
|
|
154
|
-
published = false;
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
ok: true,
|
|
158
|
-
published,
|
|
159
|
-
session: { relayUrl: origin(input.nodeUrl), account, credential, deviceSecret },
|
|
160
|
-
};
|
|
161
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A node's admin API, as an app writes against it: mero-js's `AdminApiClient`
|
|
3
|
-
* with `createNamespace` made whole. On a node a namespace can only be created
|
|
4
|
-
* for an application the node already holds, so this installs the provider's
|
|
5
|
-
* package from the registry first when it is missing — the step an account
|
|
6
|
-
* never sees, because founding through a relay names the package and the relay
|
|
7
|
-
* fetches it. Same call, same result, on both.
|
|
8
|
-
*/
|
|
9
|
-
import type { AdminApiClient } from '../admin-api/index.js';
|
|
10
|
-
export interface NodeAdminDeps {
|
|
11
|
-
latestVersion?: (registryUrl: string, pkg: string) => Promise<string>;
|
|
12
|
-
}
|
|
13
|
-
export declare function createNodeAdmin(input: {
|
|
14
|
-
admin: AdminApiClient;
|
|
15
|
-
app: {
|
|
16
|
-
packageName?: string;
|
|
17
|
-
packageVersion?: string;
|
|
18
|
-
registryUrl?: string;
|
|
19
|
-
};
|
|
20
|
-
}, deps?: NodeAdminDeps): AdminApiClient;
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { latestPublishedVersion } from './create-context.js';
|
|
2
|
-
export function createNodeAdmin(input, deps = {}) {
|
|
3
|
-
const { admin, app } = input;
|
|
4
|
-
const latestVersion = deps.latestVersion ?? latestPublishedVersion;
|
|
5
|
-
async function ensureInstalled(applicationId) {
|
|
6
|
-
const { apps } = await admin.listApplications();
|
|
7
|
-
if (apps.some((a) => a.id === applicationId))
|
|
8
|
-
return;
|
|
9
|
-
if (!app.packageName) {
|
|
10
|
-
throw new Error(`application ${applicationId} is not installed on this node, and no packageName is set to install it`);
|
|
11
|
-
}
|
|
12
|
-
const version = app.packageVersion ?? (await latestVersion(app.registryUrl ?? 'https://apps.calimero.network', app.packageName));
|
|
13
|
-
const { applicationId: installed } = await admin.installApplication({ package: app.packageName, version });
|
|
14
|
-
// A registry bundle signed by a different key is a different application:
|
|
15
|
-
// creating against it would found a workspace for some other app.
|
|
16
|
-
if (installed !== applicationId) {
|
|
17
|
-
throw new Error(`installed ${app.packageName}@${version} as ${installed}, not ${applicationId}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const overrides = {
|
|
21
|
-
async createNamespace(req) {
|
|
22
|
-
await ensureInstalled(req.applicationId);
|
|
23
|
-
return admin.createNamespace(req);
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
return new Proxy(admin, {
|
|
27
|
-
get(target, prop, receiver) {
|
|
28
|
-
if (typeof prop === 'string' && prop in overrides)
|
|
29
|
-
return overrides[prop];
|
|
30
|
-
return Reflect.get(target, prop, receiver);
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolving a relay from an INVITATION rather than from an account.
|
|
3
|
-
*
|
|
4
|
-
* # The gap this closes
|
|
5
|
-
*
|
|
6
|
-
* `ConnectButtonAccount` finds a relay by asking the cloud what serves the
|
|
7
|
-
* *account* (`CloudClient.getAccountRelays`). A freshly enrolled account is a
|
|
8
|
-
* member of nothing, so that read correctly answers `[]` and the button
|
|
9
|
-
* correctly refuses to connect: there is nowhere to write, and a typed guess
|
|
10
|
-
* would be a node that refuses every warrant.
|
|
11
|
-
*
|
|
12
|
-
* But a newcomer is not stuck — they are holding an invitation, and an
|
|
13
|
-
* invitation names a namespace. `CloudClient.getNamespaceRouting` answers which
|
|
14
|
-
* nodes serve *that namespace*, and it is deliberately anonymous: a joiner has
|
|
15
|
-
* no cloud session, and needing one is exactly what this path exists to avoid.
|
|
16
|
-
* So the bootstrap order is namespace first, account second — admission is what
|
|
17
|
-
* makes `getAccountRelays` answer at all, not the other way round.
|
|
18
|
-
*
|
|
19
|
-
* # Two sources, one intersection, and why it must not be skipped
|
|
20
|
-
*
|
|
21
|
-
* The invitation's `admitters` list sits INSIDE the body the group admin
|
|
22
|
-
* signed, so it is authorization: merod answers `403` to an `/admit` from a node
|
|
23
|
-
* that list does not name, whatever else is true of it. The cloud's list is
|
|
24
|
-
* every node currently assigned to the namespace — it cannot know which
|
|
25
|
-
* invitation the caller holds, so it cannot filter by the signed list, and a
|
|
26
|
-
* node assigned *after* the invitation was minted looks perfect in every field
|
|
27
|
-
* the cloud reports and still refuses the claim.
|
|
28
|
-
*
|
|
29
|
-
* Intersecting here is therefore not belt-and-braces: it is the check that
|
|
30
|
-
* turns an unexplainable `403` into "your invitation names none of these nodes,
|
|
31
|
-
* ask for a fresh one". It is never relaxed to make a flow work.
|
|
32
|
-
*
|
|
33
|
-
* # `servable` and `canAdmit` are advisory; `writable` is information
|
|
34
|
-
*
|
|
35
|
-
* `servable` and `canAdmit` are derived from assignment heartbeats, and a lapsed
|
|
36
|
-
* heartbeat is not the same fact as a node that will refuse. Measured against
|
|
37
|
-
* prod: the cloud reported `servable: false, canAdmit: false, fresh: false` for a
|
|
38
|
-
* node whose `/admin-api/health` answered `{"status":"alive"}` and which had just
|
|
39
|
-
* minted the invitation being claimed. Gating on them meant refusing to knock on
|
|
40
|
-
* an open door, so a node with an address is tried and the NODE decides —
|
|
41
|
-
* `admit_join` re-verifies the invitation, the signature and the admitters list,
|
|
42
|
-
* so asking loosens nothing and a refusal names its own cause. `stale` on the
|
|
43
|
-
* result says the attempt rests on advisory data.
|
|
44
|
-
*
|
|
45
|
-
* `writable` — and its per-node form, `canExecute` — is about `CAN_AUTHOR_ON_BEHALF`,
|
|
46
|
-
* which admission does **not** require: relaying a join the joiner signed is not
|
|
47
|
-
* authoring on anyone's behalf. So a non-writable namespace is not a refusal
|
|
48
|
-
* here; it is reported on the result, because it is precisely why the writes
|
|
49
|
-
* would fail *after* connecting, and a caller that cannot tell the two apart
|
|
50
|
-
* sends the user to the wrong place.
|
|
51
|
-
*/
|
|
52
|
-
import { CloudClient } from '../cloud/index.js';
|
|
53
|
-
/**
|
|
54
|
-
* Which step could not be completed. Each one needs a different action from the
|
|
55
|
-
* person, which is why this is an enum and not a message.
|
|
56
|
-
*
|
|
57
|
-
* - `admitters-lookup` — the cloud read itself failed (offline, 5xx, a refused
|
|
58
|
-
* routing proof). Nothing has been learned about the namespace.
|
|
59
|
-
* - `no-nodes` - the cloud lists no node to admit through. An address on the
|
|
60
|
-
* invitation is not one: `admitter_addrs` is unsigned.
|
|
61
|
-
* - `not-invited` — nodes serve the namespace, and the invitation names none of
|
|
62
|
-
* them. Terminal for this invitation: ask for a fresh one.
|
|
63
|
-
* - `invited-unreachable` — a named node exists but cannot take a join now (no
|
|
64
|
-
* fresh heartbeat, or the cloud reports the namespace as not servable). A
|
|
65
|
-
* wait, not a wrong invitation.
|
|
66
|
-
* - `no-relay-url` — a named node can admit but the cloud knows no address for
|
|
67
|
-
* it, so there is nothing to connect through afterwards. Also a wait: it is
|
|
68
|
-
* the shape a node reports before its first heartbeat lands.
|
|
69
|
-
*/
|
|
70
|
-
export type RelayResolutionStep = 'admitters-lookup' | 'no-nodes' | 'not-invited' | 'invited-unreachable' | 'no-relay-url';
|
|
71
|
-
/** A relay that can take this invitation's join, and connect afterwards. */
|
|
72
|
-
export interface ResolvedInvitationRelay {
|
|
73
|
-
readonly ok: true;
|
|
74
|
-
/** The node origin to connect through. */
|
|
75
|
-
readonly relayUrl: string;
|
|
76
|
-
/** Where to POST the signed join — the cloud's own, never rebuilt when given. */
|
|
77
|
-
readonly admitUrl: string;
|
|
78
|
-
/** The admitter's account, as the cloud reported it. */
|
|
79
|
-
readonly admitterAccount: string | null;
|
|
80
|
-
/** The node's peer id, as the cloud reported it. */
|
|
81
|
-
readonly peerId: string;
|
|
82
|
-
/**
|
|
83
|
-
* Whether this node can also run delegated writes (`CAN_AUTHOR_ON_BEHALF`).
|
|
84
|
-
*
|
|
85
|
-
* `false` is not a failure of admission — see the module note. It means the
|
|
86
|
-
* connection that follows will read and receive events but have its intents
|
|
87
|
-
* refused, which an admin grants away, so it is worth saying out loud.
|
|
88
|
-
*/
|
|
89
|
-
readonly writable: boolean;
|
|
90
|
-
/** Whether the chosen node's heartbeat for this namespace has lapsed. */
|
|
91
|
-
readonly stale: boolean;
|
|
92
|
-
}
|
|
93
|
-
export interface RelayResolutionFailure {
|
|
94
|
-
readonly ok: false;
|
|
95
|
-
readonly step: RelayResolutionStep;
|
|
96
|
-
/** What to go and do about it, in one sentence. */
|
|
97
|
-
readonly reason: string;
|
|
98
|
-
}
|
|
99
|
-
export type ResolveRelayResult = ResolvedInvitationRelay | RelayResolutionFailure;
|
|
100
|
-
export interface ResolveRelayFromInvitationInput {
|
|
101
|
-
/** The namespace the invitation admits you to, 64 hex. */
|
|
102
|
-
readonly namespaceId: string;
|
|
103
|
-
/**
|
|
104
|
-
* The invitation's signed `admitters` — `invitation.invitation.admitters`.
|
|
105
|
-
*
|
|
106
|
-
* An EMPTY list is the legacy "admission by broadcast" shape, where any ready
|
|
107
|
-
* peer may admit, and is treated as such. Omitting the field and passing an
|
|
108
|
-
* empty array are therefore the same thing on purpose: there is no spelling
|
|
109
|
-
* here that means "nobody may admit", because an invitation cannot say that.
|
|
110
|
-
*/
|
|
111
|
-
readonly admitters?: readonly string[];
|
|
112
|
-
/** The delegated credential — an `AccountProof<DeviceCert>`, hex. */
|
|
113
|
-
readonly credential: string;
|
|
114
|
-
/** The certified device's ed25519 signing secret, hex. */
|
|
115
|
-
readonly deviceSecret: string;
|
|
116
|
-
/** Point at a cloud other than the hosted manager. For local rigs. */
|
|
117
|
-
readonly cloudBaseUrl?: string;
|
|
118
|
-
/** Injected in tests. Anything with the one method this needs. */
|
|
119
|
-
readonly cloud?: Pick<CloudClient, 'getNamespaceRouting'>;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Normalise a hex account for comparison: lower-cased, `0x` stripped.
|
|
123
|
-
*
|
|
124
|
-
* Both sides are hex for the same 32 bytes, and a spelling mismatch would
|
|
125
|
-
* present as "your invitation does not name this node" — a refusal that looks
|
|
126
|
-
* like policy and is really formatting.
|
|
127
|
-
*/
|
|
128
|
-
export declare function normaliseAccount(account: string | null | undefined): string | null;
|
|
129
|
-
/**
|
|
130
|
-
* Resolve a relay to bootstrap through, from an invitation and nothing else.
|
|
131
|
-
*
|
|
132
|
-
* Reads the cloud's routing for the namespace, intersects it with the
|
|
133
|
-
* invitation's signed `admitters`, and returns the node to use — or which step
|
|
134
|
-
* stopped it and what that means. It performs no admission and mints no keys:
|
|
135
|
-
* the credential passed in is the one the wallet already certified.
|
|
136
|
-
*/
|
|
137
|
-
export declare function resolveRelayFromInvitation(input: ResolveRelayFromInvitationInput): Promise<ResolveRelayResult>;
|