@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.
Files changed (107) hide show
  1. package/README.md +158 -9
  2. package/dist/auth/authentication-manager.d.ts +37 -5
  3. package/dist/auth/authentication-manager.d.ts.map +1 -1
  4. package/dist/auth/authentication-manager.js +242 -52
  5. package/dist/auth/authentication-manager.js.map +1 -1
  6. package/dist/auth/createIdentityAttributeHooks.d.ts +2 -2
  7. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  8. package/dist/auth/identity-attributes-manager.d.ts +12 -5
  9. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  10. package/dist/auth/identity-attributes-manager.js +42 -28
  11. package/dist/auth/identity-attributes-manager.js.map +1 -1
  12. package/dist/auth/identity-attributes.d.ts +1 -1
  13. package/dist/auth/identity-attributes.d.ts.map +1 -1
  14. package/dist/auth/identity-attributes.js +1 -1
  15. package/dist/auth/identity-attributes.js.map +1 -1
  16. package/dist/auth/index.d.ts +7 -7
  17. package/dist/auth/index.d.ts.map +1 -1
  18. package/dist/auth/index.js +7 -7
  19. package/dist/auth/index.js.map +1 -1
  20. package/dist/auth/types.d.ts +123 -16
  21. package/dist/auth/types.d.ts.map +1 -1
  22. package/dist/createActorHooks.d.ts +4 -4
  23. package/dist/createActorHooks.d.ts.map +1 -1
  24. package/dist/createActorHooks.js +6 -6
  25. package/dist/createActorHooks.js.map +1 -1
  26. package/dist/createInfiniteQuery.d.ts +12 -10
  27. package/dist/createInfiniteQuery.d.ts.map +1 -1
  28. package/dist/createInfiniteQuery.js +20 -17
  29. package/dist/createInfiniteQuery.js.map +1 -1
  30. package/dist/createMutation.d.ts +1 -1
  31. package/dist/createMutation.d.ts.map +1 -1
  32. package/dist/createMutation.js +22 -2
  33. package/dist/createMutation.js.map +1 -1
  34. package/dist/createQuery.d.ts +1 -1
  35. package/dist/createQuery.d.ts.map +1 -1
  36. package/dist/createQuery.js +6 -2
  37. package/dist/createQuery.js.map +1 -1
  38. package/dist/createSuspenseInfiniteQuery.d.ts +11 -10
  39. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  40. package/dist/createSuspenseInfiniteQuery.js +18 -16
  41. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  42. package/dist/createSuspenseQuery.d.ts +1 -1
  43. package/dist/createSuspenseQuery.d.ts.map +1 -1
  44. package/dist/createSuspenseQuery.js +6 -2
  45. package/dist/createSuspenseQuery.js.map +1 -1
  46. package/dist/defineReactor.d.ts +54 -4
  47. package/dist/defineReactor.d.ts.map +1 -1
  48. package/dist/defineReactor.js +77 -7
  49. package/dist/defineReactor.js.map +1 -1
  50. package/dist/hooks/createAuthHooks.d.ts +2 -2
  51. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  52. package/dist/hooks/createAuthHooks.js +7 -2
  53. package/dist/hooks/createAuthHooks.js.map +1 -1
  54. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  55. package/dist/hooks/useActorInfiniteQuery.js +16 -2
  56. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  57. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  58. package/dist/hooks/useActorMethod.js +66 -33
  59. package/dist/hooks/useActorMethod.js.map +1 -1
  60. package/dist/hooks/useActorMutation.d.ts +8 -1
  61. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  62. package/dist/hooks/useActorMutation.js +9 -1
  63. package/dist/hooks/useActorMutation.js.map +1 -1
  64. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  65. package/dist/hooks/useActorSuspenseInfiniteQuery.js +16 -2
  66. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  67. package/dist/index.d.ts +11 -11
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +11 -11
  70. package/dist/index.js.map +1 -1
  71. package/dist/types.d.ts +8 -2
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/utils.d.ts +14 -5
  74. package/dist/utils.d.ts.map +1 -1
  75. package/dist/utils.js +21 -10
  76. package/dist/utils.js.map +1 -1
  77. package/llms.txt +1 -1
  78. package/package.json +14 -9
  79. package/src/auth/authentication-manager.ts +267 -64
  80. package/src/auth/createIdentityAttributeHooks.ts +2 -2
  81. package/src/auth/identity-attributes-manager.ts +53 -44
  82. package/src/auth/identity-attributes.ts +2 -2
  83. package/src/auth/index.ts +7 -7
  84. package/src/auth/types.ts +134 -18
  85. package/src/createActorHooks.ts +9 -9
  86. package/src/createInfiniteQuery.ts +49 -35
  87. package/src/createMutation.ts +33 -3
  88. package/src/createQuery.ts +10 -3
  89. package/src/createSuspenseInfiniteQuery.ts +45 -33
  90. package/src/createSuspenseQuery.ts +10 -3
  91. package/src/defineReactor.ts +136 -14
  92. package/src/hooks/createAuthHooks.ts +8 -3
  93. package/src/hooks/useActorInfiniteQuery.ts +25 -3
  94. package/src/hooks/useActorMethod.ts +82 -34
  95. package/src/hooks/useActorMutation.ts +19 -4
  96. package/src/hooks/useActorSuspenseInfiniteQuery.ts +25 -3
  97. package/src/index.ts +11 -11
  98. package/src/types.ts +8 -2
  99. package/src/utils.ts +21 -10
  100. package/src/defineReactor.test.ts +0 -70
  101. package/src/hooks/useActorInfiniteQuery.test.tsx +0 -457
  102. package/src/hooks/useActorMethod.test.tsx +0 -798
  103. package/src/hooks/useActorMutation.test.tsx +0 -220
  104. package/src/hooks/useActorQuery.test.tsx +0 -287
  105. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +0 -472
  106. package/src/hooks/useActorSuspenseQuery.test.tsx +0 -254
  107. 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
- identityProvider?: string | URL
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
- canisterEnv?.[INTERNET_IDENTITY_PROVIDER_ENV_KEY] ||
79
- canisterEnv?.["PUBLIC_INTERNET_IDENTITY_PROVIDER"]
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
- canisterEnv?.["internet_identity"] ||
83
- canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"] ||
84
- canisterEnv?.["CANISTER_ID_INTERNET_IDENTITY"]
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 before a user gesture is needed.
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 = getAuthClientOptions({
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
- return this.authState.identity || undefined
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
- getAuthClientOptions({
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
- this.clientManager.updateAgent(identity)
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 identityProvider =
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, please install the auth package: npm install @icp-sdk/auth"
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, please install the auth package: npm install @icp-sdk/auth"
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
- await this.authClient.signOut(options)
259
- const identity = await this.authClient.getIdentity()
260
- this.clientManager.updateAgent(identity)
261
- this.updateState({
262
- identity,
263
- isAuthenticated: false,
264
- isAuthenticating: false,
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, please install the auth package: npm install @icp-sdk/auth"
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
- return !this.authClientWasProvided && hasAuthClientOptions(options)
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 = getAuthClientOptions({
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
- // Keep the optional auth peer out of non-auth bundles. Vite/Rollup/esbuild
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
- function hasAuthClientOptions(options?: AuthenticationClientOptions): boolean {
469
- return Boolean(
470
- options?.identityProvider ||
471
- options?.windowOpenerFeatures ||
472
- options?.openIdProvider
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
- AuthenticationClientOptions,
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({ keys: ["name"] })
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
- const authClient = await this.authentication.ensureClient(
39
- getAuthClientOptions({
40
- identityProvider,
41
- windowOpenerFeatures,
42
- openIdProvider,
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({ keys, nonce })
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 = async ({
92
- nonce,
93
- openIdProvider,
102
+ public requestOpenId = ({
94
103
  keys,
95
- identityProvider,
96
- windowOpenerFeatures,
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
- keys: identityAttributeKeys({ openIdProvider, keys }),
103
- nonce,
104
- identityProvider,
109
+ ...rest,
105
110
  openIdProvider,
106
- windowOpenerFeatures,
107
- signIn,
108
- maxTimeToLive,
109
- targets,
111
+ keys: identityAttributeKeys({ openIdProvider, keys }),
110
112
  })
111
113
  }
112
114
  }
113
115
 
114
- function getAuthClientOptions(
115
- options: AuthenticationClientOptions
116
- ): AuthenticationClientOptions {
117
- return {
118
- identityProvider: options.identityProvider,
119
- windowOpenerFeatures: options.windowOpenerFeatures,
120
- openIdProvider:
121
- options.openIdProvider === "google" ||
122
- options.openIdProvider === "apple" ||
123
- options.openIdProvider === "microsoft"
124
- ? options.openIdProvider
125
- : undefined,
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