@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.
Files changed (76) hide show
  1. package/dist/account/account.d.ts +99 -4
  2. package/dist/account/account.js +0 -0
  3. package/dist/account/index.d.ts +6 -2
  4. package/dist/account/index.js +3 -1
  5. package/dist/admin-api/admin-client.d.ts +138 -2
  6. package/dist/admin-api/admin-client.js +237 -1
  7. package/dist/admin-api/admin-types.d.ts +159 -5
  8. package/dist/capabilities.d.ts +42 -0
  9. package/dist/capabilities.js +64 -0
  10. package/dist/cloud/cloud-client.d.ts +38 -8
  11. package/dist/cloud/cloud-client.js +38 -21
  12. package/dist/cloud/connect.d.ts +101 -2
  13. package/dist/cloud/connect.js +84 -8
  14. package/dist/cloud/enrol-redirect.d.ts +195 -0
  15. package/dist/cloud/enrol-redirect.js +236 -0
  16. package/dist/cloud/index.d.ts +6 -2
  17. package/dist/cloud/index.js +7 -1
  18. package/dist/cloud/link-redirect.d.ts +85 -0
  19. package/dist/cloud/link-redirect.js +105 -0
  20. package/dist/crypto/internal.d.ts +15 -0
  21. package/dist/crypto/internal.js +18 -0
  22. package/dist/device-cert/device-cert.d.ts +85 -2
  23. package/dist/device-cert/device-cert.js +100 -9
  24. package/dist/device-cert/index.d.ts +2 -2
  25. package/dist/device-cert/index.js +1 -1
  26. package/dist/events/sse.d.ts +22 -2
  27. package/dist/events/sse.js +20 -11
  28. package/dist/http-client/http-types.d.ts +18 -0
  29. package/dist/http-client/web-client.js +21 -1
  30. package/dist/index.browser.mjs +2 -2
  31. package/dist/index.browser.mjs.map +4 -4
  32. package/dist/index.cjs +1999 -536
  33. package/dist/index.cjs.map +4 -4
  34. package/dist/index.d.ts +11 -6
  35. package/dist/index.js +17 -3
  36. package/dist/index.mjs +1999 -536
  37. package/dist/index.mjs.map +4 -4
  38. package/dist/login/login.d.ts +11 -1
  39. package/dist/login/login.js +5 -4
  40. package/dist/login/session.d.ts +21 -5
  41. package/dist/login/session.js +7 -15
  42. package/dist/namespace-op/namespace-op.d.ts +14 -2
  43. package/dist/namespace-op/namespace-op.js +9 -5
  44. package/dist/relay/index.d.ts +3 -3
  45. package/dist/relay/index.js +1 -1
  46. package/dist/relay/nonce-source.d.ts +135 -0
  47. package/dist/relay/nonce-source.js +187 -19
  48. package/dist/relay/relay-client.d.ts +148 -5
  49. package/dist/relay/relay-client.js +187 -7
  50. package/dist/relay-attestation/attest-node-key.d.ts +66 -0
  51. package/dist/relay-attestation/attest-node-key.js +110 -0
  52. package/dist/relay-attestation/index.d.ts +1 -0
  53. package/dist/relay-attestation/index.js +1 -0
  54. package/dist/request-proof/authorizer.d.ts +48 -0
  55. package/dist/request-proof/authorizer.js +65 -0
  56. package/dist/request-proof/index.d.ts +4 -0
  57. package/dist/request-proof/index.js +2 -0
  58. package/dist/request-proof/request-proof.d.ts +56 -0
  59. package/dist/request-proof/request-proof.js +111 -0
  60. package/dist/rpc/index.d.ts +21 -0
  61. package/dist/rpc/index.js +20 -0
  62. package/dist/transport/index.d.ts +6 -0
  63. package/dist/transport/index.js +3 -0
  64. package/dist/transport/mero-client.d.ts +286 -0
  65. package/dist/transport/mero-client.js +379 -0
  66. package/dist/transport/relay-observer.d.ts +152 -0
  67. package/dist/transport/relay-observer.js +173 -0
  68. package/dist/transport/relay-transport.d.ts +73 -0
  69. package/dist/transport/relay-transport.js +94 -0
  70. package/dist/transport/types.d.ts +100 -0
  71. package/dist/transport/types.js +32 -0
  72. package/dist/warrant/governance-op.d.ts +47 -1
  73. package/dist/warrant/governance-op.js +62 -2
  74. package/dist/warrant/index.d.ts +2 -2
  75. package/dist/warrant/index.js +1 -1
  76. 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
- /** Withdraw a device from an account, terminally. */
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
+ }
@@ -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
@@ -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 rootSecret the account root's signing secret, 64 hex. It never
382
- * leaves this process — only a signature over the cloud's nonce travels.
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(rootSecret: string): Promise<CloudSession>;
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 rootSecret the account root's signing secret, 64 hex — from
616
- * {@link generateAccountRoot}, from a restored recovery phrase, or from a
617
- * desktop app or hardware key that already holds one.
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(rootSecret: string): Promise<CloudAccountLink>;
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: string;
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 { accountRootFromSecret, signAccountLink, signAccountLogin, } from '../account/index.js';
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
- /** Where the hosted cloud lives when a caller names no other. */
31
- const DEFAULT_CLOUD_BASE_URL = 'https://cloud.calimero.network';
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 rootSecret the account root's signing secret, 64 hex. It never
121
- * leaves this process — only a signature over the cloud's nonce travels.
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(rootSecret) {
130
- const root = await accountRootFromSecret(rootSecret);
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: root.publicKey,
146
+ rootPublicKey: publicKey,
134
147
  nonce,
135
- signature: await signAccountLogin({ rootSecret, nonce }),
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 rootSecret the account root's signing secret, 64 hex — from
545
- * {@link generateAccountRoot}, from a restored recovery phrase, or from a
546
- * desktop app or hardware key that already holds one.
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(rootSecret) {
553
- const root = await accountRootFromSecret(rootSecret);
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: root.accountId,
557
- rootPublicKey: root.publicKey,
573
+ accountId,
574
+ rootPublicKey: publicKey,
558
575
  nonce,
559
- signature: await signAccountLink({ rootSecret, nonce }),
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 root = await accountRootFromSecret(options.rootSecret);
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: root.publicKey,
701
+ root_public_key: publicKey,
685
702
  signature: await signAccountLink({
686
- rootSecret: options.rootSecret,
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 ?? root.accountId),
710
+ accountId: String(body.account_id ?? accountId),
694
711
  linkedAt: body.linked_at ?? null,
695
712
  alreadyLinked: body.already_linked === true,
696
713
  };
@@ -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
- /** The author device's ed25519 signing secret, hex. Never transmitted. */
56
- deviceSecret: string;
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>;