@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
package/src/client/index.ts
CHANGED
|
@@ -2,13 +2,29 @@
|
|
|
2
2
|
* @module api/client
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export { type AppDelegationSource, SessionGoneError } from './app-delegation-source.js';
|
|
5
6
|
export * from './auth-client.js';
|
|
6
|
-
export { type DBCreateOptions, IdbKeyVal } from './db.js';
|
|
7
|
-
export * from './idle-manager.js';
|
|
8
7
|
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './
|
|
8
|
+
CookieStateStorage,
|
|
9
|
+
type CookieStateStorageOptions,
|
|
10
|
+
} from './cookie-state-storage.js';
|
|
11
|
+
export type { Credential, CredentialStorage } from './credential-storage.js';
|
|
12
|
+
export { DB_VERSION, type DBCreateOptions, IdbKeyVal } from './db.js';
|
|
13
|
+
export { watchActivity, watchForeground } from './foreground-refresh.js';
|
|
14
|
+
export { IdbCredentialStorage } from './idb-credential-storage.js';
|
|
15
|
+
export { LocalCredentialStorage } from './local-credential-storage.js';
|
|
16
|
+
export { MemoryCredentialStorage } from './memory-credential-storage.js';
|
|
17
|
+
export { InteractionRequiredError, requestSessionDelegation } from './session-delegation.js';
|
|
18
|
+
export { SessionIdentity, type SessionIdentityOptions } from './session-identity.js';
|
|
19
|
+
export { SessionMinter, type SessionMinterOptions } from './session-minter.js';
|
|
20
|
+
export {
|
|
21
|
+
DEFAULT_SHARED_MEMORY_NAME,
|
|
22
|
+
SharedMemoryCredentialStorage,
|
|
23
|
+
} from './shared-memory-credential-storage.js';
|
|
24
|
+
export { isValidSsoDomain } from './sso.js';
|
|
25
|
+
export {
|
|
26
|
+
LocalStateStorage,
|
|
27
|
+
MemoryStateStorage,
|
|
28
|
+
type SessionState,
|
|
29
|
+
type StateStorage,
|
|
30
|
+
} from './state-storage.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DelegationChain, Ed25519KeyIdentity } from '@icp-sdk/core/identity';
|
|
2
|
+
import type { Credential, CredentialStorage } from './credential-storage.js';
|
|
3
|
+
|
|
4
|
+
/** What one slot holds: the key as JSON, and the chain as JSON. */
|
|
5
|
+
interface StoredCredential {
|
|
6
|
+
key: string;
|
|
7
|
+
chain?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Credentials in `localStorage`, for where IndexedDB is unavailable.
|
|
12
|
+
*
|
|
13
|
+
* `localStorage` holds strings, so the key has to be one: this generates an
|
|
14
|
+
* Ed25519 identity, whose private bytes serialise, rather than the
|
|
15
|
+
* non-extractable key {@link IdbCredentialStorage} uses. That is the trade the
|
|
16
|
+
* medium imposes, and it is why generation belongs to the store.
|
|
17
|
+
* @see implements {@link CredentialStorage}
|
|
18
|
+
*/
|
|
19
|
+
export class LocalCredentialStorage implements CredentialStorage<Ed25519KeyIdentity> {
|
|
20
|
+
public readonly shared = true;
|
|
21
|
+
public readonly durable = true;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param prefix - Prepended to every slot, so two clients under one origin can
|
|
25
|
+
* be kept apart.
|
|
26
|
+
*/
|
|
27
|
+
constructor(public readonly prefix = 'ic-') {}
|
|
28
|
+
|
|
29
|
+
public create(): Promise<Ed25519KeyIdentity> {
|
|
30
|
+
return Promise.resolve(Ed25519KeyIdentity.generate());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public get(slot: string): Promise<Credential<Ed25519KeyIdentity> | null> {
|
|
34
|
+
const raw = this.#localStorage().getItem(this.prefix + slot);
|
|
35
|
+
if (raw === null) return Promise.resolve(null);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const stored = JSON.parse(raw) as StoredCredential;
|
|
39
|
+
const identity = Ed25519KeyIdentity.fromJSON(stored.key);
|
|
40
|
+
const chain = stored.chain === undefined ? undefined : DelegationChain.fromJSON(stored.chain);
|
|
41
|
+
return Promise.resolve({ identity, chain });
|
|
42
|
+
} catch {
|
|
43
|
+
return Promise.resolve(null);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public set(slot: string, credential: Credential<Ed25519KeyIdentity>): Promise<void> {
|
|
48
|
+
const stored: StoredCredential = {
|
|
49
|
+
key: JSON.stringify(credential.identity.toJSON()),
|
|
50
|
+
chain: credential.chain && JSON.stringify(credential.chain.toJSON()),
|
|
51
|
+
};
|
|
52
|
+
this.#localStorage().setItem(this.prefix + slot, JSON.stringify(stored));
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public remove(slot: string): Promise<void> {
|
|
57
|
+
this.#localStorage().removeItem(this.prefix + slot);
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#localStorage(): Storage {
|
|
62
|
+
const ls = globalThis.localStorage;
|
|
63
|
+
if (!ls) {
|
|
64
|
+
throw new Error('Could not find local storage.');
|
|
65
|
+
}
|
|
66
|
+
return ls;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ECDSAKeyIdentity } from '@icp-sdk/core/identity';
|
|
2
|
+
import type { Credential, CredentialStorage } from './credential-storage.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Credentials held for the lifetime of the instance and shared with nothing.
|
|
6
|
+
*
|
|
7
|
+
* For tests and for environments with neither IndexedDB nor `localStorage`. A
|
|
8
|
+
* reload starts from nothing, and other tabs see nothing, so every tab of an
|
|
9
|
+
* origin is alone.
|
|
10
|
+
*
|
|
11
|
+
* Records are handed back as they were given rather than round-tripped through
|
|
12
|
+
* an encoding, which is what lets this hold a non-extractable key at all.
|
|
13
|
+
* @see implements {@link CredentialStorage}
|
|
14
|
+
*/
|
|
15
|
+
export class MemoryCredentialStorage implements CredentialStorage<ECDSAKeyIdentity> {
|
|
16
|
+
public readonly shared = false;
|
|
17
|
+
public readonly durable = false;
|
|
18
|
+
|
|
19
|
+
// On the instance and never on the module: two clients on one page must not
|
|
20
|
+
// see each other's slots, and no state may carry between tests in one process.
|
|
21
|
+
#credentials = new Map<string, Credential<ECDSAKeyIdentity>>();
|
|
22
|
+
|
|
23
|
+
public create(): Promise<ECDSAKeyIdentity> {
|
|
24
|
+
// Nothing is serialised here, so the key never has to be readable.
|
|
25
|
+
return ECDSAKeyIdentity.generate({ extractable: false });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public get(slot: string): Promise<Credential<ECDSAKeyIdentity> | null> {
|
|
29
|
+
return Promise.resolve(this.#credentials.get(slot) ?? null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public set(slot: string, credential: Credential<ECDSAKeyIdentity>): Promise<void> {
|
|
33
|
+
this.#credentials.set(slot, credential);
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public remove(slot: string): Promise<void> {
|
|
38
|
+
this.#credentials.delete(slot);
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { DerEncodedPublicKey } from '@icp-sdk/core/agent';
|
|
2
|
+
import { Delegation, DelegationChain } from '@icp-sdk/core/identity';
|
|
3
|
+
import { Principal } from '@icp-sdk/core/principal';
|
|
4
|
+
import type { Signer } from '@icp-sdk/signer';
|
|
5
|
+
import { fromBase64, toBase64 } from './base64.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The method Internet Identity answers with a session rather than a long-lived
|
|
9
|
+
* delegation. Named for its provider because a JSON-RPC method travels over a
|
|
10
|
+
* transport shared with other signers, unlike a canister method.
|
|
11
|
+
*/
|
|
12
|
+
const SESSION_DELEGATION_METHOD = 'ii_session_delegation';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The code Internet Identity denies a `prompt: 'none'` request with.
|
|
16
|
+
*
|
|
17
|
+
* In ICRC-25's 3xxx user-action range, which is what lets a caller tell a request
|
|
18
|
+
* that needs a ceremony apart from a transport or protocol failure.
|
|
19
|
+
*/
|
|
20
|
+
const INTERACTION_REQUIRED_CODE = 3002;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Internet Identity had nothing to answer a silent request from.
|
|
24
|
+
*
|
|
25
|
+
* The one denial that is not a failure: it says a ceremony is needed, so the
|
|
26
|
+
* caller signs in interactively rather than retrying. It is also the only
|
|
27
|
+
* dependable evidence that a shared record is stale rather than replaced, which
|
|
28
|
+
* is what lets an origin retract a record it did not write.
|
|
29
|
+
*
|
|
30
|
+
* Thrown only for a request that asked not to render anything. An interactive
|
|
31
|
+
* sign-in that the user abandons is a different outcome and is not this.
|
|
32
|
+
*/
|
|
33
|
+
export class InteractionRequiredError extends Error {
|
|
34
|
+
/**
|
|
35
|
+
* Why, where Internet Identity said: `login_required` where it holds no
|
|
36
|
+
* session, `account_selection_required` where it holds more than one and the
|
|
37
|
+
* request named none.
|
|
38
|
+
*
|
|
39
|
+
* A caller may word its prompt from this and does not have to branch on it, so
|
|
40
|
+
* it is deliberately a string rather than a union — a value this library does
|
|
41
|
+
* not know is not a reason to fail.
|
|
42
|
+
*/
|
|
43
|
+
readonly reason: string | undefined;
|
|
44
|
+
|
|
45
|
+
constructor(message: string, reason?: string) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'InteractionRequiredError';
|
|
48
|
+
this.reason = reason;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SessionDelegationResult {
|
|
53
|
+
publicKey: string;
|
|
54
|
+
signerDelegation: {
|
|
55
|
+
delegation: { pubkey: string; expiration: string; targets?: string[] };
|
|
56
|
+
signature: string;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const looksLikeResult = (value: unknown): value is SessionDelegationResult => {
|
|
61
|
+
const result = value as SessionDelegationResult | undefined;
|
|
62
|
+
return (
|
|
63
|
+
typeof result?.publicKey === 'string' &&
|
|
64
|
+
Array.isArray(result?.signerDelegation) &&
|
|
65
|
+
// An empty list builds a chain whose earliest expiry is 0, so it reads as a
|
|
66
|
+
// session that expired at the epoch rather than as a bad response.
|
|
67
|
+
result.signerDelegation.length > 0
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Asks the identity provider for a session, and returns the chain it signed.
|
|
73
|
+
*
|
|
74
|
+
* The chain is restricted to the Internet Identity canister, so it is not
|
|
75
|
+
* something an application can call its own canisters with. It is what app
|
|
76
|
+
* delegations are minted from.
|
|
77
|
+
*/
|
|
78
|
+
export async function requestSessionDelegation(
|
|
79
|
+
signer: Signer,
|
|
80
|
+
params: {
|
|
81
|
+
sessionPublicKey: DerEncodedPublicKey;
|
|
82
|
+
maxTimeToLive?: bigint;
|
|
83
|
+
maxTimeToIdle?: bigint;
|
|
84
|
+
derivationOrigin?: string;
|
|
85
|
+
},
|
|
86
|
+
): Promise<DelegationChain> {
|
|
87
|
+
const response = await signer.sendRequest({
|
|
88
|
+
jsonrpc: '2.0',
|
|
89
|
+
id: globalThis.crypto.randomUUID(),
|
|
90
|
+
method: SESSION_DELEGATION_METHOD,
|
|
91
|
+
params: {
|
|
92
|
+
sessionPublicKey: toBase64(params.sessionPublicKey),
|
|
93
|
+
...(params.maxTimeToLive === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { maxTimeToLive: params.maxTimeToLive.toString() }),
|
|
96
|
+
...(params.maxTimeToIdle === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { maxTimeToIdle: params.maxTimeToIdle.toString() }),
|
|
99
|
+
...(params.derivationOrigin === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { icrc95DerivationOrigin: params.derivationOrigin }),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if ('error' in response) {
|
|
106
|
+
// The code carries the only thing a caller can act on, and dropping it left
|
|
107
|
+
// `interaction_required` indistinguishable from a transport failure except by
|
|
108
|
+
// matching a message this library does not define.
|
|
109
|
+
if (response.error.code === INTERACTION_REQUIRED_CODE) {
|
|
110
|
+
const data = response.error.data;
|
|
111
|
+
const reason =
|
|
112
|
+
typeof data === 'object' && data !== null && 'reason' in data
|
|
113
|
+
? String((data as { reason: unknown }).reason)
|
|
114
|
+
: undefined;
|
|
115
|
+
throw new InteractionRequiredError(response.error.message, reason);
|
|
116
|
+
}
|
|
117
|
+
throw new Error(response.error.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = response.result;
|
|
121
|
+
if (!looksLikeResult(result)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'Invalid session response: missing publicKey, or missing or empty signerDelegation',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return DelegationChain.fromDelegations(
|
|
128
|
+
result.signerDelegation.map(({ delegation, signature }) => ({
|
|
129
|
+
delegation: new Delegation(
|
|
130
|
+
fromBase64(delegation.pubkey),
|
|
131
|
+
BigInt(delegation.expiration),
|
|
132
|
+
delegation.targets?.map((target) => Principal.fromText(target)),
|
|
133
|
+
),
|
|
134
|
+
signature: fromBase64(signature) as DelegationChain['delegations'][number]['signature'],
|
|
135
|
+
})),
|
|
136
|
+
fromBase64(result.publicKey) as DerEncodedPublicKey,
|
|
137
|
+
);
|
|
138
|
+
}
|