@ic-reactor/react 3.7.0 → 3.9.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 +158 -9
- package/dist/auth/authentication-manager.d.ts +37 -5
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +242 -52
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts +2 -2
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.d.ts +12 -5
- package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.js +42 -28
- package/dist/auth/identity-attributes-manager.js.map +1 -1
- package/dist/auth/identity-attributes.d.ts +1 -1
- package/dist/auth/identity-attributes.d.ts.map +1 -1
- package/dist/auth/identity-attributes.js +1 -1
- package/dist/auth/identity-attributes.js.map +1 -1
- package/dist/auth/index.d.ts +7 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +7 -7
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/types.d.ts +123 -16
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/createActorHooks.d.ts +4 -4
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +6 -6
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +12 -10
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +20 -17
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +22 -2
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts +1 -1
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +6 -2
- package/dist/createQuery.js.map +1 -1
- package/dist/createSuspenseInfiniteQuery.d.ts +11 -10
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +18 -16
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts +1 -1
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +6 -2
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineReactor.d.ts +54 -4
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +77 -7
- package/dist/defineReactor.js.map +1 -1
- package/dist/hooks/createAuthHooks.d.ts +2 -2
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +7 -2
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +16 -2
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +66 -33
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +8 -1
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +9 -1
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +16 -2
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +14 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +21 -10
- package/dist/utils.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +14 -9
- package/src/auth/authentication-manager.ts +267 -64
- package/src/auth/createIdentityAttributeHooks.ts +2 -2
- package/src/auth/identity-attributes-manager.ts +53 -44
- package/src/auth/identity-attributes.ts +2 -2
- package/src/auth/index.ts +7 -7
- package/src/auth/types.ts +134 -18
- package/src/createActorHooks.ts +9 -9
- package/src/createInfiniteQuery.ts +49 -35
- package/src/createMutation.ts +33 -3
- package/src/createQuery.ts +10 -3
- package/src/createSuspenseInfiniteQuery.ts +45 -33
- package/src/createSuspenseQuery.ts +10 -3
- package/src/defineReactor.ts +136 -14
- package/src/hooks/createAuthHooks.ts +8 -3
- package/src/hooks/useActorInfiniteQuery.ts +25 -3
- package/src/hooks/useActorMethod.ts +82 -34
- package/src/hooks/useActorMutation.ts +19 -4
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +25 -3
- package/src/index.ts +11 -11
- package/src/types.ts +8 -2
- package/src/utils.ts +21 -10
- package/src/defineReactor.test.ts +0 -70
- package/src/hooks/useActorInfiniteQuery.test.tsx +0 -457
- package/src/hooks/useActorMethod.test.tsx +0 -798
- package/src/hooks/useActorMutation.test.tsx +0 -220
- package/src/hooks/useActorQuery.test.tsx +0 -287
- package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +0 -472
- package/src/hooks/useActorSuspenseQuery.test.tsx +0 -254
- package/src/utils.test.ts +0 -14
|
@@ -5,20 +5,25 @@ import type {
|
|
|
5
5
|
AuthState,
|
|
6
6
|
AuthenticationClientOptions,
|
|
7
7
|
AuthenticationSignInOptions,
|
|
8
|
-
} from "./types"
|
|
9
|
-
import { ClientManager, isDev } from "@ic-reactor/core"
|
|
8
|
+
} from "./types.js"
|
|
9
|
+
import { ClientManager, isDev, isMainnetHost } from "@ic-reactor/core"
|
|
10
10
|
|
|
11
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
11
12
|
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
|
|
12
13
|
import {
|
|
13
14
|
IC_INTERNET_IDENTITY_PROVIDER,
|
|
14
15
|
INTERNET_IDENTITY_PROVIDER_ENV_KEY,
|
|
15
16
|
localInternetIdentityProvider,
|
|
16
|
-
} from "./constants"
|
|
17
|
+
} from "./constants.js"
|
|
17
18
|
|
|
18
|
-
export interface AuthenticationManagerParameters {
|
|
19
|
+
export interface AuthenticationManagerParameters extends AuthenticationClientOptions {
|
|
19
20
|
clientManager: ClientManager
|
|
21
|
+
/**
|
|
22
|
+
* Bring your own client instance. When provided, IC Reactor never constructs
|
|
23
|
+
* one and never applies the options below to it.
|
|
24
|
+
*/
|
|
20
25
|
authClient?: AuthClientLike
|
|
21
|
-
|
|
26
|
+
/** Canister ID of a locally deployed Internet Identity. */
|
|
22
27
|
internetIdentityId?: string
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -26,8 +31,6 @@ type AuthClientConstructor = {
|
|
|
26
31
|
new (options?: AuthenticationClientOptions): AuthClientLike
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
const AUTH_CLIENT_MODULE = "@icp-sdk/auth/client"
|
|
30
|
-
|
|
31
34
|
/**
|
|
32
35
|
* Manages Internet Identity sign-in, session restoration, and authentication
|
|
33
36
|
* state for a {@link ClientManager}.
|
|
@@ -49,6 +52,7 @@ export class AuthenticationManager {
|
|
|
49
52
|
AuthClientConstructor | undefined
|
|
50
53
|
>
|
|
51
54
|
private authModuleMissing = false
|
|
55
|
+
private authClientOptions?: AuthenticationClientOptions
|
|
52
56
|
private authStateValue: AuthState = {
|
|
53
57
|
identity: null,
|
|
54
58
|
isAuthenticating: false,
|
|
@@ -57,6 +61,7 @@ export class AuthenticationManager {
|
|
|
57
61
|
}
|
|
58
62
|
private readonly identityProvider?: string | URL
|
|
59
63
|
private readonly internetIdentityId?: string
|
|
64
|
+
private readonly defaultClientOptions: AuthenticationClientOptions
|
|
60
65
|
public readonly clientManager: ClientManager
|
|
61
66
|
|
|
62
67
|
/** The current authentication state. */
|
|
@@ -69,19 +74,26 @@ export class AuthenticationManager {
|
|
|
69
74
|
authClient,
|
|
70
75
|
identityProvider,
|
|
71
76
|
internetIdentityId,
|
|
77
|
+
...clientOptions
|
|
72
78
|
}: AuthenticationManagerParameters) {
|
|
73
79
|
this.clientManager = clientManager
|
|
74
80
|
const canisterEnv =
|
|
75
81
|
typeof window !== "undefined" ? getAuthenticationCanisterEnv() : undefined
|
|
76
82
|
this.identityProvider =
|
|
77
83
|
identityProvider ||
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
acceptEnvIdentityProvider(
|
|
85
|
+
canisterEnv?.[INTERNET_IDENTITY_PROVIDER_ENV_KEY] ||
|
|
86
|
+
canisterEnv?.["PUBLIC_INTERNET_IDENTITY_PROVIDER"],
|
|
87
|
+
clientManager
|
|
88
|
+
)
|
|
80
89
|
this.internetIdentityId =
|
|
81
90
|
internetIdentityId ||
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
acceptEnvCanisterId(
|
|
92
|
+
canisterEnv?.["internet_identity"] ||
|
|
93
|
+
canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"] ||
|
|
94
|
+
canisterEnv?.["CANISTER_ID_INTERNET_IDENTITY"]
|
|
95
|
+
)
|
|
96
|
+
this.defaultClientOptions = clientOptions
|
|
85
97
|
|
|
86
98
|
if (authClient) {
|
|
87
99
|
this.authClientWasProvided = true
|
|
@@ -111,28 +123,57 @@ export class AuthenticationManager {
|
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
/**
|
|
114
|
-
* Preloads and creates an AuthClient
|
|
126
|
+
* Preloads the auth module and creates an AuthClient ahead of time.
|
|
127
|
+
*
|
|
128
|
+
* Call (and await) this before wiring up a login button: it makes
|
|
129
|
+
* {@link login} and {@link IdentityAttributesManager.request} able to open
|
|
130
|
+
* the identity provider synchronously inside the click handler, which is
|
|
131
|
+
* what browser popup blockers and the ICRC-29 transport require.
|
|
115
132
|
*/
|
|
116
133
|
public async prepareClient(options?: AuthenticationClientOptions) {
|
|
117
|
-
const clientOptions =
|
|
118
|
-
...options,
|
|
119
|
-
identityProvider:
|
|
120
|
-
options?.identityProvider ?? this.getDefaultIdentityProvider(),
|
|
121
|
-
})
|
|
134
|
+
const clientOptions = this.resolveClientOptions(options)
|
|
122
135
|
|
|
123
|
-
if (
|
|
124
|
-
this.authClient &&
|
|
125
|
-
(!this.shouldRecreateClient(clientOptions) || this.authClientWasProvided)
|
|
126
|
-
) {
|
|
136
|
+
if (this.authClient && !this.shouldRecreateClient(clientOptions)) {
|
|
127
137
|
return this.authClient
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
return this.initializeClient(clientOptions)
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Returns an AuthClient without awaiting, or `undefined` when the auth
|
|
145
|
+
* module has not been loaded yet.
|
|
146
|
+
*
|
|
147
|
+
* Callers that must stay inside a user gesture use this instead of
|
|
148
|
+
* {@link ensureClient}; awaiting anything before opening the identity
|
|
149
|
+
* provider window loses the gesture.
|
|
150
|
+
*/
|
|
151
|
+
public getPreparedClient(
|
|
152
|
+
options?: AuthenticationClientOptions
|
|
153
|
+
): AuthClientLike | undefined {
|
|
154
|
+
return this.ensurePreparedClient(this.resolveClientOptions(options))
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
public authenticate = async (): Promise<Identity | undefined> => {
|
|
134
158
|
if (this.authState.isAuthenticated) {
|
|
135
|
-
|
|
159
|
+
// Returning on the cached flag alone meant a delegation that expired
|
|
160
|
+
// mid-session was never re-observed: the UI kept rendering a signed-in
|
|
161
|
+
// state while every update call failed, and only a reload recovered.
|
|
162
|
+
// Re-ask the client, which reads the cached expiry rather than hitting
|
|
163
|
+
// storage. A throw here is treated as "still valid" so a transient
|
|
164
|
+
// failure cannot sign anyone out.
|
|
165
|
+
if (!this.authClient) {
|
|
166
|
+
return this.authState.identity || undefined
|
|
167
|
+
}
|
|
168
|
+
const stillValid = await Promise.resolve(
|
|
169
|
+
this.authClient.isAuthenticated()
|
|
170
|
+
).catch(() => true)
|
|
171
|
+
if (stillValid) {
|
|
172
|
+
return this.authState.identity || undefined
|
|
173
|
+
}
|
|
174
|
+
// Expired — drop the stale flag and fall through to re-derive state,
|
|
175
|
+
// which resets the agent to the anonymous identity.
|
|
176
|
+
this.updateState({ isAuthenticated: false })
|
|
136
177
|
}
|
|
137
178
|
if (this.authPromise) {
|
|
138
179
|
return this.authPromise
|
|
@@ -157,9 +198,7 @@ export class AuthenticationManager {
|
|
|
157
198
|
try {
|
|
158
199
|
if (!this.authClient) {
|
|
159
200
|
const authClient = await this.initializeClient(
|
|
160
|
-
|
|
161
|
-
identityProvider: this.getDefaultIdentityProvider(),
|
|
162
|
-
})
|
|
201
|
+
this.resolveClientOptions()
|
|
163
202
|
)
|
|
164
203
|
if (!authClient) {
|
|
165
204
|
this.updateState({ isAuthenticating: false })
|
|
@@ -168,7 +207,12 @@ export class AuthenticationManager {
|
|
|
168
207
|
}
|
|
169
208
|
const identity = await this.authClient!.getIdentity()
|
|
170
209
|
const isAuthenticated = await this.authClient!.isAuthenticated()
|
|
171
|
-
|
|
210
|
+
// Restoring an anonymous session is the common first-load case; pushing
|
|
211
|
+
// it through updateAgent would invalidate the whole query cache on
|
|
212
|
+
// every mount for nothing.
|
|
213
|
+
if (isAuthenticated || !identity.getPrincipal().isAnonymous()) {
|
|
214
|
+
this.clientManager.updateAgent(identity)
|
|
215
|
+
}
|
|
172
216
|
this.updateState({
|
|
173
217
|
identity,
|
|
174
218
|
isAuthenticated,
|
|
@@ -191,13 +235,10 @@ export class AuthenticationManager {
|
|
|
191
235
|
let didCompleteSignIn = false
|
|
192
236
|
|
|
193
237
|
try {
|
|
194
|
-
const
|
|
195
|
-
loginOptions?.identityProvider || this.getDefaultIdentityProvider()
|
|
196
|
-
const authClientOptions = getAuthClientOptions({
|
|
197
|
-
...loginOptions,
|
|
198
|
-
identityProvider,
|
|
199
|
-
})
|
|
238
|
+
const authClientOptions = this.resolveClientOptions(loginOptions)
|
|
200
239
|
|
|
240
|
+
// Stays synchronous when `prepareClient()` has already loaded the auth
|
|
241
|
+
// module, so `signIn()` still runs inside the caller's click handler.
|
|
201
242
|
if (!this.ensurePreparedClient(authClientOptions)) {
|
|
202
243
|
await this.initializeClient(authClientOptions)
|
|
203
244
|
}
|
|
@@ -208,7 +249,7 @@ export class AuthenticationManager {
|
|
|
208
249
|
|
|
209
250
|
if (!this.authClient) {
|
|
210
251
|
throw new Error(
|
|
211
|
-
"Authentication module is missing or failed to initialize. To use login,
|
|
252
|
+
"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(...) })"
|
|
212
253
|
)
|
|
213
254
|
}
|
|
214
255
|
|
|
@@ -251,18 +292,26 @@ export class AuthenticationManager {
|
|
|
251
292
|
public logout = async (options?: { returnTo?: string }) => {
|
|
252
293
|
if (!this.authClient) {
|
|
253
294
|
throw new Error(
|
|
254
|
-
"Authentication module is missing or failed to initialize. To use logout,
|
|
295
|
+
"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(...) })"
|
|
255
296
|
)
|
|
256
297
|
}
|
|
257
298
|
this.updateState({ isAuthenticating: true, error: undefined })
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
299
|
+
try {
|
|
300
|
+
await this.authClient.signOut(options)
|
|
301
|
+
const identity = await this.authClient.getIdentity()
|
|
302
|
+
this.clientManager.updateAgent(identity)
|
|
303
|
+
this.updateState({
|
|
304
|
+
identity,
|
|
305
|
+
isAuthenticated: false,
|
|
306
|
+
isAuthenticating: false,
|
|
307
|
+
})
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// Without this the manager was left with `isAuthenticating: true` and no
|
|
310
|
+
// recorded error, so a button disabled on `isAuthenticating` stayed stuck
|
|
311
|
+
// and nothing told the app why.
|
|
312
|
+
this.updateState({ error: error as Error, isAuthenticating: false })
|
|
313
|
+
throw error
|
|
314
|
+
}
|
|
266
315
|
}
|
|
267
316
|
|
|
268
317
|
private async initializeClient(
|
|
@@ -275,6 +324,7 @@ export class AuthenticationManager {
|
|
|
275
324
|
}
|
|
276
325
|
|
|
277
326
|
this.authClient = new AuthClient(options)
|
|
327
|
+
this.authClientOptions = options
|
|
278
328
|
return this.authClient
|
|
279
329
|
}
|
|
280
330
|
|
|
@@ -284,7 +334,7 @@ export class AuthenticationManager {
|
|
|
284
334
|
): Promise<Identity> {
|
|
285
335
|
if (!this.authClient) {
|
|
286
336
|
throw new Error(
|
|
287
|
-
"Authentication module is missing or failed to initialize. To use login,
|
|
337
|
+
"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(...) })"
|
|
288
338
|
)
|
|
289
339
|
}
|
|
290
340
|
|
|
@@ -309,10 +359,7 @@ export class AuthenticationManager {
|
|
|
309
359
|
private ensurePreparedClient(
|
|
310
360
|
options?: AuthenticationClientOptions
|
|
311
361
|
): AuthClientLike | undefined {
|
|
312
|
-
if (
|
|
313
|
-
this.authClient &&
|
|
314
|
-
(!this.shouldRecreateClient(options) || this.authClientWasProvided)
|
|
315
|
-
) {
|
|
362
|
+
if (this.authClient && !this.shouldRecreateClient(options)) {
|
|
316
363
|
return this.authClient
|
|
317
364
|
}
|
|
318
365
|
|
|
@@ -322,11 +369,41 @@ export class AuthenticationManager {
|
|
|
322
369
|
}
|
|
323
370
|
|
|
324
371
|
this.authClient = new AuthClient(options)
|
|
372
|
+
this.authClientOptions = options
|
|
325
373
|
return this.authClient
|
|
326
374
|
}
|
|
327
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Only rebuild the client when the effective options actually changed.
|
|
378
|
+
*
|
|
379
|
+
* Recreating on every call would discard the client warmed up by
|
|
380
|
+
* `prepareClient()` and register a duplicate sign-out callback on the
|
|
381
|
+
* shared IdleManager singleton each time.
|
|
382
|
+
*/
|
|
328
383
|
private shouldRecreateClient(options?: AuthenticationClientOptions): boolean {
|
|
329
|
-
|
|
384
|
+
if (this.authClientWasProvided) {
|
|
385
|
+
return false
|
|
386
|
+
}
|
|
387
|
+
return !isSameAuthClientOptions(this.authClientOptions, options)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Merges constructor-level defaults with per-call overrides and fills in the
|
|
392
|
+
* network-appropriate identity provider.
|
|
393
|
+
*/
|
|
394
|
+
private resolveClientOptions(
|
|
395
|
+
options?: AuthenticationClientOptions
|
|
396
|
+
): AuthenticationClientOptions {
|
|
397
|
+
const merged = getAuthClientOptions({
|
|
398
|
+
...this.defaultClientOptions,
|
|
399
|
+
...options,
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
...merged,
|
|
404
|
+
identityProvider:
|
|
405
|
+
merged?.identityProvider ?? this.getDefaultIdentityProvider(),
|
|
406
|
+
}
|
|
330
407
|
}
|
|
331
408
|
|
|
332
409
|
private async syncStateFromClient(revision = this.authStateRevision) {
|
|
@@ -350,11 +427,7 @@ export class AuthenticationManager {
|
|
|
350
427
|
|
|
351
428
|
/** @internal Used by IdentityAttributesManager. */
|
|
352
429
|
public async ensureClient(options?: AuthenticationClientOptions) {
|
|
353
|
-
const clientOptions =
|
|
354
|
-
...options,
|
|
355
|
-
identityProvider:
|
|
356
|
-
options?.identityProvider ?? this.getDefaultIdentityProvider(),
|
|
357
|
-
})
|
|
430
|
+
const clientOptions = this.resolveClientOptions(options)
|
|
358
431
|
if (!this.ensurePreparedClient(clientOptions)) {
|
|
359
432
|
await this.initializeClient(clientOptions)
|
|
360
433
|
}
|
|
@@ -407,11 +480,7 @@ export class AuthenticationManager {
|
|
|
407
480
|
}
|
|
408
481
|
|
|
409
482
|
if (!this.authClientConstructorPromise) {
|
|
410
|
-
|
|
411
|
-
// can otherwise try to resolve this package even when auth APIs are unused.
|
|
412
|
-
this.authClientConstructorPromise = import(
|
|
413
|
-
/* @vite-ignore */ AUTH_CLIENT_MODULE
|
|
414
|
-
)
|
|
483
|
+
this.authClientConstructorPromise = importAuthClientModule()
|
|
415
484
|
.then((authModule) => {
|
|
416
485
|
const AuthClient = (
|
|
417
486
|
authModule as { AuthClient?: AuthClientConstructor }
|
|
@@ -441,6 +510,37 @@ export class AuthenticationManager {
|
|
|
441
510
|
}
|
|
442
511
|
}
|
|
443
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Loads the optional `@icp-sdk/auth` peer.
|
|
515
|
+
*
|
|
516
|
+
* The `try` is load-bearing, not defensive. webpack flags a dynamic import
|
|
517
|
+
* that sits lexically inside a `try` block as an *optional* dependency
|
|
518
|
+
* (`ImportParserPlugin`: `dep.optional = Boolean(parser.scope.inTry)`), and
|
|
519
|
+
* `Compilation` reports an unresolvable optional dependency as a build
|
|
520
|
+
* *warning* instead of a fatal "Module not found" error, emitting a module
|
|
521
|
+
* that rejects at runtime. Without the `try`, every webpack-based toolchain
|
|
522
|
+
* (webpack, `next build --webpack`, Rspack) fails to build for consumers who
|
|
523
|
+
* never touch authentication, because npm/pnpm do not install optional peers.
|
|
524
|
+
* Do not hoist this import out of the `try`, and do not wrap it in a nested
|
|
525
|
+
* function — webpack resets `inTry` at every function boundary.
|
|
526
|
+
*
|
|
527
|
+
* The specifier must also stay a literal: a variable specifier (or
|
|
528
|
+
* `@vite-ignore`) makes bundlers skip this import entirely, so the bare
|
|
529
|
+
* specifier survives into the browser, where it cannot be resolved without an
|
|
530
|
+
* import map and every login path fails. A literal lets Vite/Rollup/webpack
|
|
531
|
+
* code-split the optional peer, and lets them tree-shake it away for apps that
|
|
532
|
+
* never reference this class.
|
|
533
|
+
*/
|
|
534
|
+
function importAuthClientModule(): Promise<unknown> {
|
|
535
|
+
try {
|
|
536
|
+
return import("@icp-sdk/auth/client")
|
|
537
|
+
} catch (error) {
|
|
538
|
+
// Native ESM without an import map (and bundlers that neither resolve nor
|
|
539
|
+
// stub the specifier) throw synchronously rather than rejecting.
|
|
540
|
+
return Promise.reject(error)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
444
544
|
function getAuthClientOptions(
|
|
445
545
|
options?: AuthenticationClientOptions
|
|
446
546
|
): AuthenticationClientOptions | undefined {
|
|
@@ -452,6 +552,12 @@ function getAuthClientOptions(
|
|
|
452
552
|
identityProvider: options.identityProvider,
|
|
453
553
|
windowOpenerFeatures: options.windowOpenerFeatures,
|
|
454
554
|
openIdProvider: getAuthClientOpenIdProvider(options.openIdProvider),
|
|
555
|
+
derivationOrigin: options.derivationOrigin,
|
|
556
|
+
storage: options.storage,
|
|
557
|
+
keyType: options.keyType,
|
|
558
|
+
idleOptions: options.idleOptions,
|
|
559
|
+
identity: options.identity,
|
|
560
|
+
transport: options.transport,
|
|
455
561
|
}
|
|
456
562
|
}
|
|
457
563
|
|
|
@@ -465,14 +571,111 @@ function getAuthClientOpenIdProvider(
|
|
|
465
571
|
: undefined
|
|
466
572
|
}
|
|
467
573
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Compares the options two AuthClients would be built from. Object-valued
|
|
576
|
+
* options (`storage`, `identity`, `idleOptions`) are compared by reference,
|
|
577
|
+
* which is what module-scoped configuration produces.
|
|
578
|
+
*/
|
|
579
|
+
function isSameAuthClientOptions(
|
|
580
|
+
current?: AuthenticationClientOptions,
|
|
581
|
+
next?: AuthenticationClientOptions
|
|
582
|
+
): boolean {
|
|
583
|
+
if (current === next) {
|
|
584
|
+
return true
|
|
585
|
+
}
|
|
586
|
+
if (!current || !next) {
|
|
587
|
+
return false
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
String(current.identityProvider ?? "") ===
|
|
592
|
+
String(next.identityProvider ?? "") &&
|
|
593
|
+
current.windowOpenerFeatures === next.windowOpenerFeatures &&
|
|
594
|
+
current.openIdProvider === next.openIdProvider &&
|
|
595
|
+
String(current.derivationOrigin ?? "") ===
|
|
596
|
+
String(next.derivationOrigin ?? "") &&
|
|
597
|
+
current.storage === next.storage &&
|
|
598
|
+
current.keyType === next.keyType &&
|
|
599
|
+
current.idleOptions === next.idleOptions &&
|
|
600
|
+
current.identity === next.identity &&
|
|
601
|
+
current.transport === next.transport
|
|
473
602
|
)
|
|
474
603
|
}
|
|
475
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Decides whether an Internet Identity provider carried by the `ic_env` cookie
|
|
607
|
+
* may be used.
|
|
608
|
+
*
|
|
609
|
+
* The cookie is written by the replica serving a local or testnet deployment,
|
|
610
|
+
* and it is only authoritative in that setting: cookies are not origin-isolated
|
|
611
|
+
* the way script-accessible storage is, so on a mainnet deployment the identity
|
|
612
|
+
* provider is configuration that belongs in code rather than something read
|
|
613
|
+
* back out of the environment at runtime. `AuthClient` treats `identityProvider`
|
|
614
|
+
* as trusted developer configuration and does not re-check it.
|
|
615
|
+
*
|
|
616
|
+
* This mirrors the root-key guard in `ClientManager` (see `packages/core`):
|
|
617
|
+
* both values arrive from the same cookie and get the same treatment — adopted
|
|
618
|
+
* off-mainnet, ignored on mainnet in favour of the pinned default.
|
|
619
|
+
*
|
|
620
|
+
* A provider on the app's own origin is always accepted: it is exactly as
|
|
621
|
+
* trustworthy as the page doing the asking. Callers that need a custom provider
|
|
622
|
+
* on mainnet pass `identityProvider` explicitly.
|
|
623
|
+
*/
|
|
624
|
+
function acceptEnvIdentityProvider(
|
|
625
|
+
value: string | undefined,
|
|
626
|
+
clientManager: ClientManager
|
|
627
|
+
): string | undefined {
|
|
628
|
+
if (!value) return undefined
|
|
629
|
+
|
|
630
|
+
const pageOrigin =
|
|
631
|
+
typeof window !== "undefined" ? window.location?.origin : undefined
|
|
632
|
+
|
|
633
|
+
let url: URL
|
|
634
|
+
try {
|
|
635
|
+
url = new URL(value, pageOrigin)
|
|
636
|
+
} catch {
|
|
637
|
+
return undefined
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (pageOrigin && url.origin === pageOrigin) {
|
|
641
|
+
return url.toString()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// `isMainnetHost` matches only the real boundary domains and defaults to true
|
|
645
|
+
// for an unknown host, so custom testnets keep working while anything that
|
|
646
|
+
// looks like mainnet fails closed. `clientManager.isLocal` would be wrong
|
|
647
|
+
// here: it treats every unrecognized hostname as "ic".
|
|
648
|
+
if (isMainnetHost(clientManager.agentHost?.toString())) {
|
|
649
|
+
console.warn(
|
|
650
|
+
`[ic-reactor] Ignoring the Internet Identity provider from the ic_env cookie ` +
|
|
651
|
+
`("${url.origin}") because this reactor targets mainnet, where the provider ` +
|
|
652
|
+
`is taken from configuration rather than the environment. Pass ` +
|
|
653
|
+
`\`identityProvider\` to AuthenticationManager to use a custom provider on mainnet.`
|
|
654
|
+
)
|
|
655
|
+
return undefined
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return url.toString()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Accepts an Internet Identity canister ID from `ic_env` only when it parses as
|
|
663
|
+
* a principal.
|
|
664
|
+
*
|
|
665
|
+
* The value is interpolated into a provider URL
|
|
666
|
+
* (`http://<id>.localhost:<port>/authorize`), so anything that is not a bare
|
|
667
|
+
* principal can change the shape of that URL rather than just its subdomain.
|
|
668
|
+
* Only reachable on local networks, since mainnet uses the pinned provider.
|
|
669
|
+
*/
|
|
670
|
+
function acceptEnvCanisterId(value: string | undefined): string | undefined {
|
|
671
|
+
if (!value) return undefined
|
|
672
|
+
try {
|
|
673
|
+
return Principal.fromText(value).toText()
|
|
674
|
+
} catch {
|
|
675
|
+
return undefined
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
476
679
|
function getAuthenticationCanisterEnv(): Record<string, string> | undefined {
|
|
477
680
|
const canisterEnv = safeGetCanisterEnv<Record<string, string>>()
|
|
478
681
|
if (canisterEnv) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useState } from "react"
|
|
2
|
-
import type { IdentityAttributesManager } from "./identity-attributes-manager"
|
|
2
|
+
import type { IdentityAttributesManager } from "./identity-attributes-manager.js"
|
|
3
3
|
import type {
|
|
4
4
|
IdentityAttributeResult,
|
|
5
5
|
RequestIdentityAttributesParameters,
|
|
6
6
|
RequestOpenIdIdentityAttributesParameters,
|
|
7
|
-
} from "./types"
|
|
7
|
+
} from "./types.js"
|
|
8
8
|
|
|
9
9
|
export interface UseIdentityAttributesReturn {
|
|
10
10
|
requestAttributes: (
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
AuthClientLike,
|
|
3
|
+
IdentityAttributeNonce,
|
|
3
4
|
IdentityAttributeResult,
|
|
4
5
|
RequestIdentityAttributesParameters,
|
|
5
6
|
RequestOpenIdIdentityAttributesParameters,
|
|
6
|
-
} from "./types"
|
|
7
|
+
} from "./types.js"
|
|
7
8
|
|
|
8
|
-
import { AuthenticationManager } from "./authentication-manager"
|
|
9
|
+
import { AuthenticationManager } from "./authentication-manager.js"
|
|
9
10
|
import {
|
|
10
11
|
decodeIdentityAttributeValues,
|
|
11
12
|
identityAttributeKeys,
|
|
12
13
|
normalizeSignedIdentityAttributes,
|
|
13
|
-
} from "./identity-attributes"
|
|
14
|
+
} from "./identity-attributes.js"
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Requests and decodes signed identity attributes for an authenticated
|
|
17
18
|
* Internet Identity session.
|
|
18
19
|
*
|
|
20
|
+
* Await `authentication.prepareClient()` once during startup (the `useAuth`
|
|
21
|
+
* hook already does) so that {@link request} can open the identity provider
|
|
22
|
+
* window synchronously from a click handler.
|
|
23
|
+
*
|
|
19
24
|
* @example
|
|
20
25
|
* ```ts
|
|
21
26
|
* const attributes = new IdentityAttributesManager(authentication)
|
|
22
|
-
* const result = await attributes.request({
|
|
27
|
+
* const result = await attributes.request({
|
|
28
|
+
* keys: ["name"],
|
|
29
|
+
* nonce: () => fetchNonceFromCanister(),
|
|
30
|
+
* })
|
|
23
31
|
* ```
|
|
24
32
|
*/
|
|
25
33
|
export class IdentityAttributesManager {
|
|
@@ -28,37 +36,40 @@ export class IdentityAttributesManager {
|
|
|
28
36
|
public request = async ({
|
|
29
37
|
keys,
|
|
30
38
|
nonce,
|
|
31
|
-
identityProvider,
|
|
32
|
-
openIdProvider,
|
|
33
|
-
windowOpenerFeatures,
|
|
34
39
|
signIn = true,
|
|
35
40
|
maxTimeToLive,
|
|
36
41
|
targets,
|
|
42
|
+
...clientOptions
|
|
37
43
|
}: RequestIdentityAttributesParameters): Promise<IdentityAttributeResult> => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// Nothing may be awaited before `requestAttributes` / `signIn` are invoked:
|
|
45
|
+
// the ICRC-29 transport only opens the identity provider window while the
|
|
46
|
+
// browser still considers us inside the user gesture. `getPreparedClient`
|
|
47
|
+
// is synchronous whenever `prepareClient()` has run.
|
|
48
|
+
const preparedClient = this.authentication.getPreparedClient(clientOptions)
|
|
49
|
+
const authClient =
|
|
50
|
+
preparedClient ?? (await this.authentication.ensureClient(clientOptions))
|
|
45
51
|
|
|
46
52
|
if (!authClient) {
|
|
47
53
|
throw new Error(
|
|
48
|
-
"Authentication module is missing or failed to initialize. To request identity attributes, please install @icp-sdk/auth v7 or provide a compatible authClient."
|
|
54
|
+
"Authentication module is missing or failed to initialize. To request identity attributes, please install @icp-sdk/auth v7 or later, or provide a compatible authClient."
|
|
49
55
|
)
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
this.authentication.setAuthenticating()
|
|
53
59
|
|
|
54
60
|
try {
|
|
61
|
+
// `signIn` first: it opens the transport channel synchronously, and the
|
|
62
|
+
// attribute request then reuses that same channel and window.
|
|
55
63
|
const identityPromise = signIn
|
|
56
64
|
? this.authentication.signInOrRecoverIdentity({
|
|
57
65
|
maxTimeToLive,
|
|
58
66
|
targets,
|
|
59
67
|
})
|
|
60
68
|
: Promise.resolve(authClient.getIdentity())
|
|
61
|
-
const requestPromise = authClient.requestAttributes({
|
|
69
|
+
const requestPromise = authClient.requestAttributes({
|
|
70
|
+
keys,
|
|
71
|
+
nonce: toAuthClientNonce(authClient, nonce),
|
|
72
|
+
})
|
|
62
73
|
|
|
63
74
|
const [signedAttributes, identity] = await Promise.all([
|
|
64
75
|
requestPromise,
|
|
@@ -88,40 +99,38 @@ export class IdentityAttributesManager {
|
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
public requestOpenId =
|
|
92
|
-
nonce,
|
|
93
|
-
openIdProvider,
|
|
102
|
+
public requestOpenId = ({
|
|
94
103
|
keys,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
signIn,
|
|
98
|
-
maxTimeToLive,
|
|
99
|
-
targets,
|
|
104
|
+
openIdProvider,
|
|
105
|
+
...rest
|
|
100
106
|
}: RequestOpenIdIdentityAttributesParameters): Promise<IdentityAttributeResult> => {
|
|
107
|
+
// Called (not awaited) synchronously so the user gesture is preserved.
|
|
101
108
|
return this.request({
|
|
102
|
-
|
|
103
|
-
nonce,
|
|
104
|
-
identityProvider,
|
|
109
|
+
...rest,
|
|
105
110
|
openIdProvider,
|
|
106
|
-
|
|
107
|
-
signIn,
|
|
108
|
-
maxTimeToLive,
|
|
109
|
-
targets,
|
|
111
|
+
keys: identityAttributeKeys({ openIdProvider, keys }),
|
|
110
112
|
})
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Bridges the two `@icp-sdk/auth` nonce contracts.
|
|
118
|
+
*
|
|
119
|
+
* v8 takes `() => Promise<Uint8Array>` (it journals the value so a redirect
|
|
120
|
+
* flow replays the same bytes); v7 takes `Uint8Array | Promise<Uint8Array>`.
|
|
121
|
+
* Both open the transport channel synchronously for the deferred forms, so we
|
|
122
|
+
* always hand over the deferred shape the installed client understands.
|
|
123
|
+
* `memoize` only exists from v8 onwards, which makes it a reliable marker.
|
|
124
|
+
*/
|
|
125
|
+
function toAuthClientNonce(
|
|
126
|
+
authClient: AuthClientLike,
|
|
127
|
+
nonce: IdentityAttributeNonce
|
|
128
|
+
): Uint8Array | Promise<Uint8Array> | (() => Promise<Uint8Array>) {
|
|
129
|
+
const supportsNonceThunk = typeof authClient.memoize === "function"
|
|
130
|
+
|
|
131
|
+
if (supportsNonceThunk) {
|
|
132
|
+
return () => Promise.resolve(typeof nonce === "function" ? nonce() : nonce)
|
|
126
133
|
}
|
|
134
|
+
|
|
135
|
+
return Promise.resolve(typeof nonce === "function" ? nonce() : nonce)
|
|
127
136
|
}
|
|
@@ -4,8 +4,8 @@ import type {
|
|
|
4
4
|
IdentityAttributeOpenIdProvider,
|
|
5
5
|
IdentityAttributeValues,
|
|
6
6
|
SignedIdentityAttributes,
|
|
7
|
-
} from "./types"
|
|
8
|
-
import { IC_INTERNET_IDENTITY_PROVIDER } from "./constants"
|
|
7
|
+
} from "./types.js"
|
|
8
|
+
import { IC_INTERNET_IDENTITY_PROVIDER } from "./constants.js"
|
|
9
9
|
|
|
10
10
|
export const IDENTITY_ATTRIBUTES_PROVIDER = IC_INTERNET_IDENTITY_PROVIDER
|
|
11
11
|
|