@calimero-network/mero-js 26.11.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 -12772
- 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 -12772
- 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 -688
- package/dist/admin-api/admin-client.js +0 -1230
- 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 -2046
- 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,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Getting an app's device key certified **by a wallet on another origin**.
|
|
3
|
-
*
|
|
4
|
-
* An app holds a device keypair and no account. It cannot certify that key
|
|
5
|
-
* itself: certifying is a signature by an account root, and the root is the one
|
|
6
|
-
* key in the system with nothing above it to revoke it — so a page that can
|
|
7
|
-
* reach it can take the account for good. The whole design is that the app
|
|
8
|
-
* never can.
|
|
9
|
-
*
|
|
10
|
-
* What separates them is the **same-origin policy**, and that is why this is a
|
|
11
|
-
* redirect rather than a message, a popup API or a shared library. The wallet
|
|
12
|
-
* keeps the root as a non-extractable key in *its* origin's IndexedDB; script
|
|
13
|
-
* running on the app's origin cannot read another origin's storage, cannot read
|
|
14
|
-
* its keys, and cannot draw its consent screen. Nothing weaker enforces that —
|
|
15
|
-
* an iframe with a `postMessage` bridge would put the approval dialog inside a
|
|
16
|
-
* document the app can style, overlay and click for you.
|
|
17
|
-
*
|
|
18
|
-
* So the shape is: send the person to the wallet naming the key you want
|
|
19
|
-
* certified, they approve there, and the wallet redirects back with an
|
|
20
|
-
* `AccountProof<DeviceCert>` over that key.
|
|
21
|
-
*
|
|
22
|
-
* ## The credential comes back in the FRAGMENT
|
|
23
|
-
*
|
|
24
|
-
* Same reason the cloud's link grant does — see `link-redirect.ts`. A fragment
|
|
25
|
-
* is not sent to the server, so it lands in nobody's access log, no referrer
|
|
26
|
-
* header and no proxy trace on the way. A query string would put a credential
|
|
27
|
-
* into every log between here and the app's host.
|
|
28
|
-
*
|
|
29
|
-
* ## What the app must check before storing anything
|
|
30
|
-
*
|
|
31
|
-
* A fragment is written by whoever sent the person to this URL, which is not
|
|
32
|
-
* necessarily the wallet. Three things therefore have to be checked locally:
|
|
33
|
-
*
|
|
34
|
-
* - the credential certifies **the key this app asked about** — otherwise the
|
|
35
|
-
* app stores a credential it cannot sign for, and every warrant it mints is
|
|
36
|
-
* refused somewhere far from here;
|
|
37
|
-
* - the credential is **internally consistent** — the root signed it, the
|
|
38
|
-
* account it names is the one that root derives, and its device id was
|
|
39
|
-
* minted for that account ({@link verifyDeviceCredential});
|
|
40
|
-
* - the account reported beside it is **the account the credential names** —
|
|
41
|
-
* the app shows that id to a person and keys state by it, so a mismatch is a
|
|
42
|
-
* credential filed under someone else's name.
|
|
43
|
-
*
|
|
44
|
-
* {@link completeDeviceEnrolment} does all three and throws on any of them,
|
|
45
|
-
* before the caller has anything to store. What none of them establish is that
|
|
46
|
-
* the account is the *person's* — anyone can mint a root offline and certify
|
|
47
|
-
* any public key with it, and a device key is public once enrolled. That is
|
|
48
|
-
* what the state narrows: {@link deviceEnrolmentUrl} remembers each request, and
|
|
49
|
-
* {@link completeDeviceEnrolment} refuses an answer to a request this browser did
|
|
50
|
-
* not start, rather than to a link someone sent.
|
|
51
|
-
*/
|
|
52
|
-
import { verifyDeviceCredential, } from '../device-cert/index.js';
|
|
53
|
-
import { beginState, takeState } from './link-state.js';
|
|
54
|
-
/** Query parameters the wallet reads. Mirrors the cloud's `link-account` shape. */
|
|
55
|
-
const DEVICE_PARAM = 'enrol-device';
|
|
56
|
-
const KEM_PARAM = 'enrol-kem';
|
|
57
|
-
const CALLBACK_PARAM = 'callback-url';
|
|
58
|
-
const STATE_PARAM = 'state';
|
|
59
|
-
const PENDING_KEY = 'calimero.device.enrol.pending';
|
|
60
|
-
/**
|
|
61
|
-
* The URL to send a person to.
|
|
62
|
-
*
|
|
63
|
-
* Built rather than documented as a string so the parameter names live in one
|
|
64
|
-
* place: a wallet and an app that disagree about them fail as a blank consent
|
|
65
|
-
* screen, which looks like an outage rather than a contract mismatch.
|
|
66
|
-
*
|
|
67
|
-
* Remembers the request under a fresh state the wallet echoes back, so
|
|
68
|
-
* {@link completeDeviceEnrolment} can match the answer to it.
|
|
69
|
-
*/
|
|
70
|
-
export function deviceEnrolmentUrl(options) {
|
|
71
|
-
assertHex(options.devicePublicKey, 'devicePublicKey');
|
|
72
|
-
assertHex(options.kemPublicKey, 'kemPublicKey');
|
|
73
|
-
assertSafeReturnTo(options.returnTo);
|
|
74
|
-
warnOnForeignCallback(options.returnTo);
|
|
75
|
-
const url = new URL(options.walletUrl);
|
|
76
|
-
url.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
|
|
77
|
-
url.searchParams.set(KEM_PARAM, options.kemPublicKey);
|
|
78
|
-
url.searchParams.set(CALLBACK_PARAM, options.returnTo);
|
|
79
|
-
url.searchParams.set(STATE_PARAM, beginState(PENDING_KEY, {}));
|
|
80
|
-
return url.toString();
|
|
81
|
-
}
|
|
82
|
-
/** Send the person to the wallet to approve this device. Does not return. */
|
|
83
|
-
export function beginDeviceEnrolment(options) {
|
|
84
|
-
window.location.assign(deviceEnrolmentUrl(options));
|
|
85
|
-
// `assign` does not stop execution, and a caller that kept going would act on
|
|
86
|
-
// a page already being replaced.
|
|
87
|
-
throw new Error('redirecting to the wallet to approve this device');
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Read an enrolment out of the current URL fragment, and strip it.
|
|
91
|
-
*
|
|
92
|
-
* Returns `null` on an ordinary page load, so a caller can run it
|
|
93
|
-
* unconditionally at startup. Throws when the person declined, because that is
|
|
94
|
-
* an outcome the app has to show rather than a state to wait in.
|
|
95
|
-
*
|
|
96
|
-
* Nothing is verified here — the shape is separated from the checking so a
|
|
97
|
-
* caller can see what arrived (to log it, or to name the account in a refusal)
|
|
98
|
-
* even when it goes on to be refused. Nothing read out of this belongs in
|
|
99
|
-
* storage until {@link completeDeviceEnrolment} has passed.
|
|
100
|
-
*/
|
|
101
|
-
export function readEnrolmentCallback(location = window.location) {
|
|
102
|
-
const params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
|
103
|
-
const error = params.get('error');
|
|
104
|
-
if (error) {
|
|
105
|
-
takeState(PENDING_KEY, params.get(STATE_PARAM));
|
|
106
|
-
stripFragment();
|
|
107
|
-
throw new Error(error === 'denied' || error === 'cancelled'
|
|
108
|
-
? 'the device was not approved at the wallet'
|
|
109
|
-
: `the wallet refused this enrolment: ${error}`);
|
|
110
|
-
}
|
|
111
|
-
const credential = params.get('credential');
|
|
112
|
-
const account = params.get('account');
|
|
113
|
-
const device = params.get('device');
|
|
114
|
-
if (!credential || !account || !device)
|
|
115
|
-
return null;
|
|
116
|
-
// Consumed once. A credential left in the address bar is replayed by a
|
|
117
|
-
// reload, ends up in the browser's history and session restore, and is the
|
|
118
|
-
// one value here worth keeping out of both.
|
|
119
|
-
stripFragment();
|
|
120
|
-
const state = params.get('state');
|
|
121
|
-
return { credential, account, device, ...(state ? { state } : {}) };
|
|
122
|
-
}
|
|
123
|
-
function stripFragment() {
|
|
124
|
-
if (typeof history === 'undefined' || !history.replaceState)
|
|
125
|
-
return;
|
|
126
|
-
history.replaceState(null, '', location.pathname + location.search);
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Verify what came back, and refuse loudly rather than returning a half-checked
|
|
130
|
-
* credential.
|
|
131
|
-
*
|
|
132
|
-
* Every throw here is a credential that must not reach storage. The order is
|
|
133
|
-
* deliberate: the cheap local comparisons first, so a fragment aimed at the
|
|
134
|
-
* wrong app is rejected without a signature verification, and the message names
|
|
135
|
-
* what disagreed rather than reporting "invalid credential".
|
|
136
|
-
*
|
|
137
|
-
* The state is spent on the first call, so an answer is accepted at most once.
|
|
138
|
-
*/
|
|
139
|
-
export async function completeDeviceEnrolment(options) {
|
|
140
|
-
if (!takeState(PENDING_KEY, options.state)) {
|
|
141
|
-
throw new Error('this enrolment answers a request this app did not make - start the ' +
|
|
142
|
-
'enrolment again from here');
|
|
143
|
-
}
|
|
144
|
-
const certificate = await verifyDeviceCredential(options.credential);
|
|
145
|
-
if (certificate.signPublicKey !== normaliseHex(options.devicePublicKey)) {
|
|
146
|
-
throw new Error(`this credential certifies device key ${certificate.signPublicKey}, but ` +
|
|
147
|
-
`this app holds ${normaliseHex(options.devicePublicKey)} — it would ` +
|
|
148
|
-
'sign with a key the certificate does not cover');
|
|
149
|
-
}
|
|
150
|
-
if (options.kemPublicKey !== undefined &&
|
|
151
|
-
certificate.kemPublicKey !== normaliseHex(options.kemPublicKey)) {
|
|
152
|
-
throw new Error(`this credential names delivery key ${certificate.kemPublicKey}, not the ` +
|
|
153
|
-
'one this app asked about — group keys would be sealed to somebody else');
|
|
154
|
-
}
|
|
155
|
-
if (certificate.account !== normaliseHex(options.account)) {
|
|
156
|
-
throw new Error(`the wallet reported account ${normaliseHex(options.account)}, but the ` +
|
|
157
|
-
`credential names ${certificate.account}`);
|
|
158
|
-
}
|
|
159
|
-
if (certificate.device !== normaliseHex(options.device)) {
|
|
160
|
-
throw new Error(`the wallet reported device ${normaliseHex(options.device)}, but the ` +
|
|
161
|
-
`credential names ${certificate.device}`);
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
credential: options.credential,
|
|
165
|
-
account: certificate.account,
|
|
166
|
-
device: certificate.device,
|
|
167
|
-
certificate,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
function assertHex(value, label) {
|
|
171
|
-
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
172
|
-
throw new Error(`${label} must be 64 lowercase hex characters`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function normaliseHex(value) {
|
|
176
|
-
return value.trim().toLowerCase();
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Where a credential may be delivered.
|
|
180
|
-
*
|
|
181
|
-
* `https:` anywhere, `http:` only on loopback. Plain HTTP elsewhere would put
|
|
182
|
-
* the fragment on a page any network position can rewrite, and the fragment is
|
|
183
|
-
* the credential; loopback is exempt because it never leaves the machine and is
|
|
184
|
-
* the only way to develop against a wallet at all.
|
|
185
|
-
*
|
|
186
|
-
* Everything else — `javascript:`, `data:`, a custom app scheme — is refused.
|
|
187
|
-
* The first two are script the wallet would be asked to run; a custom scheme is
|
|
188
|
-
* a legitimate thing to want for a native app, but which installed handler
|
|
189
|
-
* claims it is not something this SDK can check, so it is refused here rather
|
|
190
|
-
* than accepted on trust.
|
|
191
|
-
*/
|
|
192
|
-
/**
|
|
193
|
-
* Is the credential coming back to somewhere other than the page that asked?
|
|
194
|
-
*
|
|
195
|
-
* Normally an app enrols its own device key and gets the answer back on its own
|
|
196
|
-
* origin, so the two match. They can legitimately differ — a callback page on a
|
|
197
|
-
* sibling host, an app mid-migration — which is why this is not refused.
|
|
198
|
-
*
|
|
199
|
-
* But it is the shape an abuse would also take: send someone to the real wallet
|
|
200
|
-
* and have the credential delivered elsewhere. Nothing here can tell the two
|
|
201
|
-
* apart, and a client-side check constrains nobody who did not call this
|
|
202
|
-
* function anyway. What it is good for is the honest case — surfacing the
|
|
203
|
-
* mismatch to whoever is building the app, and giving a wallet the same fact to
|
|
204
|
-
* put in front of the person approving.
|
|
205
|
-
*
|
|
206
|
-
* Exported so a wallet can render it, rather than each caller re-deriving it.
|
|
207
|
-
*/
|
|
208
|
-
export function callbackIsForeign(returnTo, pageOrigin) {
|
|
209
|
-
const here = pageOrigin ?? (typeof window === 'undefined' ? undefined : window.location.origin);
|
|
210
|
-
if (!here)
|
|
211
|
-
return false;
|
|
212
|
-
try {
|
|
213
|
-
return new URL(returnTo).origin !== here;
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
function warnOnForeignCallback(returnTo) {
|
|
220
|
-
if (!callbackIsForeign(returnTo))
|
|
221
|
-
return;
|
|
222
|
-
// A warning, not a throw: see `callbackIsForeign` for why this is a smell
|
|
223
|
-
// rather than a verdict.
|
|
224
|
-
console.warn(`[calimero] enrolling a device but sending the credential to ${new URL(returnTo).origin}, ` +
|
|
225
|
-
`which is not this page's origin. Legitimate when deliberate; worth a second look otherwise.`);
|
|
226
|
-
}
|
|
227
|
-
function assertSafeReturnTo(returnTo) {
|
|
228
|
-
let url;
|
|
229
|
-
try {
|
|
230
|
-
url = new URL(returnTo);
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
throw new Error(`returnTo must be an absolute URL, got ${returnTo}`);
|
|
234
|
-
}
|
|
235
|
-
const loopback = url.hostname === 'localhost' ||
|
|
236
|
-
url.hostname === '127.0.0.1' ||
|
|
237
|
-
url.hostname === '[::1]' ||
|
|
238
|
-
url.hostname === '::1';
|
|
239
|
-
if (url.protocol === 'https:')
|
|
240
|
-
return;
|
|
241
|
-
if (url.protocol === 'http:' && loopback)
|
|
242
|
-
return;
|
|
243
|
-
throw new Error(`returnTo must be https:, or http: on localhost — got ${url.protocol}//${url.hostname}`);
|
|
244
|
-
}
|
package/dist/cloud/index.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export { CloudClient, admitterAddrsFromNetworkStatus, AccountHaRefusedError, AccountNotLinkedError, AccountLinkedToSeveralUsersError, HaRequestPendingError, UnknownRelayError, RelayNotDialableError, } from './cloud-client.js';
|
|
2
|
-
export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudRelayJoin, CloudAccountRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, AccountLinkHandoff, AccountLinkCallback, AccountLoginChallenge, AccountLoginProof, EnableHAOptions, DisableHAOptions, EnableHaAsAccountOptions, } from './cloud-client.js';
|
|
3
|
-
export { connectCloud, connectCloudWithAccount } from './connect.js';
|
|
4
|
-
export type { ConnectCloudOptions, ConnectCloudWithAccountOptions, CloudConnection, } from './connect.js';
|
|
5
|
-
export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
|
|
6
|
-
export type { RoutingCredential, DiscoveryChallenge, RoutingChallenge, RoutingProofHeaders, } from './routing-proof.js';
|
|
7
|
-
export { cloudLinkUrl, beginCloudLink, readCloudLinkCallback, completeCloudLink, } from './link-redirect.js';
|
|
8
|
-
export type { BeginCloudLinkOptions, CloudLinkCallback, CompleteCloudLinkOptions, } from './link-redirect.js';
|
|
9
|
-
export { deviceEnrolmentUrl, beginDeviceEnrolment, readEnrolmentCallback, completeDeviceEnrolment, callbackIsForeign, } from './enrol-redirect.js';
|
|
10
|
-
export type { DeviceEnrolmentOptions, DeviceEnrolmentCallback, CompleteDeviceEnrolmentOptions, EnrolledDevice, } from './enrol-redirect.js';
|
|
11
|
-
export { signAccountOwnershipClaim, signAccountHaClaim, ACCOUNT_OWNERSHIP_AUDIENCE, ACCOUNT_HA_AUDIENCE, } from './account-ownership.js';
|
|
12
|
-
export type { AccountOwnershipProof, AccountOwnershipClaimInput, AccountHaClaimInput, } from './account-ownership.js';
|
package/dist/cloud/index.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export { CloudClient, admitterAddrsFromNetworkStatus, AccountHaRefusedError, AccountNotLinkedError, AccountLinkedToSeveralUsersError, HaRequestPendingError, UnknownRelayError, RelayNotDialableError, } from './cloud-client.js';
|
|
2
|
-
export { connectCloud, connectCloudWithAccount } from './connect.js';
|
|
3
|
-
export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
|
|
4
|
-
// Linking an account to a cloud login from a browser — the redirect half the
|
|
5
|
-
// cloud's consent screen expects an app to implement.
|
|
6
|
-
export { cloudLinkUrl, beginCloudLink, readCloudLinkCallback, completeCloudLink, } from './link-redirect.js';
|
|
7
|
-
// Getting this app's device key certified by a wallet on another origin — the
|
|
8
|
-
// redirect that keeps the account root out of the app's reach.
|
|
9
|
-
export { deviceEnrolmentUrl, beginDeviceEnrolment, readEnrolmentCallback, completeDeviceEnrolment, callbackIsForeign, } from './enrol-redirect.js';
|
|
10
|
-
export { signAccountOwnershipClaim, signAccountHaClaim, ACCOUNT_OWNERSHIP_AUDIENCE, ACCOUNT_HA_AUDIENCE, } from './account-ownership.js';
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Linking an account to a cloud login **from a browser**.
|
|
3
|
-
*
|
|
4
|
-
* The cloud's own accounts screen says it plainly: *"The cloud cannot link an
|
|
5
|
-
* account on your behalf: proving you own it is a signature by the account root
|
|
6
|
-
* key, which the cloud never holds."* So the link needs two things that live in
|
|
7
|
-
* different places — a cloud session, which only Google can mint, and a root
|
|
8
|
-
* signature, which only the holder can make. Neither side can do it alone.
|
|
9
|
-
*
|
|
10
|
-
* The cloud already implements its half: send a person to
|
|
11
|
-
* `?link-account=<id>&challenge=<sha256(verifier)>&callback-url=<url>`, it
|
|
12
|
-
* shows a consent screen and redirects back with a **grant** — that login's
|
|
13
|
-
* sealed consent to link one named account. This module is the missing other
|
|
14
|
-
* half, and it is deliberately small: send them there, then spend what comes
|
|
15
|
-
* back.
|
|
16
|
-
*
|
|
17
|
-
* ## Why a grant rather than a session
|
|
18
|
-
*
|
|
19
|
-
* A grant is spendable only with the account's root and the verifier this
|
|
20
|
-
* browser kept when it began the link. That is what lets the page complete the
|
|
21
|
-
* link while holding **no cloud session at all** - and it is why a wallet
|
|
22
|
-
* should never be handed one. A session token is a credential for everything
|
|
23
|
-
* that login can do; a grant is a credential for exactly one link, to exactly
|
|
24
|
-
* one account, sealed at the moment the person read the id on screen and
|
|
25
|
-
* agreed to it.
|
|
26
|
-
*
|
|
27
|
-
* ## The account id is chosen by us and read by them
|
|
28
|
-
*
|
|
29
|
-
* The cloud seals the account into the grant at consent time, and its own note
|
|
30
|
-
* says why: the server can check that the key which later signs derives that
|
|
31
|
-
* same account, but *"it cannot know whether the person meant this account"*.
|
|
32
|
-
* The consent screen is that check. So show the same id the person will see,
|
|
33
|
-
* before sending them — a wallet that hides it has removed the only safeguard.
|
|
34
|
-
*/
|
|
35
|
-
import type { CloudAccountLink } from './cloud-client.js';
|
|
36
|
-
import { type RootSource } from '../account/account.js';
|
|
37
|
-
export interface BeginCloudLinkOptions {
|
|
38
|
-
/** The account to link — 64 hex. Show this to the person before redirecting. */
|
|
39
|
-
accountId: string;
|
|
40
|
-
/**
|
|
41
|
-
* Where the cloud sends them back. Must be `https:`, or `http:` on localhost.
|
|
42
|
-
*
|
|
43
|
-
* The grant returns in the URL **fragment**, which is never sent to a server,
|
|
44
|
-
* so it does not land in anyone's access log on the way.
|
|
45
|
-
*/
|
|
46
|
-
returnTo: string;
|
|
47
|
-
/** The cloud's web origin. Defaults to the hosted one. */
|
|
48
|
-
cloudWebUrl?: string;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* The consent URL to send a person to. Remembers the link, and the verifier its
|
|
52
|
-
* grant will be bound to, so only this browser can match the callback and spend it.
|
|
53
|
-
*/
|
|
54
|
-
export declare function cloudLinkUrl(options: BeginCloudLinkOptions): Promise<string>;
|
|
55
|
-
/** Send the person to the cloud to consent. Does not return. */
|
|
56
|
-
export declare function beginCloudLink(options: BeginCloudLinkOptions): Promise<never>;
|
|
57
|
-
/** What came back in the fragment, if anything. */
|
|
58
|
-
export interface CloudLinkCallback {
|
|
59
|
-
grant: string;
|
|
60
|
-
/** The account the cloud sealed into the grant. */
|
|
61
|
-
account: string;
|
|
62
|
-
/** The cloud login that consented. Show it before spending: anyone holding the consent URL could have answered it. */
|
|
63
|
-
login: string;
|
|
64
|
-
/** This browser's secret for the grant, kept from the redirect. Spend it with the grant. */
|
|
65
|
-
verifier: string;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Read a grant out of the current URL fragment, and strip it.
|
|
69
|
-
*
|
|
70
|
-
* Returns `null` when this is an ordinary page load, so a caller can run it
|
|
71
|
-
* unconditionally at startup. Throws when declined or when this browser did not
|
|
72
|
-
* begin the link.
|
|
73
|
-
*/
|
|
74
|
-
export declare function readCloudLinkCallback(location?: {
|
|
75
|
-
hash: string;
|
|
76
|
-
search?: string;
|
|
77
|
-
}): CloudLinkCallback | null;
|
|
78
|
-
export interface CompleteCloudLinkOptions extends CloudLinkCallback {
|
|
79
|
-
/** The account root — a 64-hex secret, or a Signer over a key you cannot read. */
|
|
80
|
-
root: RootSource;
|
|
81
|
-
/** The **manager** API base, e.g. `https://manager.cloud.calimero.network`. */
|
|
82
|
-
cloudBaseUrl?: string;
|
|
83
|
-
fetch?: typeof fetch;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Spend the grant: sign it with the root and hand it back to the cloud.
|
|
87
|
-
*
|
|
88
|
-
* Anonymous by construction — the client is built with no session, because the
|
|
89
|
-
* whole point is that the page never needed one.
|
|
90
|
-
*
|
|
91
|
-
* @throws if the grant names a different account than the root derives, before
|
|
92
|
-
* anything is sent. The cloud checks this too, but failing here says which two
|
|
93
|
-
* accounts disagreed rather than returning a bare 403.
|
|
94
|
-
*/
|
|
95
|
-
export declare function completeCloudLink(options: CompleteCloudLinkOptions): Promise<CloudAccountLink>;
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Linking an account to a cloud login **from a browser**.
|
|
3
|
-
*
|
|
4
|
-
* The cloud's own accounts screen says it plainly: *"The cloud cannot link an
|
|
5
|
-
* account on your behalf: proving you own it is a signature by the account root
|
|
6
|
-
* key, which the cloud never holds."* So the link needs two things that live in
|
|
7
|
-
* different places — a cloud session, which only Google can mint, and a root
|
|
8
|
-
* signature, which only the holder can make. Neither side can do it alone.
|
|
9
|
-
*
|
|
10
|
-
* The cloud already implements its half: send a person to
|
|
11
|
-
* `?link-account=<id>&challenge=<sha256(verifier)>&callback-url=<url>`, it
|
|
12
|
-
* shows a consent screen and redirects back with a **grant** — that login's
|
|
13
|
-
* sealed consent to link one named account. This module is the missing other
|
|
14
|
-
* half, and it is deliberately small: send them there, then spend what comes
|
|
15
|
-
* back.
|
|
16
|
-
*
|
|
17
|
-
* ## Why a grant rather than a session
|
|
18
|
-
*
|
|
19
|
-
* A grant is spendable only with the account's root and the verifier this
|
|
20
|
-
* browser kept when it began the link. That is what lets the page complete the
|
|
21
|
-
* link while holding **no cloud session at all** - and it is why a wallet
|
|
22
|
-
* should never be handed one. A session token is a credential for everything
|
|
23
|
-
* that login can do; a grant is a credential for exactly one link, to exactly
|
|
24
|
-
* one account, sealed at the moment the person read the id on screen and
|
|
25
|
-
* agreed to it.
|
|
26
|
-
*
|
|
27
|
-
* ## The account id is chosen by us and read by them
|
|
28
|
-
*
|
|
29
|
-
* The cloud seals the account into the grant at consent time, and its own note
|
|
30
|
-
* says why: the server can check that the key which later signs derives that
|
|
31
|
-
* same account, but *"it cannot know whether the person meant this account"*.
|
|
32
|
-
* The consent screen is that check. So show the same id the person will see,
|
|
33
|
-
* before sending them — a wallet that hides it has removed the only safeguard.
|
|
34
|
-
*/
|
|
35
|
-
import { CloudClient } from './cloud-client.js';
|
|
36
|
-
import { resolveRoot } from '../account/account.js';
|
|
37
|
-
import { beginLinkState, linkGrantClaims, takeLinkVerifier, withoutLinkState } from './link-state.js';
|
|
38
|
-
/** Where the consent screen lives. The web app, not the manager API. */
|
|
39
|
-
const DEFAULT_CLOUD_WEB_URL = 'https://cloud.calimero.network';
|
|
40
|
-
/**
|
|
41
|
-
* The consent URL to send a person to. Remembers the link, and the verifier its
|
|
42
|
-
* grant will be bound to, so only this browser can match the callback and spend it.
|
|
43
|
-
*/
|
|
44
|
-
export async function cloudLinkUrl(options) {
|
|
45
|
-
const url = new URL(options.cloudWebUrl ?? DEFAULT_CLOUD_WEB_URL);
|
|
46
|
-
const { callbackUrl, challenge } = await beginLinkState(options.accountId, options.returnTo);
|
|
47
|
-
url.searchParams.set('link-account', options.accountId);
|
|
48
|
-
url.searchParams.set('challenge', challenge);
|
|
49
|
-
url.searchParams.set('callback-url', callbackUrl);
|
|
50
|
-
return url.toString();
|
|
51
|
-
}
|
|
52
|
-
/** Send the person to the cloud to consent. Does not return. */
|
|
53
|
-
export async function beginCloudLink(options) {
|
|
54
|
-
window.location.assign(await cloudLinkUrl(options));
|
|
55
|
-
// `assign` does not stop execution, and a caller that kept going would act on
|
|
56
|
-
// a page already being replaced.
|
|
57
|
-
throw new Error('redirecting to the cloud to authorise this link');
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Read a grant out of the current URL fragment, and strip it.
|
|
61
|
-
*
|
|
62
|
-
* Returns `null` when this is an ordinary page load, so a caller can run it
|
|
63
|
-
* unconditionally at startup. Throws when declined or when this browser did not
|
|
64
|
-
* begin the link.
|
|
65
|
-
*/
|
|
66
|
-
export function readCloudLinkCallback(location = window.location) {
|
|
67
|
-
const params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
|
68
|
-
const error = params.get('error');
|
|
69
|
-
if (error) {
|
|
70
|
-
takeLinkVerifier(location.search ?? '', null);
|
|
71
|
-
stripFragment();
|
|
72
|
-
throw new Error(error === 'denied'
|
|
73
|
-
? 'the account link was declined at the cloud'
|
|
74
|
-
: `the cloud refused this link: ${error}`);
|
|
75
|
-
}
|
|
76
|
-
const grant = params.get('grant');
|
|
77
|
-
const account = params.get('account');
|
|
78
|
-
if (!grant || !account)
|
|
79
|
-
return null;
|
|
80
|
-
const verifier = takeLinkVerifier(location.search ?? '', account);
|
|
81
|
-
const claims = linkGrantClaims(grant);
|
|
82
|
-
if (!verifier || claims?.account !== account) {
|
|
83
|
-
stripFragment();
|
|
84
|
-
throw new Error('this account link answers a request this browser did not make - start the link again from here');
|
|
85
|
-
}
|
|
86
|
-
// Consumed once. Leaving it in the bar means a reload replays a spent grant
|
|
87
|
-
// and reports a confusing failure instead of the link that already worked.
|
|
88
|
-
stripFragment();
|
|
89
|
-
return { grant, account, login: claims.login, verifier };
|
|
90
|
-
}
|
|
91
|
-
function stripFragment() {
|
|
92
|
-
if (typeof history === 'undefined' || !history.replaceState)
|
|
93
|
-
return;
|
|
94
|
-
history.replaceState(null, '', location.pathname + withoutLinkState(location.search));
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Spend the grant: sign it with the root and hand it back to the cloud.
|
|
98
|
-
*
|
|
99
|
-
* Anonymous by construction — the client is built with no session, because the
|
|
100
|
-
* whole point is that the page never needed one.
|
|
101
|
-
*
|
|
102
|
-
* @throws if the grant names a different account than the root derives, before
|
|
103
|
-
* anything is sent. The cloud checks this too, but failing here says which two
|
|
104
|
-
* accounts disagreed rather than returning a bare 403.
|
|
105
|
-
*/
|
|
106
|
-
export async function completeCloudLink(options) {
|
|
107
|
-
const root = await resolveRoot(options.root);
|
|
108
|
-
if (root.accountId !== options.account) {
|
|
109
|
-
throw new Error(`this grant is for account ${options.account}, but the root here derives ` +
|
|
110
|
-
`${root.accountId} — it was minted for a different account`);
|
|
111
|
-
}
|
|
112
|
-
const cloud = new CloudClient({
|
|
113
|
-
cloudBaseUrl: options.cloudBaseUrl,
|
|
114
|
-
fetch: options.fetch,
|
|
115
|
-
});
|
|
116
|
-
return cloud.linkAccountWithGrant({
|
|
117
|
-
grant: options.grant,
|
|
118
|
-
verifier: options.verifier,
|
|
119
|
-
signer: root.signer,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Redirects this browser began, so a callback can be matched to one. `localStorage`,
|
|
3
|
-
* not `sessionStorage`: consent opens in a `noopener` tab with empty session storage.
|
|
4
|
-
*/
|
|
5
|
-
export declare const ACCOUNT_HEX: RegExp;
|
|
6
|
-
export declare const VERIFIER_HEX: RegExp;
|
|
7
|
-
/** Remember `payload` in the `key` store under a fresh random state, and return the state. */
|
|
8
|
-
export declare function beginState<T extends object>(key: string, payload: T): string;
|
|
9
|
-
/** Removes and returns what `state` was begun with, or null when this browser did not begin it. */
|
|
10
|
-
export declare function takeState<T extends object>(key: string, state: string | null | undefined): T | null;
|
|
11
|
-
/** What a link grant claims, read without its seal, which only the cloud can check. */
|
|
12
|
-
export interface LinkGrantClaims {
|
|
13
|
-
/** The cloud login that consented. */
|
|
14
|
-
login: string;
|
|
15
|
-
account: string;
|
|
16
|
-
challenge: string;
|
|
17
|
-
}
|
|
18
|
-
/** The challenge a verifier commits to: sha256 of its bytes, hex. */
|
|
19
|
-
export declare function linkChallenge(verifier: string): Promise<string>;
|
|
20
|
-
/**
|
|
21
|
-
* Remember a link for `account` under a fresh verifier. Returns `returnTo`
|
|
22
|
-
* carrying its state, and the challenge to send the cloud in place of the verifier.
|
|
23
|
-
*/
|
|
24
|
-
export declare function beginLinkState(account: string, returnTo: string): Promise<{
|
|
25
|
-
callbackUrl: string;
|
|
26
|
-
challenge: string;
|
|
27
|
-
}>;
|
|
28
|
-
/**
|
|
29
|
-
* Removes the link a callback answers and returns its verifier, or null when
|
|
30
|
-
* this browser did not begin a link for `account` with this state.
|
|
31
|
-
*/
|
|
32
|
-
export declare function takeLinkVerifier(search: string, account: string | null): string | null;
|
|
33
|
-
/** The claims of a link grant, or null when `grant` is not one. */
|
|
34
|
-
export declare function linkGrantClaims(grant: string): LinkGrantClaims | null;
|
|
35
|
-
/** The query with the link state removed, for rewriting the address bar. */
|
|
36
|
-
export declare function withoutLinkState(search: string): string;
|
package/dist/cloud/link-state.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Redirects this browser began, so a callback can be matched to one. `localStorage`,
|
|
3
|
-
* not `sessionStorage`: consent opens in a `noopener` tab with empty session storage.
|
|
4
|
-
*/
|
|
5
|
-
import { hex } from "../crypto/internal.js";
|
|
6
|
-
const KEY = "calimero.cloud.link.pending";
|
|
7
|
-
const STATE_PARAM = "link_state";
|
|
8
|
-
const LIFETIME_MS = 10 * 60 * 1000; // a grant lives 5 minutes, plus time to consent
|
|
9
|
-
export const ACCOUNT_HEX = /^[0-9a-f]{64}$/;
|
|
10
|
-
const MAX_PENDING = 20; // bounds storage when a page builds a consent URL on every render
|
|
11
|
-
const VERIFIER_BYTES = 32; // the cloud refuses any other length
|
|
12
|
-
export const VERIFIER_HEX = /^[0-9a-f]{64}$/;
|
|
13
|
-
const LINK_GRANT_USE = "account-link-grant";
|
|
14
|
-
function readPending(key, now) {
|
|
15
|
-
try {
|
|
16
|
-
const raw = localStorage.getItem(key);
|
|
17
|
-
const pending = raw ? JSON.parse(raw) : {};
|
|
18
|
-
return Object.fromEntries(Object.entries(pending).filter(([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now));
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function writePending(key, pending) {
|
|
25
|
-
try {
|
|
26
|
-
localStorage.setItem(key, JSON.stringify(pending));
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Storage blocked: the redirect cannot be matched later and fails closed.
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/** Remember `payload` in the `key` store under a fresh random state, and return the state. */
|
|
33
|
-
export function beginState(key, payload) {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
const pending = readPending(key, now);
|
|
36
|
-
const state = hex(crypto.getRandomValues(new Uint8Array(16)));
|
|
37
|
-
pending[state] = { ...payload, expiresAt: now + LIFETIME_MS };
|
|
38
|
-
writePending(key, Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
|
|
39
|
-
return state;
|
|
40
|
-
}
|
|
41
|
-
/** Removes and returns what `state` was begun with, or null when this browser did not begin it. */
|
|
42
|
-
export function takeState(key, state) {
|
|
43
|
-
const pending = readPending(key, Date.now());
|
|
44
|
-
if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
|
|
45
|
-
return null;
|
|
46
|
-
const entry = pending[state];
|
|
47
|
-
delete pending[state];
|
|
48
|
-
writePending(key, pending);
|
|
49
|
-
return entry;
|
|
50
|
-
}
|
|
51
|
-
/** The challenge a verifier commits to: sha256 of its bytes, hex. */
|
|
52
|
-
export async function linkChallenge(verifier) {
|
|
53
|
-
const bytes = Uint8Array.from(verifier.match(/../g) ?? [], (b) => parseInt(b, 16));
|
|
54
|
-
return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Remember a link for `account` under a fresh verifier. Returns `returnTo`
|
|
58
|
-
* carrying its state, and the challenge to send the cloud in place of the verifier.
|
|
59
|
-
*/
|
|
60
|
-
export async function beginLinkState(account, returnTo) {
|
|
61
|
-
if (!ACCOUNT_HEX.test(account)) {
|
|
62
|
-
throw new Error("accountId must be 64 lowercase hex characters");
|
|
63
|
-
}
|
|
64
|
-
const url = new URL(returnTo);
|
|
65
|
-
const verifier = hex(crypto.getRandomValues(new Uint8Array(VERIFIER_BYTES)));
|
|
66
|
-
const challenge = await linkChallenge(verifier);
|
|
67
|
-
url.searchParams.set(STATE_PARAM, beginState(KEY, { account, verifier }));
|
|
68
|
-
return { callbackUrl: url.toString(), challenge };
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Removes the link a callback answers and returns its verifier, or null when
|
|
72
|
-
* this browser did not begin a link for `account` with this state.
|
|
73
|
-
*/
|
|
74
|
-
export function takeLinkVerifier(search, account) {
|
|
75
|
-
const pending = takeState(KEY, new URLSearchParams(search).get(STATE_PARAM));
|
|
76
|
-
if (!pending || pending.account !== account)
|
|
77
|
-
return null;
|
|
78
|
-
return typeof pending.verifier === "string" && VERIFIER_HEX.test(pending.verifier)
|
|
79
|
-
? pending.verifier
|
|
80
|
-
: null;
|
|
81
|
-
}
|
|
82
|
-
/** The claims of a link grant, or null when `grant` is not one. */
|
|
83
|
-
export function linkGrantClaims(grant) {
|
|
84
|
-
try {
|
|
85
|
-
const body = grant.split(".")[0].replace(/-/g, "+").replace(/_/g, "/");
|
|
86
|
-
const claims = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(body), (c) => c.charCodeAt(0))));
|
|
87
|
-
const { use, sub, acc, chal } = claims;
|
|
88
|
-
if (use !== LINK_GRANT_USE)
|
|
89
|
-
return null;
|
|
90
|
-
if (typeof sub !== "string" || typeof acc !== "string" || typeof chal !== "string")
|
|
91
|
-
return null;
|
|
92
|
-
return { login: sub, account: acc, challenge: chal };
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/** The query with the link state removed, for rewriting the address bar. */
|
|
99
|
-
export function withoutLinkState(search) {
|
|
100
|
-
const params = new URLSearchParams(search);
|
|
101
|
-
params.delete(STATE_PARAM);
|
|
102
|
-
const rest = params.toString();
|
|
103
|
-
return rest ? `?${rest}` : "";
|
|
104
|
-
}
|