@calimero-network/mero-js 19.8.1 → 19.10.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 +25 -0
- package/dist/account/account.js +0 -0
- package/dist/account/index.d.ts +2 -2
- package/dist/account/index.js +1 -1
- package/dist/cloud/cloud-client.d.ts +135 -0
- package/dist/cloud/cloud-client.js +156 -1
- package/dist/cloud/index.d.ts +1 -1
- package/dist/http-client/web-client.js +9 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +178 -1
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +178 -1
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
|
@@ -74,3 +74,28 @@ export interface AccountLinkInput {
|
|
|
74
74
|
* serve.
|
|
75
75
|
*/
|
|
76
76
|
export declare function signAccountLink(input: AccountLinkInput): Promise<string>;
|
|
77
|
+
/** What {@link signAccountLogin} needs. */
|
|
78
|
+
export interface AccountLoginInput {
|
|
79
|
+
/** The account root's signing secret, 64 hex. */
|
|
80
|
+
rootSecret: string;
|
|
81
|
+
/** The challenge from `GET /api/auth/account/challenge`, verbatim UTF-8. */
|
|
82
|
+
nonce: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Sign a cloud login challenge with the account root. Returns base64.
|
|
86
|
+
*
|
|
87
|
+
* The statement this makes is *"I hold the root of account X"*, and it is the
|
|
88
|
+
* one statement a device credential can never make. A routing read proves the
|
|
89
|
+
* caller holds a device the root certified — the credential carries the root's
|
|
90
|
+
* signature over that device — but a certificate is public and travels in the
|
|
91
|
+
* clear inside every device-link op, so anyone who has seen one can say "a
|
|
92
|
+
* device of X is asking". Only the root can say the account is theirs.
|
|
93
|
+
*
|
|
94
|
+
* Which is why this is worth doing **once**: the cloud records the claim, and
|
|
95
|
+
* from then on it knows the account behind a device proof was proven by its
|
|
96
|
+
* owner. The session it also mints is the ordinary MDMA session token, accepted
|
|
97
|
+
* everywhere a Google-issued one is.
|
|
98
|
+
*
|
|
99
|
+
* Standard base64, not base64url — mdma decodes with `validate=True`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function signAccountLogin(input: AccountLoginInput): Promise<string>;
|
package/dist/account/account.js
CHANGED
|
Binary file
|
package/dist/account/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account.js';
|
|
2
|
-
export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, } from './account.js';
|
|
1
|
+
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account.js';
|
|
2
|
+
export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, AccountLoginInput, } from './account.js';
|
package/dist/account/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Account roots — minting one, restoring it from a phrase, and proving you hold it.
|
|
2
|
-
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account.js';
|
|
2
|
+
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account.js';
|
|
@@ -240,6 +240,44 @@ export interface AccountLinkProof {
|
|
|
240
240
|
/** `signAccountLink`'s output — standard base64. */
|
|
241
241
|
signature: string;
|
|
242
242
|
}
|
|
243
|
+
/** Where to send someone so a cloud login can consent to linking an account. */
|
|
244
|
+
export interface AccountLinkHandoff {
|
|
245
|
+
/** The cloud portal URL to open — a new tab, a popup, or the system browser. */
|
|
246
|
+
url: string;
|
|
247
|
+
/** The account the consent will be for, echoed so a caller can show it. */
|
|
248
|
+
accountId: string;
|
|
249
|
+
}
|
|
250
|
+
/** What comes back on the callback: a grant, a refusal, or neither. */
|
|
251
|
+
export interface AccountLinkCallback {
|
|
252
|
+
/** The grant to spend with {@link CloudClient.linkAccountWithGrant}. */
|
|
253
|
+
grant?: string;
|
|
254
|
+
/** The account the grant is for, as the portal echoed it. */
|
|
255
|
+
accountId?: string;
|
|
256
|
+
/** `'denied'` when the person cancelled. Absent on success. */
|
|
257
|
+
error?: string;
|
|
258
|
+
}
|
|
259
|
+
/** A single-use challenge for an account root to sign itself in with. */
|
|
260
|
+
export interface AccountLoginChallenge {
|
|
261
|
+
/** Sign this verbatim. Opaque and sealed by the cloud; nonces live ~2 minutes. */
|
|
262
|
+
nonce: string;
|
|
263
|
+
/** When it stops being accepted, epoch milliseconds. */
|
|
264
|
+
expiresAtMs: number;
|
|
265
|
+
}
|
|
266
|
+
/** A proof assembled by whoever holds the root, for {@link CloudClient.submitAccountLogin}. */
|
|
267
|
+
export interface AccountLoginProof {
|
|
268
|
+
/**
|
|
269
|
+
* The root that names the account, 64 hex.
|
|
270
|
+
*
|
|
271
|
+
* No `accountId` field, unlike {@link AccountLinkProof}: the cloud derives
|
|
272
|
+
* the account from this key rather than accepting one alongside it, so there
|
|
273
|
+
* is nothing a caller could state that the signature would then contradict.
|
|
274
|
+
*/
|
|
275
|
+
rootPublicKey: string;
|
|
276
|
+
/** The challenge that was signed. */
|
|
277
|
+
nonce: string;
|
|
278
|
+
/** `signAccountLogin`'s output — standard base64. */
|
|
279
|
+
signature: string;
|
|
280
|
+
}
|
|
243
281
|
/** The outcome of {@link CloudClient.linkAccount}. */
|
|
244
282
|
export interface CloudAccountLink {
|
|
245
283
|
accountId: string;
|
|
@@ -282,6 +320,45 @@ export declare class CloudClient {
|
|
|
282
320
|
* be kept or re-minted per call.
|
|
283
321
|
*/
|
|
284
322
|
signInWithGoogle(idToken: string): Promise<CloudSession>;
|
|
323
|
+
/**
|
|
324
|
+
* Sign in as a Calimero account, proving it with its root key.
|
|
325
|
+
*
|
|
326
|
+
* The password-free counterpart to {@link signInWithGoogle}: no Google, no
|
|
327
|
+
* password, no server-held secret — the cloud mints a challenge, the account
|
|
328
|
+
* root signs it, and the session that comes back is the same MDMA session
|
|
329
|
+
* token every other cloud route already accepts.
|
|
330
|
+
*
|
|
331
|
+
* Doing this **once** is what makes an account's later device proofs mean
|
|
332
|
+
* something. A routing read shows that the caller holds a device the root
|
|
333
|
+
* certified, but certificates are public, so it can never show that anyone
|
|
334
|
+
* held the root; the cloud records this claim and from then on knows the
|
|
335
|
+
* account behind those reads was claimed by its owner. The claim is recorded
|
|
336
|
+
* even when the session is refused.
|
|
337
|
+
*
|
|
338
|
+
* @param rootSecret the account root's signing secret, 64 hex. It never
|
|
339
|
+
* leaves this process — only a signature over the cloud's nonce travels.
|
|
340
|
+
*
|
|
341
|
+
* A `403` means either the signature did not verify, or the account is not
|
|
342
|
+
* linked to a cloud login: ownership alone says who you are, and the link
|
|
343
|
+
* says what you are entitled to. Anyone can mint a root offline, so a session
|
|
344
|
+
* on the proof alone would authenticate perfectly and authorize nothing. The
|
|
345
|
+
* ownership claim is recorded either way, so linking later needs no re-proof.
|
|
346
|
+
*/
|
|
347
|
+
signInWithAccount(rootSecret: string): Promise<CloudSession>;
|
|
348
|
+
/**
|
|
349
|
+
* Mint a challenge for an account root to sign.
|
|
350
|
+
*
|
|
351
|
+
* Public, and deliberately so: the caller has no session — obtaining one is
|
|
352
|
+
* what this exchange does. The nonce is not a capability, it is the thing a
|
|
353
|
+
* capability gets demonstrated against, and demonstrating it needs the root.
|
|
354
|
+
*
|
|
355
|
+
* Split out of {@link signInWithAccount} for the case that call cannot serve:
|
|
356
|
+
* a root held by a hardware key, a desktop app or an air-gapped machine,
|
|
357
|
+
* which signs without the secret ever reaching this process.
|
|
358
|
+
*/
|
|
359
|
+
getAccountLoginChallenge(): Promise<AccountLoginChallenge>;
|
|
360
|
+
/** Post a proof assembled elsewhere. The other half of {@link getAccountLoginChallenge}. */
|
|
361
|
+
submitAccountLogin(proof: AccountLoginProof): Promise<CloudSession>;
|
|
285
362
|
/**
|
|
286
363
|
* Refresh the session token explicitly.
|
|
287
364
|
*
|
|
@@ -483,6 +560,64 @@ export declare class CloudClient {
|
|
|
483
560
|
* key rather than letting a user type either.
|
|
484
561
|
*/
|
|
485
562
|
submitAccountLink(proof: AccountLinkProof): Promise<CloudAccountLink>;
|
|
563
|
+
/**
|
|
564
|
+
* Where to send someone to authorise linking this account, and what to expect
|
|
565
|
+
* back. Pairs with {@link readAccountLinkCallback} and
|
|
566
|
+
* {@link linkAccountWithGrant}.
|
|
567
|
+
*
|
|
568
|
+
* This exists because a browser app is in a bind: it holds an account root and
|
|
569
|
+
* can never hold a *first* cloud session, since only Google issues one. So the
|
|
570
|
+
* person goes to the cloud, where they are signed in, and consents there. What
|
|
571
|
+
* comes back is a grant — consent to link this one account, spendable only by
|
|
572
|
+
* whoever can sign with its root.
|
|
573
|
+
*
|
|
574
|
+
* @param callbackUrl where the portal sends the person back. Must be an origin
|
|
575
|
+
* the cloud allows; unlisted ones are refused when the grant is minted, not
|
|
576
|
+
* here. The grant arrives in the URL **fragment**, which browsers do not
|
|
577
|
+
* send to servers.
|
|
578
|
+
*
|
|
579
|
+
* Static, so it needs no client and no session: there is nothing to
|
|
580
|
+
* authenticate as yet.
|
|
581
|
+
*/
|
|
582
|
+
static accountLinkHandoff(options: {
|
|
583
|
+
/** The cloud portal's origin, e.g. `https://cloud.calimero.network`. */
|
|
584
|
+
portalUrl: string;
|
|
585
|
+
/** The account to be linked, 64 hex — derive it, never let a user type it. */
|
|
586
|
+
accountId: string;
|
|
587
|
+
/** Where to come back to. */
|
|
588
|
+
callbackUrl: string;
|
|
589
|
+
}): AccountLinkHandoff;
|
|
590
|
+
/**
|
|
591
|
+
* Read the portal's answer out of a callback URL's fragment.
|
|
592
|
+
*
|
|
593
|
+
* Returns `{}` when there is nothing to read, so it is safe to call on every
|
|
594
|
+
* load — which is how a redirected app finds out it has been sent back.
|
|
595
|
+
*
|
|
596
|
+
* @param href defaults to the current location, when there is one.
|
|
597
|
+
*/
|
|
598
|
+
static readAccountLinkCallback(href?: string): AccountLinkCallback;
|
|
599
|
+
/**
|
|
600
|
+
* Spend a link grant: sign it with the account root and post the pair.
|
|
601
|
+
*
|
|
602
|
+
* Anonymous, and it has to be — the app doing this holds no cloud session,
|
|
603
|
+
* which is the whole reason the grant exists. The grant carries the consenting
|
|
604
|
+
* login sealed inside it, so nothing here names one; a caller-supplied email
|
|
605
|
+
* would be a field to lie in.
|
|
606
|
+
*
|
|
607
|
+
* The signature is an ordinary account-link signature with the grant as the
|
|
608
|
+
* nonce, so the same {@link signAccountLink} every other link path uses signs
|
|
609
|
+
* this one. Both halves are required: a grant without the key links nothing,
|
|
610
|
+
* which is what makes it safe to carry through a browser redirect.
|
|
611
|
+
*
|
|
612
|
+
* A `403` means the grant was bad, expired, already spent, or signed by a key
|
|
613
|
+
* that names a different account; `409` that the account belongs to another
|
|
614
|
+
* login; `402` that the plan is full.
|
|
615
|
+
*/
|
|
616
|
+
linkAccountWithGrant(options: {
|
|
617
|
+
grant: string;
|
|
618
|
+
/** The root of the account the grant names, 64 hex. */
|
|
619
|
+
rootSecret: string;
|
|
620
|
+
}): Promise<CloudAccountLink>;
|
|
486
621
|
/** The accounts linked to this login, and how many the plan allows. */
|
|
487
622
|
getMyAccounts(): Promise<CloudLinkedAccounts>;
|
|
488
623
|
private adoptSession;
|
|
@@ -24,7 +24,7 @@
|
|
|
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, } from '../account/index.js';
|
|
27
|
+
import { accountRootFromSecret, 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
30
|
/** Where the hosted cloud lives when a caller names no other. */
|
|
@@ -77,6 +77,66 @@ export class CloudClient {
|
|
|
77
77
|
const body = await this.request('POST', '/api/auth/google', { body: { id_token: idToken }, anonymous: true });
|
|
78
78
|
return this.adoptSession(body);
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Sign in as a Calimero account, proving it with its root key.
|
|
82
|
+
*
|
|
83
|
+
* The password-free counterpart to {@link signInWithGoogle}: no Google, no
|
|
84
|
+
* password, no server-held secret — the cloud mints a challenge, the account
|
|
85
|
+
* root signs it, and the session that comes back is the same MDMA session
|
|
86
|
+
* token every other cloud route already accepts.
|
|
87
|
+
*
|
|
88
|
+
* Doing this **once** is what makes an account's later device proofs mean
|
|
89
|
+
* something. A routing read shows that the caller holds a device the root
|
|
90
|
+
* certified, but certificates are public, so it can never show that anyone
|
|
91
|
+
* held the root; the cloud records this claim and from then on knows the
|
|
92
|
+
* account behind those reads was claimed by its owner. The claim is recorded
|
|
93
|
+
* even when the session is refused.
|
|
94
|
+
*
|
|
95
|
+
* @param rootSecret the account root's signing secret, 64 hex. It never
|
|
96
|
+
* leaves this process — only a signature over the cloud's nonce travels.
|
|
97
|
+
*
|
|
98
|
+
* A `403` means either the signature did not verify, or the account is not
|
|
99
|
+
* linked to a cloud login: ownership alone says who you are, and the link
|
|
100
|
+
* says what you are entitled to. Anyone can mint a root offline, so a session
|
|
101
|
+
* on the proof alone would authenticate perfectly and authorize nothing. The
|
|
102
|
+
* ownership claim is recorded either way, so linking later needs no re-proof.
|
|
103
|
+
*/
|
|
104
|
+
async signInWithAccount(rootSecret) {
|
|
105
|
+
const root = await accountRootFromSecret(rootSecret);
|
|
106
|
+
const { nonce } = await this.getAccountLoginChallenge();
|
|
107
|
+
return this.submitAccountLogin({
|
|
108
|
+
rootPublicKey: root.publicKey,
|
|
109
|
+
nonce,
|
|
110
|
+
signature: await signAccountLogin({ rootSecret, nonce }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Mint a challenge for an account root to sign.
|
|
115
|
+
*
|
|
116
|
+
* Public, and deliberately so: the caller has no session — obtaining one is
|
|
117
|
+
* what this exchange does. The nonce is not a capability, it is the thing a
|
|
118
|
+
* capability gets demonstrated against, and demonstrating it needs the root.
|
|
119
|
+
*
|
|
120
|
+
* Split out of {@link signInWithAccount} for the case that call cannot serve:
|
|
121
|
+
* a root held by a hardware key, a desktop app or an air-gapped machine,
|
|
122
|
+
* which signs without the secret ever reaching this process.
|
|
123
|
+
*/
|
|
124
|
+
async getAccountLoginChallenge() {
|
|
125
|
+
const body = await this.request('GET', '/api/auth/account/challenge', { anonymous: true });
|
|
126
|
+
return { nonce: body.nonce, expiresAtMs: body.expires_at_ms };
|
|
127
|
+
}
|
|
128
|
+
/** Post a proof assembled elsewhere. The other half of {@link getAccountLoginChallenge}. */
|
|
129
|
+
async submitAccountLogin(proof) {
|
|
130
|
+
const body = await this.request('POST', '/api/auth/account', {
|
|
131
|
+
body: {
|
|
132
|
+
root_public_key: proof.rootPublicKey,
|
|
133
|
+
nonce: proof.nonce,
|
|
134
|
+
signature: proof.signature,
|
|
135
|
+
},
|
|
136
|
+
anonymous: true,
|
|
137
|
+
});
|
|
138
|
+
return this.adoptSession(body);
|
|
139
|
+
}
|
|
80
140
|
/**
|
|
81
141
|
* Refresh the session token explicitly.
|
|
82
142
|
*
|
|
@@ -429,6 +489,101 @@ export class CloudClient {
|
|
|
429
489
|
alreadyLinked: body.already_linked === true,
|
|
430
490
|
};
|
|
431
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Where to send someone to authorise linking this account, and what to expect
|
|
494
|
+
* back. Pairs with {@link readAccountLinkCallback} and
|
|
495
|
+
* {@link linkAccountWithGrant}.
|
|
496
|
+
*
|
|
497
|
+
* This exists because a browser app is in a bind: it holds an account root and
|
|
498
|
+
* can never hold a *first* cloud session, since only Google issues one. So the
|
|
499
|
+
* person goes to the cloud, where they are signed in, and consents there. What
|
|
500
|
+
* comes back is a grant — consent to link this one account, spendable only by
|
|
501
|
+
* whoever can sign with its root.
|
|
502
|
+
*
|
|
503
|
+
* @param callbackUrl where the portal sends the person back. Must be an origin
|
|
504
|
+
* the cloud allows; unlisted ones are refused when the grant is minted, not
|
|
505
|
+
* here. The grant arrives in the URL **fragment**, which browsers do not
|
|
506
|
+
* send to servers.
|
|
507
|
+
*
|
|
508
|
+
* Static, so it needs no client and no session: there is nothing to
|
|
509
|
+
* authenticate as yet.
|
|
510
|
+
*/
|
|
511
|
+
static accountLinkHandoff(options) {
|
|
512
|
+
const portal = options.portalUrl.replace(/\/+$/, '');
|
|
513
|
+
const query = new URLSearchParams({
|
|
514
|
+
'link-account': options.accountId,
|
|
515
|
+
'callback-url': options.callbackUrl,
|
|
516
|
+
});
|
|
517
|
+
return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Read the portal's answer out of a callback URL's fragment.
|
|
521
|
+
*
|
|
522
|
+
* Returns `{}` when there is nothing to read, so it is safe to call on every
|
|
523
|
+
* load — which is how a redirected app finds out it has been sent back.
|
|
524
|
+
*
|
|
525
|
+
* @param href defaults to the current location, when there is one.
|
|
526
|
+
*/
|
|
527
|
+
static readAccountLinkCallback(href) {
|
|
528
|
+
const source = href ?? (typeof window === 'undefined' ? '' : window.location.href);
|
|
529
|
+
if (!source)
|
|
530
|
+
return {};
|
|
531
|
+
let hash;
|
|
532
|
+
try {
|
|
533
|
+
hash = new URL(source).hash;
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return {};
|
|
537
|
+
}
|
|
538
|
+
const params = new URLSearchParams(hash.replace(/^#/, ''));
|
|
539
|
+
const grant = params.get('grant');
|
|
540
|
+
const accountId = params.get('account');
|
|
541
|
+
const error = params.get('error');
|
|
542
|
+
const out = {};
|
|
543
|
+
if (grant)
|
|
544
|
+
out.grant = grant;
|
|
545
|
+
if (accountId)
|
|
546
|
+
out.accountId = accountId;
|
|
547
|
+
if (error)
|
|
548
|
+
out.error = error;
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Spend a link grant: sign it with the account root and post the pair.
|
|
553
|
+
*
|
|
554
|
+
* Anonymous, and it has to be — the app doing this holds no cloud session,
|
|
555
|
+
* which is the whole reason the grant exists. The grant carries the consenting
|
|
556
|
+
* login sealed inside it, so nothing here names one; a caller-supplied email
|
|
557
|
+
* would be a field to lie in.
|
|
558
|
+
*
|
|
559
|
+
* The signature is an ordinary account-link signature with the grant as the
|
|
560
|
+
* nonce, so the same {@link signAccountLink} every other link path uses signs
|
|
561
|
+
* this one. Both halves are required: a grant without the key links nothing,
|
|
562
|
+
* which is what makes it safe to carry through a browser redirect.
|
|
563
|
+
*
|
|
564
|
+
* A `403` means the grant was bad, expired, already spent, or signed by a key
|
|
565
|
+
* that names a different account; `409` that the account belongs to another
|
|
566
|
+
* login; `402` that the plan is full.
|
|
567
|
+
*/
|
|
568
|
+
async linkAccountWithGrant(options) {
|
|
569
|
+
const root = await accountRootFromSecret(options.rootSecret);
|
|
570
|
+
const body = await this.request('POST', '/api/cloud/accounts/link', {
|
|
571
|
+
body: {
|
|
572
|
+
grant: options.grant,
|
|
573
|
+
root_public_key: root.publicKey,
|
|
574
|
+
signature: await signAccountLink({
|
|
575
|
+
rootSecret: options.rootSecret,
|
|
576
|
+
nonce: options.grant,
|
|
577
|
+
}),
|
|
578
|
+
},
|
|
579
|
+
anonymous: true,
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
accountId: String(body.account_id ?? root.accountId),
|
|
583
|
+
linkedAt: body.linked_at ?? null,
|
|
584
|
+
alreadyLinked: body.already_linked === true,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
432
587
|
/** The accounts linked to this login, and how many the plan allows. */
|
|
433
588
|
async getMyAccounts() {
|
|
434
589
|
const body = await this.request('GET', '/api/cloud/me/accounts');
|
package/dist/cloud/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CloudClient } from './cloud-client.js';
|
|
2
|
-
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
|
|
2
|
+
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, AccountLinkHandoff, AccountLinkCallback, AccountLoginChallenge, AccountLoginProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
|
|
3
3
|
export { connectCloud } from './connect.js';
|
|
4
4
|
export type { ConnectCloudOptions, CloudConnection } from './connect.js';
|
|
5
5
|
export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
|
|
@@ -18,12 +18,20 @@ function extractErrorMessage(bodyText) {
|
|
|
18
18
|
if (!bodyText)
|
|
19
19
|
return undefined;
|
|
20
20
|
try {
|
|
21
|
-
const
|
|
21
|
+
const body = JSON.parse(bodyText);
|
|
22
|
+
const error = body?.error;
|
|
22
23
|
if (typeof error === 'string' && error.trim() !== '')
|
|
23
24
|
return error;
|
|
24
25
|
const nested = error?.message;
|
|
25
26
|
if (typeof nested === 'string' && nested.trim() !== '')
|
|
26
27
|
return nested;
|
|
28
|
+
// FastAPI's spelling, which is what every Calimero Cloud route answers
|
|
29
|
+
// with. Without it a manager's carefully worded refusal — "ownership is
|
|
30
|
+
// recorded, but the account is not linked" — reached callers as a bare
|
|
31
|
+
// "HTTP 403", and the one thing they could do about it was invisible.
|
|
32
|
+
const detail = body?.detail;
|
|
33
|
+
if (typeof detail === 'string' && detail.trim() !== '')
|
|
34
|
+
return detail;
|
|
27
35
|
}
|
|
28
36
|
catch {
|
|
29
37
|
// Not JSON — the status line is all we can honestly report.
|