@calimero-network/mero-js 22.6.0 → 22.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account/account.d.ts +99 -4
- package/dist/account/account.js +0 -0
- package/dist/account/index.d.ts +6 -2
- package/dist/account/index.js +3 -1
- package/dist/admin-api/admin-client.d.ts +138 -2
- package/dist/admin-api/admin-client.js +237 -1
- package/dist/admin-api/admin-types.d.ts +159 -5
- package/dist/capabilities.d.ts +42 -0
- package/dist/capabilities.js +64 -0
- package/dist/cloud/cloud-client.d.ts +38 -8
- package/dist/cloud/cloud-client.js +38 -21
- package/dist/cloud/connect.d.ts +101 -2
- package/dist/cloud/connect.js +84 -8
- package/dist/cloud/enrol-redirect.d.ts +195 -0
- package/dist/cloud/enrol-redirect.js +236 -0
- package/dist/cloud/index.d.ts +6 -2
- package/dist/cloud/index.js +7 -1
- package/dist/cloud/link-redirect.d.ts +85 -0
- package/dist/cloud/link-redirect.js +105 -0
- package/dist/crypto/internal.d.ts +15 -0
- package/dist/crypto/internal.js +18 -0
- package/dist/device-cert/device-cert.d.ts +85 -2
- package/dist/device-cert/device-cert.js +100 -9
- package/dist/device-cert/index.d.ts +2 -2
- package/dist/device-cert/index.js +1 -1
- package/dist/events/sse.d.ts +22 -2
- package/dist/events/sse.js +20 -11
- package/dist/http-client/http-types.d.ts +18 -0
- package/dist/http-client/web-client.js +21 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +1999 -536
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +11 -6
- package/dist/index.js +17 -3
- package/dist/index.mjs +1999 -536
- package/dist/index.mjs.map +4 -4
- package/dist/login/login.d.ts +11 -1
- package/dist/login/login.js +5 -4
- package/dist/login/session.d.ts +21 -5
- package/dist/login/session.js +7 -15
- package/dist/namespace-op/namespace-op.d.ts +14 -2
- package/dist/namespace-op/namespace-op.js +9 -5
- package/dist/relay/index.d.ts +3 -3
- package/dist/relay/index.js +1 -1
- package/dist/relay/nonce-source.d.ts +135 -0
- package/dist/relay/nonce-source.js +187 -19
- package/dist/relay/relay-client.d.ts +148 -5
- package/dist/relay/relay-client.js +187 -7
- package/dist/relay-attestation/attest-node-key.d.ts +66 -0
- package/dist/relay-attestation/attest-node-key.js +110 -0
- package/dist/relay-attestation/index.d.ts +1 -0
- package/dist/relay-attestation/index.js +1 -0
- package/dist/request-proof/authorizer.d.ts +48 -0
- package/dist/request-proof/authorizer.js +65 -0
- package/dist/request-proof/index.d.ts +4 -0
- package/dist/request-proof/index.js +2 -0
- package/dist/request-proof/request-proof.d.ts +56 -0
- package/dist/request-proof/request-proof.js +111 -0
- package/dist/rpc/index.d.ts +21 -0
- package/dist/rpc/index.js +20 -0
- package/dist/transport/index.d.ts +6 -0
- package/dist/transport/index.js +3 -0
- package/dist/transport/mero-client.d.ts +286 -0
- package/dist/transport/mero-client.js +379 -0
- package/dist/transport/relay-observer.d.ts +152 -0
- package/dist/transport/relay-observer.js +173 -0
- package/dist/transport/relay-transport.d.ts +73 -0
- package/dist/transport/relay-transport.js +94 -0
- package/dist/transport/types.d.ts +100 -0
- package/dist/transport/types.js +32 -0
- package/dist/warrant/governance-op.d.ts +47 -1
- package/dist/warrant/governance-op.js +62 -2
- package/dist/warrant/index.d.ts +2 -2
- package/dist/warrant/index.js +1 -1
- package/package.json +1 -1
|
@@ -1026,6 +1026,21 @@ export interface AccountDeviceEntry {
|
|
|
1026
1026
|
signingKey: string;
|
|
1027
1027
|
/** Set only on the device this node itself presents. */
|
|
1028
1028
|
isSelf: boolean;
|
|
1029
|
+
/**
|
|
1030
|
+
* Whether this device has been withdrawn from the account.
|
|
1031
|
+
*
|
|
1032
|
+
* A revoked device stays in the listing rather than vanishing from it, so a
|
|
1033
|
+
* settings UI can render "revoked" as a state of a known device instead of
|
|
1034
|
+
* silently losing the row. Never filter on this by default: a device that
|
|
1035
|
+
* disappeared and a device that was withdrawn are different facts, and only
|
|
1036
|
+
* the second one is a fact the account holder took an action to create.
|
|
1037
|
+
*
|
|
1038
|
+
* Revocation is **forward-only**. It withdraws the device's authority to
|
|
1039
|
+
* write from here on; it does not retract, unsign or rewrite anything the
|
|
1040
|
+
* device already authored, and ops it signed before the revocation stay valid
|
|
1041
|
+
* and stay in the DAG. `revoked: true` therefore means "may not act any more",
|
|
1042
|
+
* never "never acted" - do not build UI that implies the past was undone.
|
|
1043
|
+
*/
|
|
1029
1044
|
revoked: boolean;
|
|
1030
1045
|
/**
|
|
1031
1046
|
* Applications this device may speak for, 64 hex each. **Empty means every
|
|
@@ -1082,7 +1097,15 @@ export interface AccountApplicationEntry {
|
|
|
1082
1097
|
/** Ids of the namespaces targeting it, 64 hex characters each. */
|
|
1083
1098
|
namespaces: string[];
|
|
1084
1099
|
}
|
|
1085
|
-
/**
|
|
1100
|
+
/**
|
|
1101
|
+
* Withdraw a device from an account, terminally.
|
|
1102
|
+
*
|
|
1103
|
+
* Terminal in one direction only: revocation is **forward-only**. It stops the
|
|
1104
|
+
* device acting from here on and does not retract past authorship - every op it
|
|
1105
|
+
* already signed keeps verifying and stays in the DAG, because the certificate
|
|
1106
|
+
* that authorised it was valid when the signature was made. There is no
|
|
1107
|
+
* "un-author" in this system, so no revocation UI should promise one.
|
|
1108
|
+
*/
|
|
1086
1109
|
export interface RevokeAccountDeviceRequest {
|
|
1087
1110
|
/** The device to withdraw, 64 hex. */
|
|
1088
1111
|
deviceId: string;
|
|
@@ -1139,6 +1162,36 @@ export interface RevokeAccountDeviceResponseData {
|
|
|
1139
1162
|
*/
|
|
1140
1163
|
revokedIn: RevocationOutcome[];
|
|
1141
1164
|
}
|
|
1165
|
+
/** One device of one member account, as the group's live bindings record it. */
|
|
1166
|
+
export interface MemberDeviceEntry {
|
|
1167
|
+
/** The device id, 64 hex - the form {@link RevokeAccountDeviceRequest.deviceId} takes. */
|
|
1168
|
+
deviceId: string;
|
|
1169
|
+
/**
|
|
1170
|
+
* The key this device's signatures carry, 64 hex. This is the join column
|
|
1171
|
+
* against the identities a context reports, which name bare signing keys and
|
|
1172
|
+
* nothing else.
|
|
1173
|
+
*/
|
|
1174
|
+
signingKey: string;
|
|
1175
|
+
}
|
|
1176
|
+
/** One member account of a group, with the devices currently bound for it. */
|
|
1177
|
+
export interface MemberDevicesEntry {
|
|
1178
|
+
/** The member's account id, 64 hex - the same id a group member listing names. */
|
|
1179
|
+
account: string;
|
|
1180
|
+
/**
|
|
1181
|
+
* Devices holding a live binding for this account. Only live ones: a revoked
|
|
1182
|
+
* device is gone from here, unlike {@link AccountDeviceEntry} which keeps it
|
|
1183
|
+
* with `revoked: true`. So this answers "who may act now", never "who ever
|
|
1184
|
+
* did" - a past author may well be absent.
|
|
1185
|
+
*/
|
|
1186
|
+
devices: MemberDeviceEntry[];
|
|
1187
|
+
}
|
|
1188
|
+
/** Paging for a member-device listing. Both are optional; the node defaults them. */
|
|
1189
|
+
export interface ListMemberDevicesOptions {
|
|
1190
|
+
/** How many entries to skip. */
|
|
1191
|
+
offset?: number;
|
|
1192
|
+
/** How many entries to return. The node clamps this to its own maximum. */
|
|
1193
|
+
limit?: number;
|
|
1194
|
+
}
|
|
1142
1195
|
export interface CreateGroupRequest {
|
|
1143
1196
|
applicationId: string;
|
|
1144
1197
|
appKey?: string;
|
|
@@ -1305,10 +1358,6 @@ export interface MemberDevicesEntry {
|
|
|
1305
1358
|
account: string;
|
|
1306
1359
|
devices: MemberDevice[];
|
|
1307
1360
|
}
|
|
1308
|
-
export interface ListMemberDevicesOptions {
|
|
1309
|
-
offset?: number;
|
|
1310
|
-
limit?: number;
|
|
1311
|
-
}
|
|
1312
1361
|
export interface ListMemberDevicesResponseData {
|
|
1313
1362
|
/**
|
|
1314
1363
|
* Every account in the group if the calling node is an admin; only its own
|
|
@@ -1730,3 +1779,108 @@ export interface AdmitJoinResponseData {
|
|
|
1730
1779
|
*/
|
|
1731
1780
|
readonly published: boolean;
|
|
1732
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Where one author device stands in its warrant-nonce sequence on one node.
|
|
1784
|
+
*
|
|
1785
|
+
* # Why `bigint` and not `number`
|
|
1786
|
+
*
|
|
1787
|
+
* A nonce is a `u64` on the node. `JSON.parse` turns every number into an IEEE
|
|
1788
|
+
* double, so any value past 2^53 arrives already rounded - and it rounds
|
|
1789
|
+
* silently, producing a nonce that looks ordinary and is refused forever. The
|
|
1790
|
+
* exhaustion rule this endpoint exists to report (`u64::MAX` spent -> no next
|
|
1791
|
+
* nonce) lives precisely in the range a `number` cannot hold, so representing it
|
|
1792
|
+
* as one would lose the very case the type is meant to make visible.
|
|
1793
|
+
*
|
|
1794
|
+
* `bigint` over a decimal `string` because callers do arithmetic with these:
|
|
1795
|
+
* the documented recovery rule is `max(ownNext, nextNonce)`, and
|
|
1796
|
+
* {@link NonceSource} already emits `bigint`. A string would make every caller
|
|
1797
|
+
* convert first, and the ones that forget would compare `"10" < "9"`.
|
|
1798
|
+
*
|
|
1799
|
+
* The digits are recovered from the raw response text rather than from a parsed
|
|
1800
|
+
* `number`, so nothing is routed through a double on the way in.
|
|
1801
|
+
*
|
|
1802
|
+
* # Why a union on `kind`
|
|
1803
|
+
*
|
|
1804
|
+
* `nextNonce` is absent in exactly one case, and a client that reads it as
|
|
1805
|
+
* `undefined` and falls back to a guess burns nonces for real. Splitting the two
|
|
1806
|
+
* cases makes the compiler ask which one this is before `nextNonce` can be read
|
|
1807
|
+
* at all.
|
|
1808
|
+
*
|
|
1809
|
+
* The discriminant is a string rather than an `exhausted: boolean` because this
|
|
1810
|
+
* package compiles with `strictNullChecks` off, and TypeScript declines to
|
|
1811
|
+
* narrow a union on a boolean-literal discriminant in that mode. A flag that
|
|
1812
|
+
* silently fails to narrow would be worse than no flag: it would read as a
|
|
1813
|
+
* guard and admit `nextNonce` on both arms.
|
|
1814
|
+
*/
|
|
1815
|
+
export type WarrantNonceState = WarrantNonceOpen | WarrantNonceExhausted;
|
|
1816
|
+
/** Fields both arms carry, whatever the sequence's state. */
|
|
1817
|
+
interface WarrantNonceCommon {
|
|
1818
|
+
/** The context asked about, echoed - read it to reject a cached answer. */
|
|
1819
|
+
readonly contextId: string;
|
|
1820
|
+
/**
|
|
1821
|
+
* The author device asked about, echoed. This is the device's SIGNING KEY,
|
|
1822
|
+
* not its `deviceId` and not the account: two devices of one account hold two
|
|
1823
|
+
* independent sequences, and a re-keyed device starts a fresh one.
|
|
1824
|
+
*
|
|
1825
|
+
* Echoed as the node spelled it, which is the spelling that was asked for -
|
|
1826
|
+
* the route accepts base58 or hex and does not normalise between them.
|
|
1827
|
+
*/
|
|
1828
|
+
readonly authorDeviceKey: string;
|
|
1829
|
+
/**
|
|
1830
|
+
* Whether this node has admitted any warrant from this device here.
|
|
1831
|
+
*
|
|
1832
|
+
* `false` is the ordinary state of a device that has not written yet - a fresh
|
|
1833
|
+
* sequence, not an error and not a row worth retrying for.
|
|
1834
|
+
*/
|
|
1835
|
+
readonly seen: boolean;
|
|
1836
|
+
/**
|
|
1837
|
+
* The highest nonce this node has accepted from this device here, absent when
|
|
1838
|
+
* `seen` is `false`.
|
|
1839
|
+
*
|
|
1840
|
+
* Reported for auditability. Do NOT compute a nonce from it: the `+ 1` is the
|
|
1841
|
+
* node's arithmetic, already done in {@link WarrantNonceOpen.nextNonce}, and
|
|
1842
|
+
* doing it again here is how a client reintroduces the overflow this endpoint
|
|
1843
|
+
* avoids.
|
|
1844
|
+
*/
|
|
1845
|
+
readonly highWaterNonce?: bigint;
|
|
1846
|
+
/**
|
|
1847
|
+
* How far below `highWaterNonce` a late warrant may still be accepted.
|
|
1848
|
+
*
|
|
1849
|
+
* Constant (64 today), and useful for sizing an in-flight window: a warrant
|
|
1850
|
+
* minted but not yet executed is refused once this many later ones have landed
|
|
1851
|
+
* ahead of it. It is NOT an input to a nonce - which parts of the window below
|
|
1852
|
+
* the mark are already spent is deliberately not reported.
|
|
1853
|
+
*/
|
|
1854
|
+
readonly windowWidth: bigint;
|
|
1855
|
+
}
|
|
1856
|
+
/** The sequence has room: {@link nextNonce} is the number to mint at. */
|
|
1857
|
+
export interface WarrantNonceOpen extends WarrantNonceCommon {
|
|
1858
|
+
readonly kind: 'open';
|
|
1859
|
+
/**
|
|
1860
|
+
* The smallest nonce this node is guaranteed to accept. **The only actionable
|
|
1861
|
+
* value here.**
|
|
1862
|
+
*
|
|
1863
|
+
* `0` on a fresh sequence (`seen: false`), which is a real answer, not a
|
|
1864
|
+
* missing one.
|
|
1865
|
+
*
|
|
1866
|
+
* A client that still holds its own counter must mint at
|
|
1867
|
+
* `max(ownNext, nextNonce)`, never at `nextNonce` alone: nonce state folds per
|
|
1868
|
+
* peer, so this node can be BEHIND a client whose warrants it has not applied
|
|
1869
|
+
* yet, and never ahead of the truth. An author spread across relays asks each
|
|
1870
|
+
* and takes the highest answer.
|
|
1871
|
+
*/
|
|
1872
|
+
readonly nextNonce: bigint;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* `u64::MAX` is spent here: there is no next nonce and this device cannot write
|
|
1876
|
+
* to this context again. It must re-key - a new signing key starts a fresh
|
|
1877
|
+
* sequence.
|
|
1878
|
+
*
|
|
1879
|
+
* The node reports this by omitting `nextNonce` rather than by wrapping to `0`,
|
|
1880
|
+
* because a wrapped `0` would look like a valid answer and be refused forever.
|
|
1881
|
+
*/
|
|
1882
|
+
export interface WarrantNonceExhausted extends WarrantNonceCommon {
|
|
1883
|
+
readonly kind: 'exhausted';
|
|
1884
|
+
readonly seen: true;
|
|
1885
|
+
readonly highWaterNonce: bigint;
|
|
1886
|
+
}
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -30,6 +30,48 @@ export declare const CAPABILITIES: {
|
|
|
30
30
|
};
|
|
31
31
|
export type CapabilityName = keyof typeof CAPABILITIES;
|
|
32
32
|
export type CapabilityBit = (typeof CAPABILITIES)[CapabilityName];
|
|
33
|
+
/**
|
|
34
|
+
* Ready-made masks for `setDefaultCapabilities` / `setMemberCapabilities` and
|
|
35
|
+
* `RelayClient.foundNamespace({ defaultCapabilities })`, so an app can name the
|
|
36
|
+
* default it gives members instead of passing a bare number.
|
|
37
|
+
*
|
|
38
|
+
* Why this matters: a namespace's default mask is copied into a non-admin
|
|
39
|
+
* member's row when they join, and a namespace starts without
|
|
40
|
+
* `CAN_CREATE_CONTEXT` in it. An app that never sets a default leaves every
|
|
41
|
+
* invited member unable to create a context until an admin grants it by hand.
|
|
42
|
+
*
|
|
43
|
+
* None of these include `CAN_AUTHOR_ON_BEHALF`; grant that deliberately (see
|
|
44
|
+
* `AdminApiClient.grantAuthorship` / `openToDelegatedExecution`).
|
|
45
|
+
*/
|
|
46
|
+
export declare const CAPABILITY_PRESETS: {
|
|
47
|
+
/** No capabilities. */
|
|
48
|
+
readonly NONE: 0;
|
|
49
|
+
/** Join Open subgroups only: what a namespace starts with. */
|
|
50
|
+
readonly JOIN_ONLY: number;
|
|
51
|
+
/**
|
|
52
|
+
* Create contexts, invite members, join Open subgroups (= 7). A member who
|
|
53
|
+
* can work in the namespace but not reshape it.
|
|
54
|
+
*/
|
|
55
|
+
readonly CONTRIBUTOR: number;
|
|
56
|
+
/**
|
|
57
|
+
* `CONTRIBUTOR` plus create and delete subgroups and manage their visibility
|
|
58
|
+
* (= 231). What mero-chat gives members, so they can start and close their
|
|
59
|
+
* own channels.
|
|
60
|
+
*/
|
|
61
|
+
readonly COLLABORATOR: number;
|
|
62
|
+
};
|
|
63
|
+
export type CapabilityPreset = keyof typeof CAPABILITY_PRESETS;
|
|
64
|
+
/**
|
|
65
|
+
* Builds a u32 mask from capability names and/or bit values:
|
|
66
|
+
* `capMask('CAN_CREATE_CONTEXT', 'CAN_INVITE_MEMBERS') === 3`.
|
|
67
|
+
* Throws on a name core does not assign, so a typo can't silently yield 0.
|
|
68
|
+
*/
|
|
69
|
+
export declare function capMask(...caps: Array<CapabilityName | number>): number;
|
|
70
|
+
/**
|
|
71
|
+
* The names of the assigned capability bits set in `mask`, in bit order.
|
|
72
|
+
* Unassigned bits (10 and above) are not reported.
|
|
73
|
+
*/
|
|
74
|
+
export declare function capNames(mask: number): CapabilityName[];
|
|
33
75
|
/**
|
|
34
76
|
* Returns true if `mask` has every bit of `cap` set. Both operands are
|
|
35
77
|
* coerced to unsigned 32-bit (`>>> 0`) before comparing so a high bit such
|
package/dist/capabilities.js
CHANGED
|
@@ -34,6 +34,70 @@ export const CAPABILITIES = {
|
|
|
34
34
|
*/
|
|
35
35
|
CAN_AUTHOR_ON_BEHALF: 1 << 9,
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Ready-made masks for `setDefaultCapabilities` / `setMemberCapabilities` and
|
|
39
|
+
* `RelayClient.foundNamespace({ defaultCapabilities })`, so an app can name the
|
|
40
|
+
* default it gives members instead of passing a bare number.
|
|
41
|
+
*
|
|
42
|
+
* Why this matters: a namespace's default mask is copied into a non-admin
|
|
43
|
+
* member's row when they join, and a namespace starts without
|
|
44
|
+
* `CAN_CREATE_CONTEXT` in it. An app that never sets a default leaves every
|
|
45
|
+
* invited member unable to create a context until an admin grants it by hand.
|
|
46
|
+
*
|
|
47
|
+
* None of these include `CAN_AUTHOR_ON_BEHALF`; grant that deliberately (see
|
|
48
|
+
* `AdminApiClient.grantAuthorship` / `openToDelegatedExecution`).
|
|
49
|
+
*/
|
|
50
|
+
export const CAPABILITY_PRESETS = {
|
|
51
|
+
/** No capabilities. */
|
|
52
|
+
NONE: 0,
|
|
53
|
+
/** Join Open subgroups only: what a namespace starts with. */
|
|
54
|
+
JOIN_ONLY: CAPABILITIES.CAN_JOIN_OPEN_SUBGROUPS,
|
|
55
|
+
/**
|
|
56
|
+
* Create contexts, invite members, join Open subgroups (= 7). A member who
|
|
57
|
+
* can work in the namespace but not reshape it.
|
|
58
|
+
*/
|
|
59
|
+
CONTRIBUTOR: CAPABILITIES.CAN_CREATE_CONTEXT |
|
|
60
|
+
CAPABILITIES.CAN_INVITE_MEMBERS |
|
|
61
|
+
CAPABILITIES.CAN_JOIN_OPEN_SUBGROUPS,
|
|
62
|
+
/**
|
|
63
|
+
* `CONTRIBUTOR` plus create and delete subgroups and manage their visibility
|
|
64
|
+
* (= 231). What mero-chat gives members, so they can start and close their
|
|
65
|
+
* own channels.
|
|
66
|
+
*/
|
|
67
|
+
COLLABORATOR: CAPABILITIES.CAN_CREATE_CONTEXT |
|
|
68
|
+
CAPABILITIES.CAN_INVITE_MEMBERS |
|
|
69
|
+
CAPABILITIES.CAN_JOIN_OPEN_SUBGROUPS |
|
|
70
|
+
CAPABILITIES.CAN_CREATE_SUBGROUP |
|
|
71
|
+
CAPABILITIES.CAN_DELETE_SUBGROUP |
|
|
72
|
+
CAPABILITIES.CAN_MANAGE_VISIBILITY,
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Builds a u32 mask from capability names and/or bit values:
|
|
76
|
+
* `capMask('CAN_CREATE_CONTEXT', 'CAN_INVITE_MEMBERS') === 3`.
|
|
77
|
+
* Throws on a name core does not assign, so a typo can't silently yield 0.
|
|
78
|
+
*/
|
|
79
|
+
export function capMask(...caps) {
|
|
80
|
+
let mask = 0;
|
|
81
|
+
for (const cap of caps) {
|
|
82
|
+
if (typeof cap === 'number') {
|
|
83
|
+
mask |= cap;
|
|
84
|
+
}
|
|
85
|
+
else if (Object.prototype.hasOwnProperty.call(CAPABILITIES, cap)) {
|
|
86
|
+
mask |= CAPABILITIES[cap];
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new Error(`unknown capability: ${String(cap)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return mask >>> 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The names of the assigned capability bits set in `mask`, in bit order.
|
|
96
|
+
* Unassigned bits (10 and above) are not reported.
|
|
97
|
+
*/
|
|
98
|
+
export function capNames(mask) {
|
|
99
|
+
return Object.keys(CAPABILITIES).filter((name) => hasCap(mask, CAPABILITIES[name]));
|
|
100
|
+
}
|
|
37
101
|
/**
|
|
38
102
|
* Returns true if `mask` has every bit of `cap` set. Both operands are
|
|
39
103
|
* coerced to unsigned 32-bit (`>>> 0`) before comparing so a high bit such
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* ever sent, and would be a throughput bottleneck on a path that has no reason
|
|
25
25
|
* to involve it.
|
|
26
26
|
*/
|
|
27
|
+
import { type RootSource } from '../account/index.js';
|
|
28
|
+
import type { Signer } from '../signer/signer.js';
|
|
27
29
|
import { type DiscoveryChallenge, type RoutingChallenge, type RoutingCredential } from './routing-proof.js';
|
|
28
30
|
/**
|
|
29
31
|
* The first relay a warrant can be spent at, or `null`.
|
|
@@ -111,6 +113,26 @@ export interface CloudRelay {
|
|
|
111
113
|
relayUrl: string | null;
|
|
112
114
|
/** The account a warrant for this relay must name as its `executor`, hex. */
|
|
113
115
|
executorAccount: string | null;
|
|
116
|
+
/**
|
|
117
|
+
* The relay node's ed25519 **device signing key**, hex — or `null`, which is
|
|
118
|
+
* what it is today.
|
|
119
|
+
*
|
|
120
|
+
* A relay is an ordinary node, so it can be logged in to and subscribed to;
|
|
121
|
+
* the login statement has to be signed against this key, supplied separately
|
|
122
|
+
* rather than read from the node being authenticated. The cloud does not
|
|
123
|
+
* publish it yet (mdma #312), so this is `null` on every row and the hosted
|
|
124
|
+
* path reports `canSubscribe === false`.
|
|
125
|
+
*
|
|
126
|
+
* It is parsed rather than omitted so the hosted path needs no API change
|
|
127
|
+
* when the field lands: `connectCloud` already passes it through, and the
|
|
128
|
+
* same call site starts observing. `peerId` is **not** a fallback — it is a
|
|
129
|
+
* libp2p identity, a different key, and signing a statement about it would
|
|
130
|
+
* authenticate nothing.
|
|
131
|
+
*
|
|
132
|
+
* Both plausible spellings are accepted because mdma #312 has not fixed one;
|
|
133
|
+
* if it lands under a third name, this mapping is the single line to change.
|
|
134
|
+
*/
|
|
135
|
+
nodeKey: string | null;
|
|
114
136
|
/** `'assigned'` (admitted pending confirm) or `'active'`. */
|
|
115
137
|
status: string;
|
|
116
138
|
/** Whether this relay holds `CAN_AUTHOR_ON_BEHALF` on the namespace. */
|
|
@@ -378,8 +400,10 @@ export declare class CloudClient {
|
|
|
378
400
|
* account behind those reads was claimed by its owner. The claim is recorded
|
|
379
401
|
* even when the session is refused.
|
|
380
402
|
*
|
|
381
|
-
* @param
|
|
382
|
-
*
|
|
403
|
+
* @param root the account root: its 64-hex secret, or a `Signer` over a root
|
|
404
|
+
* this process may use and cannot read — the form a phrase-held root in a
|
|
405
|
+
* browser takes. Either way nothing but a signature over the cloud's nonce
|
|
406
|
+
* travels.
|
|
383
407
|
*
|
|
384
408
|
* A `403` means either the signature did not verify, or the account is not
|
|
385
409
|
* linked to a cloud login: ownership alone says who you are, and the link
|
|
@@ -387,7 +411,7 @@ export declare class CloudClient {
|
|
|
387
411
|
* on the proof alone would authenticate perfectly and authorize nothing. The
|
|
388
412
|
* ownership claim is recorded either way, so linking later needs no re-proof.
|
|
389
413
|
*/
|
|
390
|
-
signInWithAccount(
|
|
414
|
+
signInWithAccount(root: RootSource): Promise<CloudSession>;
|
|
391
415
|
/**
|
|
392
416
|
* Mint a challenge for an account root to sign.
|
|
393
417
|
*
|
|
@@ -612,15 +636,16 @@ export declare class CloudClient {
|
|
|
612
636
|
* an intent; the cloud needs it to know whose plan a hosted node is spending,
|
|
613
637
|
* and to sign that account back in later.
|
|
614
638
|
*
|
|
615
|
-
* @param
|
|
616
|
-
* {@link generateAccountRoot},
|
|
617
|
-
*
|
|
639
|
+
* @param root the account root: its 64-hex secret — from
|
|
640
|
+
* {@link generateAccountRoot}, a restored recovery phrase, or a desktop app
|
|
641
|
+
* or hardware key that already holds one — or a `Signer` over a root this
|
|
642
|
+
* process cannot read, which is what a phrase-held browser root is.
|
|
618
643
|
*
|
|
619
644
|
* Idempotent for the same login. An account already linked to a *different*
|
|
620
645
|
* cloud login is a `409`; a replayed or expired challenge is a `403`; and
|
|
621
646
|
* exceeding the plan's account limit is a `402`.
|
|
622
647
|
*/
|
|
623
|
-
linkAccount(
|
|
648
|
+
linkAccount(root: RootSource): Promise<CloudAccountLink>;
|
|
624
649
|
/**
|
|
625
650
|
* Mint a single-use challenge for an account root to sign.
|
|
626
651
|
*
|
|
@@ -701,7 +726,12 @@ export declare class CloudClient {
|
|
|
701
726
|
linkAccountWithGrant(options: {
|
|
702
727
|
grant: string;
|
|
703
728
|
/** The root of the account the grant names, 64 hex. */
|
|
704
|
-
rootSecret
|
|
729
|
+
rootSecret?: string;
|
|
730
|
+
/**
|
|
731
|
+
* The root's signer — use instead of `rootSecret` when the root is a key
|
|
732
|
+
* this process may use and cannot read.
|
|
733
|
+
*/
|
|
734
|
+
signer?: Signer;
|
|
705
735
|
}): Promise<CloudAccountLink>;
|
|
706
736
|
/** The accounts linked to this login, and how many the plan allows. */
|
|
707
737
|
getMyAccounts(): Promise<CloudLinkedAccounts>;
|
|
@@ -24,11 +24,22 @@
|
|
|
24
24
|
* ever sent, and would be a throughput bottleneck on a path that has no reason
|
|
25
25
|
* to involve it.
|
|
26
26
|
*/
|
|
27
|
-
import {
|
|
27
|
+
import { resolveRoot, resolveRootPair, signAccountLink, signAccountLogin, } from '../account/index.js';
|
|
28
28
|
import { HTTPError } from '../http-client/web-client.js';
|
|
29
29
|
import { routingProofHeaders, } from './routing-proof.js';
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The hosted cloud's **API** when a caller names no other.
|
|
32
|
+
*
|
|
33
|
+
* `manager.cloud.calimero.network`, not `cloud.calimero.network`: the latter
|
|
34
|
+
* serves the React app, which answers an API call with 405 and an HTML body.
|
|
35
|
+
* Every method on this client is an API call, so the bare `new CloudClient()`
|
|
36
|
+
* used to fail at the first request with a parse error rather than at
|
|
37
|
+
* construction — and the URL it was talking to looked right in the message.
|
|
38
|
+
*
|
|
39
|
+
* The web origin still exists in this package: the consent screen in
|
|
40
|
+
* `link-redirect.ts` is a page a person visits, and that one is the app.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_CLOUD_BASE_URL = 'https://manager.cloud.calimero.network';
|
|
32
43
|
/**
|
|
33
44
|
* The cloud's own `can_execute` where it sends one, else `legacy`.
|
|
34
45
|
*
|
|
@@ -117,8 +128,10 @@ export class CloudClient {
|
|
|
117
128
|
* account behind those reads was claimed by its owner. The claim is recorded
|
|
118
129
|
* even when the session is refused.
|
|
119
130
|
*
|
|
120
|
-
* @param
|
|
121
|
-
*
|
|
131
|
+
* @param root the account root: its 64-hex secret, or a `Signer` over a root
|
|
132
|
+
* this process may use and cannot read — the form a phrase-held root in a
|
|
133
|
+
* browser takes. Either way nothing but a signature over the cloud's nonce
|
|
134
|
+
* travels.
|
|
122
135
|
*
|
|
123
136
|
* A `403` means either the signature did not verify, or the account is not
|
|
124
137
|
* linked to a cloud login: ownership alone says who you are, and the link
|
|
@@ -126,13 +139,13 @@ export class CloudClient {
|
|
|
126
139
|
* on the proof alone would authenticate perfectly and authorize nothing. The
|
|
127
140
|
* ownership claim is recorded either way, so linking later needs no re-proof.
|
|
128
141
|
*/
|
|
129
|
-
async signInWithAccount(
|
|
130
|
-
const
|
|
142
|
+
async signInWithAccount(root) {
|
|
143
|
+
const { signer, publicKey } = await resolveRoot(root);
|
|
131
144
|
const { nonce } = await this.getAccountLoginChallenge();
|
|
132
145
|
return this.submitAccountLogin({
|
|
133
|
-
rootPublicKey:
|
|
146
|
+
rootPublicKey: publicKey,
|
|
134
147
|
nonce,
|
|
135
|
-
signature: await signAccountLogin({
|
|
148
|
+
signature: await signAccountLogin({ signer, nonce }),
|
|
136
149
|
});
|
|
137
150
|
}
|
|
138
151
|
/**
|
|
@@ -221,6 +234,9 @@ export class CloudClient {
|
|
|
221
234
|
peerId: String(row.peer_id ?? ''),
|
|
222
235
|
relayUrl,
|
|
223
236
|
executorAccount,
|
|
237
|
+
nodeKey: row.node_key ??
|
|
238
|
+
row.node_public_key ??
|
|
239
|
+
null,
|
|
224
240
|
status: String(row.status ?? ''),
|
|
225
241
|
authorshipReady,
|
|
226
242
|
teeRole: teeRoleOf(row),
|
|
@@ -541,22 +557,23 @@ export class CloudClient {
|
|
|
541
557
|
* an intent; the cloud needs it to know whose plan a hosted node is spending,
|
|
542
558
|
* and to sign that account back in later.
|
|
543
559
|
*
|
|
544
|
-
* @param
|
|
545
|
-
* {@link generateAccountRoot},
|
|
546
|
-
*
|
|
560
|
+
* @param root the account root: its 64-hex secret — from
|
|
561
|
+
* {@link generateAccountRoot}, a restored recovery phrase, or a desktop app
|
|
562
|
+
* or hardware key that already holds one — or a `Signer` over a root this
|
|
563
|
+
* process cannot read, which is what a phrase-held browser root is.
|
|
547
564
|
*
|
|
548
565
|
* Idempotent for the same login. An account already linked to a *different*
|
|
549
566
|
* cloud login is a `409`; a replayed or expired challenge is a `403`; and
|
|
550
567
|
* exceeding the plan's account limit is a `402`.
|
|
551
568
|
*/
|
|
552
|
-
async linkAccount(
|
|
553
|
-
const
|
|
569
|
+
async linkAccount(root) {
|
|
570
|
+
const { signer, publicKey, accountId } = await resolveRoot(root);
|
|
554
571
|
const { nonce } = await this.getAccountLinkChallenge();
|
|
555
572
|
return this.submitAccountLink({
|
|
556
|
-
accountId
|
|
557
|
-
rootPublicKey:
|
|
573
|
+
accountId,
|
|
574
|
+
rootPublicKey: publicKey,
|
|
558
575
|
nonce,
|
|
559
|
-
signature: await signAccountLink({
|
|
576
|
+
signature: await signAccountLink({ signer, nonce }),
|
|
560
577
|
});
|
|
561
578
|
}
|
|
562
579
|
/**
|
|
@@ -677,20 +694,20 @@ export class CloudClient {
|
|
|
677
694
|
* login; `402` that the plan is full.
|
|
678
695
|
*/
|
|
679
696
|
async linkAccountWithGrant(options) {
|
|
680
|
-
const
|
|
697
|
+
const { signer, publicKey, accountId } = await resolveRootPair(options.rootSecret, options.signer);
|
|
681
698
|
const body = await this.request('POST', '/api/cloud/accounts/link', {
|
|
682
699
|
body: {
|
|
683
700
|
grant: options.grant,
|
|
684
|
-
root_public_key:
|
|
701
|
+
root_public_key: publicKey,
|
|
685
702
|
signature: await signAccountLink({
|
|
686
|
-
|
|
703
|
+
signer,
|
|
687
704
|
nonce: options.grant,
|
|
688
705
|
}),
|
|
689
706
|
},
|
|
690
707
|
anonymous: true,
|
|
691
708
|
});
|
|
692
709
|
return {
|
|
693
|
-
accountId: String(body.account_id ??
|
|
710
|
+
accountId: String(body.account_id ?? accountId),
|
|
694
711
|
linkedAt: body.linked_at ?? null,
|
|
695
712
|
alreadyLinked: body.already_linked === true,
|
|
696
713
|
};
|
package/dist/cloud/connect.d.ts
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
* function is that missing answer, which is why the login step it replaces is
|
|
20
20
|
* the *node URL prompt* rather than the authentication.
|
|
21
21
|
*
|
|
22
|
+
* `connectCloudWithAccount(...)` is the same connection reached by the other
|
|
23
|
+
* door: the caller holds an account root and signs in as the account itself,
|
|
24
|
+
* with no Google login anywhere in it. Identical from there on, deliberately —
|
|
25
|
+
* which login proved who you are changes nothing about how a relay is found or
|
|
26
|
+
* how a write is authorised.
|
|
27
|
+
*
|
|
22
28
|
* # What it does not do
|
|
23
29
|
*
|
|
24
30
|
* # Sealing the relay
|
|
@@ -38,6 +44,10 @@ import { CloudClient } from './cloud-client.js';
|
|
|
38
44
|
import type { CloudClientConfig, CloudNamespace, CloudRelay } from './cloud-client.js';
|
|
39
45
|
import { RelayClient } from '../relay/relay-client.js';
|
|
40
46
|
import type { IntentResult, NonceSource } from '../relay/index.js';
|
|
47
|
+
import { type Signer } from '../signer/signer.js';
|
|
48
|
+
import { type RootSource } from '../account/account.js';
|
|
49
|
+
import { MeroClient } from '../transport/mero-client.js';
|
|
50
|
+
import type { Audience } from '../login/index.js';
|
|
41
51
|
import type { VerifyTransportQuote } from '../sealed/sealed.js';
|
|
42
52
|
export interface ConnectCloudOptions {
|
|
43
53
|
/** Defaults to the hosted cloud. */
|
|
@@ -52,8 +62,24 @@ export interface ConnectCloudOptions {
|
|
|
52
62
|
authorAccount: string;
|
|
53
63
|
/** The author's `AccountProof<DeviceCert>`, hex-encoded borsh. */
|
|
54
64
|
authorProof: string;
|
|
55
|
-
/**
|
|
56
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The author device's ed25519 signing secret, hex. Never transmitted.
|
|
67
|
+
*
|
|
68
|
+
* Mutually exclusive with {@link ConnectCloudOptions.signer}, and the weaker
|
|
69
|
+
* of the two in a browser — a secret that exists as a string can be read by
|
|
70
|
+
* anything on the origin.
|
|
71
|
+
*/
|
|
72
|
+
deviceSecret?: string;
|
|
73
|
+
/**
|
|
74
|
+
* The author device's signer — use instead of
|
|
75
|
+
* {@link ConnectCloudOptions.deviceSecret}.
|
|
76
|
+
*
|
|
77
|
+
* A browser wallet holds its device key as a non-extractable `CryptoKey`,
|
|
78
|
+
* which has no hex form to pass. Without this, that wallet — the case this
|
|
79
|
+
* whole path was built for — had to hand-assemble the `RelayClient` the
|
|
80
|
+
* one-call path exists to build.
|
|
81
|
+
*/
|
|
82
|
+
signer?: Signer;
|
|
57
83
|
/**
|
|
58
84
|
* Which namespace to write in.
|
|
59
85
|
*
|
|
@@ -86,6 +112,25 @@ export interface ConnectCloudOptions {
|
|
|
86
112
|
seal?: VerifyTransportQuote;
|
|
87
113
|
fetch?: typeof fetch;
|
|
88
114
|
timeoutMs?: number;
|
|
115
|
+
/**
|
|
116
|
+
* The chosen relay node's device signing key, hex — if you know it.
|
|
117
|
+
*
|
|
118
|
+
* Supplying it makes `connection.client` able to *observe* as well as write:
|
|
119
|
+
* a relay is an ordinary node, and a keyholder with an account proof can log
|
|
120
|
+
* in to it and subscribe. The key has to come from somewhere the caller
|
|
121
|
+
* already trusts, which today means out of band, because the cloud does not
|
|
122
|
+
* publish it (mdma #312).
|
|
123
|
+
*
|
|
124
|
+
* Left unset, the key the cloud reported is used — `null` for now, which
|
|
125
|
+
* means `connection.client.canSubscribe === false` and nothing is guessed.
|
|
126
|
+
* When #312 lands, unchanged callers start observing.
|
|
127
|
+
*/
|
|
128
|
+
relayNodeKey?: string;
|
|
129
|
+
/**
|
|
130
|
+
* The audience the observing session is bound to. Defaults to this runtime's
|
|
131
|
+
* own origin (or `cli`). See {@link RelayObserveConfig.audience}.
|
|
132
|
+
*/
|
|
133
|
+
observeAudience?: Audience;
|
|
89
134
|
}
|
|
90
135
|
/** A signed-in cloud session with a relay ready to write through. */
|
|
91
136
|
export interface CloudConnection {
|
|
@@ -100,6 +145,23 @@ export interface CloudConnection {
|
|
|
100
145
|
relayInfo: CloudRelay;
|
|
101
146
|
/** Run one method in one context, on the author's behalf. */
|
|
102
147
|
execute<T = unknown>(contextId: string, method: string, argsJson?: unknown): Promise<IntentResult<T>>;
|
|
148
|
+
/**
|
|
149
|
+
* The same connection behind the transport-neutral surface.
|
|
150
|
+
*
|
|
151
|
+
* `client.rpc.execute({ contextId, method, argsJson })` is the call a
|
|
152
|
+
* node-backed app already writes, so an app reaching the cloud this way keeps
|
|
153
|
+
* every call site it has — signing in *is* the transport decision, and it
|
|
154
|
+
* happens here, once. `relay` and `execute` above are unchanged and remain
|
|
155
|
+
* the direct path for a caller that wants `IntentResult` in hand.
|
|
156
|
+
*
|
|
157
|
+
* Whether it can also *observe* depends on one input: the relay node's
|
|
158
|
+
* device signing key. A relay is an ordinary node and serves `/sse` and
|
|
159
|
+
* `/ws`, but a login statement must be signed against a key learned out of
|
|
160
|
+
* band. Pass {@link ConnectCloudOptions.relayNodeKey} if you have it; the
|
|
161
|
+
* cloud does not publish it yet (mdma #312), so by default
|
|
162
|
+
* `client.canSubscribe === false` and `client.events` throws saying so.
|
|
163
|
+
*/
|
|
164
|
+
client: MeroClient;
|
|
103
165
|
}
|
|
104
166
|
/**
|
|
105
167
|
* Sign in to the cloud and return a connection that can write.
|
|
@@ -112,3 +174,40 @@ export interface CloudConnection {
|
|
|
112
174
|
* cannot tell them what.
|
|
113
175
|
*/
|
|
114
176
|
export declare function connectCloud(options: ConnectCloudOptions): Promise<CloudConnection>;
|
|
177
|
+
/** What {@link connectCloudWithAccount} needs on top of a root. */
|
|
178
|
+
export interface ConnectCloudWithAccountOptions extends Omit<ConnectCloudOptions, 'sessionToken' | 'googleIdToken' | 'authorAccount'> {
|
|
179
|
+
/**
|
|
180
|
+
* The account root — a 64-hex secret, or a `Signer` over a key that cannot be
|
|
181
|
+
* exported. The whole credential: no Google login is involved.
|
|
182
|
+
*/
|
|
183
|
+
root: RootSource;
|
|
184
|
+
/**
|
|
185
|
+
* Whose writes these are, hex. Defaults to the account the root derives,
|
|
186
|
+
* which is the only account it can open a session as.
|
|
187
|
+
*
|
|
188
|
+
* Worth passing only when the device certificate was issued by a *different*
|
|
189
|
+
* root than the one signing in — which core accepts and nothing here needs to
|
|
190
|
+
* forbid, but which is rare enough that defaulting to the signing root is the
|
|
191
|
+
* safer shape.
|
|
192
|
+
*/
|
|
193
|
+
authorAccount?: string;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The same connection, opened with an account root instead of a cloud session.
|
|
197
|
+
*
|
|
198
|
+
* The two sign-in paths end in the same place, and until now only one of them
|
|
199
|
+
* ended in a usable client: a caller signing in with a root had to open the
|
|
200
|
+
* session, read the relays and assemble a {@link RelayClient} by hand, which is
|
|
201
|
+
* three chances to pair the wrong author with the wrong executor.
|
|
202
|
+
*
|
|
203
|
+
* It is one call rather than one function: the session is minted here and
|
|
204
|
+
* everything after it is {@link connectCloud}, unchanged. Duplicating the
|
|
205
|
+
* namespace choice and the three no-relay diagnostics would mean two copies of
|
|
206
|
+
* the messages that tell a user what to go and do.
|
|
207
|
+
*
|
|
208
|
+
* The author defaults to the account the root derives. That is not a
|
|
209
|
+
* convenience — passing an account the root does not own is the mistake this
|
|
210
|
+
* shape removes, since the cloud would answer with that root's namespaces while
|
|
211
|
+
* every warrant claimed somebody else's authorship.
|
|
212
|
+
*/
|
|
213
|
+
export declare function connectCloudWithAccount(options: ConnectCloudWithAccountOptions): Promise<CloudConnection>;
|