@icp-sdk/auth 8.0.3 → 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.
Files changed (85) hide show
  1. package/dist/esm/client/app-delegation-source.d.ts +65 -0
  2. package/dist/esm/client/app-delegation-source.js +58 -0
  3. package/dist/esm/client/app-delegation-source.js.map +1 -0
  4. package/dist/esm/client/app-lock.d.ts +63 -0
  5. package/dist/esm/client/app-lock.js +92 -0
  6. package/dist/esm/client/app-lock.js.map +1 -0
  7. package/dist/esm/client/auth-client.d.ts +270 -53
  8. package/dist/esm/client/auth-client.js +1017 -414
  9. package/dist/esm/client/auth-client.js.map +1 -1
  10. package/dist/esm/client/base64.d.ts +8 -0
  11. package/dist/esm/client/base64.js +28 -0
  12. package/dist/esm/client/base64.js.map +1 -0
  13. package/dist/esm/client/cookie-state-storage.d.ts +59 -0
  14. package/dist/esm/client/cookie-state-storage.js +245 -0
  15. package/dist/esm/client/cookie-state-storage.js.map +1 -0
  16. package/dist/esm/client/credential-storage.d.ts +48 -0
  17. package/dist/esm/client/credential-storage.js +2 -0
  18. package/dist/esm/client/credential-storage.js.map +1 -0
  19. package/dist/esm/client/db.d.ts +6 -1
  20. package/dist/esm/client/db.js +56 -15
  21. package/dist/esm/client/db.js.map +1 -1
  22. package/dist/esm/client/foreground-refresh.d.ts +35 -0
  23. package/dist/esm/client/foreground-refresh.js +83 -0
  24. package/dist/esm/client/foreground-refresh.js.map +1 -0
  25. package/dist/esm/client/idb-credential-storage.d.ts +25 -0
  26. package/dist/esm/client/idb-credential-storage.js +69 -0
  27. package/dist/esm/client/idb-credential-storage.js.map +1 -0
  28. package/dist/esm/client/index.d.ts +14 -3
  29. package/dist/esm/client/index.js +13 -3
  30. package/dist/esm/client/index.js.map +1 -1
  31. package/dist/esm/client/local-credential-storage.d.ts +26 -0
  32. package/dist/esm/client/local-credential-storage.js +59 -0
  33. package/dist/esm/client/local-credential-storage.js.map +1 -0
  34. package/dist/esm/client/memory-credential-storage.d.ts +22 -0
  35. package/dist/esm/client/memory-credential-storage.js +35 -0
  36. package/dist/esm/client/memory-credential-storage.js.map +1 -0
  37. package/dist/esm/client/session-delegation.d.ts +40 -0
  38. package/dist/esm/client/session-delegation.js +100 -0
  39. package/dist/esm/client/session-delegation.js.map +1 -0
  40. package/dist/esm/client/session-identity.d.ts +97 -0
  41. package/dist/esm/client/session-identity.js +392 -0
  42. package/dist/esm/client/session-identity.js.map +1 -0
  43. package/dist/esm/client/session-minter.d.ts +94 -0
  44. package/dist/esm/client/session-minter.js +185 -0
  45. package/dist/esm/client/session-minter.js.map +1 -0
  46. package/dist/esm/client/shared-memory-credential-storage.d.ts +44 -0
  47. package/dist/esm/client/shared-memory-credential-storage.js +243 -0
  48. package/dist/esm/client/shared-memory-credential-storage.js.map +1 -0
  49. package/dist/esm/client/slots.d.ts +59 -0
  50. package/dist/esm/client/slots.js +67 -0
  51. package/dist/esm/client/slots.js.map +1 -0
  52. package/dist/esm/client/sso.d.ts +33 -0
  53. package/dist/esm/client/sso.js +120 -0
  54. package/dist/esm/client/sso.js.map +1 -0
  55. package/dist/esm/client/state-storage.d.ts +135 -0
  56. package/dist/esm/client/state-storage.js +178 -0
  57. package/dist/esm/client/state-storage.js.map +1 -0
  58. package/package.json +2 -1
  59. package/src/client/app-delegation-source.ts +86 -0
  60. package/src/client/app-lock.ts +120 -0
  61. package/src/client/auth-client.ts +1300 -487
  62. package/src/client/base64.ts +28 -0
  63. package/src/client/cookie-state-storage.ts +277 -0
  64. package/src/client/credential-storage.ts +56 -0
  65. package/src/client/db.ts +56 -12
  66. package/src/client/foreground-refresh.ts +92 -0
  67. package/src/client/idb-credential-storage.ts +82 -0
  68. package/src/client/index.ts +24 -8
  69. package/src/client/local-credential-storage.ts +68 -0
  70. package/src/client/memory-credential-storage.ts +41 -0
  71. package/src/client/session-delegation.ts +138 -0
  72. package/src/client/session-identity.ts +510 -0
  73. package/src/client/session-minter.ts +286 -0
  74. package/src/client/shared-memory-credential-storage.ts +303 -0
  75. package/src/client/slots.ts +78 -0
  76. package/src/client/sso.ts +122 -0
  77. package/src/client/state-storage.ts +284 -0
  78. package/dist/esm/client/idle-manager.d.ts +0 -83
  79. package/dist/esm/client/idle-manager.js +0 -100
  80. package/dist/esm/client/idle-manager.js.map +0 -1
  81. package/dist/esm/client/storage.d.ts +0 -51
  82. package/dist/esm/client/storage.js +0 -90
  83. package/dist/esm/client/storage.js.map +0 -1
  84. package/src/client/idle-manager.ts +0 -162
  85. package/src/client/storage.ts +0 -114
@@ -0,0 +1,67 @@
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
+ const SLOT_NAMES = {
17
+ /** The session's key and the chain issued to it. */
18
+ 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
+ sessionPending: 'session-pending',
26
+ /** The key an application signs with, and its delegation. */
27
+ 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
+ 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
+ state: 'ic-session-state',
45
+ };
46
+ /**
47
+ * What separates a namespace from the name it prefixes.
48
+ *
49
+ * A cross-origin contract rather than a detail: a sibling subdomain reads the
50
+ * state record by name, so both sides have to agree on this byte.
51
+ */
52
+ const NAMESPACE_SEPARATOR = ':';
53
+ /**
54
+ * The names one client writes under, prefixed where a namespace is set.
55
+ *
56
+ * Assigned in one place rather than defaulted by each store: implementations
57
+ * choosing their own is what produced three colliding slots in the arrangement
58
+ * this replaces, and one assigner cannot collide with itself. A namespace moves
59
+ * all of them at once, so an application running two clients under one origin
60
+ * separates them with one string rather than renaming some and missing others.
61
+ * @param namespace - Prefix for every name, or `undefined` for the bare ones.
62
+ */
63
+ export function slotsFor(namespace) {
64
+ const prefix = namespace === undefined ? '' : `${namespace}${NAMESPACE_SEPARATOR}`;
65
+ return Object.fromEntries(Object.entries(SLOT_NAMES).map(([key, name]) => [key, `${prefix}${name}`]));
66
+ }
67
+ //# sourceMappingURL=slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slots.js","sourceRoot":"","sources":["../../../src/client/slots.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,GAAG;IACjB,oDAAoD;IACpD,OAAO,EAAE,SAAS;IAElB;;;;;OAKG;IACH,cAAc,EAAE,iBAAiB;IAEjC,6DAA6D;IAC7D,GAAG,EAAE,KAAK;IAEV;;;;;;;OAOG;IACH,UAAU,EAAE,aAAa;IAEzB;;;;;;OAMG;IACH,KAAK,EAAE,kBAAkB;CACjB,CAAC;AAEX;;;;;GAKG;AACH,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAKhC;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,SAAkB;IACzC,MAAM,MAAM,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,mBAAmB,EAAE,CAAC;IACnF,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC,CAClE,CAAC;AACb,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Normalizes an SSO domain to the authority the identity provider fetches the
3
+ * discovery document from: lowercased, IDNA-encoded, host and optional port.
4
+ *
5
+ * @param domain - The organization domain.
6
+ * @returns The normalized authority.
7
+ * @throws When `domain` is not a domain with an optional port.
8
+ */
9
+ export declare function normalizeSsoDomain(domain: string): string;
10
+ /**
11
+ * Checks whether an organization domain can be used for SSO sign-in: it is a
12
+ * domain name, and it publishes `/.well-known/ii-openid-configuration` with a
13
+ * `client_id` and an `openid_configuration` URL.
14
+ *
15
+ * The document must be served with `Access-Control-Allow-Origin: *`.
16
+ *
17
+ * The check waits on a third-party server and has no deadline of its own, so
18
+ * `signal` sets one. Pass `AbortSignal.timeout(...)` for a one-shot check, or a
19
+ * controller you abort per keystroke when checking as the user types.
20
+ *
21
+ * @param domain - The organization domain.
22
+ * @param signal - Bounds the check.
23
+ * @returns Whether the domain is usable for SSO sign-in.
24
+ * @throws When `signal` aborts — an abandoned check is not a verdict on the
25
+ * domain, so it must not be read as `false`.
26
+ *
27
+ * @example
28
+ * if (await isValidSsoDomain(input.value, AbortSignal.timeout(5_000))) {
29
+ * const authClient = new AuthClient({ ssoDomain: input.value });
30
+ * await authClient.signIn();
31
+ * }
32
+ */
33
+ export declare function isValidSsoDomain(domain: string, signal: AbortSignal): Promise<boolean>;
@@ -0,0 +1,120 @@
1
+ // The identity provider's cap on the domain it will fetch.
2
+ const MAX_AUTHORITY_LENGTH = 255;
3
+ const WELL_KNOWN_PATH = '/.well-known/ii-openid-configuration';
4
+ /**
5
+ * `localhost` / `127.0.0.1`, optionally followed by `:<port>`. IPv6 loopback
6
+ * (`[::1]`, etc.) is intentionally not handled — the identity provider doesn't
7
+ * recognise it either, and its e2e setup uses the hostname form.
8
+ */
9
+ function isLoopbackHost(host) {
10
+ let url;
11
+ try {
12
+ // Parse as a URL authority so the optional `:<port>` is split off for us
13
+ // rather than by hand. Invalid input throws and is treated as non-loopback.
14
+ url = new URL(`http://${host}`);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ // A bare `host[:port]` has no path/query/fragment; reject e.g. `localhost/x`.
20
+ if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
21
+ return false;
22
+ }
23
+ return url.hostname === 'localhost' || url.hostname === '127.0.0.1';
24
+ }
25
+ /**
26
+ * Normalizes an SSO domain to the authority the identity provider fetches the
27
+ * discovery document from: lowercased, IDNA-encoded, host and optional port.
28
+ *
29
+ * @param domain - The organization domain.
30
+ * @returns The normalized authority.
31
+ * @throws When `domain` is not a domain with an optional port.
32
+ */
33
+ export function normalizeSsoDomain(domain) {
34
+ const trimmed = domain.trim();
35
+ if (trimmed.length === 0) {
36
+ throw new Error('ssoDomain cannot be empty');
37
+ }
38
+ let url;
39
+ try {
40
+ url = new URL(`https://${trimmed}`);
41
+ }
42
+ catch {
43
+ throw new Error(`ssoDomain is not a domain: ${trimmed}`);
44
+ }
45
+ const authority = url.host;
46
+ // Rebuilding from the host and port alone has to reproduce what was parsed,
47
+ // so anything else the domain carried shows up as a difference.
48
+ if (new URL(`https://${authority}`).href !== url.href) {
49
+ throw new Error(`ssoDomain must be a domain and optional port, nothing else: ${trimmed}`);
50
+ }
51
+ if (authority.length > MAX_AUTHORITY_LENGTH) {
52
+ throw new Error(`ssoDomain exceeds ${MAX_AUTHORITY_LENGTH} characters`);
53
+ }
54
+ // A bare hostname is a half-typed domain, not something worth a request.
55
+ if (!authority.includes('.') && !isLoopbackHost(authority)) {
56
+ throw new Error(`ssoDomain is not a domain name: ${trimmed}`);
57
+ }
58
+ return authority;
59
+ }
60
+ function publishesSsoConfiguration(value) {
61
+ if (typeof value !== 'object' || value === null) {
62
+ return false;
63
+ }
64
+ const { client_id, openid_configuration } = value;
65
+ return typeof client_id === 'string' && typeof openid_configuration === 'string';
66
+ }
67
+ /**
68
+ * Checks whether an organization domain can be used for SSO sign-in: it is a
69
+ * domain name, and it publishes `/.well-known/ii-openid-configuration` with a
70
+ * `client_id` and an `openid_configuration` URL.
71
+ *
72
+ * The document must be served with `Access-Control-Allow-Origin: *`.
73
+ *
74
+ * The check waits on a third-party server and has no deadline of its own, so
75
+ * `signal` sets one. Pass `AbortSignal.timeout(...)` for a one-shot check, or a
76
+ * controller you abort per keystroke when checking as the user types.
77
+ *
78
+ * @param domain - The organization domain.
79
+ * @param signal - Bounds the check.
80
+ * @returns Whether the domain is usable for SSO sign-in.
81
+ * @throws When `signal` aborts — an abandoned check is not a verdict on the
82
+ * domain, so it must not be read as `false`.
83
+ *
84
+ * @example
85
+ * if (await isValidSsoDomain(input.value, AbortSignal.timeout(5_000))) {
86
+ * const authClient = new AuthClient({ ssoDomain: input.value });
87
+ * await authClient.signIn();
88
+ * }
89
+ */
90
+ export async function isValidSsoDomain(domain, signal) {
91
+ signal.throwIfAborted();
92
+ let normalized;
93
+ try {
94
+ normalized = normalizeSsoDomain(domain);
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ const scheme = isLoopbackHost(normalized) ? 'http' : 'https';
100
+ try {
101
+ const response = await fetch(`${scheme}://${normalized}${WELL_KNOWN_PATH}`, {
102
+ headers: { Accept: 'application/json' },
103
+ signal,
104
+ });
105
+ if (!response.ok) {
106
+ return false;
107
+ }
108
+ const valid = publishesSsoConfiguration(await response.json());
109
+ signal.throwIfAborted();
110
+ return valid;
111
+ }
112
+ catch (error) {
113
+ if (signal.aborted) {
114
+ throw error;
115
+ }
116
+ // A DNS, TLS, CORS, or parse failure all leave the domain unusable.
117
+ return false;
118
+ }
119
+ }
120
+ //# sourceMappingURL=sso.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sso.js","sourceRoot":"","sources":["../../../src/client/sso.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import { Principal } from '@icp-sdk/core/principal';
2
+ /**
3
+ * What a client knows about its sign-in without holding a credential: which
4
+ * account is signed in here, and when that ends.
5
+ *
6
+ * Both fields are public. The principal is what an application's canisters see
7
+ * as the caller, and the expiration is what the identity provider granted, so
8
+ * neither is a secret and both can live somewhere a page reads synchronously.
9
+ */
10
+ export interface SessionState {
11
+ principal: Principal;
12
+ /** Nanoseconds since the epoch, matching `Delegation.expiration`. */
13
+ expiration: bigint;
14
+ /**
15
+ * Whether this origin holds a credential for that account, or only knows the
16
+ * sign-in exists.
17
+ *
18
+ * Derived on read and never published: where the record reaches further than
19
+ * one origin, the record itself cannot carry a per-origin fact. A sibling
20
+ * subdomain that has not acquired its own credential reads the sign-in with
21
+ * `held: false`, which is what tells it to acquire one.
22
+ */
23
+ held: boolean;
24
+ }
25
+ /**
26
+ * Holds the state of a sign-in.
27
+ *
28
+ * This is what decides whether this origin is signed in and as whom. It is a
29
+ * store of its own because it has to be readable without awaiting, which a
30
+ * credential store does not.
31
+ *
32
+ * A record can be stale — a session revoked at the canister still reads as
33
+ * signed in until something calls and is refused — so it is the state as this
34
+ * browser knows it rather than an authority against the network.
35
+ */
36
+ export interface StateStorage {
37
+ /** The state, or `null` where nothing is signed in within this store's reach. */
38
+ get(key: string): SessionState | null;
39
+ set(key: string, state: Omit<SessionState, 'held'>): void;
40
+ /**
41
+ * Removes the state, and anything this store publishes beyond this origin.
42
+ *
43
+ * What signing out does: the user ended the sign-in, so a sibling reading a
44
+ * shared record must stop seeing one.
45
+ */
46
+ remove(key: string): void;
47
+ /**
48
+ * Removes what this origin holds, leaving anything shared alone.
49
+ *
50
+ * What finding out does, which is a different act. An origin whose chain turns
51
+ * out to be dead cannot tell whether the session was revoked or replaced by a
52
+ * sibling signing in — and in the second case the shared record was written by
53
+ * that sibling a moment ago, so retracting it would tell it that the session it
54
+ * just obtained is gone.
55
+ *
56
+ * A store that publishes nothing beyond this origin answers it with
57
+ * {@link remove}, which is one line and the right answer.
58
+ */
59
+ discard(key: string): void;
60
+ /**
61
+ * Whether a sign-in kept in this store may be resumed without a ceremony.
62
+ *
63
+ * True only where the record reaches past this origin, because that is the
64
+ * case a silent re-issue exists for: a sibling reads the sign-in, holds no
65
+ * credential of its own, and needs the identity provider to remember the
66
+ * session it can extend. A store the origin keeps to itself has nothing to
67
+ * gain and would only be asking the provider to persist a sign-in on a device
68
+ * where nothing wanted one kept.
69
+ *
70
+ * Absent means false, which is the answer for a store that never considered
71
+ * the question.
72
+ */
73
+ readonly resumable?: boolean;
74
+ /**
75
+ * Registers a listener fired when the state under `key` changes, including
76
+ * when something other than this client changed it. Returns a function that
77
+ * unregisters it.
78
+ *
79
+ * Required rather than optional: two clients can share one store, even a
80
+ * store with no medium behind it, and a client that cannot be told the record
81
+ * changed under it goes on answering for a sign-in that is no longer there.
82
+ */
83
+ subscribe(key: string, listener: () => void): () => void;
84
+ }
85
+ /**
86
+ * State held for the lifetime of the instance and shared with nothing.
87
+ *
88
+ * For tests and for environments with no `localStorage`, where a client is
89
+ * alone and a reload starts from nothing.
90
+ * @see implements {@link StateStorage}
91
+ */
92
+ export declare class MemoryStateStorage implements StateStorage {
93
+ #private;
94
+ get(key: string): SessionState | null;
95
+ set(key: string, state: Omit<SessionState, 'held'>): void;
96
+ remove(key: string): void;
97
+ /** The same as {@link remove}: this store publishes nothing at all. */
98
+ discard(key: string): void;
99
+ /**
100
+ * Fires when the state changes, which here means when something writes to
101
+ * this instance.
102
+ *
103
+ * Nothing outside the process can reach a `Map`, so a medium is not what makes
104
+ * this worth having: two clients can be handed the same instance, and this is
105
+ * how the one that did not write finds out. Each listener is told only when the
106
+ * record actually changed since it was last told, so a client's own writes do
107
+ * not come back to it as news.
108
+ */
109
+ subscribe(key: string, listener: () => void): () => void;
110
+ }
111
+ /**
112
+ * State in `localStorage`, so every tab of an origin agrees on it and it
113
+ * survives a reload.
114
+ *
115
+ * The two fields are written as `<principal>|<expiration>` rather than JSON,
116
+ * because an expiration is a `bigint` and JSON has no way to carry one.
117
+ * @see implements {@link StateStorage}
118
+ */
119
+ export declare class LocalStateStorage implements StateStorage {
120
+ #private;
121
+ get(key: string): SessionState | null;
122
+ set(key: string, state: Omit<SessionState, 'held'>): void;
123
+ remove(key: string): void;
124
+ /** The same as {@link remove}: this store publishes nothing beyond the origin. */
125
+ discard(key: string): void;
126
+ /**
127
+ * Fires when the state changes, here or in another tab of this origin.
128
+ *
129
+ * `localStorage` raises `storage` in every tab except the one that wrote, so
130
+ * that event carries a change between tabs and a same-tab write is announced
131
+ * directly. Either way the record is readable before it is announced, so a
132
+ * listener asking who is signed in sees what it was told about.
133
+ */
134
+ subscribe(key: string, listener: () => void): () => void;
135
+ }
@@ -0,0 +1,178 @@
1
+ import { Principal } from '@icp-sdk/core/principal';
2
+ /**
3
+ * State held for the lifetime of the instance and shared with nothing.
4
+ *
5
+ * For tests and for environments with no `localStorage`, where a client is
6
+ * alone and a reload starts from nothing.
7
+ * @see implements {@link StateStorage}
8
+ */
9
+ export class MemoryStateStorage {
10
+ #state = new Map();
11
+ #subscribers = new Map();
12
+ get(key) {
13
+ // Nothing reaches past this instance, so a record here is always one it holds.
14
+ const state = this.#state.get(key);
15
+ return state === undefined ? null : { ...state, held: true };
16
+ }
17
+ set(key, state) {
18
+ this.#state.set(key, state);
19
+ this.#fire(key);
20
+ }
21
+ remove(key) {
22
+ this.#state.delete(key);
23
+ this.#fire(key);
24
+ }
25
+ /** The same as {@link remove}: this store publishes nothing at all. */
26
+ discard(key) {
27
+ this.remove(key);
28
+ }
29
+ /**
30
+ * Fires when the state changes, which here means when something writes to
31
+ * this instance.
32
+ *
33
+ * Nothing outside the process can reach a `Map`, so a medium is not what makes
34
+ * this worth having: two clients can be handed the same instance, and this is
35
+ * how the one that did not write finds out. Each listener is told only when the
36
+ * record actually changed since it was last told, so a client's own writes do
37
+ * not come back to it as news.
38
+ */
39
+ subscribe(key, listener) {
40
+ let last = this.#raw(key);
41
+ const check = () => {
42
+ const now = this.#raw(key);
43
+ if (now !== last) {
44
+ last = now;
45
+ listener();
46
+ }
47
+ };
48
+ const listeners = this.#subscribers.get(key) ?? new Set();
49
+ listeners.add(check);
50
+ this.#subscribers.set(key, listeners);
51
+ return () => {
52
+ listeners.delete(check);
53
+ };
54
+ }
55
+ #fire(key) {
56
+ for (const check of [...(this.#subscribers.get(key) ?? [])])
57
+ check();
58
+ }
59
+ // The same shape `LocalStateStorage` keeps, so "changed" means the same thing
60
+ // in both: a record is one string, and comparing strings needs no field walk.
61
+ #raw(key) {
62
+ const state = this.#state.get(key);
63
+ return state === undefined
64
+ ? null
65
+ : `${state.principal.toText()}|${state.expiration.toString()}`;
66
+ }
67
+ }
68
+ /**
69
+ * State in `localStorage`, so every tab of an origin agrees on it and it
70
+ * survives a reload.
71
+ *
72
+ * The two fields are written as `<principal>|<expiration>` rather than JSON,
73
+ * because an expiration is a `bigint` and JSON has no way to carry one.
74
+ * @see implements {@link StateStorage}
75
+ */
76
+ export class LocalStateStorage {
77
+ // Fired on a same-tab write: `storage` reaches the other tabs of an origin and
78
+ // deliberately not the one that wrote. Per key, because one store answers for
79
+ // whatever key it is handed.
80
+ #subscribers = new Map();
81
+ get(key) {
82
+ const raw = this.#localStorage().getItem(key);
83
+ if (raw === null)
84
+ return null;
85
+ const [principalText, expiration] = raw.split('|');
86
+ if (principalText === undefined || expiration === undefined)
87
+ return null;
88
+ try {
89
+ return {
90
+ principal: Principal.fromText(principalText),
91
+ expiration: BigInt(expiration),
92
+ // This record is written by this origin and read by no other, so having
93
+ // one and holding a credential for it are the same thing.
94
+ held: true,
95
+ };
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ set(key, state) {
102
+ this.#localStorage().setItem(key, `${state.principal.toText()}|${state.expiration.toString()}`);
103
+ this.#fire(key);
104
+ }
105
+ remove(key) {
106
+ this.#localStorage().removeItem(key);
107
+ this.#fire(key);
108
+ }
109
+ /** The same as {@link remove}: this store publishes nothing beyond the origin. */
110
+ discard(key) {
111
+ this.remove(key);
112
+ }
113
+ /**
114
+ * Fires when the state changes, here or in another tab of this origin.
115
+ *
116
+ * `localStorage` raises `storage` in every tab except the one that wrote, so
117
+ * that event carries a change between tabs and a same-tab write is announced
118
+ * directly. Either way the record is readable before it is announced, so a
119
+ * listener asking who is signed in sees what it was told about.
120
+ */
121
+ subscribe(key, listener) {
122
+ let last = this.#raw(key);
123
+ const check = () => {
124
+ const now = this.#raw(key);
125
+ if (now !== last) {
126
+ last = now;
127
+ listener();
128
+ }
129
+ };
130
+ const listeners = this.#subscribers.get(key) ?? new Set();
131
+ listeners.add(check);
132
+ this.#subscribers.set(key, listeners);
133
+ const onStorage = (event) => {
134
+ // `key` is null when a tab cleared the whole store, which changes this too.
135
+ if (event.key === key || event.key === null)
136
+ check();
137
+ };
138
+ globalThis.addEventListener('storage', onStorage);
139
+ // `storage` is the only notice this store gets, and a page restored from the
140
+ // back-forward cache was not listening while it was frozen. So the record is
141
+ // re-read when the page is shown or its window regains focus, which is when
142
+ // somebody is about to act on the answer.
143
+ const onVisible = () => {
144
+ if (document.visibilityState === 'visible')
145
+ check();
146
+ };
147
+ const hooked = typeof document !== 'undefined';
148
+ if (hooked) {
149
+ document.addEventListener('visibilitychange', onVisible);
150
+ globalThis.addEventListener('pageshow', check);
151
+ globalThis.addEventListener('focus', check);
152
+ }
153
+ return () => {
154
+ listeners.delete(check);
155
+ globalThis.removeEventListener('storage', onStorage);
156
+ if (hooked) {
157
+ document.removeEventListener('visibilitychange', onVisible);
158
+ globalThis.removeEventListener('pageshow', check);
159
+ globalThis.removeEventListener('focus', check);
160
+ }
161
+ };
162
+ }
163
+ #fire(key) {
164
+ for (const check of [...(this.#subscribers.get(key) ?? [])])
165
+ check();
166
+ }
167
+ #raw(key) {
168
+ return this.#localStorage().getItem(key);
169
+ }
170
+ #localStorage() {
171
+ const ls = globalThis.localStorage;
172
+ if (!ls) {
173
+ throw new Error('Could not find local storage.');
174
+ }
175
+ return ls;
176
+ }
177
+ }
178
+ //# sourceMappingURL=state-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-storage.js","sourceRoot":"","sources":["../../../src/client/state-storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icp-sdk/auth",
3
- "version": "8.0.3",
3
+ "version": "9.0.0",
4
4
  "author": "DFINITY Stiftung <sdk@dfinity.org>",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Authentication library for Internet Computer web apps",
@@ -60,6 +60,7 @@
60
60
  "scripts": {
61
61
  "build": "tsc -p tsconfig.build.json && pnpm lint:package",
62
62
  "test": "vitest",
63
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
63
64
  "codestyle:check": "biome check",
64
65
  "codestyle:fix": "biome check --write",
65
66
  "lint:package": "publint --strict"
@@ -0,0 +1,86 @@
1
+ import type { DerEncodedPublicKey } from '@icp-sdk/core/agent';
2
+ import type { DelegationChain } from '@icp-sdk/core/identity';
3
+
4
+ /**
5
+ * Thrown when the canister has no session behind the caller: revoked, expired,
6
+ * pruned, or never one at all.
7
+ *
8
+ * This is the one terminal outcome. Everything else a mint can fail with, a
9
+ * transport error or an internal canister error, leaves the session alone and is
10
+ * worth retrying, so only this type may end a session.
11
+ */
12
+ export class SessionGoneError extends Error {
13
+ constructor(message = 'The session behind this identity no longer exists') {
14
+ super(message);
15
+ this.name = 'SessionGoneError';
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Thrown when the session is one this library cannot act for, however healthy it
21
+ * is.
22
+ *
23
+ * The third outcome, and the reason it is not either of the others: the session
24
+ * is neither gone nor worth retrying. Retrying returns the same answer forever,
25
+ * and reporting it as gone would retract a record for a sign-in that is alive,
26
+ * signing every tab on the domain out of it.
27
+ */
28
+ export class SessionUnsupportedError extends Error {
29
+ constructor(message: string) {
30
+ super(message);
31
+ this.name = 'SessionUnsupportedError';
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Thrown when a mint answers for an account the caller was not expecting.
37
+ *
38
+ * The session is healthy and the mint succeeded; it is the pairing that is
39
+ * wrong. So it is neither gone nor retryable — asking again returns the same
40
+ * account — and the caller's own record is what has to give way.
41
+ */
42
+ export class AccountMismatchError extends Error {
43
+ constructor() {
44
+ super('The minted delegation is not for the account this origin acts as');
45
+ this.name = 'AccountMismatchError';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Where {@link SessionIdentity} gets an app delegation from.
51
+ *
52
+ * The identity decides *when* to mint; this decides *how*. Splitting them is
53
+ * what lets the timing be tested without a replica, and what keeps the identity
54
+ * free of any agent or canister id.
55
+ */
56
+ export interface AppDelegationSource {
57
+ /**
58
+ * Mint a delegation from the session to `appPublicKey`.
59
+ *
60
+ * Three outcomes. {@link SessionGoneError} where the session no longer
61
+ * exists, {@link SessionUnsupportedError} where it exists but this library
62
+ * cannot act for it, and anything else where the attempt may be worth
63
+ * repeating.
64
+ */
65
+ mint(appPublicKey: DerEncodedPublicKey): Promise<DelegationChain>;
66
+ }
67
+
68
+ /** Whether two DER-encoded keys are the same bytes. */
69
+ export const sameKey = (a: Uint8Array, b: Uint8Array): boolean =>
70
+ a.length === b.length && a.every((byte, i) => byte === b[i]);
71
+
72
+ /** The leaf of a chain: the key it authorises. */
73
+ export const delegatesTo = (chain: DelegationChain): Uint8Array | undefined =>
74
+ chain.delegations[chain.delegations.length - 1]?.delegation.pubkey;
75
+
76
+ /**
77
+ * Whether a chain's leaf authorises `keyDer` — the key that would sign with it.
78
+ *
79
+ * A chain paired with a key it does not authorise signs nothing the replica will
80
+ * accept, and fails with a signature error that points at neither half. Every
81
+ * place that holds both halves checks this.
82
+ */
83
+ export const chainAuthorisesKey = (chain: DelegationChain, keyDer: Uint8Array): boolean => {
84
+ const leaf = delegatesTo(chain);
85
+ return leaf !== undefined && sameKey(new Uint8Array(leaf), new Uint8Array(keyDer));
86
+ };