@icp-sdk/auth 8.0.2 → 9.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/dist/esm/client/app-delegation-source.d.ts +65 -0
- package/dist/esm/client/app-delegation-source.js +58 -0
- package/dist/esm/client/app-delegation-source.js.map +1 -0
- package/dist/esm/client/app-lock.d.ts +63 -0
- package/dist/esm/client/app-lock.js +92 -0
- package/dist/esm/client/app-lock.js.map +1 -0
- package/dist/esm/client/auth-client.d.ts +270 -53
- package/dist/esm/client/auth-client.js +1017 -414
- package/dist/esm/client/auth-client.js.map +1 -1
- package/dist/esm/client/base64.d.ts +8 -0
- package/dist/esm/client/base64.js +28 -0
- package/dist/esm/client/base64.js.map +1 -0
- package/dist/esm/client/cookie-state-storage.d.ts +59 -0
- package/dist/esm/client/cookie-state-storage.js +245 -0
- package/dist/esm/client/cookie-state-storage.js.map +1 -0
- package/dist/esm/client/credential-storage.d.ts +48 -0
- package/dist/esm/client/credential-storage.js +2 -0
- package/dist/esm/client/credential-storage.js.map +1 -0
- package/dist/esm/client/db.d.ts +6 -1
- package/dist/esm/client/db.js +56 -15
- package/dist/esm/client/db.js.map +1 -1
- package/dist/esm/client/foreground-refresh.d.ts +35 -0
- package/dist/esm/client/foreground-refresh.js +83 -0
- package/dist/esm/client/foreground-refresh.js.map +1 -0
- package/dist/esm/client/idb-credential-storage.d.ts +25 -0
- package/dist/esm/client/idb-credential-storage.js +69 -0
- package/dist/esm/client/idb-credential-storage.js.map +1 -0
- package/dist/esm/client/index.d.ts +14 -3
- package/dist/esm/client/index.js +13 -3
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/local-credential-storage.d.ts +26 -0
- package/dist/esm/client/local-credential-storage.js +59 -0
- package/dist/esm/client/local-credential-storage.js.map +1 -0
- package/dist/esm/client/memory-credential-storage.d.ts +22 -0
- package/dist/esm/client/memory-credential-storage.js +35 -0
- package/dist/esm/client/memory-credential-storage.js.map +1 -0
- package/dist/esm/client/session-delegation.d.ts +40 -0
- package/dist/esm/client/session-delegation.js +100 -0
- package/dist/esm/client/session-delegation.js.map +1 -0
- package/dist/esm/client/session-identity.d.ts +97 -0
- package/dist/esm/client/session-identity.js +392 -0
- package/dist/esm/client/session-identity.js.map +1 -0
- package/dist/esm/client/session-minter.d.ts +94 -0
- package/dist/esm/client/session-minter.js +185 -0
- package/dist/esm/client/session-minter.js.map +1 -0
- package/dist/esm/client/shared-memory-credential-storage.d.ts +44 -0
- package/dist/esm/client/shared-memory-credential-storage.js +243 -0
- package/dist/esm/client/shared-memory-credential-storage.js.map +1 -0
- package/dist/esm/client/slots.d.ts +59 -0
- package/dist/esm/client/slots.js +67 -0
- package/dist/esm/client/slots.js.map +1 -0
- package/dist/esm/client/sso.d.ts +33 -0
- package/dist/esm/client/sso.js +120 -0
- package/dist/esm/client/sso.js.map +1 -0
- package/dist/esm/client/state-storage.d.ts +135 -0
- package/dist/esm/client/state-storage.js +178 -0
- package/dist/esm/client/state-storage.js.map +1 -0
- package/package.json +3 -2
- package/src/client/app-delegation-source.ts +86 -0
- package/src/client/app-lock.ts +120 -0
- package/src/client/auth-client.ts +1300 -487
- package/src/client/base64.ts +28 -0
- package/src/client/cookie-state-storage.ts +277 -0
- package/src/client/credential-storage.ts +56 -0
- package/src/client/db.ts +56 -12
- package/src/client/foreground-refresh.ts +92 -0
- package/src/client/idb-credential-storage.ts +82 -0
- package/src/client/index.ts +24 -8
- package/src/client/local-credential-storage.ts +68 -0
- package/src/client/memory-credential-storage.ts +41 -0
- package/src/client/session-delegation.ts +138 -0
- package/src/client/session-identity.ts +510 -0
- package/src/client/session-minter.ts +286 -0
- package/src/client/shared-memory-credential-storage.ts +303 -0
- package/src/client/slots.ts +78 -0
- package/src/client/sso.ts +122 -0
- package/src/client/state-storage.ts +284 -0
- package/dist/esm/client/idle-manager.d.ts +0 -83
- package/dist/esm/client/idle-manager.js +0 -100
- package/dist/esm/client/idle-manager.js.map +0 -1
- package/dist/esm/client/storage.d.ts +0 -51
- package/dist/esm/client/storage.js +0 -90
- package/dist/esm/client/storage.js.map +0 -1
- package/src/client/idle-manager.ts +0 -162
- package/src/client/storage.ts +0 -114
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type DerEncodedPublicKey, type HttpAgentOptions, type SignIdentity } from '@icp-sdk/core/agent';
|
|
2
|
+
import { DelegationChain } from '@icp-sdk/core/identity';
|
|
3
|
+
import type { Principal } from '@icp-sdk/core/principal';
|
|
4
|
+
import { type AppDelegationSource } from './app-delegation-source.js';
|
|
5
|
+
type AppSessionError = {
|
|
6
|
+
NoSuchSession: null;
|
|
7
|
+
} | {
|
|
8
|
+
NoSuchDelegation: null;
|
|
9
|
+
} | {
|
|
10
|
+
InternalCanisterError: string;
|
|
11
|
+
};
|
|
12
|
+
type Result<T> = {
|
|
13
|
+
Ok: T;
|
|
14
|
+
} | {
|
|
15
|
+
Err: AppSessionError;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Unwraps a canister result, turning the one terminal error into
|
|
19
|
+
* {@link SessionGoneError}, the retryable one into
|
|
20
|
+
* {@link NoSuchDelegationError}, and everything else into a plain failure.
|
|
21
|
+
*/
|
|
22
|
+
export declare const unwrapSessionResult: <T>(result: Result<T>) => T;
|
|
23
|
+
export interface SessionMinterOptions {
|
|
24
|
+
/** The key the session chain delegates to. Signs the calls made here. */
|
|
25
|
+
sessionKey: SignIdentity;
|
|
26
|
+
/** The chain proving those calls come from the session. Restricted to II. */
|
|
27
|
+
sessionChain: DelegationChain;
|
|
28
|
+
/** The canister these calls go to. */
|
|
29
|
+
canisterId: Principal;
|
|
30
|
+
/**
|
|
31
|
+
* Options for the agent making them. `identity` is not among them: the agent
|
|
32
|
+
* signs as the session, which is what a mint call rests on.
|
|
33
|
+
*/
|
|
34
|
+
agentOptions?: Omit<HttpAgentOptions, 'identity'>;
|
|
35
|
+
}
|
|
36
|
+
/** Mints app delegations by asking the Internet Identity canister for one. */
|
|
37
|
+
export declare class SessionMinter implements AppDelegationSource {
|
|
38
|
+
#private;
|
|
39
|
+
private constructor();
|
|
40
|
+
static create(options: SessionMinterOptions): Promise<SessionMinter>;
|
|
41
|
+
/**
|
|
42
|
+
* Prepares and fetches an app delegation, once more if the fetch finds nothing
|
|
43
|
+
* signed for what the prepare handed out.
|
|
44
|
+
*
|
|
45
|
+
* The pair is two calls, and only the first of them is an update: an expiration
|
|
46
|
+
* the canister will not sign for is one this session no longer holds, and the
|
|
47
|
+
* answer is to prepare again rather than to sign in again. Attempted a second
|
|
48
|
+
* time and no further — a third would be pursuing something other than the race
|
|
49
|
+
* this covers.
|
|
50
|
+
*/
|
|
51
|
+
mint(appPublicKey: DerEncodedPublicKey): Promise<DelegationChain>;
|
|
52
|
+
/**
|
|
53
|
+
* Ends the session at the canister.
|
|
54
|
+
*
|
|
55
|
+
* Idempotent at the canister: a session that is already gone is `Ok`, because
|
|
56
|
+
* the caller wanted it gone and it is gone. What comes back as an error is the
|
|
57
|
+
* canister failing to write, and it is raised rather than dropped — an
|
|
58
|
+
* application that told a user it signed them out of their apps should be able
|
|
59
|
+
* to find out that it did not. The call can also fail the way any update call
|
|
60
|
+
* can, and that rejection is the caller's to decide about too.
|
|
61
|
+
*/
|
|
62
|
+
revoke(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Assembles the one-hop chain an app signs its calls with, rooted at the
|
|
66
|
+
* account's own key.
|
|
67
|
+
*/
|
|
68
|
+
export declare function appDelegationChain(userKey: Uint8Array, signed: {
|
|
69
|
+
delegation: {
|
|
70
|
+
pubkey: Uint8Array;
|
|
71
|
+
expiration: bigint;
|
|
72
|
+
targets: [] | [Principal[]];
|
|
73
|
+
permissions: [] | [string];
|
|
74
|
+
};
|
|
75
|
+
signature: Uint8Array;
|
|
76
|
+
}): DelegationChain;
|
|
77
|
+
/**
|
|
78
|
+
* Refuses a chain that is not a session for `canisterId`.
|
|
79
|
+
*
|
|
80
|
+
* Targets restrict what the chain's final key may call, and the effective
|
|
81
|
+
* restriction is the intersection of the hops that name any: a hop with no
|
|
82
|
+
* targets imposes nothing, so one restricting hop is enough to confine the key.
|
|
83
|
+
* Internet Identity names the canister on both of its hops — on the one the
|
|
84
|
+
* canister signs to the provider's own key, and again on the provider's
|
|
85
|
+
* extension to the application's key — and the intersection is what is checked,
|
|
86
|
+
* so a hop that stopped naming it would still leave the chain confined by the
|
|
87
|
+
* other.
|
|
88
|
+
*
|
|
89
|
+
* So what is refused is a chain no hop restricts, which the session key would
|
|
90
|
+
* otherwise be able to sign any call with, and one whose restrictions do not
|
|
91
|
+
* come out as this canister alone.
|
|
92
|
+
*/
|
|
93
|
+
export declare function assertChainReaches(chain: DelegationChain, canisterId: Principal): void;
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Actor, HttpAgent, } from '@icp-sdk/core/agent';
|
|
2
|
+
import { Delegation, DelegationChain, DelegationIdentity } from '@icp-sdk/core/identity';
|
|
3
|
+
import { SessionGoneError } from './app-delegation-source.js';
|
|
4
|
+
/**
|
|
5
|
+
* Just the three methods an app calls with a session chain. Written out rather
|
|
6
|
+
* than generated because this package carries no Internet Identity declarations,
|
|
7
|
+
* and generating them for three methods would mean carrying the whole interface.
|
|
8
|
+
*/
|
|
9
|
+
const idlFactory = ({ IDL }) => {
|
|
10
|
+
const SessionKey = IDL.Vec(IDL.Nat8);
|
|
11
|
+
const Timestamp = IDL.Nat64;
|
|
12
|
+
const AppSessionError = IDL.Variant({
|
|
13
|
+
NoSuchSession: IDL.Null,
|
|
14
|
+
NoSuchDelegation: IDL.Null,
|
|
15
|
+
InternalCanisterError: IDL.Text,
|
|
16
|
+
});
|
|
17
|
+
const CandidDelegation = IDL.Record({
|
|
18
|
+
pubkey: SessionKey,
|
|
19
|
+
expiration: Timestamp,
|
|
20
|
+
targets: IDL.Opt(IDL.Vec(IDL.Principal)),
|
|
21
|
+
permissions: IDL.Opt(IDL.Text),
|
|
22
|
+
});
|
|
23
|
+
return IDL.Service({
|
|
24
|
+
app_prepare_delegation: IDL.Func([IDL.Record({ session_key: SessionKey })], [
|
|
25
|
+
IDL.Variant({
|
|
26
|
+
Ok: IDL.Record({ user_key: SessionKey, expiration: Timestamp }),
|
|
27
|
+
Err: AppSessionError,
|
|
28
|
+
}),
|
|
29
|
+
], []),
|
|
30
|
+
app_get_delegation: IDL.Func([IDL.Record({ session_key: SessionKey, expiration: Timestamp })], [
|
|
31
|
+
IDL.Variant({
|
|
32
|
+
Ok: IDL.Record({ delegation: CandidDelegation, signature: SessionKey }),
|
|
33
|
+
Err: AppSessionError,
|
|
34
|
+
}),
|
|
35
|
+
], ['query']),
|
|
36
|
+
app_revoke_session: IDL.Func([], [IDL.Variant({ Ok: IDL.Null, Err: AppSessionError })], []),
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const isGone = (error) => 'NoSuchSession' in error;
|
|
40
|
+
/**
|
|
41
|
+
* Raised when the canister signed nothing for the key and expiration asked for,
|
|
42
|
+
* which means the expiration was not one `app_prepare_delegation` returned.
|
|
43
|
+
*
|
|
44
|
+
* Its own error because the remedy is its own: prepare again and use what comes
|
|
45
|
+
* back. Signing in afresh is not it — the session is live, or the call would have
|
|
46
|
+
* been refused for being no session at all.
|
|
47
|
+
*/
|
|
48
|
+
class NoSuchDelegationError extends Error {
|
|
49
|
+
constructor() {
|
|
50
|
+
super('Internet Identity signed no delegation for this key and expiration');
|
|
51
|
+
this.name = 'NoSuchDelegationError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Unwraps a canister result, turning the one terminal error into
|
|
56
|
+
* {@link SessionGoneError}, the retryable one into
|
|
57
|
+
* {@link NoSuchDelegationError}, and everything else into a plain failure.
|
|
58
|
+
*/
|
|
59
|
+
export const unwrapSessionResult = (result) => {
|
|
60
|
+
if ('Ok' in result)
|
|
61
|
+
return result.Ok;
|
|
62
|
+
if (isGone(result.Err))
|
|
63
|
+
throw new SessionGoneError();
|
|
64
|
+
if ('NoSuchDelegation' in result.Err)
|
|
65
|
+
throw new NoSuchDelegationError();
|
|
66
|
+
// A candid variant, so interpolating it directly renders "[object Object]" and
|
|
67
|
+
// throws away the canister's own explanation of what went wrong.
|
|
68
|
+
const detail = 'InternalCanisterError' in result.Err ? result.Err.InternalCanisterError : '';
|
|
69
|
+
throw new Error(`Internet Identity could not mint a delegation${detail === '' ? '' : `: ${detail}`}`);
|
|
70
|
+
};
|
|
71
|
+
/** Mints app delegations by asking the Internet Identity canister for one. */
|
|
72
|
+
export class SessionMinter {
|
|
73
|
+
#service;
|
|
74
|
+
constructor(service) {
|
|
75
|
+
this.#service = service;
|
|
76
|
+
}
|
|
77
|
+
static async create(options) {
|
|
78
|
+
const { canisterId } = options;
|
|
79
|
+
assertChainReaches(options.sessionChain, canisterId);
|
|
80
|
+
const agent = await HttpAgent.create({
|
|
81
|
+
...options.agentOptions,
|
|
82
|
+
// Last, so no agent option can replace the session these calls are made as.
|
|
83
|
+
identity: DelegationIdentity.fromDelegation(options.sessionKey, options.sessionChain),
|
|
84
|
+
});
|
|
85
|
+
return new SessionMinter(Actor.createActor(idlFactory, { agent, canisterId }));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Prepares and fetches an app delegation, once more if the fetch finds nothing
|
|
89
|
+
* signed for what the prepare handed out.
|
|
90
|
+
*
|
|
91
|
+
* The pair is two calls, and only the first of them is an update: an expiration
|
|
92
|
+
* the canister will not sign for is one this session no longer holds, and the
|
|
93
|
+
* answer is to prepare again rather than to sign in again. Attempted a second
|
|
94
|
+
* time and no further — a third would be pursuing something other than the race
|
|
95
|
+
* this covers.
|
|
96
|
+
*/
|
|
97
|
+
async mint(appPublicKey) {
|
|
98
|
+
try {
|
|
99
|
+
return await this.#mintOnce(appPublicKey);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!(error instanceof NoSuchDelegationError))
|
|
103
|
+
throw error;
|
|
104
|
+
return await this.#mintOnce(appPublicKey);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Ends the session at the canister.
|
|
109
|
+
*
|
|
110
|
+
* Idempotent at the canister: a session that is already gone is `Ok`, because
|
|
111
|
+
* the caller wanted it gone and it is gone. What comes back as an error is the
|
|
112
|
+
* canister failing to write, and it is raised rather than dropped — an
|
|
113
|
+
* application that told a user it signed them out of their apps should be able
|
|
114
|
+
* to find out that it did not. The call can also fail the way any update call
|
|
115
|
+
* can, and that rejection is the caller's to decide about too.
|
|
116
|
+
*/
|
|
117
|
+
async revoke() {
|
|
118
|
+
unwrapSessionResult(await this.#service.app_revoke_session());
|
|
119
|
+
}
|
|
120
|
+
async #mintOnce(appPublicKey) {
|
|
121
|
+
const prepared = unwrapSessionResult(await this.#service.app_prepare_delegation({ session_key: appPublicKey }));
|
|
122
|
+
const signed = unwrapSessionResult(await this.#service.app_get_delegation({
|
|
123
|
+
session_key: appPublicKey,
|
|
124
|
+
// Exactly what was prepared. A different value is a different signature.
|
|
125
|
+
expiration: prepared.expiration,
|
|
126
|
+
}));
|
|
127
|
+
return appDelegationChain(prepared.user_key, signed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Assembles the one-hop chain an app signs its calls with, rooted at the
|
|
132
|
+
* account's own key.
|
|
133
|
+
*/
|
|
134
|
+
export function appDelegationChain(userKey, signed) {
|
|
135
|
+
// The signature covers whatever fields the canister set, and a delegation
|
|
136
|
+
// carrying permissions cannot be represented by the chain type, so sending one
|
|
137
|
+
// without it would fail verification at the boundary node with nothing to point
|
|
138
|
+
// at. Read-only sessions are unsupported until the type carries the field.
|
|
139
|
+
if (signed.delegation.permissions.length > 0) {
|
|
140
|
+
throw new Error('This session is read-only, which @icp-sdk/auth cannot act for yet: its delegations carry permissions that a delegation chain has no room for');
|
|
141
|
+
}
|
|
142
|
+
return DelegationChain.fromDelegations([
|
|
143
|
+
{
|
|
144
|
+
delegation: new Delegation(signed.delegation.pubkey, signed.delegation.expiration, signed.delegation.targets[0]),
|
|
145
|
+
signature: signed.signature,
|
|
146
|
+
},
|
|
147
|
+
], userKey);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Refuses a chain that is not a session for `canisterId`.
|
|
151
|
+
*
|
|
152
|
+
* Targets restrict what the chain's final key may call, and the effective
|
|
153
|
+
* restriction is the intersection of the hops that name any: a hop with no
|
|
154
|
+
* targets imposes nothing, so one restricting hop is enough to confine the key.
|
|
155
|
+
* Internet Identity names the canister on both of its hops — on the one the
|
|
156
|
+
* canister signs to the provider's own key, and again on the provider's
|
|
157
|
+
* extension to the application's key — and the intersection is what is checked,
|
|
158
|
+
* so a hop that stopped naming it would still leave the chain confined by the
|
|
159
|
+
* other.
|
|
160
|
+
*
|
|
161
|
+
* So what is refused is a chain no hop restricts, which the session key would
|
|
162
|
+
* otherwise be able to sign any call with, and one whose restrictions do not
|
|
163
|
+
* come out as this canister alone.
|
|
164
|
+
*/
|
|
165
|
+
export function assertChainReaches(chain, canisterId) {
|
|
166
|
+
const hops = chain.delegations.map(({ delegation }) => delegation.targets);
|
|
167
|
+
const restricting = hops.filter((hopTargets) => hopTargets !== undefined);
|
|
168
|
+
if (restricting.length === 0) {
|
|
169
|
+
throw new Error(`A session chain must be restricted to ${canisterId.toText()}, but no hop of this one names any canister`);
|
|
170
|
+
}
|
|
171
|
+
// An empty list names nothing rather than naming this canister, and a chain
|
|
172
|
+
// that can call nothing is not a session either.
|
|
173
|
+
const namingNothing = restricting.some((hopTargets) => hopTargets.length === 0);
|
|
174
|
+
if (namingNothing) {
|
|
175
|
+
throw new Error(`A session chain must be restricted to ${canisterId.toText()}, but a hop of this one names nothing`);
|
|
176
|
+
}
|
|
177
|
+
const elsewhere = restricting
|
|
178
|
+
.flat()
|
|
179
|
+
.filter((target) => target.compareTo(canisterId) !== 'eq')
|
|
180
|
+
.map((target) => target.toText());
|
|
181
|
+
if (elsewhere.length > 0) {
|
|
182
|
+
throw new Error(`A session chain must be restricted to ${canisterId.toText()}, but this one also names ${[...new Set(elsewhere)].join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=session-minter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-minter.js","sourceRoot":"","sources":["../../../src/client/session-minter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ECDSAKeyIdentity } from '@icp-sdk/core/identity';
|
|
2
|
+
import type { Credential, CredentialStorage } from './credential-storage.js';
|
|
3
|
+
/** The channel and lock prefix every instance agrees on by default. */
|
|
4
|
+
export declare const DEFAULT_SHARED_MEMORY_NAME = "ic-credentials";
|
|
5
|
+
/**
|
|
6
|
+
* Credentials in memory, replicated to the other tabs of this origin.
|
|
7
|
+
*
|
|
8
|
+
* For an environment with no durable medium — a browser with IndexedDB and
|
|
9
|
+
* `localStorage` both unavailable, or an application that has decided nothing may
|
|
10
|
+
* reach disk — where every tab minting for itself is still worth avoiding.
|
|
11
|
+
*
|
|
12
|
+
* Every write is broadcast and every instance applies what it receives, so a
|
|
13
|
+
* steady-state read is local and immediate: the medium is a `Map`, and the channel
|
|
14
|
+
* is what keeps the Maps agreeing. A `CryptoKeyPair` survives a structured clone
|
|
15
|
+
* as a handle that signs and cannot be exported, so what crosses is usable
|
|
16
|
+
* without key material having been sent.
|
|
17
|
+
*
|
|
18
|
+
* Nothing survives the document, which is what `durable` says. What that costs is
|
|
19
|
+
* the last tab of an origin closing: the credentials go with it, and the next load
|
|
20
|
+
* signs in again.
|
|
21
|
+
* @see implements {@link CredentialStorage}
|
|
22
|
+
*/
|
|
23
|
+
export declare class SharedMemoryCredentialStorage implements CredentialStorage<ECDSAKeyIdentity> {
|
|
24
|
+
#private;
|
|
25
|
+
readonly shared = true;
|
|
26
|
+
readonly durable = false;
|
|
27
|
+
/**
|
|
28
|
+
* @param options - The channel and lock prefix the tabs of this origin share.
|
|
29
|
+
*/
|
|
30
|
+
constructor(options?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Stops replicating and lets go of the name a peer waits on.
|
|
35
|
+
*
|
|
36
|
+
* Only needed when you replace a store while the page lives; the document
|
|
37
|
+
* going away does the same thing.
|
|
38
|
+
*/
|
|
39
|
+
close(): void;
|
|
40
|
+
create(): Promise<ECDSAKeyIdentity>;
|
|
41
|
+
get(slot: string): Promise<Credential<ECDSAKeyIdentity> | null>;
|
|
42
|
+
set(slot: string, credential: Credential<ECDSAKeyIdentity>): Promise<void>;
|
|
43
|
+
remove(slot: string): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { DelegationChain, ECDSAKeyIdentity } from '@icp-sdk/core/identity';
|
|
2
|
+
/** The channel and lock prefix every instance agrees on by default. */
|
|
3
|
+
export const DEFAULT_SHARED_MEMORY_NAME = 'ic-credentials';
|
|
4
|
+
// Narrowing, not protection. A channel reaches one origin, so a stranger on the
|
|
5
|
+
// name is our own code through a deploy or an application posting on it, and what
|
|
6
|
+
// keeps those harmless is elsewhere: the switch answers nothing it does not
|
|
7
|
+
// recognise, and rebuilding a record catches its own failures. This is what gives
|
|
8
|
+
// `data` a type, so the switch can read the fields it needs.
|
|
9
|
+
const isMessage = (value) => {
|
|
10
|
+
if (typeof value !== 'object' || value === null || !('kind' in value))
|
|
11
|
+
return false;
|
|
12
|
+
const message = value;
|
|
13
|
+
switch (message.kind) {
|
|
14
|
+
case 'ask':
|
|
15
|
+
return true;
|
|
16
|
+
case 'drop':
|
|
17
|
+
return typeof message.slot === 'string';
|
|
18
|
+
case 'put':
|
|
19
|
+
return typeof message.slot === 'string' && 'keyPair' in message;
|
|
20
|
+
case 'offer':
|
|
21
|
+
return Array.isArray(message.entries);
|
|
22
|
+
default:
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Credentials in memory, replicated to the other tabs of this origin.
|
|
28
|
+
*
|
|
29
|
+
* For an environment with no durable medium — a browser with IndexedDB and
|
|
30
|
+
* `localStorage` both unavailable, or an application that has decided nothing may
|
|
31
|
+
* reach disk — where every tab minting for itself is still worth avoiding.
|
|
32
|
+
*
|
|
33
|
+
* Every write is broadcast and every instance applies what it receives, so a
|
|
34
|
+
* steady-state read is local and immediate: the medium is a `Map`, and the channel
|
|
35
|
+
* is what keeps the Maps agreeing. A `CryptoKeyPair` survives a structured clone
|
|
36
|
+
* as a handle that signs and cannot be exported, so what crosses is usable
|
|
37
|
+
* without key material having been sent.
|
|
38
|
+
*
|
|
39
|
+
* Nothing survives the document, which is what `durable` says. What that costs is
|
|
40
|
+
* the last tab of an origin closing: the credentials go with it, and the next load
|
|
41
|
+
* signs in again.
|
|
42
|
+
* @see implements {@link CredentialStorage}
|
|
43
|
+
*/
|
|
44
|
+
export class SharedMemoryCredentialStorage {
|
|
45
|
+
shared = true;
|
|
46
|
+
durable = false;
|
|
47
|
+
#name;
|
|
48
|
+
#channel;
|
|
49
|
+
// On the instance and never on the module, so two clients on one page keep
|
|
50
|
+
// their own replicas and no state carries between tests in one process.
|
|
51
|
+
#replica = new Map();
|
|
52
|
+
// Held for this instance's life under a name only this instance uses, so a peer
|
|
53
|
+
// can wait for the grant that means this one is gone. The browser releases it
|
|
54
|
+
// when the document is torn down, which is the signal no message could carry.
|
|
55
|
+
#id = crypto.randomUUID();
|
|
56
|
+
#releasePresence;
|
|
57
|
+
#announceOffer;
|
|
58
|
+
// Applying an incoming record is asynchronous, because a key handle and a chain
|
|
59
|
+
// have to be rebuilt from what crossed. Queued so they land in the order they
|
|
60
|
+
// arrived: a put applied after the drop that followed it would resurrect a slot
|
|
61
|
+
// its owner had already cleared.
|
|
62
|
+
#applying = Promise.resolve();
|
|
63
|
+
/**
|
|
64
|
+
* Resolves once there is nothing left to wait for: a peer has answered, or
|
|
65
|
+
* every peer that existed when this instance started has gone.
|
|
66
|
+
*
|
|
67
|
+
* Reads await it, so the first one after a cold start sees what the other tabs
|
|
68
|
+
* hold rather than a miss. There is no deadline in it — waiting ends on a lock
|
|
69
|
+
* grant, and a lone instance waits not at all because there is no peer to hold
|
|
70
|
+
* one.
|
|
71
|
+
*/
|
|
72
|
+
#synced;
|
|
73
|
+
/**
|
|
74
|
+
* @param options - The channel and lock prefix the tabs of this origin share.
|
|
75
|
+
*/
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.#name = options?.name ?? DEFAULT_SHARED_MEMORY_NAME;
|
|
78
|
+
this.#channel =
|
|
79
|
+
typeof BroadcastChannel === 'undefined' ? undefined : new BroadcastChannel(this.#name);
|
|
80
|
+
this.#channel?.addEventListener('message', (event) => this.#receive(event.data));
|
|
81
|
+
this.#synced = this.#openReplica();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Stops replicating and lets go of the name a peer waits on.
|
|
85
|
+
*
|
|
86
|
+
* Only needed when you replace a store while the page lives; the document
|
|
87
|
+
* going away does the same thing.
|
|
88
|
+
*/
|
|
89
|
+
close() {
|
|
90
|
+
this.#channel?.close();
|
|
91
|
+
this.#releasePresence?.();
|
|
92
|
+
this.#releasePresence = undefined;
|
|
93
|
+
}
|
|
94
|
+
create() {
|
|
95
|
+
return ECDSAKeyIdentity.generate({ extractable: false });
|
|
96
|
+
}
|
|
97
|
+
async get(slot) {
|
|
98
|
+
await this.#synced;
|
|
99
|
+
// A read reflects everything received, not everything that has finished being
|
|
100
|
+
// rebuilt: applying is asynchronous, so a message already in hand would
|
|
101
|
+
// otherwise read as a miss and cost the caller a credential it had.
|
|
102
|
+
await this.#applying;
|
|
103
|
+
return this.#replica.get(slot) ?? null;
|
|
104
|
+
}
|
|
105
|
+
async set(slot, credential) {
|
|
106
|
+
this.#replica.set(slot, credential);
|
|
107
|
+
this.#post({ kind: 'put', ...this.#toWire(slot, credential) });
|
|
108
|
+
}
|
|
109
|
+
async remove(slot) {
|
|
110
|
+
this.#replica.delete(slot);
|
|
111
|
+
this.#post({ kind: 'drop', slot });
|
|
112
|
+
}
|
|
113
|
+
#presenceName(id) {
|
|
114
|
+
return `${this.#name}:alive:${id}`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Takes this instance's presence lock, then waits for the peers that already
|
|
118
|
+
* had one — asking them for what they hold.
|
|
119
|
+
*/
|
|
120
|
+
async #openReplica() {
|
|
121
|
+
const locks = globalThis.navigator?.locks;
|
|
122
|
+
if (locks === undefined || this.#channel === undefined) {
|
|
123
|
+
// Nothing can be enumerated and nothing can answer, so waiting could only
|
|
124
|
+
// cost a load. Every instance is alone, which is calls and not correctness.
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const peers = await this.#claimPresence(locks);
|
|
128
|
+
if (peers.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
const answered = new Promise((resolve) => {
|
|
131
|
+
this.#announceOffer = resolve;
|
|
132
|
+
});
|
|
133
|
+
this.#post({ kind: 'ask' });
|
|
134
|
+
// A peer that answers ends the wait; a peer that dies without answering ends
|
|
135
|
+
// it too, because its lock is granted the moment the browser releases it. So
|
|
136
|
+
// the wait is bounded by the peers themselves rather than by a number.
|
|
137
|
+
const goneEverywhere = Promise.all(peers.map((name) => locks.request(name, { mode: 'exclusive' }, () => undefined)));
|
|
138
|
+
await Promise.race([answered, goneEverywhere]);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Enumerates the peers holding a presence lock, then takes this instance's own.
|
|
142
|
+
*
|
|
143
|
+
* In that order: a snapshot taken after would include this instance, and it
|
|
144
|
+
* would then wait for a lock it holds itself and never be granted.
|
|
145
|
+
*/
|
|
146
|
+
async #claimPresence(locks) {
|
|
147
|
+
const mine = this.#presenceName(this.#id);
|
|
148
|
+
const prefix = `${this.#name}:alive:`;
|
|
149
|
+
let peers = [];
|
|
150
|
+
try {
|
|
151
|
+
const snapshot = await locks.query();
|
|
152
|
+
peers = (snapshot.held ?? [])
|
|
153
|
+
.map((lock) => lock.name)
|
|
154
|
+
.filter((name) => name !== undefined && name !== mine)
|
|
155
|
+
.filter((name) => name.startsWith(prefix));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// query() is not everywhere, and a caller cannot act on its absence. With
|
|
159
|
+
// no snapshot there is no peer to wait for, so this instance starts alone.
|
|
160
|
+
}
|
|
161
|
+
// Not awaited: the request resolves only when the lock is released, which is
|
|
162
|
+
// when this instance is done with it.
|
|
163
|
+
void locks
|
|
164
|
+
.request(mine, { mode: 'exclusive' }, () => {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
this.#releasePresence = resolve;
|
|
167
|
+
});
|
|
168
|
+
})
|
|
169
|
+
.catch(() => undefined);
|
|
170
|
+
return peers;
|
|
171
|
+
}
|
|
172
|
+
#receive(data) {
|
|
173
|
+
if (!isMessage(data))
|
|
174
|
+
return;
|
|
175
|
+
if (data.kind === 'ask') {
|
|
176
|
+
// Answered from what is queued as well as what has landed, so an instance
|
|
177
|
+
// asked while still applying an earlier message does not describe a state
|
|
178
|
+
// it is halfway out of.
|
|
179
|
+
this.#enqueue(() => {
|
|
180
|
+
const entries = [...this.#replica].map(([slot, credential]) => this.#toWire(slot, credential));
|
|
181
|
+
// Answered even when empty, because the asker is waiting on an answer and
|
|
182
|
+
// "I have nothing" is one. Staying silent would make it wait for this
|
|
183
|
+
// instance to close.
|
|
184
|
+
this.#post({ kind: 'offer', entries });
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.#enqueue(async () => {
|
|
189
|
+
switch (data.kind) {
|
|
190
|
+
case 'offer':
|
|
191
|
+
for (const entry of data.entries)
|
|
192
|
+
await this.#apply(entry, { overwrite: false });
|
|
193
|
+
// Last, so the answer is complete before anything waiting on it wakes.
|
|
194
|
+
this.#announceOffer?.();
|
|
195
|
+
this.#announceOffer = undefined;
|
|
196
|
+
return;
|
|
197
|
+
case 'put':
|
|
198
|
+
await this.#apply(data, { overwrite: true });
|
|
199
|
+
return;
|
|
200
|
+
case 'drop':
|
|
201
|
+
this.#replica.delete(data.slot);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
#enqueue(work) {
|
|
207
|
+
this.#applying = this.#applying.then(work).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
async #apply(entry, { overwrite }) {
|
|
210
|
+
// An offer answers a question asked before anything local was written, so it
|
|
211
|
+
// fills gaps rather than replacing: a write that happened in the meantime is
|
|
212
|
+
// this instance's own and newer than the snapshot being described.
|
|
213
|
+
if (!overwrite && this.#replica.has(entry.slot))
|
|
214
|
+
return;
|
|
215
|
+
try {
|
|
216
|
+
const identity = await ECDSAKeyIdentity.fromKeyPair(entry.keyPair);
|
|
217
|
+
const chain = entry.chain === undefined ? undefined : DelegationChain.fromJSON(entry.chain);
|
|
218
|
+
this.#replica.set(entry.slot, { identity, chain });
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// A record from an incompatible version, or a handle this context cannot
|
|
222
|
+
// adopt. Holding nothing lets the reader acquire again.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
#toWire(slot, credential) {
|
|
226
|
+
return {
|
|
227
|
+
slot,
|
|
228
|
+
keyPair: credential.identity.getKeyPair(),
|
|
229
|
+
chain: credential.chain && JSON.stringify(credential.chain.toJSON()),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
#post(message) {
|
|
233
|
+
try {
|
|
234
|
+
this.#channel?.postMessage(message);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// A closed channel, or a payload this browser will not clone. The write has
|
|
238
|
+
// already landed in this instance's own replica, so the cost is a peer
|
|
239
|
+
// missing it and minting for itself.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=shared-memory-credential-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-memory-credential-storage.js","sourceRoot":"","sources":["../../../src/client/shared-memory-credential-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every name one client writes under, and the one thing that knows the whole set.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than living beside the credential store or the state
|
|
5
|
+
* store, because it belongs to neither: the credentials and the state of a
|
|
6
|
+
* sign-in are named from one namespace, and a list that sat in either file would
|
|
7
|
+
* have to reach across to the other.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The bare names, before a namespace is applied.
|
|
11
|
+
*
|
|
12
|
+
* One list, so a name cannot be added to some of the places that need it and
|
|
13
|
+
* missed in others. It was two lists and a hand-written mapping, and the state
|
|
14
|
+
* record was left out of the namespacing for exactly that reason.
|
|
15
|
+
*/
|
|
16
|
+
declare const SLOT_NAMES: {
|
|
17
|
+
/** The session's key and the chain issued to it. */
|
|
18
|
+
readonly session: "session";
|
|
19
|
+
/**
|
|
20
|
+
* Where a sign-in ceremony keeps its key until it returns.
|
|
21
|
+
*
|
|
22
|
+
* Its own name rather than {@link SLOT_NAMES.session}, so a ceremony that is
|
|
23
|
+
* cancelled or never comes back cannot take a working session with it.
|
|
24
|
+
*/
|
|
25
|
+
readonly sessionPending: "session-pending";
|
|
26
|
+
/** The key an application signs with, and its delegation. */
|
|
27
|
+
readonly app: "app";
|
|
28
|
+
/**
|
|
29
|
+
* Where a ceremony keeps the credential it minted until its session is stored.
|
|
30
|
+
*
|
|
31
|
+
* The store reaches every tab of the origin, so writing to the app name during
|
|
32
|
+
* a ceremony — or clearing it first — would change what those tabs act with
|
|
33
|
+
* before the sign-in has succeeded. This is promoted instead, once there is a
|
|
34
|
+
* session behind it.
|
|
35
|
+
*/
|
|
36
|
+
readonly appPending: "app-pending";
|
|
37
|
+
/**
|
|
38
|
+
* The record of who is signed in here and until when.
|
|
39
|
+
*
|
|
40
|
+
* Not a credential slot, and named here anyway: a namespace that moved the
|
|
41
|
+
* credentials and left this fixed would give two clients their own keys and one
|
|
42
|
+
* shared answer to who is signed in.
|
|
43
|
+
*/
|
|
44
|
+
readonly state: "ic-session-state";
|
|
45
|
+
};
|
|
46
|
+
/** Every name a client writes under, with its namespace already applied. */
|
|
47
|
+
export type Slots = Record<keyof typeof SLOT_NAMES, string>;
|
|
48
|
+
/**
|
|
49
|
+
* The names one client writes under, prefixed where a namespace is set.
|
|
50
|
+
*
|
|
51
|
+
* Assigned in one place rather than defaulted by each store: implementations
|
|
52
|
+
* choosing their own is what produced three colliding slots in the arrangement
|
|
53
|
+
* this replaces, and one assigner cannot collide with itself. A namespace moves
|
|
54
|
+
* all of them at once, so an application running two clients under one origin
|
|
55
|
+
* separates them with one string rather than renaming some and missing others.
|
|
56
|
+
* @param namespace - Prefix for every name, or `undefined` for the bare ones.
|
|
57
|
+
*/
|
|
58
|
+
export declare function slotsFor(namespace?: string): Slots;
|
|
59
|
+
export {};
|