@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
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minting a group invitation without a node.
|
|
3
|
-
*
|
|
4
|
-
* An invitation is not a governance op. Nothing is published when one is made:
|
|
5
|
-
* it is a bearer credential, `GroupInvitationFromAdmin` borsh-encoded, hashed
|
|
6
|
-
* with SHA-256 and signed by the inviter's key. A node mints one with its own
|
|
7
|
-
* namespace key; an account with **no node** signs it here with its device key.
|
|
8
|
-
*
|
|
9
|
-
* Every peer that later folds the join resolves `inviter_identity` — the
|
|
10
|
-
* signing key — to an account through the namespace's device bindings, and then
|
|
11
|
-
* checks that account holds admin or `CAN_INVITE_MEMBERS` at the join's causal
|
|
12
|
-
* cut. So the device key must be **bound** in the namespace first. It is when
|
|
13
|
-
* the account joined through an invitation; an account that was added by
|
|
14
|
-
* account (`MemberAdded`) has no binding, and a relay has to carry its device
|
|
15
|
-
* link first — see `AdminApiClient.linkAccountDevice` and `signDeviceScope`.
|
|
16
|
-
* Without that, every peer refuses the join as signed by a key "bound to no
|
|
17
|
-
* account".
|
|
18
|
-
*
|
|
19
|
-
* Derived from core:
|
|
20
|
-
*
|
|
21
|
-
* - `GroupInvitationFromAdmin` / `SignedGroupOpenInvitation` —
|
|
22
|
-
* `crates/context/config/src/types.rs`
|
|
23
|
-
* - the signing — `crates/context/src/handlers/create_group_invitation.rs`:
|
|
24
|
-
* `sign(sha256(borsh(invitation)))`
|
|
25
|
-
* - the admitter default — `NamespaceMembershipService::default_admitters`
|
|
26
|
-
*
|
|
27
|
-
* The unit tests pin bytes and a signature produced by core's own types.
|
|
28
|
-
*/
|
|
29
|
-
import type { GroupInvitationFromAdmin, GroupMember, SignedGroupOpenInvitation } from '../admin-api/admin-types.js';
|
|
30
|
-
import { type Signer } from '../signer/signer.js';
|
|
31
|
-
/**
|
|
32
|
-
* Core's `MAX_INVITATION_VALIDITY_SECS`: one day. Core clamps what a node mints
|
|
33
|
-
* to it, and this does the same, so a leaked invitation is redeemable no longer
|
|
34
|
-
* than one a node would have issued.
|
|
35
|
-
*/
|
|
36
|
-
export declare const MAX_INVITATION_VALIDITY_SECS: number;
|
|
37
|
-
/** `invited_role` values, as core numbers them. */
|
|
38
|
-
export declare const INVITED_ROLE: {
|
|
39
|
-
readonly Admin: 0;
|
|
40
|
-
readonly Member: 1;
|
|
41
|
-
readonly ReadOnly: 2;
|
|
42
|
-
};
|
|
43
|
-
/** Borsh-encode the signed body of an invitation, field for field with core. */
|
|
44
|
-
export declare function encodeGroupInvitation(body: GroupInvitationFromAdmin): Uint8Array;
|
|
45
|
-
/** `sha256(borsh(invitation))`: the 32 bytes the inviter's key signs. */
|
|
46
|
-
export declare function groupInvitationHash(body: GroupInvitationFromAdmin): Promise<Uint8Array>;
|
|
47
|
-
/**
|
|
48
|
-
* The admitters a node names when its caller names none: the group's admins,
|
|
49
|
-
* sorted by account bytes and deduplicated, as core's `BTreeSet` leaves them.
|
|
50
|
-
*
|
|
51
|
-
* Core also adds TEE-admitted accounts, which a member list cannot tell apart
|
|
52
|
-
* from members by role alone; name those in `admitters` explicitly.
|
|
53
|
-
*/
|
|
54
|
-
export declare function defaultAdmitters(members: readonly GroupMember[]): string[];
|
|
55
|
-
/** What {@link signGroupInvitation} needs. */
|
|
56
|
-
export interface GroupInvitationInput {
|
|
57
|
-
/** The group (or namespace) invited to, 64 hex. */
|
|
58
|
-
groupId: string;
|
|
59
|
-
/**
|
|
60
|
-
* The account the inviter acts as, 64 hex. An unsigned bootstrap hint the
|
|
61
|
-
* joiner seeds the group's admin view from; it confers nothing.
|
|
62
|
-
*/
|
|
63
|
-
inviterAccount: string;
|
|
64
|
-
/** The device key as a {@link Signer}. Pass this or {@link deviceSecret}. */
|
|
65
|
-
signer?: Signer;
|
|
66
|
-
/** The device signing secret, 64 hex. Pass this or {@link signer}. */
|
|
67
|
-
deviceSecret?: string;
|
|
68
|
-
/**
|
|
69
|
-
* Accounts permitted to admit a claim, 64 hex each. Signed.
|
|
70
|
-
*
|
|
71
|
-
* Omitted or empty, it defaults to the group's admins, which is what a node
|
|
72
|
-
* does — pass {@link members} (from `listGroupMembers`) to derive them. An
|
|
73
|
-
* empty list would mean *claimable by broadcast*, which publishes the
|
|
74
|
-
* invitation to every subscriber of the namespace topic, so this never
|
|
75
|
-
* produces one.
|
|
76
|
-
*/
|
|
77
|
-
admitters?: readonly string[];
|
|
78
|
-
/** The group's members, to default {@link admitters} from. */
|
|
79
|
-
members?: readonly GroupMember[];
|
|
80
|
-
/**
|
|
81
|
-
* The role granted, as {@link INVITED_ROLE} numbers it. Defaults to Member.
|
|
82
|
-
* Only an admin may invite an admin; peers refuse the join otherwise.
|
|
83
|
-
*/
|
|
84
|
-
invitedRole?: number;
|
|
85
|
-
/**
|
|
86
|
-
* Seconds from now until it expires, clamped to
|
|
87
|
-
* {@link MAX_INVITATION_VALIDITY_SECS} (also the default).
|
|
88
|
-
*/
|
|
89
|
-
validForSecs?: number;
|
|
90
|
-
/** Unix seconds to count from. Defaults to the clock; for tests. */
|
|
91
|
-
now?: number;
|
|
92
|
-
/** The 32-byte uniqueness nonce. Defaults to fresh random bytes. */
|
|
93
|
-
nonce?: Uint8Array;
|
|
94
|
-
/** Unsigned bootstrap: the group's application id, 64 hex. */
|
|
95
|
-
applicationId?: string;
|
|
96
|
-
/** Unsigned bootstrap: the group's `app_key` (core's `bytecode_id`), 64 hex. */
|
|
97
|
-
appKey?: string;
|
|
98
|
-
/** Unsigned bootstrap: multiaddrs for the admitters, `/p2p/<peer>` included. */
|
|
99
|
-
admitterAddrs?: readonly string[];
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Sign a group invitation with a device key, returning it in the JSON shape a
|
|
103
|
-
* node's `createGroupInvitation` returns — so it goes to `joinGroup`,
|
|
104
|
-
* `signMemberJoinOp` and `admitJoin` unchanged.
|
|
105
|
-
*/
|
|
106
|
-
export declare function signGroupInvitation(input: GroupInvitationInput): Promise<SignedGroupOpenInvitation>;
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { concat, fromHex, hex, u32le, u64le } from '../crypto/internal.js';
|
|
2
|
-
import { resolveSigner } from '../signer/signer.js';
|
|
3
|
-
/**
|
|
4
|
-
* Core's `MAX_INVITATION_VALIDITY_SECS`: one day. Core clamps what a node mints
|
|
5
|
-
* to it, and this does the same, so a leaked invitation is redeemable no longer
|
|
6
|
-
* than one a node would have issued.
|
|
7
|
-
*/
|
|
8
|
-
export const MAX_INVITATION_VALIDITY_SECS = 24 * 60 * 60;
|
|
9
|
-
/** `invited_role` values, as core numbers them. */
|
|
10
|
-
export const INVITED_ROLE = { Admin: 0, Member: 1, ReadOnly: 2 };
|
|
11
|
-
/** Borsh-encode the signed body of an invitation, field for field with core. */
|
|
12
|
-
export function encodeGroupInvitation(body) {
|
|
13
|
-
const admitters = body.admitters ?? [];
|
|
14
|
-
return concat(
|
|
15
|
-
// `SignerId` and `ContextGroupId` cross JSON as 32 numbers; `AccountId`
|
|
16
|
-
// crosses as hex. Same width, different spelling.
|
|
17
|
-
bytes32(body.inviter_identity, 'inviter_identity'), bytes32(body.group_id, 'group_id'), u64le(body.expiration_timestamp), bytes32(body.secret_salt, 'secret_salt'), new Uint8Array([body.invited_role]), u32le(admitters.length), ...admitters.map((a, i) => fromHex(a, `admitters[${i}]`, 32)));
|
|
18
|
-
}
|
|
19
|
-
/** `sha256(borsh(invitation))`: the 32 bytes the inviter's key signs. */
|
|
20
|
-
export async function groupInvitationHash(body) {
|
|
21
|
-
return new Uint8Array(await crypto.subtle.digest('SHA-256', encodeGroupInvitation(body)));
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* The admitters a node names when its caller names none: the group's admins,
|
|
25
|
-
* sorted by account bytes and deduplicated, as core's `BTreeSet` leaves them.
|
|
26
|
-
*
|
|
27
|
-
* Core also adds TEE-admitted accounts, which a member list cannot tell apart
|
|
28
|
-
* from members by role alone; name those in `admitters` explicitly.
|
|
29
|
-
*/
|
|
30
|
-
export function defaultAdmitters(members) {
|
|
31
|
-
const admins = new Set(members.filter((m) => m.role === 'Admin').map((m) => m.identity.toLowerCase()));
|
|
32
|
-
// Lowercase hex sorts as the bytes do.
|
|
33
|
-
return [...admins].sort();
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Sign a group invitation with a device key, returning it in the JSON shape a
|
|
37
|
-
* node's `createGroupInvitation` returns — so it goes to `joinGroup`,
|
|
38
|
-
* `signMemberJoinOp` and `admitJoin` unchanged.
|
|
39
|
-
*/
|
|
40
|
-
export async function signGroupInvitation(input) {
|
|
41
|
-
const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
|
|
42
|
-
const inviterKey = fromHex(signer.publicKey, 'signer.publicKey', 32);
|
|
43
|
-
const groupId = fromHex(input.groupId, 'groupId', 32);
|
|
44
|
-
fromHex(input.inviterAccount, 'inviterAccount', 32);
|
|
45
|
-
let admitters = [...(input.admitters ?? [])].map((a) => a.trim().toLowerCase());
|
|
46
|
-
if (admitters.length === 0) {
|
|
47
|
-
if (!input.members) {
|
|
48
|
-
throw new Error('name the admitters, or pass the group members to default them to its admins: ' +
|
|
49
|
-
'an invitation with no admitters is claimable by broadcast');
|
|
50
|
-
}
|
|
51
|
-
admitters = defaultAdmitters(input.members);
|
|
52
|
-
if (admitters.length === 0) {
|
|
53
|
-
throw new Error('the member list names no admin, so there is nobody to default the admitters to');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
admitters.forEach((a, i) => fromHex(a, `admitters[${i}]`, 32));
|
|
57
|
-
const invitedRole = input.invitedRole ?? INVITED_ROLE.Member;
|
|
58
|
-
if (![0, 1, 2].includes(invitedRole)) {
|
|
59
|
-
throw new Error(`invitedRole must be 0 (Admin), 1 (Member) or 2 (ReadOnly), got ${invitedRole}`);
|
|
60
|
-
}
|
|
61
|
-
const now = input.now ?? Math.floor(Date.now() / 1000);
|
|
62
|
-
const validFor = Math.min(input.validForSecs ?? MAX_INVITATION_VALIDITY_SECS, MAX_INVITATION_VALIDITY_SECS);
|
|
63
|
-
if (!Number.isInteger(validFor) || validFor <= 0) {
|
|
64
|
-
throw new Error(`validForSecs must be a positive whole number, got ${input.validForSecs}`);
|
|
65
|
-
}
|
|
66
|
-
const nonce = input.nonce ?? crypto.getRandomValues(new Uint8Array(32));
|
|
67
|
-
if (nonce.length !== 32) {
|
|
68
|
-
throw new Error(`nonce must be 32 bytes, got ${nonce.length}`);
|
|
69
|
-
}
|
|
70
|
-
const invitation = {
|
|
71
|
-
inviter_identity: Array.from(inviterKey),
|
|
72
|
-
group_id: Array.from(groupId),
|
|
73
|
-
expiration_timestamp: now + validFor,
|
|
74
|
-
secret_salt: Array.from(nonce),
|
|
75
|
-
invited_role: invitedRole,
|
|
76
|
-
admitters,
|
|
77
|
-
};
|
|
78
|
-
const signature = await signer.sign(await groupInvitationHash(invitation));
|
|
79
|
-
if (signature.length !== 64) {
|
|
80
|
-
throw new Error(`signer returned ${signature.length} bytes, expected a 64-byte Ed25519 signature`);
|
|
81
|
-
}
|
|
82
|
-
const signed = {
|
|
83
|
-
invitation,
|
|
84
|
-
inviter_signature: hex(signature),
|
|
85
|
-
inviter_account: input.inviterAccount.trim().toLowerCase(),
|
|
86
|
-
};
|
|
87
|
-
if (input.admitterAddrs?.length)
|
|
88
|
-
signed.admitter_addrs = [...input.admitterAddrs];
|
|
89
|
-
if (input.applicationId) {
|
|
90
|
-
signed.application_id = Array.from(fromHex(input.applicationId, 'applicationId', 32));
|
|
91
|
-
}
|
|
92
|
-
if (input.appKey)
|
|
93
|
-
signed.app_key = Array.from(fromHex(input.appKey, 'appKey', 32));
|
|
94
|
-
// The brand marks a value as one to pass through unchanged. This one is
|
|
95
|
-
// signed, so it qualifies the same way a node's does.
|
|
96
|
-
return signed;
|
|
97
|
-
}
|
|
98
|
-
/** A required `[u8; 32]`, as JSON carries it: 32 byte values. */
|
|
99
|
-
function bytes32(value, label) {
|
|
100
|
-
if (value.length !== 32) {
|
|
101
|
-
throw new Error(`${label} must be 32 bytes, got ${value.length}`);
|
|
102
|
-
}
|
|
103
|
-
return new Uint8Array(value);
|
|
104
|
-
}
|
package/dist/login/index.d.ts
DELETED
package/dist/login/index.js
DELETED
package/dist/login/login.d.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sign the statement a device key presents to obtain a session on a node.
|
|
3
|
-
*
|
|
4
|
-
* This is the half of password-free login that has to run where the device key
|
|
5
|
-
* lives, and for the case delegated execution exists for that is a browser tab.
|
|
6
|
-
* Until this module, the only thing that could produce a login statement was
|
|
7
|
-
* `merod account login-statement` — which is to say, a client had to hold a node
|
|
8
|
-
* to ask a node for a session.
|
|
9
|
-
*
|
|
10
|
-
* **What this gets you, and what it does not.** A statement proves a device key
|
|
11
|
-
* asked for this session. It says nothing about which account that device
|
|
12
|
-
* belongs to: that is the `AccountProof` travelling beside it, which the node
|
|
13
|
-
* checks separately. Signing here and presenting no proof gets a 401 that looks
|
|
14
|
-
* exactly like a bad signature.
|
|
15
|
-
*
|
|
16
|
-
* **The session key is minted by the caller, not here.** The device key signs
|
|
17
|
-
* once, over a short-lived key the caller generates and keeps; that key does the
|
|
18
|
-
* talking for the rest of the session, which is what keeps the device key off
|
|
19
|
-
* the wire. Passing a key you do not hold the private half of produces a session
|
|
20
|
-
* you cannot speak on.
|
|
21
|
-
*
|
|
22
|
-
* **`node` must come from something you pinned.** Not from the challenge
|
|
23
|
-
* response, and not from any field the node itself chose — an attacker who can
|
|
24
|
-
* answer on the node's behalf would otherwise pick what your device signs about,
|
|
25
|
-
* which is the whole reason the field exists. Read it out of band from the node
|
|
26
|
-
* operator.
|
|
27
|
-
*
|
|
28
|
-
* **The byte contract is pinned in core**, at
|
|
29
|
-
* `crates/account/src/tests/login_wire_fixture.rs`, for the same reason the
|
|
30
|
-
* warrant one is: `AUTH_LOGIN_SIGN_DOMAIN` is `pub(crate)` there and the field
|
|
31
|
-
* order is implicit in a `derive`, so nothing on that side forces anyone to
|
|
32
|
-
* notice this file depends on both. This module's test asserts the same vectors.
|
|
33
|
-
*/
|
|
34
|
-
import { type Signer } from '../signer/signer.js';
|
|
35
|
-
/**
|
|
36
|
-
* The client surface a session is bound to.
|
|
37
|
-
*
|
|
38
|
-
* A session minted for one surface cannot be presented from another, so a single
|
|
39
|
-
* compromised surface does not yield sessions usable everywhere.
|
|
40
|
-
*/
|
|
41
|
-
export type Audience = {
|
|
42
|
-
/**
|
|
43
|
-
* A browser origin, spelled exactly as the browser spells it
|
|
44
|
-
* (`https://host:port`, no trailing slash).
|
|
45
|
-
*
|
|
46
|
-
* Compared byte for byte by the node. Do not normalize it here: two
|
|
47
|
-
* spellings of one origin would mean a verifier that disagrees with the
|
|
48
|
-
* browser about which one a token was for. `location.origin` is already
|
|
49
|
-
* the spelling you want.
|
|
50
|
-
*/
|
|
51
|
-
kind: 'webOrigin';
|
|
52
|
-
origin: string;
|
|
53
|
-
} | {
|
|
54
|
-
/** A signed native client, named by its code-signing identity. */
|
|
55
|
-
kind: 'codeSigningId';
|
|
56
|
-
id: string;
|
|
57
|
-
} | {
|
|
58
|
-
/**
|
|
59
|
-
* A command-line client, which has no origin and no signing identity.
|
|
60
|
-
*
|
|
61
|
-
* Carries no payload deliberately: a string here would be an audience that
|
|
62
|
-
* binds nothing while looking like it binds something.
|
|
63
|
-
*/
|
|
64
|
-
kind: 'cli';
|
|
65
|
-
};
|
|
66
|
-
/** What a device is asking for. */
|
|
67
|
-
export interface LoginStatementInput {
|
|
68
|
-
/**
|
|
69
|
-
* The node this session is for, hex (32 bytes) — its identity public key, as
|
|
70
|
-
* you pinned it. See the module note: this must not come from the node.
|
|
71
|
-
*/
|
|
72
|
-
node: string;
|
|
73
|
-
/** The client surface the session is bound to. */
|
|
74
|
-
audience: Audience;
|
|
75
|
-
/** The challenge this node issued, hex (32 bytes). Proves freshness. */
|
|
76
|
-
challenge: string;
|
|
77
|
-
/**
|
|
78
|
-
* The public half of the ephemeral key this session will speak with, hex (32
|
|
79
|
-
* bytes). Minted per session by the caller and thrown away with it.
|
|
80
|
-
*/
|
|
81
|
-
sessionKey: string;
|
|
82
|
-
/** Unix seconds at signing. */
|
|
83
|
-
issuedAt: number | bigint;
|
|
84
|
-
/** Unix seconds after which the node must refuse it. */
|
|
85
|
-
expiresAt: number | bigint;
|
|
86
|
-
/**
|
|
87
|
-
* The device's ed25519 signing secret, hex (32 bytes).
|
|
88
|
-
*
|
|
89
|
-
* Never sent anywhere. It signs locally and only the signature travels, which
|
|
90
|
-
* is what lets a statement be minted by something holding no node.
|
|
91
|
-
*
|
|
92
|
-
* Mutually exclusive with {@link LoginStatementInput.signer}, and the weaker
|
|
93
|
-
* of the two in a browser: a secret that exists as a string can be read by
|
|
94
|
-
* anything running on the origin.
|
|
95
|
-
*/
|
|
96
|
-
deviceSecret?: string;
|
|
97
|
-
/**
|
|
98
|
-
* The device's signer — use instead of
|
|
99
|
-
* {@link LoginStatementInput.deviceSecret} when the key cannot be exported.
|
|
100
|
-
*/
|
|
101
|
-
signer?: Signer;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Sign a login statement and return it hex-encoded, ready to present with an
|
|
105
|
-
* account proof.
|
|
106
|
-
*
|
|
107
|
-
* Returns the encoding rather than an object for the reason `signWarrant` does:
|
|
108
|
-
* the signature covers exactly these bytes, and a caller that rebuilt the fields
|
|
109
|
-
* from JSON would have a second spelling able to disagree with what was signed.
|
|
110
|
-
*/
|
|
111
|
-
export declare function signLoginStatement(input: LoginStatementInput): Promise<string>;
|
package/dist/login/login.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sign the statement a device key presents to obtain a session on a node.
|
|
3
|
-
*
|
|
4
|
-
* This is the half of password-free login that has to run where the device key
|
|
5
|
-
* lives, and for the case delegated execution exists for that is a browser tab.
|
|
6
|
-
* Until this module, the only thing that could produce a login statement was
|
|
7
|
-
* `merod account login-statement` — which is to say, a client had to hold a node
|
|
8
|
-
* to ask a node for a session.
|
|
9
|
-
*
|
|
10
|
-
* **What this gets you, and what it does not.** A statement proves a device key
|
|
11
|
-
* asked for this session. It says nothing about which account that device
|
|
12
|
-
* belongs to: that is the `AccountProof` travelling beside it, which the node
|
|
13
|
-
* checks separately. Signing here and presenting no proof gets a 401 that looks
|
|
14
|
-
* exactly like a bad signature.
|
|
15
|
-
*
|
|
16
|
-
* **The session key is minted by the caller, not here.** The device key signs
|
|
17
|
-
* once, over a short-lived key the caller generates and keeps; that key does the
|
|
18
|
-
* talking for the rest of the session, which is what keeps the device key off
|
|
19
|
-
* the wire. Passing a key you do not hold the private half of produces a session
|
|
20
|
-
* you cannot speak on.
|
|
21
|
-
*
|
|
22
|
-
* **`node` must come from something you pinned.** Not from the challenge
|
|
23
|
-
* response, and not from any field the node itself chose — an attacker who can
|
|
24
|
-
* answer on the node's behalf would otherwise pick what your device signs about,
|
|
25
|
-
* which is the whole reason the field exists. Read it out of band from the node
|
|
26
|
-
* operator.
|
|
27
|
-
*
|
|
28
|
-
* **The byte contract is pinned in core**, at
|
|
29
|
-
* `crates/account/src/tests/login_wire_fixture.rs`, for the same reason the
|
|
30
|
-
* warrant one is: `AUTH_LOGIN_SIGN_DOMAIN` is `pub(crate)` there and the field
|
|
31
|
-
* order is implicit in a `derive`, so nothing on that side forces anyone to
|
|
32
|
-
* notice this file depends on both. This module's test asserts the same vectors.
|
|
33
|
-
*/
|
|
34
|
-
import { concat, domainHash, fromHex, hex, u32le, u64le, } from '../crypto/internal.js';
|
|
35
|
-
import { resolveSigner } from '../signer/signer.js';
|
|
36
|
-
const SIGN_DOMAIN = new TextEncoder().encode('calimero.auth.login.v1');
|
|
37
|
-
/** The borsh enum tag for each variant, in core's declaration order. */
|
|
38
|
-
const AUDIENCE_TAG = {
|
|
39
|
-
webOrigin: 0,
|
|
40
|
-
codeSigningId: 1,
|
|
41
|
-
cli: 2,
|
|
42
|
-
};
|
|
43
|
-
/** The variant's payload as UTF-8 — empty for the payload-free `cli`. */
|
|
44
|
-
function audienceBody(audience) {
|
|
45
|
-
const encoder = new TextEncoder();
|
|
46
|
-
switch (audience.kind) {
|
|
47
|
-
case 'webOrigin':
|
|
48
|
-
return encoder.encode(audience.origin);
|
|
49
|
-
case 'codeSigningId':
|
|
50
|
-
return encoder.encode(audience.id);
|
|
51
|
-
case 'cli':
|
|
52
|
-
return new Uint8Array(0);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* What the audience contributes to the **signing preimage**: tag then body, with
|
|
57
|
-
* no length between them.
|
|
58
|
-
*
|
|
59
|
-
* `domainHash` length-prefixes every part it is handed, so a length written here
|
|
60
|
-
* would be counted twice and the preimage would verify nowhere. This is the one
|
|
61
|
-
* field spelled differently in the two encodings — see {@link audienceWire}.
|
|
62
|
-
*/
|
|
63
|
-
function audienceSigningBytes(audience) {
|
|
64
|
-
return concat(Uint8Array.from([AUDIENCE_TAG[audience.kind]]), audienceBody(audience));
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* What the audience contributes to the **wire encoding**: the borsh enum tag,
|
|
68
|
-
* then for the two payload-carrying variants a borsh `String` — its own `u32`
|
|
69
|
-
* little-endian length, then the UTF-8.
|
|
70
|
-
*
|
|
71
|
-
* `cli` is a bare tag. Writing a `u32` zero after it instead produces four extra
|
|
72
|
-
* bytes that a node reads as the start of the challenge, so the statement fails
|
|
73
|
-
* as a bad signature rather than as a malformed one.
|
|
74
|
-
*/
|
|
75
|
-
function audienceWire(audience) {
|
|
76
|
-
const tag = Uint8Array.from([AUDIENCE_TAG[audience.kind]]);
|
|
77
|
-
if (audience.kind === 'cli') {
|
|
78
|
-
return tag;
|
|
79
|
-
}
|
|
80
|
-
const body = audienceBody(audience);
|
|
81
|
-
return concat(tag, u32le(body.length), body);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Sign a login statement and return it hex-encoded, ready to present with an
|
|
85
|
-
* account proof.
|
|
86
|
-
*
|
|
87
|
-
* Returns the encoding rather than an object for the reason `signWarrant` does:
|
|
88
|
-
* the signature covers exactly these bytes, and a caller that rebuilt the fields
|
|
89
|
-
* from JSON would have a second spelling able to disagree with what was signed.
|
|
90
|
-
*/
|
|
91
|
-
export async function signLoginStatement(input) {
|
|
92
|
-
const node = fromHex(input.node, 'node', 32);
|
|
93
|
-
const challenge = fromHex(input.challenge, 'challenge', 32);
|
|
94
|
-
const sessionKey = fromHex(input.sessionKey, 'sessionKey', 32);
|
|
95
|
-
const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
|
|
96
|
-
const deviceKey = fromHex(signer.publicKey, 'signer.publicKey', 32);
|
|
97
|
-
const issuedAt = u64le(input.issuedAt);
|
|
98
|
-
const expiresAt = u64le(input.expiresAt);
|
|
99
|
-
const preimage = await domainHash(SIGN_DOMAIN, [
|
|
100
|
-
node,
|
|
101
|
-
audienceSigningBytes(input.audience),
|
|
102
|
-
challenge,
|
|
103
|
-
sessionKey,
|
|
104
|
-
deviceKey,
|
|
105
|
-
issuedAt,
|
|
106
|
-
expiresAt,
|
|
107
|
-
]);
|
|
108
|
-
const signature = await signer.sign(preimage);
|
|
109
|
-
return hex(concat(node, audienceWire(input.audience), challenge, sessionKey, deviceKey, issuedAt, expiresAt, signature));
|
|
110
|
-
}
|
package/dist/login/session.d.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Obtain a session on a node using only a device key — the handshake around
|
|
3
|
-
* {@link signLoginStatement}.
|
|
4
|
-
*
|
|
5
|
-
* Signing a statement was never the whole job. A client also has to fetch a
|
|
6
|
-
* challenge, mint a session keypair, assemble a request body whose shape it
|
|
7
|
-
* cannot see from here, and read a token out of the reply. Until now every
|
|
8
|
-
* consumer wrote those two HTTP calls by hand, which is how the same three
|
|
9
|
-
* mistakes kept being available to make:
|
|
10
|
-
*
|
|
11
|
-
* 1. **Asking for permissions.** `POST /auth/token` takes an optional
|
|
12
|
-
* `permissions` list. Left unset, the node grants the account-proof
|
|
13
|
-
* provider's own scopes — `context:intent`, `context:query`,
|
|
14
|
-
* `context:subscribe` — which are the whole delegated surface and
|
|
15
|
-
* deliberately nothing above it. This module never sends the field.
|
|
16
|
-
* `authenticate()` in this same package hardcodes `['admin']` (#84); a
|
|
17
|
-
* device-key session would be refused for that and should never ask.
|
|
18
|
-
* 2. **Learning the node's key from the node.** {@link LoginConfig.node} is
|
|
19
|
-
* required and has no default, because a value read from the challenge
|
|
20
|
-
* response would let whoever answered choose what the device signs about.
|
|
21
|
-
* That is the whole attack the field exists to stop, so the awkwardness of
|
|
22
|
-
* having to supply it is the point rather than something to smooth away.
|
|
23
|
-
* 3. **Losing the refresh token.** A browser tab outlives a short-lived
|
|
24
|
-
* session, so both tokens are returned. Nothing here refreshes on a timer
|
|
25
|
-
* (#87, #88 track that); this only makes it possible without a second
|
|
26
|
-
* breaking change.
|
|
27
|
-
*
|
|
28
|
-
* **The session key never leaves.** It is minted here, the device key signs
|
|
29
|
-
* over it once, and the secret half is handed back to the caller. That is what
|
|
30
|
-
* keeps a device key off the wire for the life of a session: a leaked token
|
|
31
|
-
* cannot be escalated into use of the key that authorised it.
|
|
32
|
-
*/
|
|
33
|
-
import { type Audience } from './login.js';
|
|
34
|
-
import { type Signer } from '../signer/signer.js';
|
|
35
|
-
/** A freshly minted ephemeral keypair, hex-encoded. */
|
|
36
|
-
export interface SessionKeyPair {
|
|
37
|
-
/** The public half — what the statement is signed over. */
|
|
38
|
-
publicKey: string;
|
|
39
|
-
/** The private half. Held by the caller; never sent anywhere. */
|
|
40
|
-
secret: string;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Mint the ephemeral keypair a session speaks with.
|
|
44
|
-
*
|
|
45
|
-
* An ed25519 secret *is* 32 random bytes, so this needs no key generation
|
|
46
|
-
* ceremony — `getRandomValues` and the same derivation every other signer here
|
|
47
|
-
* uses. Exported because a caller that wants to persist a session across a page
|
|
48
|
-
* reload needs to mint the pair itself and keep the secret.
|
|
49
|
-
*/
|
|
50
|
-
export declare function generateSessionKey(): Promise<SessionKeyPair>;
|
|
51
|
-
/** What a device needs in order to ask for a session. */
|
|
52
|
-
export interface LoginConfig {
|
|
53
|
-
/** The node's base URL, e.g. `https://node.example`. */
|
|
54
|
-
nodeUrl: string;
|
|
55
|
-
/**
|
|
56
|
-
* The node's device signing key, hex (32 bytes), **as this client pinned it**.
|
|
57
|
-
*
|
|
58
|
-
* Learn it out of band — from the node's operator, or a certificate you
|
|
59
|
-
* already trust. Never from the node you are about to log in to: see the
|
|
60
|
-
* module note.
|
|
61
|
-
*/
|
|
62
|
-
node: string;
|
|
63
|
-
/**
|
|
64
|
-
* The device's ed25519 signing secret, hex (32 bytes). Never sent.
|
|
65
|
-
*
|
|
66
|
-
* Mutually exclusive with {@link LoginConfig.signer}, and the weaker of the
|
|
67
|
-
* two in a browser — see {@link Signer}.
|
|
68
|
-
*/
|
|
69
|
-
deviceSecret?: string;
|
|
70
|
-
/**
|
|
71
|
-
* The device's signer — use instead of {@link LoginConfig.deviceSecret} when
|
|
72
|
-
* the key cannot be exported to hex.
|
|
73
|
-
*/
|
|
74
|
-
signer?: Signer;
|
|
75
|
-
/**
|
|
76
|
-
* The device's account proof, hex — the credential certifying that this
|
|
77
|
-
* device belongs to its account.
|
|
78
|
-
*
|
|
79
|
-
* Travels beside the statement and is what settles *which account* is asking.
|
|
80
|
-
* The statement alone proves only that a device key asked, so logging in
|
|
81
|
-
* without this yields a 401 that looks exactly like a bad signature.
|
|
82
|
-
*/
|
|
83
|
-
accountProof: string;
|
|
84
|
-
/** The client surface this session is bound to. */
|
|
85
|
-
audience: Audience;
|
|
86
|
-
/**
|
|
87
|
-
* The token request's `client_name`. Defaults to {@link LoginConfig.nodeUrl}.
|
|
88
|
-
*
|
|
89
|
-
* mero-auth records this value as the session's `node_url` and, on every
|
|
90
|
-
* later request, refuses one whose Host is not that URL's host ("Token is not
|
|
91
|
-
* valid for this host"). So it must name the node being logged in to — which
|
|
92
|
-
* is what core's own clients send. The audience is not a substitute: a browser
|
|
93
|
-
* page's origin is a different host from the relay it talks to, and a token
|
|
94
|
-
* bound to the page's host is refused on the first call.
|
|
95
|
-
*/
|
|
96
|
-
clientName?: string;
|
|
97
|
-
/** Seconds the statement stays valid. Defaults to {@link DEFAULT_TTL_SECONDS}. */
|
|
98
|
-
ttlSeconds?: number;
|
|
99
|
-
/** Injectable for tests and non-browser runtimes. */
|
|
100
|
-
fetch?: typeof fetch;
|
|
101
|
-
/** Per-request timeout. Defaults to 10s. */
|
|
102
|
-
timeoutMs?: number;
|
|
103
|
-
}
|
|
104
|
-
/** A session minted without a password. */
|
|
105
|
-
export interface DelegatedSession {
|
|
106
|
-
/** Bearer token for the delegated surface. */
|
|
107
|
-
accessToken: string;
|
|
108
|
-
/** Presented to renew the access token. See #87/#88 — nothing here schedules it. */
|
|
109
|
-
refreshToken: string;
|
|
110
|
-
/** The session's public half, as the node knows it. */
|
|
111
|
-
sessionKey: string;
|
|
112
|
-
/** The session's private half. The caller keeps this; it was never sent. */
|
|
113
|
-
sessionSecret: string;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Log in with a device key and return the session.
|
|
117
|
-
*
|
|
118
|
-
* The challenge is fetched immediately before signing on purpose: it is
|
|
119
|
-
* single-use and short-lived, so a statement minted against a stale one is
|
|
120
|
-
* refused before any signature work is done.
|
|
121
|
-
*
|
|
122
|
-
* @throws {HTTPError} if the node refuses the challenge or the token request.
|
|
123
|
-
*/
|
|
124
|
-
export declare function login(config: LoginConfig): Promise<DelegatedSession>;
|