@ic-reactor/react 3.12.4 → 3.13.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 +410 -44
- package/dist/auth/auth-client-compat.d.ts +122 -0
- package/dist/auth/auth-client-compat.d.ts.map +1 -0
- package/dist/auth/auth-client-compat.js +162 -0
- package/dist/auth/auth-client-compat.js.map +1 -0
- package/dist/auth/authentication-manager.d.ts +287 -5
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +920 -150
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.js +36 -20
- package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
- package/dist/auth/identity-attributes-manager.d.ts +2 -1
- package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.js +90 -6
- package/dist/auth/identity-attributes-manager.js.map +1 -1
- package/dist/auth/identity-attributes.d.ts.map +1 -1
- package/dist/auth/identity-attributes.js +57 -0
- package/dist/auth/identity-attributes.js.map +1 -1
- package/dist/auth/local-ii-probe.d.ts +12 -1
- package/dist/auth/local-ii-probe.d.ts.map +1 -1
- package/dist/auth/local-ii-probe.js +22 -3
- package/dist/auth/local-ii-probe.js.map +1 -1
- package/dist/auth/types.d.ts +48 -5
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/createActorHooks.d.ts +9 -20
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +51 -10
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +39 -15
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts +4 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +121 -84
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts +35 -2
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +104 -17
- package/dist/createQuery.js.map +1 -1
- package/dist/createReactorProvider.d.ts +158 -0
- package/dist/createReactorProvider.d.ts.map +1 -0
- package/dist/createReactorProvider.js +256 -0
- package/dist/createReactorProvider.js.map +1 -0
- package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +59 -27
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts +23 -2
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +68 -21
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineDisplayReactor.d.ts +43 -0
- package/dist/defineDisplayReactor.d.ts.map +1 -0
- package/dist/defineDisplayReactor.js +42 -0
- package/dist/defineDisplayReactor.js.map +1 -0
- package/dist/defineReactor.d.ts +46 -72
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +11 -176
- package/dist/defineReactor.js.map +1 -1
- package/dist/defineReactorShared.d.ts +84 -0
- package/dist/defineReactorShared.d.ts.map +1 -0
- package/dist/defineReactorShared.js +139 -0
- package/dist/defineReactorShared.js.map +1 -0
- package/dist/hooks/createAuthHooks.d.ts +9 -2
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +184 -24
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +36 -8
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +54 -21
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +37 -4
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +201 -57
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +15 -12
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +14 -13
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorQuery.d.ts +17 -4
- package/dist/hooks/useActorQuery.d.ts.map +1 -1
- package/dist/hooks/useActorQuery.js +30 -9
- package/dist/hooks/useActorQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +17 -5
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
- package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.js +20 -9
- package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/ownedAuthentication.d.ts +52 -0
- package/dist/ownedAuthentication.d.ts.map +1 -0
- package/dist/ownedAuthentication.js +49 -0
- package/dist/ownedAuthentication.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +23 -0
- package/dist/server.js.map +1 -0
- package/dist/testing.d.ts +19 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +428 -21
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +159 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +301 -1
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +12 -7
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +34 -15
- package/dist/validation.js.map +1 -1
- package/llms.txt +259 -33
- package/package.json +17 -5
- package/src/auth/auth-client-compat.ts +273 -0
- package/src/auth/authentication-manager.ts +918 -96
- package/src/auth/createIdentityAttributeHooks.ts +47 -21
- package/src/auth/identity-attributes-manager.ts +100 -5
- package/src/auth/identity-attributes.ts +75 -0
- package/src/auth/local-ii-probe.ts +29 -3
- package/src/auth/types.ts +49 -6
- package/src/createActorHooks.ts +50 -42
- package/src/createInfiniteQuery.ts +120 -28
- package/src/createMutation.ts +213 -132
- package/src/createQuery.ts +164 -32
- package/src/createReactorProvider.ts +365 -0
- package/src/createSuspenseInfiniteQuery.ts +93 -43
- package/src/createSuspenseQuery.ts +102 -32
- package/src/defineDisplayReactor.ts +62 -0
- package/src/defineReactor.ts +81 -263
- package/src/defineReactorShared.ts +268 -0
- package/src/hooks/createAuthHooks.ts +210 -28
- package/src/hooks/useActorInfiniteQuery.ts +156 -55
- package/src/hooks/useActorMethod.ts +295 -92
- package/src/hooks/useActorMutation.ts +42 -30
- package/src/hooks/useActorQuery.ts +43 -10
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +110 -54
- package/src/hooks/useActorSuspenseQuery.ts +30 -15
- package/src/index.ts +8 -0
- package/src/ownedAuthentication.ts +81 -0
- package/src/server.ts +23 -0
- package/src/testing.ts +18 -0
- package/src/types.ts +492 -22
- package/src/utils.ts +387 -3
- package/src/validation.ts +43 -19
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Identity } from "@icp-sdk/core/agent"
|
|
2
2
|
import { AnonymousIdentity } from "@icp-sdk/core/agent"
|
|
3
|
+
import { isDelegationValid, type DelegationChain } from "@icp-sdk/core/identity"
|
|
3
4
|
import type {
|
|
4
5
|
AuthClientLike,
|
|
5
6
|
AuthClientSignInOptions,
|
|
@@ -22,6 +23,15 @@ import {
|
|
|
22
23
|
LOCAL_INTERNET_IDENTITY_CANISTER_ID,
|
|
23
24
|
localInternetIdentityProvider,
|
|
24
25
|
} from "./constants.js"
|
|
26
|
+
import { recordAuthentication } from "../ownedAuthentication.js"
|
|
27
|
+
import {
|
|
28
|
+
detectAuthClientFlavor,
|
|
29
|
+
detectAuthClientInstanceFlavor,
|
|
30
|
+
toAuthClientConstructorOptions,
|
|
31
|
+
toAuthClientSignInOptions,
|
|
32
|
+
type AuthClientFlavor,
|
|
33
|
+
type IdentityProviderPairing,
|
|
34
|
+
} from "./auth-client-compat.js"
|
|
25
35
|
|
|
26
36
|
export interface AuthenticationManagerParameters extends AuthenticationClientOptions {
|
|
27
37
|
clientManager: ClientManager
|
|
@@ -34,8 +44,20 @@ export interface AuthenticationManagerParameters extends AuthenticationClientOpt
|
|
|
34
44
|
internetIdentityId?: string
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
/**
|
|
48
|
+
* What `@icp-sdk/auth` v9 and later add to a client, read by shape because
|
|
49
|
+
* {@link AuthClientLike} describes the methods every supported major shares.
|
|
50
|
+
*/
|
|
51
|
+
type SubscribableAuthClient = AuthClientLike & {
|
|
52
|
+
subscribe?: (listener: () => void) => () => void
|
|
53
|
+
getPrincipal?: () => Principal | undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
type AuthClientConstructor = {
|
|
38
|
-
|
|
57
|
+
// The translated options are the installed client's shape, not IC Reactor's:
|
|
58
|
+
// `toAuthClientConstructorOptions` rewrites them per detected flavor, so this
|
|
59
|
+
// stays deliberately open rather than asserting a contract that varies.
|
|
60
|
+
new (options?: unknown): AuthClientLike
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
/**
|
|
@@ -59,6 +81,7 @@ export class AuthenticationManager {
|
|
|
59
81
|
AuthClientConstructor | undefined
|
|
60
82
|
>
|
|
61
83
|
private authModuleMissing = false
|
|
84
|
+
private authClientFlavor: AuthClientFlavor = "legacy"
|
|
62
85
|
private authClientOptions?: AuthenticationClientOptions
|
|
63
86
|
private authStateValue: AuthState = {
|
|
64
87
|
identity: null,
|
|
@@ -66,8 +89,26 @@ export class AuthenticationManager {
|
|
|
66
89
|
isAuthenticated: false,
|
|
67
90
|
error: undefined,
|
|
68
91
|
}
|
|
92
|
+
/** See {@link sessionChecked}. */
|
|
93
|
+
private sessionCheckedValue = false
|
|
94
|
+
private sessionCheckedSubscribers: Array<() => void> = []
|
|
95
|
+
/** Stops following the current client's session record; see `watchClient()`. */
|
|
96
|
+
private unwatchClient?: () => void
|
|
97
|
+
/**
|
|
98
|
+
* Set when the client's session record changed while an operation of this
|
|
99
|
+
* manager's own was running; see `followClient()`.
|
|
100
|
+
*/
|
|
101
|
+
private clientChangedDuringOperation = false
|
|
102
|
+
/** Counts `followClient()` passes, so that only the latest one publishes. */
|
|
103
|
+
private followRevision = 0
|
|
104
|
+
/** Counts `dispose()` calls; see {@link releaseCount}. */
|
|
105
|
+
private releases = 0
|
|
69
106
|
private readonly identityProvider?: string | URL
|
|
107
|
+
/** The provider taken from the `ic_env` cookie, when no caller set one. */
|
|
108
|
+
private readonly envIdentityProvider?: string | URL
|
|
70
109
|
private readonly internetIdentityId?: string
|
|
110
|
+
/** Whether `internetIdentityId` came from the caller rather than the cookie. */
|
|
111
|
+
private readonly internetIdentityIdIsExplicit: boolean
|
|
71
112
|
/**
|
|
72
113
|
* Which authorize path the locally deployed Internet Identity serves, once
|
|
73
114
|
* probed. `undefined` means not probed yet; `null` means it serves no sign-in
|
|
@@ -91,15 +132,19 @@ export class AuthenticationManager {
|
|
|
91
132
|
...clientOptions
|
|
92
133
|
}: AuthenticationManagerParameters) {
|
|
93
134
|
this.clientManager = clientManager
|
|
135
|
+
// For `createReactorProvider`, which disposes the managers built while
|
|
136
|
+
// its factory ran, and leaves alone those built elsewhere.
|
|
137
|
+
recordAuthentication(this)
|
|
94
138
|
const canisterEnv =
|
|
95
139
|
typeof window !== "undefined" ? getAuthenticationCanisterEnv() : undefined
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
acceptEnvIdentityProvider(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
this.envIdentityProvider = identityProvider
|
|
141
|
+
? undefined
|
|
142
|
+
: acceptEnvIdentityProvider(
|
|
143
|
+
canisterEnv?.[INTERNET_IDENTITY_PROVIDER_ENV_KEY] ||
|
|
144
|
+
canisterEnv?.["PUBLIC_INTERNET_IDENTITY_PROVIDER"],
|
|
145
|
+
clientManager
|
|
146
|
+
)
|
|
147
|
+
this.identityProvider = identityProvider || this.envIdentityProvider
|
|
103
148
|
// Same cookie, same decision. This one only ever reaches a local provider
|
|
104
149
|
// URL, but `allowEnvConfig: false` has to mean the cookie is not consulted
|
|
105
150
|
// rather than mostly not consulted.
|
|
@@ -112,11 +157,18 @@ export class AuthenticationManager {
|
|
|
112
157
|
canisterEnv?.["CANISTER_ID_INTERNET_IDENTITY"]
|
|
113
158
|
)
|
|
114
159
|
: undefined)
|
|
160
|
+
this.internetIdentityIdIsExplicit = Boolean(internetIdentityId)
|
|
115
161
|
this.defaultClientOptions = clientOptions
|
|
116
162
|
|
|
117
163
|
if (authClient) {
|
|
118
164
|
this.authClientWasProvided = true
|
|
119
165
|
this.authClient = authClient
|
|
166
|
+
// A caller-built client never goes through the module loader that
|
|
167
|
+
// detects the flavor, so read it off the instance. Without this a v10
|
|
168
|
+
// client handed in here was treated as v8, and `targets` reached it
|
|
169
|
+
// without the warning that it is ignored.
|
|
170
|
+
this.authClientFlavor = detectAuthClientInstanceFlavor(authClient)
|
|
171
|
+
this.watchClient(authClient)
|
|
120
172
|
this.syncStateFromClient(this.authStateRevision).catch((error) => {
|
|
121
173
|
this.updateState({ error: error as Error, isAuthenticating: false })
|
|
122
174
|
})
|
|
@@ -132,11 +184,137 @@ export class AuthenticationManager {
|
|
|
132
184
|
return this.authClient
|
|
133
185
|
}
|
|
134
186
|
|
|
187
|
+
/**
|
|
188
|
+
* @internal Used by the auth hooks.
|
|
189
|
+
*
|
|
190
|
+
* Whether this manager has checked its client for a session: a restore has
|
|
191
|
+
* settled, whether it found a session, found none or failed, or a sign-in or
|
|
192
|
+
* sign-out has completed. Until then {@link authState} is the signed-out state
|
|
193
|
+
* the manager starts in, which says nothing about the session, so the auth
|
|
194
|
+
* hooks report `isAuthenticating: true` instead.
|
|
195
|
+
*/
|
|
196
|
+
public get sessionChecked(): boolean {
|
|
197
|
+
return this.sessionCheckedValue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @internal Used by the auth hooks.
|
|
202
|
+
*
|
|
203
|
+
* Records that the session has been checked, and tells the auth hooks the
|
|
204
|
+
* first time. A restore that failed counts: waiting on one that will not be
|
|
205
|
+
* retried would leave the hooks reporting `isAuthenticating: true` for good.
|
|
206
|
+
*/
|
|
207
|
+
public markSessionChecked() {
|
|
208
|
+
if (this.sessionCheckedValue) return
|
|
209
|
+
this.sessionCheckedValue = true
|
|
210
|
+
const subscribers = this.sessionCheckedSubscribers
|
|
211
|
+
this.sessionCheckedSubscribers = []
|
|
212
|
+
for (const subscriber of subscribers) subscriber()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @internal Used by the auth hooks.
|
|
217
|
+
*
|
|
218
|
+
* How many times {@link dispose} has run. A restore the hooks started
|
|
219
|
+
* compares it with the count it began with, to tell whether the manager was
|
|
220
|
+
* released while it ran.
|
|
221
|
+
*/
|
|
222
|
+
public get releaseCount(): number {
|
|
223
|
+
return this.releases
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @internal Used by the auth hooks.
|
|
228
|
+
*
|
|
229
|
+
* Calls `callback` once, when {@link sessionChecked} turns true. Nothing else
|
|
230
|
+
* announces it when the check that settles it publishes no state, as a
|
|
231
|
+
* restore that failed before it read the client does not.
|
|
232
|
+
*
|
|
233
|
+
* @returns An unsubscribe function.
|
|
234
|
+
*/
|
|
235
|
+
public subscribeSessionChecked(callback: () => void) {
|
|
236
|
+
if (this.sessionCheckedValue) return () => {}
|
|
237
|
+
const subscription = () => callback()
|
|
238
|
+
this.sessionCheckedSubscribers.push(subscription)
|
|
239
|
+
return () => {
|
|
240
|
+
this.sessionCheckedSubscribers = this.sessionCheckedSubscribers.filter(
|
|
241
|
+
(subscriber) => subscriber !== subscription
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Releases the `@icp-sdk/auth` client this manager built.
|
|
248
|
+
*
|
|
249
|
+
* A v10 client hooks the page when it is built: activity listeners on
|
|
250
|
+
* `document`, focus and visibility listeners, a watch on the session record
|
|
251
|
+
* that every tab shares and, once signed in, a refresh timer. Nothing
|
|
252
|
+
* releases them when the manager is dropped, so a manager built per mount,
|
|
253
|
+
* as a server-rendered app builds one in its provider, left one live client
|
|
254
|
+
* behind on each remount. Call this when you discard the manager, from the
|
|
255
|
+
* cleanup of whatever built it.
|
|
256
|
+
*
|
|
257
|
+
* The client is disposed and forgotten, so a later {@link prepareClient},
|
|
258
|
+
* {@link login}, {@link logout} or restore that needs a client builds a new
|
|
259
|
+
* one. That keeps it safe in an effect cleanup under React's StrictMode,
|
|
260
|
+
* which runs the cleanup and then the effect again on the same manager. With
|
|
261
|
+
* `@icp-sdk/auth` v8 there is nothing to release, and the manager only drops
|
|
262
|
+
* its reference. A client passed in as `authClient` belongs to the caller: it
|
|
263
|
+
* is never disposed, and the manager goes on using it, but stops following its
|
|
264
|
+
* session record until its next {@link prepareClient}, {@link login} or
|
|
265
|
+
* {@link authenticate}, so that the caller's client does not keep a
|
|
266
|
+
* discarded manager alive.
|
|
267
|
+
*
|
|
268
|
+
* The auth state and the identity on the agent are left as they are. A
|
|
269
|
+
* restore reading the client when it is released ends without publishing
|
|
270
|
+
* what it read, and a sign-in or sign-out under way ends with what the
|
|
271
|
+
* released client reports. A restore `useAuth()` started stops, and
|
|
272
|
+
* releases any client it built meanwhile, once no `useAuth()` of this
|
|
273
|
+
* manager is mounted; one mounted later restores again.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```tsx
|
|
277
|
+
* const [authentication] = useState(
|
|
278
|
+
* () => new AuthenticationManager({ clientManager })
|
|
279
|
+
* )
|
|
280
|
+
* useEffect(() => () => authentication.dispose(), [authentication])
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
public dispose(): void {
|
|
284
|
+
this.releases++
|
|
285
|
+
this.stopWatchingClient()
|
|
286
|
+
if (this.authClientWasProvided) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const client = this.authClient
|
|
290
|
+
this.authClient = undefined
|
|
291
|
+
this.authClientOptions = undefined
|
|
292
|
+
if (client) {
|
|
293
|
+
disposeClient(client)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Subscribes to auth state changes.
|
|
299
|
+
*
|
|
300
|
+
* Callbacks run in the order they subscribed, after the state has changed. A
|
|
301
|
+
* callback that throws does not stop the others; the first error is rethrown
|
|
302
|
+
* to whatever made the change once they have all run. When a callback itself
|
|
303
|
+
* changes the state, the newer state is the last each callback hears.
|
|
304
|
+
*
|
|
305
|
+
* @param callback - Function called with the new state.
|
|
306
|
+
* @returns An unsubscribe function.
|
|
307
|
+
*/
|
|
135
308
|
public subscribeAuthState(callback: (state: AuthState) => void) {
|
|
136
|
-
|
|
309
|
+
// Each subscription gets an entry of its own, so the unsubscribe it returns
|
|
310
|
+
// removes that one registration and no other. Filtering on the callback
|
|
311
|
+
// itself removed every registration of a function subscribed twice, as
|
|
312
|
+
// `ClientManager.subscribe` did before #513.
|
|
313
|
+
const subscription = (state: AuthState) => callback(state)
|
|
314
|
+
this.authStateSubscribers.push(subscription)
|
|
137
315
|
return () => {
|
|
138
316
|
this.authStateSubscribers = this.authStateSubscribers.filter(
|
|
139
|
-
(subscriber) => subscriber !==
|
|
317
|
+
(subscriber) => subscriber !== subscription
|
|
140
318
|
)
|
|
141
319
|
}
|
|
142
320
|
}
|
|
@@ -158,6 +336,7 @@ export class AuthenticationManager {
|
|
|
158
336
|
const clientOptions = this.resolveClientOptions(options)
|
|
159
337
|
|
|
160
338
|
if (this.authClient && !this.shouldRecreateClient(clientOptions)) {
|
|
339
|
+
this.watchClient(this.authClient)
|
|
161
340
|
return this.authClient
|
|
162
341
|
}
|
|
163
342
|
|
|
@@ -187,7 +366,19 @@ export class AuthenticationManager {
|
|
|
187
366
|
// An inconclusive probe must not change behaviour: the canister may be
|
|
188
367
|
// fine and merely unreachable from here, and a diagnostic that blocks a
|
|
189
368
|
// working login is worse than the failure it explains.
|
|
190
|
-
|
|
369
|
+
const authorizePath = inconclusive ? "/authorize" : path
|
|
370
|
+
|
|
371
|
+
// With no sign-in UI, login fails with advice that depends on the
|
|
372
|
+
// installed @icp-sdk/auth major. A client IC Reactor builds itself shows
|
|
373
|
+
// its major only once the module has loaded, and that import is usually
|
|
374
|
+
// still in flight when the probe answers. Wait for it before recording the
|
|
375
|
+
// finding, so nothing reads `null` while the flavor still holds its
|
|
376
|
+
// `legacy` default and hands a v10 app the v8 advice.
|
|
377
|
+
if (authorizePath === null && !this.authClientWasProvided) {
|
|
378
|
+
await this.loadAuthClientConstructor().catch(() => undefined)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.localAuthorizePath = authorizePath
|
|
191
382
|
|
|
192
383
|
if (path === "/#authorize" && !inconclusive) {
|
|
193
384
|
console.warn(
|
|
@@ -213,28 +404,46 @@ export class AuthenticationManager {
|
|
|
213
404
|
}
|
|
214
405
|
|
|
215
406
|
public authenticate = async (): Promise<Identity | undefined> => {
|
|
407
|
+
const releases = this.releases
|
|
408
|
+
try {
|
|
409
|
+
return await this.checkSession()
|
|
410
|
+
} finally {
|
|
411
|
+
// Marked once the result is published, so the hooks never show the
|
|
412
|
+
// starting state as the answer. A failed restore settles it too. One
|
|
413
|
+
// that `dispose()` cut short read nothing, and leaves the check to the
|
|
414
|
+
// restore of whatever mounts this manager again.
|
|
415
|
+
if (releases === this.releases || this.authClient) {
|
|
416
|
+
this.markSessionChecked()
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private async checkSession(): Promise<Identity | undefined> {
|
|
422
|
+
if (this.authClient) {
|
|
423
|
+
this.watchClient(this.authClient)
|
|
424
|
+
}
|
|
216
425
|
if (this.authState.isAuthenticated) {
|
|
217
426
|
// Returning on the cached flag alone meant a delegation that expired
|
|
218
427
|
// mid-session was never re-observed: the UI kept rendering a signed-in
|
|
219
428
|
// state while every update call failed, and only a reload recovered.
|
|
220
429
|
// Re-ask the client, which reads the cached expiry rather than hitting
|
|
221
430
|
// storage. A throw here is treated as "still valid" so a transient
|
|
222
|
-
// failure cannot sign anyone out.
|
|
431
|
+
// failure cannot sign anyone out. A v8 delegation that has expired is
|
|
432
|
+
// not valid whatever the client answers: see vouchesFor().
|
|
223
433
|
if (!this.authClient) {
|
|
224
434
|
return this.authState.identity || undefined
|
|
225
435
|
}
|
|
226
|
-
const
|
|
436
|
+
const isAuthenticated = await Promise.resolve(
|
|
227
437
|
this.authClient.isAuthenticated()
|
|
228
438
|
).catch(() => true)
|
|
229
|
-
if (
|
|
439
|
+
if (this.vouchesFor(this.authState.identity, isAuthenticated)) {
|
|
230
440
|
return this.authState.identity || undefined
|
|
231
441
|
}
|
|
232
|
-
// Expired
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
await this.expireSession()
|
|
442
|
+
// Expired, or ended in another tab. The v8 client keeps handing out the
|
|
443
|
+
// lapsed delegation from getIdentity() until signOut() runs (only a
|
|
444
|
+
// fresh page load purges it). End the session explicitly, in this tab
|
|
445
|
+
// only: see expireSession().
|
|
446
|
+
await this.expireSession(isAuthenticated)
|
|
238
447
|
return undefined
|
|
239
448
|
}
|
|
240
449
|
if (this.authPromise) {
|
|
@@ -263,17 +472,36 @@ export class AuthenticationManager {
|
|
|
263
472
|
// would put the signed-out user's delegation back on the agent.
|
|
264
473
|
const revision = this.authStateRevision
|
|
265
474
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
475
|
+
let client =
|
|
476
|
+
this.authClient ??
|
|
477
|
+
(await this.initializeClient(this.resolveClientOptions()))
|
|
478
|
+
if (!client) {
|
|
479
|
+
this.updateState({ isAuthenticating: false })
|
|
480
|
+
return undefined
|
|
481
|
+
}
|
|
482
|
+
let { clientIdentity, isAuthenticated } =
|
|
483
|
+
await this.readClientSession(client)
|
|
484
|
+
// Per-call options can replace the client while it is read, and
|
|
485
|
+
// `dispose()` can release it. Both answers used to be read from
|
|
486
|
+
// whichever client was current at the time, so they could come from two
|
|
487
|
+
// clients. A v10 client disposed while it restores hands out the
|
|
488
|
+
// anonymous identity, and the record its replacement reads still says
|
|
489
|
+
// signed in: the manager reported the anonymous principal signed in.
|
|
490
|
+
// The client in use now is read instead, and a manager that let go of
|
|
491
|
+
// its client publishes nothing. Nothing may be signed with what a
|
|
492
|
+
// released client held.
|
|
493
|
+
while (
|
|
494
|
+
revision === this.authStateRevision &&
|
|
495
|
+
client !== this.authClient
|
|
496
|
+
) {
|
|
497
|
+
if (!this.authClient) {
|
|
271
498
|
this.updateState({ isAuthenticating: false })
|
|
272
|
-
return undefined
|
|
499
|
+
return this.authState.identity || undefined
|
|
273
500
|
}
|
|
501
|
+
client = this.authClient
|
|
502
|
+
;({ clientIdentity, isAuthenticated } =
|
|
503
|
+
await this.readClientSession(client))
|
|
274
504
|
}
|
|
275
|
-
const clientIdentity = await this.authClient!.getIdentity()
|
|
276
|
-
const isAuthenticated = await this.authClient!.isAuthenticated()
|
|
277
505
|
|
|
278
506
|
if (revision !== this.authStateRevision) {
|
|
279
507
|
// Superseded — leave whatever ran in the meantime in place.
|
|
@@ -281,7 +509,9 @@ export class AuthenticationManager {
|
|
|
281
509
|
}
|
|
282
510
|
// A client that says it is not authenticated but still hands out a
|
|
283
511
|
// non-anonymous identity is holding a delegation it will no longer
|
|
284
|
-
// vouch for (expired, mid-session).
|
|
512
|
+
// vouch for (expired, mid-session). So is a v8 client that says it is,
|
|
513
|
+
// once another tab has signed in again, while it hands out the
|
|
514
|
+
// delegation that lapsed in this one. Nothing may be signed with it.
|
|
285
515
|
const identity =
|
|
286
516
|
isAuthenticated || clientIdentity.getPrincipal().isAnonymous()
|
|
287
517
|
? clientIdentity
|
|
@@ -347,7 +577,7 @@ export class AuthenticationManager {
|
|
|
347
577
|
}
|
|
348
578
|
|
|
349
579
|
this.clientManager.updateAgent(identity)
|
|
350
|
-
this.
|
|
580
|
+
this.publishSession({
|
|
351
581
|
identity,
|
|
352
582
|
isAuthenticated: true,
|
|
353
583
|
isAuthenticating: false,
|
|
@@ -363,37 +593,80 @@ export class AuthenticationManager {
|
|
|
363
593
|
}
|
|
364
594
|
} catch (error) {
|
|
365
595
|
if (!didCompleteSignIn) {
|
|
366
|
-
|
|
596
|
+
// Recorded before the callback runs, as on success: an onError that
|
|
597
|
+
// rejected used to skip this and strand `isAuthenticating: true` with
|
|
598
|
+
// no error on record.
|
|
367
599
|
this.updateState({
|
|
368
600
|
error: error as Error,
|
|
369
601
|
isAuthenticating: false,
|
|
370
602
|
})
|
|
603
|
+
await loginOptions?.onError?.((error as Error).message)
|
|
371
604
|
}
|
|
372
605
|
throw error
|
|
373
606
|
}
|
|
374
607
|
}
|
|
375
608
|
|
|
376
609
|
public logout = async (options?: { returnTo?: string }) => {
|
|
377
|
-
|
|
610
|
+
// None built yet, or released by `dispose()`. Signing out needs no user
|
|
611
|
+
// gesture, so one can be built here.
|
|
612
|
+
const client = this.authClient ?? (await this.ensureClient())
|
|
613
|
+
if (!client) {
|
|
378
614
|
throw new Error(
|
|
379
615
|
"Authentication module is missing or failed to initialize. To use logout, install the optional auth peer: npm install @icp-sdk/auth. If it is already installed and your bundler could not resolve it, pass a pre-constructed client instead: new AuthenticationManager({ clientManager, authClient: new AuthClient(...) })"
|
|
380
616
|
)
|
|
381
617
|
}
|
|
382
618
|
this.updateState({ isAuthenticating: true, error: undefined })
|
|
383
619
|
try {
|
|
384
|
-
|
|
385
|
-
|
|
620
|
+
// The client the sign-out started on, even once `dispose()` has released
|
|
621
|
+
// it, as when the sign-out closes the widget that built this manager.
|
|
622
|
+
// Reading `this.authClient` then failed the sign-out with a TypeError.
|
|
623
|
+
await client.signOut(options)
|
|
624
|
+
const identity = await client.getIdentity()
|
|
386
625
|
this.clientManager.updateAgent(identity)
|
|
387
|
-
this.
|
|
626
|
+
this.publishSession({
|
|
388
627
|
identity,
|
|
389
628
|
isAuthenticated: false,
|
|
390
629
|
isAuthenticating: false,
|
|
391
630
|
})
|
|
392
631
|
} catch (error) {
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
632
|
+
// A failed signOut does not always mean the session survived. v10 wipes
|
|
633
|
+
// the device and drops to an anonymous identity before it raises a revoke
|
|
634
|
+
// the canister did not answer, so keeping the session here left the app
|
|
635
|
+
// signed in and the agent signing as the user who had just signed out.
|
|
636
|
+
// Follow the client instead: once it no longer vouches for the session,
|
|
637
|
+
// nothing may sign with it, which is the rule `authenticate()` applies
|
|
638
|
+
// too. A v8 client that failed before forgetting anything still vouches
|
|
639
|
+
// for its session and keeps it, unless its delegation has lapsed (see
|
|
640
|
+
// vouchesFor()). A check that throws keeps it as well.
|
|
641
|
+
//
|
|
642
|
+
// Anything that changes auth state while that check is in flight, such
|
|
643
|
+
// as a login that finishes meanwhile, bumps this. The check then
|
|
644
|
+
// describes a client that has been used since, and its answer must not
|
|
645
|
+
// replace the newer state, as in `authenticate()`.
|
|
646
|
+
const revision = this.authStateRevision
|
|
647
|
+
const stillSignedIn = await Promise.resolve()
|
|
648
|
+
.then(() => client.isAuthenticated())
|
|
649
|
+
.catch(() => true)
|
|
650
|
+
if (revision !== this.authStateRevision) {
|
|
651
|
+
throw error
|
|
652
|
+
}
|
|
653
|
+
if (this.vouchesFor(this.authState.identity, stillSignedIn)) {
|
|
654
|
+
// Without this the manager was left with `isAuthenticating: true` and
|
|
655
|
+
// no recorded error, so a button disabled on `isAuthenticating` stayed
|
|
656
|
+
// stuck and nothing told the app why.
|
|
657
|
+
this.publishSession({ error: error as Error, isAuthenticating: false })
|
|
658
|
+
} else {
|
|
659
|
+
const identity = new AnonymousIdentity()
|
|
660
|
+
this.clientManager.updateAgent(identity)
|
|
661
|
+
// The error stays recorded: the device is signed out, but the session
|
|
662
|
+
// may still be live at the identity provider.
|
|
663
|
+
this.publishSession({
|
|
664
|
+
identity,
|
|
665
|
+
isAuthenticated: false,
|
|
666
|
+
isAuthenticating: false,
|
|
667
|
+
error: error as Error,
|
|
668
|
+
})
|
|
669
|
+
}
|
|
397
670
|
throw error
|
|
398
671
|
}
|
|
399
672
|
}
|
|
@@ -407,32 +680,225 @@ export class AuthenticationManager {
|
|
|
407
680
|
return undefined
|
|
408
681
|
}
|
|
409
682
|
|
|
410
|
-
|
|
683
|
+
// Every mounted `useAuth()` prepares the client at the same moment, and
|
|
684
|
+
// each call arrives here after the same await. Building one per caller
|
|
685
|
+
// left all but the last running with nothing able to reach them: on v8
|
|
686
|
+
// each registered the app's `onIdle` on the shared IdleManager again, so it
|
|
687
|
+
// fired once per consumer, and on v10 each kept its browser listeners and
|
|
688
|
+
// its session's refresh timer. Take the client an earlier caller built for
|
|
689
|
+
// the same options instead.
|
|
690
|
+
if (this.authClient && !this.shouldRecreateClient(options)) {
|
|
691
|
+
return this.authClient
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return this.installClient(
|
|
695
|
+
new AuthClient(this.toClientOptions(options)),
|
|
696
|
+
options
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Makes `client`, which this manager built for `options`, the current one.
|
|
702
|
+
*
|
|
703
|
+
* The client it replaces was built here too, for other options: a caller's
|
|
704
|
+
* `authClient` is never replaced. It was dropped with nothing released, so a
|
|
705
|
+
* v10 client kept its browser listeners, its state subscription and its
|
|
706
|
+
* session's refresh timer for the life of the page, one more for each switch
|
|
707
|
+
* between option sets, such as a one-click sign-in and a plain one (#729).
|
|
708
|
+
* v10 asks for `dispose()` on a client being discarded, and a new
|
|
709
|
+
* interaction already takes its signer channel from the old client, so this
|
|
710
|
+
* adds no failure of its own. A v8 client has nothing to dispose; see
|
|
711
|
+
* `withSharedIdleCallback()` for the callback it leaves registered.
|
|
712
|
+
*/
|
|
713
|
+
private installClient(
|
|
714
|
+
client: AuthClientLike,
|
|
715
|
+
options?: AuthenticationClientOptions
|
|
716
|
+
): AuthClientLike {
|
|
717
|
+
const replaced = this.authClient
|
|
718
|
+
this.stopWatchingClient()
|
|
719
|
+
this.authClient = client
|
|
411
720
|
this.authClientOptions = options
|
|
412
|
-
|
|
721
|
+
if (replaced && replaced !== client) {
|
|
722
|
+
disposeClient(replaced)
|
|
723
|
+
}
|
|
724
|
+
this.watchClient(client)
|
|
725
|
+
return client
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Follows a v10 client's session record, which every tab of the origin
|
|
730
|
+
* shares.
|
|
731
|
+
*
|
|
732
|
+
* The manager learned about the session only through its own calls, while a
|
|
733
|
+
* v10 client follows the other tabs. After a sign-out in another tab, this
|
|
734
|
+
* tab's client dropped the session and its manager went on reporting the
|
|
735
|
+
* user signed in, with the replaced identity on the agent signing calls as
|
|
736
|
+
* the account the user had left. After a sign-in there as another account,
|
|
737
|
+
* the manager kept the old account while the client held the new one. When
|
|
738
|
+
* the old identity's app delegation then lapsed, its mint was refused and
|
|
739
|
+
* the client removed the record every tab reads, signing the new account out
|
|
740
|
+
* of every tab (#754). `subscribe()` fires after the record changes, here or
|
|
741
|
+
* in another tab, and the manager then reads the client again.
|
|
742
|
+
*
|
|
743
|
+
* v8 has no notification and never revokes a session, so a v8 client is not
|
|
744
|
+
* followed.
|
|
745
|
+
*/
|
|
746
|
+
private watchClient(client: AuthClientLike) {
|
|
747
|
+
if (this.unwatchClient || this.authClientFlavor !== "session") {
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
const { subscribe } = client as SubscribableAuthClient
|
|
751
|
+
if (typeof subscribe !== "function") {
|
|
752
|
+
return
|
|
753
|
+
}
|
|
754
|
+
this.unwatchClient = subscribe.call(client, () => {
|
|
755
|
+
// The client tells its listeners before it starts restoring for the new
|
|
756
|
+
// record, which it does right after they return. Reading it a microtask
|
|
757
|
+
// later waits for that restore. The state is published whether or not a
|
|
758
|
+
// subscriber throws, and there is no caller to hand that error to.
|
|
759
|
+
void Promise.resolve()
|
|
760
|
+
.then(() => this.followClient(client))
|
|
761
|
+
.catch(() => undefined)
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private stopWatchingClient() {
|
|
766
|
+
this.unwatchClient?.()
|
|
767
|
+
this.unwatchClient = undefined
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Reads the session from `client` after its record changed, and publishes
|
|
772
|
+
* it when it differs from what this manager holds, as `syncStateFromClient()`
|
|
773
|
+
* derives it. An error recorded for the session it replaces goes with it.
|
|
774
|
+
*
|
|
775
|
+
* An operation of the manager's own, which sets `isAuthenticating`, writes
|
|
776
|
+
* the record itself and publishes what the client holds when it ends, so a
|
|
777
|
+
* change during one is read again once it has. So is a change whose read
|
|
778
|
+
* something else published over.
|
|
779
|
+
*/
|
|
780
|
+
private async followClient(client: AuthClientLike): Promise<void> {
|
|
781
|
+
if (client !== this.authClient) {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
if (this.authState.isAuthenticating) {
|
|
785
|
+
this.clientChangedDuringOperation = true
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
const pass = ++this.followRevision
|
|
789
|
+
const revision = this.authStateRevision
|
|
790
|
+
const session = await this.readFollowedSession(client)
|
|
791
|
+
if (pass !== this.followRevision || client !== this.authClient) {
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
if (revision !== this.authStateRevision) {
|
|
795
|
+
return this.followClient(client)
|
|
796
|
+
}
|
|
797
|
+
if (!session) {
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
const current = this.authState
|
|
801
|
+
const anonymous = session.identity.getPrincipal().isAnonymous()
|
|
802
|
+
const unchanged =
|
|
803
|
+
session.isAuthenticated === current.isAuthenticated &&
|
|
804
|
+
(session.identity === current.identity ||
|
|
805
|
+
(!session.isAuthenticated &&
|
|
806
|
+
anonymous &&
|
|
807
|
+
current.identity?.getPrincipal().isAnonymous() === true))
|
|
808
|
+
if (unchanged) {
|
|
809
|
+
this.markSessionChecked()
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
// As in `authenticate()`, an agent that is anonymous already is left as
|
|
813
|
+
// it is.
|
|
814
|
+
if (!anonymous || !this.agentIsAnonymous()) {
|
|
815
|
+
this.clientManager.updateAgent(session.identity)
|
|
816
|
+
}
|
|
817
|
+
this.publishSession({
|
|
818
|
+
identity: session.identity,
|
|
819
|
+
isAuthenticated: session.isAuthenticated,
|
|
820
|
+
isAuthenticating: false,
|
|
821
|
+
error: undefined,
|
|
822
|
+
})
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* The identity `client` hands out, and whether it vouches for it (see
|
|
827
|
+
* `vouchesFor()`). Both are read from the same client.
|
|
828
|
+
*/
|
|
829
|
+
private async readClientSession(client: AuthClientLike) {
|
|
830
|
+
const clientIdentity = await client.getIdentity()
|
|
831
|
+
const isAuthenticated = this.vouchesFor(
|
|
832
|
+
clientIdentity,
|
|
833
|
+
await client.isAuthenticated()
|
|
834
|
+
)
|
|
835
|
+
return { clientIdentity, isAuthenticated }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* The session `client` holds, or `undefined` to leave the manager's as it is.
|
|
840
|
+
*
|
|
841
|
+
* v10 refuses to hand out an identity while the record names a sign-in it
|
|
842
|
+
* holds no credential for, as when restoring the account another tab signed
|
|
843
|
+
* in as failed. The session this manager holds is kept then only while the
|
|
844
|
+
* record still names its account. One for any other account must not stay
|
|
845
|
+
* on the agent.
|
|
846
|
+
*/
|
|
847
|
+
private async readFollowedSession(
|
|
848
|
+
client: AuthClientLike
|
|
849
|
+
): Promise<{ identity: Identity; isAuthenticated: boolean } | undefined> {
|
|
850
|
+
try {
|
|
851
|
+
const { clientIdentity, isAuthenticated } =
|
|
852
|
+
await this.readClientSession(client)
|
|
853
|
+
// The rule `authenticate()` applies to an identity the client no longer
|
|
854
|
+
// vouches for.
|
|
855
|
+
const identity =
|
|
856
|
+
isAuthenticated || clientIdentity.getPrincipal().isAnonymous()
|
|
857
|
+
? clientIdentity
|
|
858
|
+
: new AnonymousIdentity()
|
|
859
|
+
return { identity, isAuthenticated }
|
|
860
|
+
} catch {
|
|
861
|
+
const { isAuthenticated, identity } = this.authState
|
|
862
|
+
const held = isAuthenticated
|
|
863
|
+
? identity?.getPrincipal().toText()
|
|
864
|
+
: undefined
|
|
865
|
+
const named = (client as SubscribableAuthClient)
|
|
866
|
+
.getPrincipal?.()
|
|
867
|
+
?.toText()
|
|
868
|
+
if (held === undefined || held === named) {
|
|
869
|
+
return undefined
|
|
870
|
+
}
|
|
871
|
+
return { identity: new AnonymousIdentity(), isAuthenticated: false }
|
|
872
|
+
}
|
|
413
873
|
}
|
|
414
874
|
|
|
415
875
|
/** @internal Used by IdentityAttributesManager. */
|
|
416
876
|
public async signInOrRecoverIdentity(
|
|
417
877
|
options?: AuthClientSignInOptions
|
|
418
878
|
): Promise<Identity> {
|
|
419
|
-
|
|
879
|
+
// Held, because `dispose()` can release it while the popup is open. The
|
|
880
|
+
// recovery below then failed with a TypeError instead of the client's own
|
|
881
|
+
// error.
|
|
882
|
+
const client = this.authClient
|
|
883
|
+
if (!client) {
|
|
420
884
|
throw new Error(
|
|
421
885
|
"Authentication module is missing or failed to initialize. To use login, install the optional auth peer: npm install @icp-sdk/auth. If it is already installed and your bundler could not resolve it, pass a pre-constructed client instead: new AuthenticationManager({ clientManager, authClient: new AuthClient(...) })"
|
|
422
886
|
)
|
|
423
887
|
}
|
|
424
888
|
|
|
425
889
|
try {
|
|
426
|
-
return await
|
|
890
|
+
return await client.signIn(
|
|
891
|
+
toAuthClientSignInOptions(options, this.authClientFlavor)
|
|
892
|
+
)
|
|
427
893
|
} catch (error) {
|
|
428
|
-
const identity = await Promise.resolve(
|
|
429
|
-
|
|
430
|
-
)
|
|
894
|
+
const identity = await Promise.resolve(client.getIdentity()).catch(
|
|
895
|
+
() => null
|
|
896
|
+
)
|
|
431
897
|
const isAuthenticated = await Promise.resolve(
|
|
432
|
-
|
|
898
|
+
client.isAuthenticated()
|
|
433
899
|
).catch(() => false)
|
|
434
900
|
|
|
435
|
-
if (identity && isAuthenticated) {
|
|
901
|
+
if (identity && this.vouchesFor(identity, isAuthenticated)) {
|
|
436
902
|
return identity
|
|
437
903
|
}
|
|
438
904
|
|
|
@@ -444,6 +910,7 @@ export class AuthenticationManager {
|
|
|
444
910
|
options?: AuthenticationClientOptions
|
|
445
911
|
): AuthClientLike | undefined {
|
|
446
912
|
if (this.authClient && !this.shouldRecreateClient(options)) {
|
|
913
|
+
this.watchClient(this.authClient)
|
|
447
914
|
return this.authClient
|
|
448
915
|
}
|
|
449
916
|
|
|
@@ -452,9 +919,111 @@ export class AuthenticationManager {
|
|
|
452
919
|
return undefined
|
|
453
920
|
}
|
|
454
921
|
|
|
455
|
-
this.
|
|
456
|
-
|
|
457
|
-
|
|
922
|
+
return this.installClient(
|
|
923
|
+
new AuthClient(this.toClientOptions(options)),
|
|
924
|
+
options
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Hands the installed client the option shape it actually accepts.
|
|
930
|
+
*
|
|
931
|
+
* `authClientOptions` keeps the untranslated values, and `shouldRecreateClient`
|
|
932
|
+
* compares them with the installed major in mind: two calls that differ only
|
|
933
|
+
* in a key that major drops count as the same options, so the client is not
|
|
934
|
+
* rebuilt for them.
|
|
935
|
+
*/
|
|
936
|
+
private toClientOptions(options?: AuthenticationClientOptions): unknown {
|
|
937
|
+
return toAuthClientConstructorOptions(
|
|
938
|
+
this.authClientFlavor === "legacy"
|
|
939
|
+
? withSharedIdleCallback(options)
|
|
940
|
+
: options,
|
|
941
|
+
this.authClientFlavor,
|
|
942
|
+
this.identityProviderPairing(options?.identityProvider),
|
|
943
|
+
this.sessionAgentOptions()
|
|
944
|
+
)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Options for the agent a v9+ client mints delegations with, off mainnet.
|
|
949
|
+
*
|
|
950
|
+
* That client makes its own calls to the Internet Identity canister, through
|
|
951
|
+
* an agent built from these options alone. Without a root key it checks every
|
|
952
|
+
* certificate against mainnet's, which a local replica or testnet cannot
|
|
953
|
+
* satisfy, so sign-in would fail at the first mint. Off mainnet it gets the
|
|
954
|
+
* replica this app already talks to and fetches that network's root key, the
|
|
955
|
+
* same trust the app's own agent needs there. When the app passed its own
|
|
956
|
+
* `agentOptions.rootKey`, which its agent keeps, the minting agent gets that
|
|
957
|
+
* key instead and verifies against it too. On mainnet nothing is passed, and
|
|
958
|
+
* the client keeps its defaults.
|
|
959
|
+
*/
|
|
960
|
+
private sessionAgentOptions(): Record<string, unknown> | undefined {
|
|
961
|
+
if (!this.clientManager.isLocal) {
|
|
962
|
+
return undefined
|
|
963
|
+
}
|
|
964
|
+
const host = this.clientManager.agentHost
|
|
965
|
+
const rootKey = this.clientManager.explicitRootKey
|
|
966
|
+
return {
|
|
967
|
+
...(host ? { host: host.toString() } : {}),
|
|
968
|
+
...(rootKey ? { rootKey } : { shouldFetchRootKey: true }),
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Which canister a v9+ client should pair with `identityProvider`.
|
|
974
|
+
*
|
|
975
|
+
* v9+ names a provider by its authorize URL and the canister that mints its
|
|
976
|
+
* delegations, and nothing about the canister follows from the URL. The
|
|
977
|
+
* mainnet URL goes with mainnet's canister unless the caller named another.
|
|
978
|
+
* A canister read from the `ic_env` cookie belongs to a local deployment, so
|
|
979
|
+
* it never overrides that. Any other URL takes `internetIdentityId`, or the
|
|
980
|
+
* well-known local canister when the URL is one IC Reactor derived for a
|
|
981
|
+
* local deployment. A URL the caller set with no canister stays `unknown`.
|
|
982
|
+
*/
|
|
983
|
+
private identityProviderPairing(
|
|
984
|
+
identityProvider?: string | URL
|
|
985
|
+
): IdentityProviderPairing {
|
|
986
|
+
if (String(identityProvider) === IC_INTERNET_IDENTITY_PROVIDER) {
|
|
987
|
+
return this.internetIdentityIdIsExplicit && this.internetIdentityId
|
|
988
|
+
? { kind: "pair", canisterId: this.internetIdentityId }
|
|
989
|
+
: { kind: "mainnet" }
|
|
990
|
+
}
|
|
991
|
+
if (this.internetIdentityId) {
|
|
992
|
+
return { kind: "pair", canisterId: this.internetIdentityId }
|
|
993
|
+
}
|
|
994
|
+
if (
|
|
995
|
+
identityProvider !== undefined &&
|
|
996
|
+
this.isDerivedLocalProvider(identityProvider)
|
|
997
|
+
) {
|
|
998
|
+
return { kind: "pair", canisterId: LOCAL_INTERNET_IDENTITY_CANISTER_ID }
|
|
999
|
+
}
|
|
1000
|
+
return { kind: "unknown" }
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Whether `identityProvider` is a local provider IC Reactor chose, from the
|
|
1005
|
+
* `ic_env` cookie or built for the local replica, rather than one a caller
|
|
1006
|
+
* configured.
|
|
1007
|
+
*/
|
|
1008
|
+
private isDerivedLocalProvider(identityProvider: string | URL): boolean {
|
|
1009
|
+
if (this.envIdentityProvider !== undefined) {
|
|
1010
|
+
return String(identityProvider) === String(this.envIdentityProvider)
|
|
1011
|
+
}
|
|
1012
|
+
if (
|
|
1013
|
+
this.identityProvider !== undefined ||
|
|
1014
|
+
!this.clientManager.isLocal ||
|
|
1015
|
+
this.localAuthorizePath === null
|
|
1016
|
+
) {
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
return (
|
|
1020
|
+
String(identityProvider) ===
|
|
1021
|
+
localInternetIdentityProvider(
|
|
1022
|
+
Number(this.clientManager.agentHost?.port) || 4943,
|
|
1023
|
+
this.internetIdentityId,
|
|
1024
|
+
this.localAuthorizePath
|
|
1025
|
+
)
|
|
1026
|
+
)
|
|
458
1027
|
}
|
|
459
1028
|
|
|
460
1029
|
/**
|
|
@@ -468,7 +1037,11 @@ export class AuthenticationManager {
|
|
|
468
1037
|
if (this.authClientWasProvided) {
|
|
469
1038
|
return false
|
|
470
1039
|
}
|
|
471
|
-
return !isSameAuthClientOptions(
|
|
1040
|
+
return !isSameAuthClientOptions(
|
|
1041
|
+
this.authClientOptions,
|
|
1042
|
+
options,
|
|
1043
|
+
this.authClientFlavor
|
|
1044
|
+
)
|
|
472
1045
|
}
|
|
473
1046
|
|
|
474
1047
|
/**
|
|
@@ -497,17 +1070,76 @@ export class AuthenticationManager {
|
|
|
497
1070
|
}
|
|
498
1071
|
|
|
499
1072
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
1073
|
+
* @internal Used by IdentityAttributesManager.
|
|
1074
|
+
*
|
|
1075
|
+
* Whether the client vouches for `identity`, given what its
|
|
1076
|
+
* `isAuthenticated()` answered.
|
|
1077
|
+
*
|
|
1078
|
+
* A v8 client's answer is not about the identity it holds. It reads the
|
|
1079
|
+
* delegation expiry v8 keeps in the `localStorage` every tab shares, while
|
|
1080
|
+
* `getIdentity()` returns the identity this client restored or signed in
|
|
1081
|
+
* with, and v8 never reads storage again once it has loaded. When the session
|
|
1082
|
+
* lapses and the user signs in again in another tab, that tab writes a new
|
|
1083
|
+
* expiry, and the answer is yes again for the delegation this tab still
|
|
1084
|
+
* holds, which the replica refuses. So a v8 identity whose own delegation
|
|
1085
|
+
* has expired is not vouched for, whatever the answer. Nor is the anonymous
|
|
1086
|
+
* identity, which a v8 client that loaded signed out, or signed out, goes on
|
|
1087
|
+
* handing out once another tab signs in and the answer turns yes. It signs
|
|
1088
|
+
* no one in.
|
|
1089
|
+
*
|
|
1090
|
+
* A v10 client's answer is about the session it holds, and its identity
|
|
1091
|
+
* replaces its short-lived delegation as it ages, so the delegation it holds
|
|
1092
|
+
* can be past its expiry while the session is live. The answer stands alone.
|
|
505
1093
|
*/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1094
|
+
public vouchesFor(
|
|
1095
|
+
identity: Identity | null | undefined,
|
|
1096
|
+
isAuthenticated: boolean
|
|
1097
|
+
): boolean {
|
|
1098
|
+
return (
|
|
1099
|
+
isAuthenticated &&
|
|
1100
|
+
!(
|
|
1101
|
+
this.authClientFlavor === "legacy" &&
|
|
1102
|
+
(identity?.getPrincipal().isAnonymous() ||
|
|
1103
|
+
hasExpiredDelegation(identity))
|
|
1104
|
+
)
|
|
1105
|
+
)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* End a session the client no longer vouches for: put the anonymous
|
|
1110
|
+
* identity on the agent -- which also sweeps the previous user's
|
|
1111
|
+
* caller-scoped cache entries and refetches the rest anonymously -- and
|
|
1112
|
+
* publish the signed-out state.
|
|
1113
|
+
*
|
|
1114
|
+
* A v8 client is also asked to forget the session when its
|
|
1115
|
+
* `isAuthenticated()` answered no, since it keeps handing out the lapsed
|
|
1116
|
+
* delegation until `signOut()` runs. That answer reads the expiry kept in
|
|
1117
|
+
* the `localStorage` every tab shares, so it is no only once the session
|
|
1118
|
+
* stored for every tab is over, and v8's `signOut()` takes no lock and
|
|
1119
|
+
* revokes nothing. `signOut` failing changes nothing here: the delegation is
|
|
1120
|
+
* already unusable, and the agent must not keep it. When the answer was yes,
|
|
1121
|
+
* the delegation this tab holds lapsed under a session another tab has
|
|
1122
|
+
* signed in to since, and `signOut()` would delete that session from the
|
|
1123
|
+
* storage every tab shares. The client is left alone, and `vouchesFor()`
|
|
1124
|
+
* keeps the lapsed delegation it goes on handing out off the agent.
|
|
1125
|
+
*
|
|
1126
|
+
* A v10 client is left alone, as `commitSignedOut()` leaves it. Its
|
|
1127
|
+
* `signOut()` ends the sign-in for every tab of the origin: it takes the
|
|
1128
|
+
* sign-in lock from a sign-in another tab has in progress, which then fails,
|
|
1129
|
+
* revokes whatever session the shared store holds, and removes the record
|
|
1130
|
+
* every tab reads. Another tab may have signed in again since this one last
|
|
1131
|
+
* looked, and finding a session over is not the user asking to sign out.
|
|
1132
|
+
* The client already stopped vouching for the session on its own.
|
|
1133
|
+
*
|
|
1134
|
+
* @param isAuthenticated - What the client's `isAuthenticated()` answered.
|
|
1135
|
+
*/
|
|
1136
|
+
private async expireSession(isAuthenticated: boolean) {
|
|
1137
|
+
if (this.authClientFlavor !== "session" && !isAuthenticated) {
|
|
1138
|
+
try {
|
|
1139
|
+
await this.authClient?.signOut()
|
|
1140
|
+
} catch {
|
|
1141
|
+
// Nothing to keep; fall through to anonymous either way.
|
|
1142
|
+
}
|
|
511
1143
|
}
|
|
512
1144
|
const identity = new AnonymousIdentity()
|
|
513
1145
|
this.clientManager.updateAgent(identity)
|
|
@@ -524,18 +1156,33 @@ export class AuthenticationManager {
|
|
|
524
1156
|
return
|
|
525
1157
|
}
|
|
526
1158
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1159
|
+
try {
|
|
1160
|
+
const clientIdentity = await this.authClient.getIdentity()
|
|
1161
|
+
const isAuthenticated = this.vouchesFor(
|
|
1162
|
+
clientIdentity,
|
|
1163
|
+
await this.authClient.isAuthenticated()
|
|
1164
|
+
)
|
|
1165
|
+
if (revision !== this.authStateRevision) {
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
// The rule `authenticate()` applies. A caller-built client can outlive
|
|
1169
|
+
// the manager it was first given to, and once its session has lapsed it
|
|
1170
|
+
// still hands out the lapsed identity while no longer vouching for it.
|
|
1171
|
+
// Nothing may be signed with that.
|
|
1172
|
+
const identity =
|
|
1173
|
+
isAuthenticated || clientIdentity.getPrincipal().isAnonymous()
|
|
1174
|
+
? clientIdentity
|
|
1175
|
+
: new AnonymousIdentity()
|
|
1176
|
+
this.clientManager.updateAgent(identity)
|
|
1177
|
+
this.updateState({
|
|
1178
|
+
identity,
|
|
1179
|
+
isAuthenticated,
|
|
1180
|
+
isAuthenticating: false,
|
|
1181
|
+
error: undefined,
|
|
1182
|
+
})
|
|
1183
|
+
} finally {
|
|
1184
|
+
this.markSessionChecked()
|
|
531
1185
|
}
|
|
532
|
-
this.clientManager.updateAgent(identity)
|
|
533
|
-
this.updateState({
|
|
534
|
-
identity,
|
|
535
|
-
isAuthenticated,
|
|
536
|
-
isAuthenticating: false,
|
|
537
|
-
error: undefined,
|
|
538
|
-
})
|
|
539
1186
|
}
|
|
540
1187
|
|
|
541
1188
|
/** @internal Used by IdentityAttributesManager. */
|
|
@@ -553,7 +1200,7 @@ export class AuthenticationManager {
|
|
|
553
1200
|
await this.clientManager.initializeAgent()
|
|
554
1201
|
}
|
|
555
1202
|
this.clientManager.updateAgent(identity)
|
|
556
|
-
this.
|
|
1203
|
+
this.publishSession({ identity, isAuthenticated, isAuthenticating: false })
|
|
557
1204
|
}
|
|
558
1205
|
|
|
559
1206
|
/** @internal Used by IdentityAttributesManager. */
|
|
@@ -566,6 +1213,34 @@ export class AuthenticationManager {
|
|
|
566
1213
|
this.updateState({ error, isAuthenticating: false })
|
|
567
1214
|
}
|
|
568
1215
|
|
|
1216
|
+
/** @internal Used by IdentityAttributesManager. */
|
|
1217
|
+
public settleAuthenticating() {
|
|
1218
|
+
this.updateState({ isAuthenticating: false })
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* @internal Used by IdentityAttributesManager.
|
|
1223
|
+
*
|
|
1224
|
+
* Signs this manager out after the client lost its session under it, as when
|
|
1225
|
+
* another tab signed out. The client itself is left alone: a v10 sign-out
|
|
1226
|
+
* clears the storage every tab shares and takes the sign-in lock, so it would
|
|
1227
|
+
* also end a sign-in the other tab has made or started since.
|
|
1228
|
+
*/
|
|
1229
|
+
public commitSignedOut() {
|
|
1230
|
+
const identity = new AnonymousIdentity()
|
|
1231
|
+
// As in `authenticate()`, an agent that is anonymous already is left as it
|
|
1232
|
+
// is: its cache holds no signed-in user's data, and re-installing it would
|
|
1233
|
+
// only refetch every query.
|
|
1234
|
+
if (!this.agentIsAnonymous()) {
|
|
1235
|
+
this.clientManager.updateAgent(identity)
|
|
1236
|
+
}
|
|
1237
|
+
this.publishSession({
|
|
1238
|
+
identity,
|
|
1239
|
+
isAuthenticated: false,
|
|
1240
|
+
isAuthenticating: false,
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
|
|
569
1244
|
private getDefaultIdentityProvider(): string | URL {
|
|
570
1245
|
if (this.identityProvider) {
|
|
571
1246
|
return this.identityProvider
|
|
@@ -582,7 +1257,10 @@ export class AuthenticationManager {
|
|
|
582
1257
|
// it, instead of opening a popup onto the gateway's verification-error page
|
|
583
1258
|
// and leaving the app waiting until the user closes it.
|
|
584
1259
|
if (this.localAuthorizePath === null) {
|
|
585
|
-
throw localInternetIdentityUnavailableError(
|
|
1260
|
+
throw localInternetIdentityUnavailableError(
|
|
1261
|
+
canisterId,
|
|
1262
|
+
this.authClientFlavor
|
|
1263
|
+
)
|
|
586
1264
|
}
|
|
587
1265
|
|
|
588
1266
|
return localInternetIdentityProvider(
|
|
@@ -592,13 +1270,63 @@ export class AuthenticationManager {
|
|
|
592
1270
|
)
|
|
593
1271
|
}
|
|
594
1272
|
|
|
1273
|
+
/**
|
|
1274
|
+
* Publishes what a check of the session found, then marks the session
|
|
1275
|
+
* checked. In that order, so the auth hooks never take the state before it
|
|
1276
|
+
* for the answer.
|
|
1277
|
+
*/
|
|
1278
|
+
private publishSession(newState: Partial<AuthState>) {
|
|
1279
|
+
try {
|
|
1280
|
+
this.updateState(newState)
|
|
1281
|
+
} finally {
|
|
1282
|
+
this.markSessionChecked()
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Records a change, then tells every subscriber about it.
|
|
1288
|
+
*
|
|
1289
|
+
* Every subscriber is called even when one throws, and the first error is
|
|
1290
|
+
* rethrown once they all have been, so the caller still sees it. A throw
|
|
1291
|
+
* used to end the loop: an app's subscriber registered at module scope comes
|
|
1292
|
+
* before every `useAuth()`, and one that failed on a sign-in left them all
|
|
1293
|
+
* showing `isAuthenticating: true` while the agent signed as the user.
|
|
1294
|
+
*
|
|
1295
|
+
* A subscriber that changes the state again from its callback has told every
|
|
1296
|
+
* subscriber about that newer state, so the loop stops rather than deliver
|
|
1297
|
+
* this older one after it. The list is copied first, so a subscriber added
|
|
1298
|
+
* during the loop is first called for the next change. `ClientManager`
|
|
1299
|
+
* notifies its subscribers the same way.
|
|
1300
|
+
*/
|
|
595
1301
|
private updateState(newState: Partial<AuthState>) {
|
|
596
1302
|
if (isDev()) console.debug("[ic-reactor] Updating Auth State:", newState)
|
|
597
|
-
this.authStateRevision
|
|
598
|
-
|
|
599
|
-
this.
|
|
600
|
-
|
|
601
|
-
|
|
1303
|
+
const revision = ++this.authStateRevision
|
|
1304
|
+
const state = { ...this.authStateValue, ...newState }
|
|
1305
|
+
this.authStateValue = state
|
|
1306
|
+
|
|
1307
|
+
let failure: { error: unknown } | undefined
|
|
1308
|
+
for (const subscriber of [...this.authStateSubscribers]) {
|
|
1309
|
+
if (revision !== this.authStateRevision) break
|
|
1310
|
+
try {
|
|
1311
|
+
subscriber(state)
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
failure ??= { error }
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
// The operation that held off `followClient()` is over: read the client
|
|
1317
|
+
// again, once its caller has moved on.
|
|
1318
|
+
const client = this.authClient
|
|
1319
|
+
if (
|
|
1320
|
+
this.clientChangedDuringOperation &&
|
|
1321
|
+
!this.authStateValue.isAuthenticating &&
|
|
1322
|
+
client
|
|
1323
|
+
) {
|
|
1324
|
+
this.clientChangedDuringOperation = false
|
|
1325
|
+
void Promise.resolve()
|
|
1326
|
+
.then(() => this.followClient(client))
|
|
1327
|
+
.catch(() => undefined)
|
|
1328
|
+
}
|
|
1329
|
+
if (failure) throw failure.error
|
|
602
1330
|
}
|
|
603
1331
|
|
|
604
1332
|
private async loadAuthClientConstructor() {
|
|
@@ -618,6 +1346,7 @@ export class AuthenticationManager {
|
|
|
618
1346
|
}
|
|
619
1347
|
|
|
620
1348
|
this.authClientConstructor = AuthClient
|
|
1349
|
+
this.authClientFlavor = detectAuthClientFlavor(AuthClient)
|
|
621
1350
|
return AuthClient
|
|
622
1351
|
})
|
|
623
1352
|
.catch((error) => {
|
|
@@ -668,6 +1397,74 @@ function importAuthClientModule(): Promise<unknown> {
|
|
|
668
1397
|
}
|
|
669
1398
|
}
|
|
670
1399
|
|
|
1400
|
+
/**
|
|
1401
|
+
* Releases a client IC Reactor built and no longer uses. `dispose()` exists
|
|
1402
|
+
* from `@icp-sdk/auth` v9; a v8 client has nothing to release. A throw is
|
|
1403
|
+
* ignored: the client is being discarded either way.
|
|
1404
|
+
*/
|
|
1405
|
+
function disposeClient(client: AuthClientLike) {
|
|
1406
|
+
try {
|
|
1407
|
+
;(client as { dispose?: () => void }).dispose?.()
|
|
1408
|
+
} catch {
|
|
1409
|
+
// Nothing more can be done for a client that failed to let go.
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** The wrapper each app `onIdle` gets; see {@link withSharedIdleCallback}. */
|
|
1414
|
+
const sharedIdleCallbacks = new WeakMap<() => unknown, () => unknown>()
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Hands every v8 client the same wrapper around the app's `idleOptions.onIdle`.
|
|
1418
|
+
*
|
|
1419
|
+
* v8's `IdleManager` is one per page. Each client registers its `onIdle` on it
|
|
1420
|
+
* once it signs in or restores a session, and a callback cannot be removed, so
|
|
1421
|
+
* a manager that rebuilt its client for per-call options ran the app's `onIdle`
|
|
1422
|
+
* once per client it had built on every idle period: three times after a
|
|
1423
|
+
* sign-in, a one-click sign-in and another sign-in (#729). The `IdleManager`
|
|
1424
|
+
* runs its callbacks in one synchronous loop, so the wrapper runs `onIdle` on
|
|
1425
|
+
* the first call of a loop and skips the calls after it. The same function
|
|
1426
|
+
* gets the same wrapper however many managers pass it.
|
|
1427
|
+
*/
|
|
1428
|
+
function withSharedIdleCallback(
|
|
1429
|
+
options?: AuthenticationClientOptions
|
|
1430
|
+
): AuthenticationClientOptions | undefined {
|
|
1431
|
+
const onIdle = options?.idleOptions?.onIdle
|
|
1432
|
+
if (!onIdle) {
|
|
1433
|
+
return options
|
|
1434
|
+
}
|
|
1435
|
+
let shared = sharedIdleCallbacks.get(onIdle)
|
|
1436
|
+
if (!shared) {
|
|
1437
|
+
let running = false
|
|
1438
|
+
shared = () => {
|
|
1439
|
+
if (running) return undefined
|
|
1440
|
+
running = true
|
|
1441
|
+
// Cleared once the loop that called it is over, so the next idle period
|
|
1442
|
+
// runs `onIdle` again.
|
|
1443
|
+
queueMicrotask(() => {
|
|
1444
|
+
running = false
|
|
1445
|
+
})
|
|
1446
|
+
return onIdle()
|
|
1447
|
+
}
|
|
1448
|
+
sharedIdleCallbacks.set(onIdle, shared)
|
|
1449
|
+
}
|
|
1450
|
+
return { ...options, idleOptions: { ...options.idleOptions, onIdle: shared } }
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Whether `identity` signs with a delegation chain that has expired, as a
|
|
1455
|
+
* `DelegationIdentity` or `PartialDelegationIdentity` does once its session
|
|
1456
|
+
* lapses. Read by shape rather than `instanceof`, which a second copy of
|
|
1457
|
+
* `@icp-sdk/core` in the app would defeat.
|
|
1458
|
+
*/
|
|
1459
|
+
function hasExpiredDelegation(identity: Identity | null | undefined): boolean {
|
|
1460
|
+
const delegated = identity as
|
|
1461
|
+
{ getDelegation?: () => DelegationChain } | null | undefined
|
|
1462
|
+
if (typeof delegated?.getDelegation !== "function") {
|
|
1463
|
+
return false
|
|
1464
|
+
}
|
|
1465
|
+
return !isDelegationValid(delegated.getDelegation())
|
|
1466
|
+
}
|
|
1467
|
+
|
|
671
1468
|
function getAuthClientOptions(
|
|
672
1469
|
options?: AuthenticationClientOptions
|
|
673
1470
|
): AuthenticationClientOptions | undefined {
|
|
@@ -685,6 +1482,7 @@ function getAuthClientOptions(
|
|
|
685
1482
|
idleOptions: options.idleOptions,
|
|
686
1483
|
identity: options.identity,
|
|
687
1484
|
transport: options.transport,
|
|
1485
|
+
disableBrowserActivity: options.disableBrowserActivity,
|
|
688
1486
|
}
|
|
689
1487
|
}
|
|
690
1488
|
|
|
@@ -703,9 +1501,18 @@ function getAuthClientOpenIdProvider(
|
|
|
703
1501
|
* options (`storage`, `identity`, `idleOptions`) are compared by reference,
|
|
704
1502
|
* which is what module-scoped configuration produces.
|
|
705
1503
|
*/
|
|
1504
|
+
/**
|
|
1505
|
+
* Whether two option sets build the same client on the installed major.
|
|
1506
|
+
*
|
|
1507
|
+
* A key that major drops cannot change the client it builds, so a difference
|
|
1508
|
+
* there must not cause a rebuild, which would throw away the prepared client:
|
|
1509
|
+
* the v8-only `storage`, `keyType`, `idleOptions` and `identity` on v10, and the
|
|
1510
|
+
* v10-only `disableBrowserActivity` on v8.
|
|
1511
|
+
*/
|
|
706
1512
|
function isSameAuthClientOptions(
|
|
707
|
-
current
|
|
708
|
-
next
|
|
1513
|
+
current: AuthenticationClientOptions | undefined,
|
|
1514
|
+
next: AuthenticationClientOptions | undefined,
|
|
1515
|
+
flavor: AuthClientFlavor
|
|
709
1516
|
): boolean {
|
|
710
1517
|
if (current === next) {
|
|
711
1518
|
return true
|
|
@@ -714,19 +1521,24 @@ function isSameAuthClientOptions(
|
|
|
714
1521
|
return false
|
|
715
1522
|
}
|
|
716
1523
|
|
|
717
|
-
|
|
1524
|
+
const sameShared =
|
|
718
1525
|
String(current.identityProvider ?? "") ===
|
|
719
1526
|
String(next.identityProvider ?? "") &&
|
|
720
1527
|
current.windowOpenerFeatures === next.windowOpenerFeatures &&
|
|
721
1528
|
current.openIdProvider === next.openIdProvider &&
|
|
722
1529
|
String(current.derivationOrigin ?? "") ===
|
|
723
1530
|
String(next.derivationOrigin ?? "") &&
|
|
724
|
-
current.storage === next.storage &&
|
|
725
|
-
current.keyType === next.keyType &&
|
|
726
|
-
current.idleOptions === next.idleOptions &&
|
|
727
|
-
current.identity === next.identity &&
|
|
728
1531
|
current.transport === next.transport
|
|
729
|
-
|
|
1532
|
+
|
|
1533
|
+
const sameForFlavor =
|
|
1534
|
+
flavor === "session"
|
|
1535
|
+
? current.disableBrowserActivity === next.disableBrowserActivity
|
|
1536
|
+
: current.storage === next.storage &&
|
|
1537
|
+
current.keyType === next.keyType &&
|
|
1538
|
+
current.idleOptions === next.idleOptions &&
|
|
1539
|
+
current.identity === next.identity
|
|
1540
|
+
|
|
1541
|
+
return sameShared && sameForFlavor
|
|
730
1542
|
}
|
|
731
1543
|
|
|
732
1544
|
/**
|
|
@@ -832,15 +1644,24 @@ function getAuthenticationCanisterEnv(): Record<string, string> | undefined {
|
|
|
832
1644
|
return undefined
|
|
833
1645
|
}
|
|
834
1646
|
|
|
1647
|
+
// Anything able to set a cookie here can write this one, a sibling subdomain
|
|
1648
|
+
// or, on localhost, an app on another port. A value that is not valid
|
|
1649
|
+
// percent-encoding is ignored, as `safeGetCanisterEnv` ignores it: throwing
|
|
1650
|
+
// would fail the constructor, and every `useAuth()` render with it.
|
|
1651
|
+
let decodedValue: string
|
|
1652
|
+
try {
|
|
1653
|
+
decodedValue = decodeURIComponent(encodedValue)
|
|
1654
|
+
} catch {
|
|
1655
|
+
return undefined
|
|
1656
|
+
}
|
|
1657
|
+
|
|
835
1658
|
const env = Object.fromEntries(
|
|
836
|
-
|
|
837
|
-
.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
: [entry.slice(0, separatorIndex), entry.slice(separatorIndex + 1)]
|
|
843
|
-
})
|
|
1659
|
+
decodedValue.split("&").map((entry) => {
|
|
1660
|
+
const separatorIndex = entry.indexOf("=")
|
|
1661
|
+
return separatorIndex === -1
|
|
1662
|
+
? [entry, ""]
|
|
1663
|
+
: [entry.slice(0, separatorIndex), entry.slice(separatorIndex + 1)]
|
|
1664
|
+
})
|
|
844
1665
|
)
|
|
845
1666
|
|
|
846
1667
|
return Object.keys(env).length ? env : undefined
|
|
@@ -855,6 +1676,7 @@ function getSignInOptions(
|
|
|
855
1676
|
|
|
856
1677
|
return {
|
|
857
1678
|
maxTimeToLive: options.maxTimeToLive,
|
|
1679
|
+
maxTimeToIdle: options.maxTimeToIdle,
|
|
858
1680
|
targets: options.targets,
|
|
859
1681
|
}
|
|
860
1682
|
}
|