@calimero-network/mero-js 26.10.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 -12767
- 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 -12767
- 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 -686
- package/dist/admin-api/admin-client.js +0 -1225
- 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 -2041
- 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,196 +0,0 @@
|
|
|
1
|
-
import { type Signer } from '../signer/signer.js';
|
|
2
|
-
/** An account root: the secret, the key it derives, and the account it names. */
|
|
3
|
-
export interface AccountRoot {
|
|
4
|
-
/**
|
|
5
|
-
* The root signing secret, 64 hex.
|
|
6
|
-
*
|
|
7
|
-
* The whole account. It signs device certificates and cloud links, and there
|
|
8
|
-
* is no recovery path that does not go through it or its phrase — so it is
|
|
9
|
-
* never sent anywhere, and in a browser it does not belong in `localStorage`.
|
|
10
|
-
*/
|
|
11
|
-
secret: string;
|
|
12
|
-
/** The root's ed25519 public key, 64 hex. Safe to publish. */
|
|
13
|
-
publicKey: string;
|
|
14
|
-
/** The account this root names, 64 hex — the content address of its genesis. */
|
|
15
|
-
accountId: string;
|
|
16
|
-
}
|
|
17
|
-
/** An account root together with the phrase that restores it. */
|
|
18
|
-
export interface RecoverableAccountRoot extends AccountRoot {
|
|
19
|
-
/**
|
|
20
|
-
* A 24-word BIP39 English recovery phrase, single-space separated.
|
|
21
|
-
*
|
|
22
|
-
* Show it once, have the holder write it down, and do not keep it: it is the
|
|
23
|
-
* secret in a form a person can copy by hand without transcription errors.
|
|
24
|
-
*/
|
|
25
|
-
phrase: string;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Mint a brand-new account root from the platform CSPRNG.
|
|
29
|
-
*
|
|
30
|
-
* This is the "generate a new key" half of cloud registration — the other half
|
|
31
|
-
* being a root the holder already has, on a desktop app or a hardware key, which
|
|
32
|
-
* signs without this ever running.
|
|
33
|
-
*
|
|
34
|
-
* **Interop note, and it matters.** The phrase encodes the root secret as BIP39
|
|
35
|
-
* *entropy*, not as a BIP39 *seed*. A tool restoring it must call the equivalent
|
|
36
|
-
* of `mnemonicToEntropy(phrase)` — 32 bytes — and use those bytes as the ed25519
|
|
37
|
-
* secret directly. `mnemonicToSeed(phrase)`, which most wallet libraries reach
|
|
38
|
-
* for first, runs PBKDF2 and returns 64 different bytes; feeding those in names
|
|
39
|
-
* a different account and fails as "wrong account", not as a decoding error.
|
|
40
|
-
* There is no passphrase and no BIP32 derivation path: the 24 words are the key.
|
|
41
|
-
*/
|
|
42
|
-
export declare function generateAccountRoot(): Promise<RecoverableAccountRoot>;
|
|
43
|
-
/**
|
|
44
|
-
* Restore an account root from its recovery phrase.
|
|
45
|
-
*
|
|
46
|
-
* Whitespace and case are normalised, so a phrase pasted out of a text editor
|
|
47
|
-
* with a trailing newline or a stray double space restores fine. A word outside
|
|
48
|
-
* the list, a wrong word count, or a failed checksum throws rather than
|
|
49
|
-
* returning some other account — a single mistyped word is the common case, and
|
|
50
|
-
* BIP39's checksum catches all but 1 in 256 of them.
|
|
51
|
-
*/
|
|
52
|
-
export declare function accountRootFromPhrase(phrase: string): Promise<RecoverableAccountRoot>;
|
|
53
|
-
/** The public half of a root you already hold, without re-deriving it by hand. */
|
|
54
|
-
export declare function accountRootFromSecret(secret: string): Promise<AccountRoot>;
|
|
55
|
-
/** An account root held as a key this page may use and cannot read. */
|
|
56
|
-
export interface AccountRootSigner {
|
|
57
|
-
/** The root's ed25519 public key, 64 hex. Safe to publish. */
|
|
58
|
-
readonly publicKey: string;
|
|
59
|
-
/** The account this root names, 64 hex. */
|
|
60
|
-
readonly accountId: string;
|
|
61
|
-
/**
|
|
62
|
-
* Signs for the root. Pass it to `signDeviceCert({ signer })`.
|
|
63
|
-
*
|
|
64
|
-
* There is deliberately no `secret` beside this: the point of the shape is
|
|
65
|
-
* that nothing downstream of here can be handed the account.
|
|
66
|
-
*/
|
|
67
|
-
readonly signer: Signer;
|
|
68
|
-
}
|
|
69
|
-
/** A freshly minted root, with the phrase that is its only backup. */
|
|
70
|
-
export interface NewAccountRootSigner extends AccountRootSigner {
|
|
71
|
-
/**
|
|
72
|
-
* The 24-word phrase. Show it **once**, and store it nowhere.
|
|
73
|
-
*
|
|
74
|
-
* Unlike {@link RecoverableAccountRoot.phrase} this is the only copy that will
|
|
75
|
-
* ever exist: the signer beside it cannot be exported, so a holder who does
|
|
76
|
-
* not write these words down has no path back to the account from another
|
|
77
|
-
* browser — and neither does anyone else, which is the trade.
|
|
78
|
-
*/
|
|
79
|
-
readonly phrase: string;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Mint a root that stays in this page and hand back its recovery phrase.
|
|
83
|
-
*
|
|
84
|
-
* The enrolment-time counterpart to {@link generateAccountRoot}: same BIP39
|
|
85
|
-
* entropy, same account derivation, but the secret never leaves as a string a
|
|
86
|
-
* caller could persist. What survives a reload is the `CryptoKey` inside the
|
|
87
|
-
* signer — structured-clone it into IndexedDB — and what survives losing the
|
|
88
|
-
* browser is the phrase.
|
|
89
|
-
*
|
|
90
|
-
* **On scrubbing.** The entropy is zeroed and the seed is imported
|
|
91
|
-
* unexportable, but deriving the public half needs the seed as hex for an
|
|
92
|
-
* instant, and a JS string cannot be wiped. So this narrows the window from
|
|
93
|
-
* "for as long as the account exists" to "during this call" — a real
|
|
94
|
-
* improvement, and not the same claim as "the secret never exists".
|
|
95
|
-
*/
|
|
96
|
-
export declare function createAccountRootSigner(): Promise<NewAccountRootSigner>;
|
|
97
|
-
/**
|
|
98
|
-
* Restore the same root in another browser from its phrase.
|
|
99
|
-
*
|
|
100
|
-
* This is what makes the design portable: the phrase names the entropy, the
|
|
101
|
-
* entropy names the root, and the root names the account — so the same 24 words
|
|
102
|
-
* reach the same account id on any device, with no server involved and nothing
|
|
103
|
-
* to migrate.
|
|
104
|
-
*
|
|
105
|
-
* Throws on a bad word count, an unknown word, or a failed checksum rather than
|
|
106
|
-
* returning some other account — see {@link accountRootFromPhrase}.
|
|
107
|
-
*/
|
|
108
|
-
export declare function accountRootSignerFromPhrase(phrase: string): Promise<AccountRootSigner>;
|
|
109
|
-
/**
|
|
110
|
-
* The account root, in either form a caller may hold it.
|
|
111
|
-
*
|
|
112
|
-
* Exists because the cloud entry points take the root **positionally** — the
|
|
113
|
-
* whole argument is "the root" — so the two forms cannot be separate optional
|
|
114
|
-
* fields the way {@link AccountLinkInput} has them.
|
|
115
|
-
*
|
|
116
|
-
* A `string` is the 64-hex secret. A {@link Signer} is a root this process may
|
|
117
|
-
* use and cannot read: pass the `signer` from
|
|
118
|
-
* {@link accountRootSignerFromPhrase}.
|
|
119
|
-
*/
|
|
120
|
-
export type RootSource = string | Signer;
|
|
121
|
-
/** A root's identity and how it signs, however the caller supplied it. */
|
|
122
|
-
export interface ResolvedRoot {
|
|
123
|
-
signer: Signer;
|
|
124
|
-
/** The root's ed25519 public key, 64 hex. */
|
|
125
|
-
publicKey: string;
|
|
126
|
-
/** The account it names, 64 hex. */
|
|
127
|
-
accountId: string;
|
|
128
|
-
}
|
|
129
|
-
/** What a root is, however it was supplied: who it is, and how it signs. */
|
|
130
|
-
export declare function resolveRoot(source: RootSource): Promise<ResolvedRoot>;
|
|
131
|
-
/**
|
|
132
|
-
* The same, for entry points that take the two forms as separate optional
|
|
133
|
-
* fields. Exactly one must be present — see {@link resolveSigner}.
|
|
134
|
-
*/
|
|
135
|
-
export declare function resolveRootPair(rootSecret: string | undefined, signer: Signer | undefined): Promise<ResolvedRoot>;
|
|
136
|
-
/** What {@link signAccountLink} needs. */
|
|
137
|
-
export interface AccountLinkInput {
|
|
138
|
-
/**
|
|
139
|
-
* The account root's signing secret, 64 hex.
|
|
140
|
-
*
|
|
141
|
-
* Mutually exclusive with {@link AccountLinkInput.signer}. Prefer the signer
|
|
142
|
-
* in a browser: this is the account, and nothing above it can revoke it.
|
|
143
|
-
*/
|
|
144
|
-
rootSecret?: string;
|
|
145
|
-
/** The root's signer — use instead of {@link AccountLinkInput.rootSecret}. */
|
|
146
|
-
signer?: Signer;
|
|
147
|
-
/**
|
|
148
|
-
* The challenge from `GET /api/cloud/me/accounts/challenge`, verbatim.
|
|
149
|
-
*
|
|
150
|
-
* Signed as its UTF-8 bytes, not decoded first: mdma seals it as an opaque
|
|
151
|
-
* string and verifies over the same string it issued.
|
|
152
|
-
*/
|
|
153
|
-
nonce: string;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Sign a cloud link challenge with the account root. Returns base64.
|
|
157
|
-
*
|
|
158
|
-
* Standard base64, not base64url — mdma decodes it with `validate=True`, so the
|
|
159
|
-
* `-`/`_` alphabet is rejected outright rather than silently mis-decoded.
|
|
160
|
-
*
|
|
161
|
-
* Root-signed rather than device-signed on purpose: this is the credential
|
|
162
|
-
* someone still holds after losing a device, which is the case linking exists to
|
|
163
|
-
* serve.
|
|
164
|
-
*/
|
|
165
|
-
export declare function signAccountLink(input: AccountLinkInput): Promise<string>;
|
|
166
|
-
/** What {@link signAccountLogin} needs. */
|
|
167
|
-
export interface AccountLoginInput {
|
|
168
|
-
/**
|
|
169
|
-
* The account root's signing secret, 64 hex.
|
|
170
|
-
*
|
|
171
|
-
* Mutually exclusive with {@link AccountLoginInput.signer}.
|
|
172
|
-
*/
|
|
173
|
-
rootSecret?: string;
|
|
174
|
-
/** The root's signer — use instead of {@link AccountLoginInput.rootSecret}. */
|
|
175
|
-
signer?: Signer;
|
|
176
|
-
/** The challenge from `GET /api/auth/account/challenge`, verbatim UTF-8. */
|
|
177
|
-
nonce: string;
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Sign a cloud login challenge with the account root. Returns base64.
|
|
181
|
-
*
|
|
182
|
-
* The statement this makes is *"I hold the root of account X"*, and it is the
|
|
183
|
-
* one statement a device credential can never make. A routing read proves the
|
|
184
|
-
* caller holds a device the root certified — the credential carries the root's
|
|
185
|
-
* signature over that device — but a certificate is public and travels in the
|
|
186
|
-
* clear inside every device-link op, so anyone who has seen one can say "a
|
|
187
|
-
* device of X is asking". Only the root can say the account is theirs.
|
|
188
|
-
*
|
|
189
|
-
* Which is why this is worth doing **once**: the cloud records the claim, and
|
|
190
|
-
* from then on it knows the account behind a device proof was proven by its
|
|
191
|
-
* owner. The session it also mints is the ordinary MDMA session token, accepted
|
|
192
|
-
* everywhere a Google-issued one is.
|
|
193
|
-
*
|
|
194
|
-
* Standard base64, not base64url — mdma decodes with `validate=True`.
|
|
195
|
-
*/
|
|
196
|
-
export declare function signAccountLogin(input: AccountLoginInput): Promise<string>;
|
package/dist/account/account.js
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creating an account root, and proving you hold one.
|
|
3
|
-
*
|
|
4
|
-
* `signDeviceCert` assumes an account root already exists and asks only what it
|
|
5
|
-
* certifies. This is the step before that: where the root comes from, how a
|
|
6
|
-
* person keeps it, and how they show a server they have it.
|
|
7
|
-
*
|
|
8
|
-
* Two things live here because they are the two halves of onboarding:
|
|
9
|
-
*
|
|
10
|
-
* - {@link generateAccountRoot} mints a root and hands back a 24-word recovery
|
|
11
|
-
* phrase. The phrase *is* the key — see the interop note on that function —
|
|
12
|
-
* so a holder who writes it down can restore the account on any device.
|
|
13
|
-
* - {@link signAccountLink} signs a cloud-issued challenge with that root, which
|
|
14
|
-
* is how Calimero Cloud binds a billing login to a Calimero account.
|
|
15
|
-
* - {@link signAccountLogin} signs a different cloud-issued challenge with the
|
|
16
|
-
* same root to claim the account outright and open a session as it — the
|
|
17
|
-
* password-free counterpart to signing in with Google.
|
|
18
|
-
*
|
|
19
|
-
* Both are offline and hold no state. Nothing here talks to a node or to the
|
|
20
|
-
* cloud; `CloudClient.linkAccount` is the wrapper that does.
|
|
21
|
-
*
|
|
22
|
-
* Derived from:
|
|
23
|
-
*
|
|
24
|
-
* - `ACCOUNT_LINK_DOMAIN` / `ACCOUNT_LOGIN_DOMAIN`, and
|
|
25
|
-
* `verify_account_link_proof` / `verify_login_proof` — mdma's
|
|
26
|
-
* `manager/app/account_proof.py`
|
|
27
|
-
* - the account derivation — `accountForRoot` in `../device-cert`, which is
|
|
28
|
-
* core's `AccountId` preimage
|
|
29
|
-
*/
|
|
30
|
-
import { concat, derivePublicKey, fromHex, hex, } from '../crypto/internal.js';
|
|
31
|
-
import { accountForRoot, accountForRootPublicKey } from '../device-cert/index.js';
|
|
32
|
-
import { resolveSigner, signerFromSecret } from '../signer/signer.js';
|
|
33
|
-
import { ENGLISH_WORDLIST } from './wordlist.js';
|
|
34
|
-
/**
|
|
35
|
-
* mdma's `ACCOUNT_LINK_DOMAIN`, trailing NUL included.
|
|
36
|
-
*
|
|
37
|
-
* Prefixed raw rather than run through `domainHash`: mdma verifies
|
|
38
|
-
* `ed25519_verify(root_pk, ACCOUNT_LINK_DOMAIN ‖ nonce_utf8)` over exactly these
|
|
39
|
-
* bytes. The separator is what stops a signature gathered here being replayable
|
|
40
|
-
* into any other protocol that has a root sign opaque bytes, so it is not
|
|
41
|
-
* cosmetic — and the NUL is part of it.
|
|
42
|
-
*/
|
|
43
|
-
const ACCOUNT_LINK_DOMAIN = new TextEncoder().encode('calimero.mdma.account-link.v1\0');
|
|
44
|
-
/**
|
|
45
|
-
* mdma's `ACCOUNT_LOGIN_DOMAIN`, trailing NUL included.
|
|
46
|
-
*
|
|
47
|
-
* A separate domain from {@link ACCOUNT_LINK_DOMAIN} because the two
|
|
48
|
-
* exchanges are otherwise byte-identical — both are a root signature over a
|
|
49
|
-
* cloud-issued nonce — and a signature gathered while linking is gathered from
|
|
50
|
-
* someone already signed in and trusting the page. Without the separation it
|
|
51
|
-
* would be replayable as a login by whoever collected it.
|
|
52
|
-
*/
|
|
53
|
-
const ACCOUNT_LOGIN_DOMAIN = new TextEncoder().encode('calimero.mdma.account-login.v1\0');
|
|
54
|
-
/** Entropy carried by a phrase. 256 bits — 32 bytes, an ed25519 seed exactly. */
|
|
55
|
-
const ENTROPY_BITS = 256;
|
|
56
|
-
/** BIP39's checksum length: one bit per 32 bits of entropy. */
|
|
57
|
-
const CHECKSUM_BITS = ENTROPY_BITS / 32;
|
|
58
|
-
/** 11 bits per word, so (256 + 8) / 11 = 24 words. */
|
|
59
|
-
const PHRASE_WORDS = (ENTROPY_BITS + CHECKSUM_BITS) / 11;
|
|
60
|
-
/**
|
|
61
|
-
* Mint a brand-new account root from the platform CSPRNG.
|
|
62
|
-
*
|
|
63
|
-
* This is the "generate a new key" half of cloud registration — the other half
|
|
64
|
-
* being a root the holder already has, on a desktop app or a hardware key, which
|
|
65
|
-
* signs without this ever running.
|
|
66
|
-
*
|
|
67
|
-
* **Interop note, and it matters.** The phrase encodes the root secret as BIP39
|
|
68
|
-
* *entropy*, not as a BIP39 *seed*. A tool restoring it must call the equivalent
|
|
69
|
-
* of `mnemonicToEntropy(phrase)` — 32 bytes — and use those bytes as the ed25519
|
|
70
|
-
* secret directly. `mnemonicToSeed(phrase)`, which most wallet libraries reach
|
|
71
|
-
* for first, runs PBKDF2 and returns 64 different bytes; feeding those in names
|
|
72
|
-
* a different account and fails as "wrong account", not as a decoding error.
|
|
73
|
-
* There is no passphrase and no BIP32 derivation path: the 24 words are the key.
|
|
74
|
-
*/
|
|
75
|
-
export async function generateAccountRoot() {
|
|
76
|
-
const entropy = crypto.getRandomValues(new Uint8Array(ENTROPY_BITS / 8));
|
|
77
|
-
return withPhrase(hex(entropy), await encodePhrase(entropy));
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Restore an account root from its recovery phrase.
|
|
81
|
-
*
|
|
82
|
-
* Whitespace and case are normalised, so a phrase pasted out of a text editor
|
|
83
|
-
* with a trailing newline or a stray double space restores fine. A word outside
|
|
84
|
-
* the list, a wrong word count, or a failed checksum throws rather than
|
|
85
|
-
* returning some other account — a single mistyped word is the common case, and
|
|
86
|
-
* BIP39's checksum catches all but 1 in 256 of them.
|
|
87
|
-
*/
|
|
88
|
-
export async function accountRootFromPhrase(phrase) {
|
|
89
|
-
const words = normalisePhrase(phrase);
|
|
90
|
-
return withPhrase(hex(await decodePhrase(words)), words.join(' '));
|
|
91
|
-
}
|
|
92
|
-
/** The public half of a root you already hold, without re-deriving it by hand. */
|
|
93
|
-
export async function accountRootFromSecret(secret) {
|
|
94
|
-
// Validates the length and alphabet before anything derives from it, so a
|
|
95
|
-
// truncated paste fails here rather than naming an unrelated account.
|
|
96
|
-
fromHex(secret, 'secret', 32);
|
|
97
|
-
return {
|
|
98
|
-
secret,
|
|
99
|
-
publicKey: hex(await derivePublicKey(secret)),
|
|
100
|
-
accountId: await accountForRoot(secret),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Mint a root that stays in this page and hand back its recovery phrase.
|
|
105
|
-
*
|
|
106
|
-
* The enrolment-time counterpart to {@link generateAccountRoot}: same BIP39
|
|
107
|
-
* entropy, same account derivation, but the secret never leaves as a string a
|
|
108
|
-
* caller could persist. What survives a reload is the `CryptoKey` inside the
|
|
109
|
-
* signer — structured-clone it into IndexedDB — and what survives losing the
|
|
110
|
-
* browser is the phrase.
|
|
111
|
-
*
|
|
112
|
-
* **On scrubbing.** The entropy is zeroed and the seed is imported
|
|
113
|
-
* unexportable, but deriving the public half needs the seed as hex for an
|
|
114
|
-
* instant, and a JS string cannot be wiped. So this narrows the window from
|
|
115
|
-
* "for as long as the account exists" to "during this call" — a real
|
|
116
|
-
* improvement, and not the same claim as "the secret never exists".
|
|
117
|
-
*/
|
|
118
|
-
export async function createAccountRootSigner() {
|
|
119
|
-
const entropy = crypto.getRandomValues(new Uint8Array(ENTROPY_BITS / 8));
|
|
120
|
-
// Encoded before the entropy is scrubbed, since the phrase is derived from it.
|
|
121
|
-
const phrase = await encodePhrase(entropy);
|
|
122
|
-
return { ...(await rootSignerFromEntropy(entropy)), phrase };
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Restore the same root in another browser from its phrase.
|
|
126
|
-
*
|
|
127
|
-
* This is what makes the design portable: the phrase names the entropy, the
|
|
128
|
-
* entropy names the root, and the root names the account — so the same 24 words
|
|
129
|
-
* reach the same account id on any device, with no server involved and nothing
|
|
130
|
-
* to migrate.
|
|
131
|
-
*
|
|
132
|
-
* Throws on a bad word count, an unknown word, or a failed checksum rather than
|
|
133
|
-
* returning some other account — see {@link accountRootFromPhrase}.
|
|
134
|
-
*/
|
|
135
|
-
export async function accountRootSignerFromPhrase(phrase) {
|
|
136
|
-
return rootSignerFromEntropy(await decodePhrase(normalisePhrase(phrase)));
|
|
137
|
-
}
|
|
138
|
-
async function rootSignerFromEntropy(entropy) {
|
|
139
|
-
const signer = await signerFromSecret(hex(entropy), 'rootSecret');
|
|
140
|
-
entropy.fill(0);
|
|
141
|
-
return {
|
|
142
|
-
publicKey: signer.publicKey,
|
|
143
|
-
accountId: await accountForRootPublicKey(signer.publicKey),
|
|
144
|
-
signer,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
/** What a root is, however it was supplied: who it is, and how it signs. */
|
|
148
|
-
export async function resolveRoot(source) {
|
|
149
|
-
const signer = typeof source === 'string'
|
|
150
|
-
? await signerFromSecret(source, 'rootSecret')
|
|
151
|
-
: source;
|
|
152
|
-
return identify(signer);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* The same, for entry points that take the two forms as separate optional
|
|
156
|
-
* fields. Exactly one must be present — see {@link resolveSigner}.
|
|
157
|
-
*/
|
|
158
|
-
export async function resolveRootPair(rootSecret, signer) {
|
|
159
|
-
return identify(await resolveSigner(rootSecret, signer, 'rootSecret'));
|
|
160
|
-
}
|
|
161
|
-
async function identify(signer) {
|
|
162
|
-
return {
|
|
163
|
-
signer,
|
|
164
|
-
publicKey: signer.publicKey,
|
|
165
|
-
accountId: await accountForRootPublicKey(signer.publicKey),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Sign a cloud link challenge with the account root. Returns base64.
|
|
170
|
-
*
|
|
171
|
-
* Standard base64, not base64url — mdma decodes it with `validate=True`, so the
|
|
172
|
-
* `-`/`_` alphabet is rejected outright rather than silently mis-decoded.
|
|
173
|
-
*
|
|
174
|
-
* Root-signed rather than device-signed on purpose: this is the credential
|
|
175
|
-
* someone still holds after losing a device, which is the case linking exists to
|
|
176
|
-
* serve.
|
|
177
|
-
*/
|
|
178
|
-
export async function signAccountLink(input) {
|
|
179
|
-
if (!input.nonce)
|
|
180
|
-
throw new Error('nonce is required to sign an account link');
|
|
181
|
-
const signer = await resolveSigner(input.rootSecret, input.signer, 'rootSecret');
|
|
182
|
-
const signature = await signer.sign(concat(ACCOUNT_LINK_DOMAIN, new TextEncoder().encode(input.nonce)));
|
|
183
|
-
return base64(signature);
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Sign a cloud login challenge with the account root. Returns base64.
|
|
187
|
-
*
|
|
188
|
-
* The statement this makes is *"I hold the root of account X"*, and it is the
|
|
189
|
-
* one statement a device credential can never make. A routing read proves the
|
|
190
|
-
* caller holds a device the root certified — the credential carries the root's
|
|
191
|
-
* signature over that device — but a certificate is public and travels in the
|
|
192
|
-
* clear inside every device-link op, so anyone who has seen one can say "a
|
|
193
|
-
* device of X is asking". Only the root can say the account is theirs.
|
|
194
|
-
*
|
|
195
|
-
* Which is why this is worth doing **once**: the cloud records the claim, and
|
|
196
|
-
* from then on it knows the account behind a device proof was proven by its
|
|
197
|
-
* owner. The session it also mints is the ordinary MDMA session token, accepted
|
|
198
|
-
* everywhere a Google-issued one is.
|
|
199
|
-
*
|
|
200
|
-
* Standard base64, not base64url — mdma decodes with `validate=True`.
|
|
201
|
-
*/
|
|
202
|
-
export async function signAccountLogin(input) {
|
|
203
|
-
if (!input.nonce)
|
|
204
|
-
throw new Error('nonce is required to sign an account login');
|
|
205
|
-
const signer = await resolveSigner(input.rootSecret, input.signer, 'rootSecret');
|
|
206
|
-
const signature = await signer.sign(concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce)));
|
|
207
|
-
return base64(signature);
|
|
208
|
-
}
|
|
209
|
-
async function withPhrase(secret, phrase) {
|
|
210
|
-
return { ...(await accountRootFromSecret(secret)), phrase };
|
|
211
|
-
}
|
|
212
|
-
function normalisePhrase(phrase) {
|
|
213
|
-
// NFKD per BIP39. A no-op for the English list, which is ASCII, but the
|
|
214
|
-
// normalisation is what the spec pins and costs nothing to honour.
|
|
215
|
-
const words = phrase.normalize('NFKD').toLowerCase().trim().split(/\s+/);
|
|
216
|
-
const count = words[0] === '' ? 0 : words.length;
|
|
217
|
-
if (count !== PHRASE_WORDS) {
|
|
218
|
-
throw new Error(`recovery phrase must be ${PHRASE_WORDS} words, got ${count}`);
|
|
219
|
-
}
|
|
220
|
-
return words;
|
|
221
|
-
}
|
|
222
|
-
/** Entropy then checksum, chopped into 11-bit indices. */
|
|
223
|
-
async function encodePhrase(entropy) {
|
|
224
|
-
const bits = bitsOf(entropy) + (await checksumBits(entropy));
|
|
225
|
-
const words = [];
|
|
226
|
-
for (let i = 0; i < PHRASE_WORDS; i += 1) {
|
|
227
|
-
const index = Number.parseInt(bits.slice(i * 11, i * 11 + 11), 2);
|
|
228
|
-
words.push(ENGLISH_WORDLIST[index]);
|
|
229
|
-
}
|
|
230
|
-
return words.join(' ');
|
|
231
|
-
}
|
|
232
|
-
async function decodePhrase(words) {
|
|
233
|
-
let bits = '';
|
|
234
|
-
for (const word of words) {
|
|
235
|
-
const index = ENGLISH_WORDLIST.indexOf(word);
|
|
236
|
-
if (index < 0) {
|
|
237
|
-
throw new Error(`"${word}" is not a word in the recovery phrase list`);
|
|
238
|
-
}
|
|
239
|
-
bits += index.toString(2).padStart(11, '0');
|
|
240
|
-
}
|
|
241
|
-
const entropy = new Uint8Array(ENTROPY_BITS / 8);
|
|
242
|
-
for (let i = 0; i < entropy.length; i += 1) {
|
|
243
|
-
entropy[i] = Number.parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
244
|
-
}
|
|
245
|
-
if (bits.slice(ENTROPY_BITS) !== (await checksumBits(entropy))) {
|
|
246
|
-
throw new Error('recovery phrase checksum failed — check for a mistyped or transposed word');
|
|
247
|
-
}
|
|
248
|
-
return entropy;
|
|
249
|
-
}
|
|
250
|
-
/** The leading `CHECKSUM_BITS` of SHA-256 over the entropy, as a bit string. */
|
|
251
|
-
async function checksumBits(entropy) {
|
|
252
|
-
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy));
|
|
253
|
-
return bitsOf(digest).slice(0, CHECKSUM_BITS);
|
|
254
|
-
}
|
|
255
|
-
function bitsOf(bytes) {
|
|
256
|
-
let bits = '';
|
|
257
|
-
for (const byte of bytes)
|
|
258
|
-
bits += byte.toString(2).padStart(8, '0');
|
|
259
|
-
return bits;
|
|
260
|
-
}
|
|
261
|
-
function base64(bytes) {
|
|
262
|
-
// Built a character at a time rather than spreading into `String.fromCharCode`:
|
|
263
|
-
// the spread is fine for a 64-byte signature but blows the argument limit on
|
|
264
|
-
// anything larger, and this has no such cliff to remember.
|
|
265
|
-
let binary = '';
|
|
266
|
-
for (const byte of bytes)
|
|
267
|
-
binary += String.fromCharCode(byte);
|
|
268
|
-
return btoa(binary);
|
|
269
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An application's id, computed the way merod computes it.
|
|
3
|
-
*
|
|
4
|
-
* core's `ApplicationId::for_bundle(package, signer_id)` is
|
|
5
|
-
* `sha256(borsh((package, signer_id)))`: the package name and the publisher's
|
|
6
|
-
* signing key together decide which app a bundle IS, and the version is not an
|
|
7
|
-
* input, so every version a publisher ships under one package installs as the
|
|
8
|
-
* same application. A node learns the id when it installs the bundle; an
|
|
9
|
-
* account installs nothing, so it has to derive the id from the same two
|
|
10
|
-
* inputs — both of which the registry's bundle listing carries.
|
|
11
|
-
*/
|
|
12
|
-
/** A bundle entry as `GET {registry}/api/v2/bundles?package=…` lists it. */
|
|
13
|
-
export interface RegistryBundle {
|
|
14
|
-
package?: string;
|
|
15
|
-
signerId?: string;
|
|
16
|
-
appVersion?: string;
|
|
17
|
-
yanked?: boolean;
|
|
18
|
-
publishedAt?: string;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* `sha256(borsh((pkg, signerId)))` as lowercase hex: core's
|
|
22
|
-
* `ApplicationId::for_bundle`. A borsh tuple is its fields concatenated.
|
|
23
|
-
*/
|
|
24
|
-
export declare function applicationIdForBundle(pkg: string, signerId: string): Promise<string>;
|
|
25
|
-
/**
|
|
26
|
-
* The entry a package resolves to: its newest version that is not yanked. The
|
|
27
|
-
* one rule for "latest", shared by the version lookup and the id derivation so
|
|
28
|
-
* the two cannot disagree about which bundle an account is founding on.
|
|
29
|
-
*/
|
|
30
|
-
export declare function selectLatestBundle<B extends RegistryBundle>(bundles: readonly B[]): (B & {
|
|
31
|
-
appVersion: string;
|
|
32
|
-
}) | undefined;
|
|
33
|
-
/** The registry's listing of `pkg`, every version. */
|
|
34
|
-
export declare function fetchRegistryBundles(registryUrl: string, pkg: string, fetchFn?: typeof fetch): Promise<RegistryBundle[]>;
|
|
35
|
-
export interface ResolvedApplication {
|
|
36
|
-
applicationId: string;
|
|
37
|
-
signerId: string;
|
|
38
|
-
version: string;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* The application id an account founds `pkg` on, learned from the registry.
|
|
42
|
-
*
|
|
43
|
-
* Refused when the non-yanked versions of `pkg` name more than one publisher:
|
|
44
|
-
* that is two different applications under one package name (the id binds the
|
|
45
|
-
* signer), and picking either silently would commit the founder — who signs
|
|
46
|
-
* `TargetApplicationSet` naming the id — to an app it did not choose.
|
|
47
|
-
*/
|
|
48
|
-
export declare function resolveApplicationIdFromRegistry(registryUrl: string, pkg: string, deps?: {
|
|
49
|
-
fetch?: typeof fetch;
|
|
50
|
-
}): Promise<ResolvedApplication>;
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An application's id, computed the way merod computes it.
|
|
3
|
-
*
|
|
4
|
-
* core's `ApplicationId::for_bundle(package, signer_id)` is
|
|
5
|
-
* `sha256(borsh((package, signer_id)))`: the package name and the publisher's
|
|
6
|
-
* signing key together decide which app a bundle IS, and the version is not an
|
|
7
|
-
* input, so every version a publisher ships under one package installs as the
|
|
8
|
-
* same application. A node learns the id when it installs the bundle; an
|
|
9
|
-
* account installs nothing, so it has to derive the id from the same two
|
|
10
|
-
* inputs — both of which the registry's bundle listing carries.
|
|
11
|
-
*/
|
|
12
|
-
/** Borsh `String`: u32 little-endian byte length, then the UTF-8 bytes. */
|
|
13
|
-
function borshString(s) {
|
|
14
|
-
const bytes = new TextEncoder().encode(s);
|
|
15
|
-
const out = new Uint8Array(4 + bytes.length);
|
|
16
|
-
new DataView(out.buffer).setUint32(0, bytes.length, true);
|
|
17
|
-
out.set(bytes, 4);
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* `sha256(borsh((pkg, signerId)))` as lowercase hex: core's
|
|
22
|
-
* `ApplicationId::for_bundle`. A borsh tuple is its fields concatenated.
|
|
23
|
-
*/
|
|
24
|
-
export async function applicationIdForBundle(pkg, signerId) {
|
|
25
|
-
const a = borshString(pkg);
|
|
26
|
-
const b = borshString(signerId);
|
|
27
|
-
const tuple = new Uint8Array(a.length + b.length);
|
|
28
|
-
tuple.set(a, 0);
|
|
29
|
-
tuple.set(b, a.length);
|
|
30
|
-
const digest = await crypto.subtle.digest('SHA-256', tuple);
|
|
31
|
-
return Array.from(new Uint8Array(digest), (x) => x.toString(16).padStart(2, '0')).join('');
|
|
32
|
-
}
|
|
33
|
-
/** Whether `a` is a newer version string than `b` (numeric, dot/dash split). */
|
|
34
|
-
function newer(a, b) {
|
|
35
|
-
const [x, y] = [a, b].map((v) => v.split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0));
|
|
36
|
-
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
37
|
-
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
38
|
-
return (x[i] ?? 0) > (y[i] ?? 0);
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* The entry a package resolves to: its newest version that is not yanked. The
|
|
44
|
-
* one rule for "latest", shared by the version lookup and the id derivation so
|
|
45
|
-
* the two cannot disagree about which bundle an account is founding on.
|
|
46
|
-
*/
|
|
47
|
-
export function selectLatestBundle(bundles) {
|
|
48
|
-
return bundles
|
|
49
|
-
.filter((b) => !b.yanked && typeof b.appVersion === 'string' && b.appVersion.length > 0)
|
|
50
|
-
.reduce((best, b) => (!best || newer(b.appVersion, best.appVersion) ? b : best), undefined);
|
|
51
|
-
}
|
|
52
|
-
/** The registry's listing of `pkg`, every version. */
|
|
53
|
-
export async function fetchRegistryBundles(registryUrl, pkg, fetchFn = globalThis.fetch) {
|
|
54
|
-
const url = `${registryUrl.replace(/\/+$/, '')}/api/v2/bundles?package=${encodeURIComponent(pkg)}`;
|
|
55
|
-
const response = await fetchFn(url, { headers: { Accept: 'application/json' } });
|
|
56
|
-
if (!response.ok)
|
|
57
|
-
throw new Error(`the registry has no ${pkg} (HTTP ${response.status})`);
|
|
58
|
-
const body = (await response.json());
|
|
59
|
-
return Array.isArray(body) ? body : [];
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* The application id an account founds `pkg` on, learned from the registry.
|
|
63
|
-
*
|
|
64
|
-
* Refused when the non-yanked versions of `pkg` name more than one publisher:
|
|
65
|
-
* that is two different applications under one package name (the id binds the
|
|
66
|
-
* signer), and picking either silently would commit the founder — who signs
|
|
67
|
-
* `TargetApplicationSet` naming the id — to an app it did not choose.
|
|
68
|
-
*/
|
|
69
|
-
export async function resolveApplicationIdFromRegistry(registryUrl, pkg, deps = {}) {
|
|
70
|
-
const bundles = await fetchRegistryBundles(registryUrl, pkg, deps.fetch);
|
|
71
|
-
const live = bundles.filter((b) => !b.yanked);
|
|
72
|
-
const signers = [...new Set(live.map((b) => b.signerId).filter((s) => typeof s === 'string' && s.length > 0))];
|
|
73
|
-
if (signers.length > 1) {
|
|
74
|
-
throw new Error(`the registry lists ${pkg} under more than one publisher (${signers.join(', ')}): refusing to guess which application it is`);
|
|
75
|
-
}
|
|
76
|
-
const latest = selectLatestBundle(live);
|
|
77
|
-
if (!latest)
|
|
78
|
-
throw new Error(`the registry lists no version of ${pkg}`);
|
|
79
|
-
const signerId = latest.signerId;
|
|
80
|
-
if (!signerId)
|
|
81
|
-
throw new Error(`the registry names no publisher for ${pkg} ${latest.appVersion}`);
|
|
82
|
-
return { applicationId: await applicationIdForBundle(pkg, signerId), signerId, version: latest.appVersion };
|
|
83
|
-
}
|