@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
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export type { Codec, EphemeralEntry } from './ephemeral/index.js';
|
|
|
17
17
|
export * from './cloud/index.js';
|
|
18
18
|
export * from './relay/index.js';
|
|
19
19
|
export { signWarrant, intentHash } from './warrant/index.js';
|
|
20
|
-
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account/index.js';
|
|
21
|
-
export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, } from './account/index.js';
|
|
20
|
+
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account/index.js';
|
|
21
|
+
export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, AccountLoginInput, } from './account/index.js';
|
|
22
22
|
export { signDeviceCert, mintDeviceId, deviceCertPayload, accountForRoot, } from './device-cert/index.js';
|
|
23
23
|
export type { DeviceCertInput } from './device-cert/index.js';
|
|
24
24
|
export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from './namespace-op/index.js';
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export * from './relay/index.js';
|
|
|
25
25
|
// Warrant signing — mint the author's consent for a relay to run one intent
|
|
26
26
|
export { signWarrant, intentHash } from './warrant/index.js';
|
|
27
27
|
// Account roots — mint one with a recovery phrase, or prove you hold one
|
|
28
|
-
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account/index.js';
|
|
28
|
+
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account/index.js';
|
|
29
29
|
export { signDeviceCert, mintDeviceId, deviceCertPayload, accountForRoot, } from './device-cert/index.js';
|
|
30
30
|
export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from './namespace-op/index.js';
|
|
31
31
|
// Login-statement signing — the device's half of a password-free session
|
package/dist/index.mjs
CHANGED
|
@@ -36,10 +36,13 @@ var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set(["token_reuse", "token_revoke
|
|
|
36
36
|
function extractErrorMessage(bodyText) {
|
|
37
37
|
if (!bodyText) return void 0;
|
|
38
38
|
try {
|
|
39
|
-
const
|
|
39
|
+
const body = JSON.parse(bodyText);
|
|
40
|
+
const error = body?.error;
|
|
40
41
|
if (typeof error === "string" && error.trim() !== "") return error;
|
|
41
42
|
const nested = error?.message;
|
|
42
43
|
if (typeof nested === "string" && nested.trim() !== "") return nested;
|
|
44
|
+
const detail = body?.detail;
|
|
45
|
+
if (typeof detail === "string" && detail.trim() !== "") return detail;
|
|
43
46
|
} catch {
|
|
44
47
|
}
|
|
45
48
|
return void 0;
|
|
@@ -4661,6 +4664,9 @@ var ENGLISH_WORDLIST = [
|
|
|
4661
4664
|
var ACCOUNT_LINK_DOMAIN = new TextEncoder().encode(
|
|
4662
4665
|
"calimero.mdma.account-link.v1\0"
|
|
4663
4666
|
);
|
|
4667
|
+
var ACCOUNT_LOGIN_DOMAIN = new TextEncoder().encode(
|
|
4668
|
+
"calimero.mdma.account-login.v1\0"
|
|
4669
|
+
);
|
|
4664
4670
|
var ENTROPY_BITS = 256;
|
|
4665
4671
|
var CHECKSUM_BITS = ENTROPY_BITS / 32;
|
|
4666
4672
|
var PHRASE_WORDS = (ENTROPY_BITS + CHECKSUM_BITS) / 11;
|
|
@@ -4692,6 +4698,18 @@ async function signAccountLink(input) {
|
|
|
4692
4698
|
);
|
|
4693
4699
|
return base64(signature);
|
|
4694
4700
|
}
|
|
4701
|
+
async function signAccountLogin(input) {
|
|
4702
|
+
if (!input.nonce) throw new Error("nonce is required to sign an account login");
|
|
4703
|
+
const key = await importSigningKey(input.rootSecret);
|
|
4704
|
+
const signature = new Uint8Array(
|
|
4705
|
+
await crypto.subtle.sign(
|
|
4706
|
+
"Ed25519",
|
|
4707
|
+
key,
|
|
4708
|
+
concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce))
|
|
4709
|
+
)
|
|
4710
|
+
);
|
|
4711
|
+
return base64(signature);
|
|
4712
|
+
}
|
|
4695
4713
|
async function withPhrase(secret, phrase) {
|
|
4696
4714
|
return { ...await accountRootFromSecret(secret), phrase };
|
|
4697
4715
|
}
|
|
@@ -4822,6 +4840,70 @@ var CloudClient = class {
|
|
|
4822
4840
|
const body = await this.request("POST", "/api/auth/google", { body: { id_token: idToken }, anonymous: true });
|
|
4823
4841
|
return this.adoptSession(body);
|
|
4824
4842
|
}
|
|
4843
|
+
/**
|
|
4844
|
+
* Sign in as a Calimero account, proving it with its root key.
|
|
4845
|
+
*
|
|
4846
|
+
* The password-free counterpart to {@link signInWithGoogle}: no Google, no
|
|
4847
|
+
* password, no server-held secret — the cloud mints a challenge, the account
|
|
4848
|
+
* root signs it, and the session that comes back is the same MDMA session
|
|
4849
|
+
* token every other cloud route already accepts.
|
|
4850
|
+
*
|
|
4851
|
+
* Doing this **once** is what makes an account's later device proofs mean
|
|
4852
|
+
* something. A routing read shows that the caller holds a device the root
|
|
4853
|
+
* certified, but certificates are public, so it can never show that anyone
|
|
4854
|
+
* held the root; the cloud records this claim and from then on knows the
|
|
4855
|
+
* account behind those reads was claimed by its owner. The claim is recorded
|
|
4856
|
+
* even when the session is refused.
|
|
4857
|
+
*
|
|
4858
|
+
* @param rootSecret the account root's signing secret, 64 hex. It never
|
|
4859
|
+
* leaves this process — only a signature over the cloud's nonce travels.
|
|
4860
|
+
*
|
|
4861
|
+
* A `403` means either the signature did not verify, or the account is not
|
|
4862
|
+
* linked to a cloud login: ownership alone says who you are, and the link
|
|
4863
|
+
* says what you are entitled to. Anyone can mint a root offline, so a session
|
|
4864
|
+
* on the proof alone would authenticate perfectly and authorize nothing. The
|
|
4865
|
+
* ownership claim is recorded either way, so linking later needs no re-proof.
|
|
4866
|
+
*/
|
|
4867
|
+
async signInWithAccount(rootSecret) {
|
|
4868
|
+
const root = await accountRootFromSecret(rootSecret);
|
|
4869
|
+
const { nonce } = await this.getAccountLoginChallenge();
|
|
4870
|
+
return this.submitAccountLogin({
|
|
4871
|
+
rootPublicKey: root.publicKey,
|
|
4872
|
+
nonce,
|
|
4873
|
+
signature: await signAccountLogin({ rootSecret, nonce })
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* Mint a challenge for an account root to sign.
|
|
4878
|
+
*
|
|
4879
|
+
* Public, and deliberately so: the caller has no session — obtaining one is
|
|
4880
|
+
* what this exchange does. The nonce is not a capability, it is the thing a
|
|
4881
|
+
* capability gets demonstrated against, and demonstrating it needs the root.
|
|
4882
|
+
*
|
|
4883
|
+
* Split out of {@link signInWithAccount} for the case that call cannot serve:
|
|
4884
|
+
* a root held by a hardware key, a desktop app or an air-gapped machine,
|
|
4885
|
+
* which signs without the secret ever reaching this process.
|
|
4886
|
+
*/
|
|
4887
|
+
async getAccountLoginChallenge() {
|
|
4888
|
+
const body = await this.request(
|
|
4889
|
+
"GET",
|
|
4890
|
+
"/api/auth/account/challenge",
|
|
4891
|
+
{ anonymous: true }
|
|
4892
|
+
);
|
|
4893
|
+
return { nonce: body.nonce, expiresAtMs: body.expires_at_ms };
|
|
4894
|
+
}
|
|
4895
|
+
/** Post a proof assembled elsewhere. The other half of {@link getAccountLoginChallenge}. */
|
|
4896
|
+
async submitAccountLogin(proof) {
|
|
4897
|
+
const body = await this.request("POST", "/api/auth/account", {
|
|
4898
|
+
body: {
|
|
4899
|
+
root_public_key: proof.rootPublicKey,
|
|
4900
|
+
nonce: proof.nonce,
|
|
4901
|
+
signature: proof.signature
|
|
4902
|
+
},
|
|
4903
|
+
anonymous: true
|
|
4904
|
+
});
|
|
4905
|
+
return this.adoptSession(body);
|
|
4906
|
+
}
|
|
4825
4907
|
/**
|
|
4826
4908
|
* Refresh the session token explicitly.
|
|
4827
4909
|
*
|
|
@@ -5195,6 +5277,100 @@ var CloudClient = class {
|
|
|
5195
5277
|
alreadyLinked: body.already_linked === true
|
|
5196
5278
|
};
|
|
5197
5279
|
}
|
|
5280
|
+
/**
|
|
5281
|
+
* Where to send someone to authorise linking this account, and what to expect
|
|
5282
|
+
* back. Pairs with {@link readAccountLinkCallback} and
|
|
5283
|
+
* {@link linkAccountWithGrant}.
|
|
5284
|
+
*
|
|
5285
|
+
* This exists because a browser app is in a bind: it holds an account root and
|
|
5286
|
+
* can never hold a *first* cloud session, since only Google issues one. So the
|
|
5287
|
+
* person goes to the cloud, where they are signed in, and consents there. What
|
|
5288
|
+
* comes back is a grant — consent to link this one account, spendable only by
|
|
5289
|
+
* whoever can sign with its root.
|
|
5290
|
+
*
|
|
5291
|
+
* @param callbackUrl where the portal sends the person back. Must be an origin
|
|
5292
|
+
* the cloud allows; unlisted ones are refused when the grant is minted, not
|
|
5293
|
+
* here. The grant arrives in the URL **fragment**, which browsers do not
|
|
5294
|
+
* send to servers.
|
|
5295
|
+
*
|
|
5296
|
+
* Static, so it needs no client and no session: there is nothing to
|
|
5297
|
+
* authenticate as yet.
|
|
5298
|
+
*/
|
|
5299
|
+
static accountLinkHandoff(options) {
|
|
5300
|
+
const portal = options.portalUrl.replace(/\/+$/, "");
|
|
5301
|
+
const query = new URLSearchParams({
|
|
5302
|
+
"link-account": options.accountId,
|
|
5303
|
+
"callback-url": options.callbackUrl
|
|
5304
|
+
});
|
|
5305
|
+
return { url: `${portal}/?${query.toString()}`, accountId: options.accountId };
|
|
5306
|
+
}
|
|
5307
|
+
/**
|
|
5308
|
+
* Read the portal's answer out of a callback URL's fragment.
|
|
5309
|
+
*
|
|
5310
|
+
* Returns `{}` when there is nothing to read, so it is safe to call on every
|
|
5311
|
+
* load — which is how a redirected app finds out it has been sent back.
|
|
5312
|
+
*
|
|
5313
|
+
* @param href defaults to the current location, when there is one.
|
|
5314
|
+
*/
|
|
5315
|
+
static readAccountLinkCallback(href) {
|
|
5316
|
+
const source = href ?? (typeof window === "undefined" ? "" : window.location.href);
|
|
5317
|
+
if (!source) return {};
|
|
5318
|
+
let hash;
|
|
5319
|
+
try {
|
|
5320
|
+
hash = new URL(source).hash;
|
|
5321
|
+
} catch {
|
|
5322
|
+
return {};
|
|
5323
|
+
}
|
|
5324
|
+
const params = new URLSearchParams(hash.replace(/^#/, ""));
|
|
5325
|
+
const grant = params.get("grant");
|
|
5326
|
+
const accountId = params.get("account");
|
|
5327
|
+
const error = params.get("error");
|
|
5328
|
+
const out = {};
|
|
5329
|
+
if (grant) out.grant = grant;
|
|
5330
|
+
if (accountId) out.accountId = accountId;
|
|
5331
|
+
if (error) out.error = error;
|
|
5332
|
+
return out;
|
|
5333
|
+
}
|
|
5334
|
+
/**
|
|
5335
|
+
* Spend a link grant: sign it with the account root and post the pair.
|
|
5336
|
+
*
|
|
5337
|
+
* Anonymous, and it has to be — the app doing this holds no cloud session,
|
|
5338
|
+
* which is the whole reason the grant exists. The grant carries the consenting
|
|
5339
|
+
* login sealed inside it, so nothing here names one; a caller-supplied email
|
|
5340
|
+
* would be a field to lie in.
|
|
5341
|
+
*
|
|
5342
|
+
* The signature is an ordinary account-link signature with the grant as the
|
|
5343
|
+
* nonce, so the same {@link signAccountLink} every other link path uses signs
|
|
5344
|
+
* this one. Both halves are required: a grant without the key links nothing,
|
|
5345
|
+
* which is what makes it safe to carry through a browser redirect.
|
|
5346
|
+
*
|
|
5347
|
+
* A `403` means the grant was bad, expired, already spent, or signed by a key
|
|
5348
|
+
* that names a different account; `409` that the account belongs to another
|
|
5349
|
+
* login; `402` that the plan is full.
|
|
5350
|
+
*/
|
|
5351
|
+
async linkAccountWithGrant(options) {
|
|
5352
|
+
const root = await accountRootFromSecret(options.rootSecret);
|
|
5353
|
+
const body = await this.request(
|
|
5354
|
+
"POST",
|
|
5355
|
+
"/api/cloud/accounts/link",
|
|
5356
|
+
{
|
|
5357
|
+
body: {
|
|
5358
|
+
grant: options.grant,
|
|
5359
|
+
root_public_key: root.publicKey,
|
|
5360
|
+
signature: await signAccountLink({
|
|
5361
|
+
rootSecret: options.rootSecret,
|
|
5362
|
+
nonce: options.grant
|
|
5363
|
+
})
|
|
5364
|
+
},
|
|
5365
|
+
anonymous: true
|
|
5366
|
+
}
|
|
5367
|
+
);
|
|
5368
|
+
return {
|
|
5369
|
+
accountId: String(body.account_id ?? root.accountId),
|
|
5370
|
+
linkedAt: body.linked_at ?? null,
|
|
5371
|
+
alreadyLinked: body.already_linked === true
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5198
5374
|
/** The accounts linked to this login, and how many the plan allows. */
|
|
5199
5375
|
async getMyAccounts() {
|
|
5200
5376
|
const body = await this.request("GET", "/api/cloud/me/accounts");
|
|
@@ -6388,6 +6564,7 @@ export {
|
|
|
6388
6564
|
probeNodeHealth,
|
|
6389
6565
|
routingProofHeaders,
|
|
6390
6566
|
signAccountLink,
|
|
6567
|
+
signAccountLogin,
|
|
6391
6568
|
signDeviceCert,
|
|
6392
6569
|
signLoginStatement,
|
|
6393
6570
|
signMemberJoinOp,
|