@ic-reactor/react 3.12.5 → 4.0.0-beta.1

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 (139) hide show
  1. package/README.md +236 -423
  2. package/dist/index.d.ts +259 -12
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +470 -17
  5. package/dist/index.js.map +1 -1
  6. package/llms.txt +82 -54
  7. package/package.json +12 -28
  8. package/src/index.tsx +611 -0
  9. package/dist/auth/authentication-manager.d.ts +0 -123
  10. package/dist/auth/authentication-manager.d.ts.map +0 -1
  11. package/dist/auth/authentication-manager.js +0 -767
  12. package/dist/auth/authentication-manager.js.map +0 -1
  13. package/dist/auth/constants.d.ts +0 -24
  14. package/dist/auth/constants.d.ts.map +0 -1
  15. package/dist/auth/constants.js +0 -24
  16. package/dist/auth/constants.js.map +0 -1
  17. package/dist/auth/createIdentityAttributeHooks.d.ts +0 -14
  18. package/dist/auth/createIdentityAttributeHooks.d.ts.map +0 -1
  19. package/dist/auth/createIdentityAttributeHooks.js +0 -106
  20. package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
  21. package/dist/auth/identity-attributes-manager.d.ts +0 -26
  22. package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
  23. package/dist/auth/identity-attributes-manager.js +0 -107
  24. package/dist/auth/identity-attributes-manager.js.map +0 -1
  25. package/dist/auth/identity-attributes.d.ts +0 -19
  26. package/dist/auth/identity-attributes.d.ts.map +0 -1
  27. package/dist/auth/identity-attributes.js +0 -170
  28. package/dist/auth/identity-attributes.js.map +0 -1
  29. package/dist/auth/index.d.ts +0 -8
  30. package/dist/auth/index.d.ts.map +0 -1
  31. package/dist/auth/index.js +0 -8
  32. package/dist/auth/index.js.map +0 -1
  33. package/dist/auth/local-ii-probe.d.ts +0 -46
  34. package/dist/auth/local-ii-probe.d.ts.map +0 -1
  35. package/dist/auth/local-ii-probe.js +0 -102
  36. package/dist/auth/local-ii-probe.js.map +0 -1
  37. package/dist/auth/types.d.ts +0 -179
  38. package/dist/auth/types.d.ts.map +0 -1
  39. package/dist/auth/types.js +0 -2
  40. package/dist/auth/types.js.map +0 -1
  41. package/dist/createActorHooks.d.ts +0 -52
  42. package/dist/createActorHooks.d.ts.map +0 -1
  43. package/dist/createActorHooks.js +0 -17
  44. package/dist/createActorHooks.js.map +0 -1
  45. package/dist/createInfiniteQuery.d.ts +0 -144
  46. package/dist/createInfiniteQuery.d.ts.map +0 -1
  47. package/dist/createInfiniteQuery.js +0 -174
  48. package/dist/createInfiniteQuery.js.map +0 -1
  49. package/dist/createMutation.d.ts +0 -30
  50. package/dist/createMutation.d.ts.map +0 -1
  51. package/dist/createMutation.js +0 -192
  52. package/dist/createMutation.js.map +0 -1
  53. package/dist/createQuery.d.ts +0 -30
  54. package/dist/createQuery.d.ts.map +0 -1
  55. package/dist/createQuery.js +0 -117
  56. package/dist/createQuery.js.map +0 -1
  57. package/dist/createSuspenseInfiniteQuery.d.ts +0 -147
  58. package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
  59. package/dist/createSuspenseInfiniteQuery.js +0 -177
  60. package/dist/createSuspenseInfiniteQuery.js.map +0 -1
  61. package/dist/createSuspenseQuery.d.ts +0 -25
  62. package/dist/createSuspenseQuery.d.ts.map +0 -1
  63. package/dist/createSuspenseQuery.js +0 -111
  64. package/dist/createSuspenseQuery.js.map +0 -1
  65. package/dist/defineReactor.d.ts +0 -125
  66. package/dist/defineReactor.d.ts.map +0 -1
  67. package/dist/defineReactor.js +0 -180
  68. package/dist/defineReactor.js.map +0 -1
  69. package/dist/hooks/createAuthHooks.d.ts +0 -43
  70. package/dist/hooks/createAuthHooks.d.ts.map +0 -1
  71. package/dist/hooks/createAuthHooks.js +0 -131
  72. package/dist/hooks/createAuthHooks.js.map +0 -1
  73. package/dist/hooks/index.d.ts +0 -21
  74. package/dist/hooks/index.d.ts.map +0 -1
  75. package/dist/hooks/index.js +0 -24
  76. package/dist/hooks/index.js.map +0 -1
  77. package/dist/hooks/useActorInfiniteQuery.d.ts +0 -39
  78. package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
  79. package/dist/hooks/useActorInfiniteQuery.js +0 -56
  80. package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
  81. package/dist/hooks/useActorMethod.d.ts +0 -115
  82. package/dist/hooks/useActorMethod.d.ts.map +0 -1
  83. package/dist/hooks/useActorMethod.js +0 -250
  84. package/dist/hooks/useActorMethod.js.map +0 -1
  85. package/dist/hooks/useActorMutation.d.ts +0 -48
  86. package/dist/hooks/useActorMutation.d.ts.map +0 -1
  87. package/dist/hooks/useActorMutation.js +0 -69
  88. package/dist/hooks/useActorMutation.js.map +0 -1
  89. package/dist/hooks/useActorQuery.d.ts +0 -32
  90. package/dist/hooks/useActorQuery.d.ts.map +0 -1
  91. package/dist/hooks/useActorQuery.js +0 -46
  92. package/dist/hooks/useActorQuery.js.map +0 -1
  93. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -39
  94. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
  95. package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -56
  96. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +0 -1
  97. package/dist/hooks/useActorSuspenseQuery.d.ts +0 -32
  98. package/dist/hooks/useActorSuspenseQuery.d.ts.map +0 -1
  99. package/dist/hooks/useActorSuspenseQuery.js +0 -47
  100. package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
  101. package/dist/types.d.ts +0 -270
  102. package/dist/types.d.ts.map +0 -1
  103. package/dist/types.js +0 -5
  104. package/dist/types.js.map +0 -1
  105. package/dist/utils.d.ts +0 -51
  106. package/dist/utils.d.ts.map +0 -1
  107. package/dist/utils.js +0 -105
  108. package/dist/utils.js.map +0 -1
  109. package/dist/validation.d.ts +0 -131
  110. package/dist/validation.d.ts.map +0 -1
  111. package/dist/validation.js +0 -125
  112. package/dist/validation.js.map +0 -1
  113. package/src/auth/authentication-manager.ts +0 -860
  114. package/src/auth/constants.ts +0 -32
  115. package/src/auth/createIdentityAttributeHooks.ts +0 -143
  116. package/src/auth/identity-attributes-manager.ts +0 -131
  117. package/src/auth/identity-attributes.ts +0 -270
  118. package/src/auth/index.ts +0 -7
  119. package/src/auth/local-ii-probe.ts +0 -147
  120. package/src/auth/types.ts +0 -200
  121. package/src/createActorHooks.ts +0 -220
  122. package/src/createInfiniteQuery.ts +0 -578
  123. package/src/createMutation.ts +0 -341
  124. package/src/createQuery.ts +0 -237
  125. package/src/createSuspenseInfiniteQuery.ts +0 -601
  126. package/src/createSuspenseQuery.ts +0 -234
  127. package/src/defineReactor.ts +0 -324
  128. package/src/hooks/createAuthHooks.ts +0 -189
  129. package/src/hooks/index.ts +0 -103
  130. package/src/hooks/useActorInfiniteQuery.ts +0 -205
  131. package/src/hooks/useActorMethod.ts +0 -507
  132. package/src/hooks/useActorMutation.ts +0 -205
  133. package/src/hooks/useActorQuery.ts +0 -124
  134. package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -205
  135. package/src/hooks/useActorSuspenseQuery.ts +0 -132
  136. package/src/index.ts +0 -23
  137. package/src/types.ts +0 -499
  138. package/src/utils.ts +0 -121
  139. package/src/validation.ts +0 -202
@@ -1,860 +0,0 @@
1
- import type { Identity } from "@icp-sdk/core/agent"
2
- import { AnonymousIdentity } from "@icp-sdk/core/agent"
3
- import type {
4
- AuthClientLike,
5
- AuthClientSignInOptions,
6
- AuthState,
7
- AuthenticationClientOptions,
8
- AuthenticationSignInOptions,
9
- } from "./types.js"
10
- import { ClientManager, isDev } from "@ic-reactor/core"
11
-
12
- import { Principal } from "@icp-sdk/core/principal"
13
- import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
14
- import {
15
- probeLocalInternetIdentity,
16
- localInternetIdentityUnavailableError,
17
- type AuthorizePath,
18
- } from "./local-ii-probe.js"
19
- import {
20
- IC_INTERNET_IDENTITY_PROVIDER,
21
- INTERNET_IDENTITY_PROVIDER_ENV_KEY,
22
- LOCAL_INTERNET_IDENTITY_CANISTER_ID,
23
- localInternetIdentityProvider,
24
- } from "./constants.js"
25
-
26
- export interface AuthenticationManagerParameters extends AuthenticationClientOptions {
27
- clientManager: ClientManager
28
- /**
29
- * Bring your own client instance. When provided, IC Reactor never constructs
30
- * one and never applies the options below to it.
31
- */
32
- authClient?: AuthClientLike
33
- /** Canister ID of a locally deployed Internet Identity. */
34
- internetIdentityId?: string
35
- }
36
-
37
- type AuthClientConstructor = {
38
- new (options?: AuthenticationClientOptions): AuthClientLike
39
- }
40
-
41
- /**
42
- * Manages Internet Identity sign-in, session restoration, and authentication
43
- * state for a {@link ClientManager}.
44
- *
45
- * @example
46
- * ```ts
47
- * const authentication = new AuthenticationManager({ clientManager })
48
- * const identity = await authentication.authenticate()
49
- * ```
50
- */
51
- export class AuthenticationManager {
52
- private authClient?: AuthClientLike
53
- private authPromise?: Promise<Identity | undefined>
54
- private authClientWasProvided = false
55
- private authStateRevision = 0
56
- private authStateSubscribers: Array<(state: AuthState) => void> = []
57
- private authClientConstructor?: AuthClientConstructor
58
- private authClientConstructorPromise?: Promise<
59
- AuthClientConstructor | undefined
60
- >
61
- private authModuleMissing = false
62
- private authClientOptions?: AuthenticationClientOptions
63
- private authStateValue: AuthState = {
64
- identity: null,
65
- isAuthenticating: false,
66
- isAuthenticated: false,
67
- error: undefined,
68
- }
69
- private readonly identityProvider?: string | URL
70
- private readonly internetIdentityId?: string
71
- /**
72
- * Which authorize path the locally deployed Internet Identity serves, once
73
- * probed. `undefined` means not probed yet; `null` means it serves no sign-in
74
- * UI and login should fail with an explanation rather than open a popup onto
75
- * a gateway error page.
76
- */
77
- private localAuthorizePath?: AuthorizePath | null
78
- private readonly defaultClientOptions: AuthenticationClientOptions
79
- public readonly clientManager: ClientManager
80
-
81
- /** The current authentication state. */
82
- public get authState(): AuthState {
83
- return this.authStateValue
84
- }
85
-
86
- constructor({
87
- clientManager,
88
- authClient,
89
- identityProvider,
90
- internetIdentityId,
91
- ...clientOptions
92
- }: AuthenticationManagerParameters) {
93
- this.clientManager = clientManager
94
- const canisterEnv =
95
- typeof window !== "undefined" ? getAuthenticationCanisterEnv() : undefined
96
- this.identityProvider =
97
- identityProvider ||
98
- acceptEnvIdentityProvider(
99
- canisterEnv?.[INTERNET_IDENTITY_PROVIDER_ENV_KEY] ||
100
- canisterEnv?.["PUBLIC_INTERNET_IDENTITY_PROVIDER"],
101
- clientManager
102
- )
103
- // Same cookie, same decision. This one only ever reaches a local provider
104
- // URL, but `allowEnvConfig: false` has to mean the cookie is not consulted
105
- // rather than mostly not consulted.
106
- this.internetIdentityId =
107
- internetIdentityId ||
108
- (clientManager.trustsEnvConfig
109
- ? acceptEnvCanisterId(
110
- canisterEnv?.["internet_identity"] ||
111
- canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity"] ||
112
- canisterEnv?.["CANISTER_ID_INTERNET_IDENTITY"]
113
- )
114
- : undefined)
115
- this.defaultClientOptions = clientOptions
116
-
117
- if (authClient) {
118
- this.authClientWasProvided = true
119
- this.authClient = authClient
120
- this.syncStateFromClient(this.authStateRevision).catch((error) => {
121
- this.updateState({ error: error as Error, isAuthenticating: false })
122
- })
123
- } else if (typeof window !== "undefined") {
124
- this.loadAuthClientConstructor().catch(() => {
125
- // Optional auth support is reported when an auth method is used.
126
- })
127
- }
128
- }
129
-
130
- /** @internal Used by IdentityAttributesManager. */
131
- public get client() {
132
- return this.authClient
133
- }
134
-
135
- public subscribeAuthState(callback: (state: AuthState) => void) {
136
- this.authStateSubscribers.push(callback)
137
- return () => {
138
- this.authStateSubscribers = this.authStateSubscribers.filter(
139
- (subscriber) => subscriber !== callback
140
- )
141
- }
142
- }
143
-
144
- /**
145
- * Preloads the auth module and creates an AuthClient ahead of time.
146
- *
147
- * Call (and await) this before wiring up a login button: it makes
148
- * {@link login} and {@link IdentityAttributesManager.request} able to open
149
- * the identity provider synchronously inside the click handler, which is
150
- * what browser popup blockers and the ICRC-29 transport require.
151
- */
152
- public async prepareClient(options?: AuthenticationClientOptions) {
153
- // Before resolving options, because resolving them is what picks the
154
- // provider URL. This is the last async point before `login()` has to stay
155
- // inside the user gesture, so the answer has to be cached by now.
156
- await this.ensureLocalAuthorizePath()
157
-
158
- const clientOptions = this.resolveClientOptions(options)
159
-
160
- if (this.authClient && !this.shouldRecreateClient(clientOptions)) {
161
- return this.authClient
162
- }
163
-
164
- return this.initializeClient(clientOptions)
165
- }
166
-
167
- /**
168
- * Probe the local Internet Identity canister once, and remember what it
169
- * serves.
170
- *
171
- * Only for the derived local provider: an explicitly configured
172
- * `identityProvider` is the caller's business, and mainnet is fixed.
173
- */
174
- private async ensureLocalAuthorizePath(): Promise<void> {
175
- if (this.localAuthorizePath !== undefined) return
176
- if (this.identityProvider) return
177
- if (!this.clientManager.isLocal) return
178
-
179
- const canisterId =
180
- this.internetIdentityId ?? LOCAL_INTERNET_IDENTITY_CANISTER_ID
181
-
182
- const { path, inconclusive } = await probeLocalInternetIdentity(
183
- this.clientManager.agent,
184
- canisterId
185
- )
186
-
187
- // An inconclusive probe must not change behaviour: the canister may be
188
- // fine and merely unreachable from here, and a diagnostic that blocks a
189
- // working login is worse than the failure it explains.
190
- this.localAuthorizePath = inconclusive ? "/authorize" : path
191
-
192
- if (path === "/#authorize" && !inconclusive) {
193
- console.warn(
194
- `[ic-reactor] Internet Identity canister ${canisterId} serves its sign-in UI at ` +
195
- `"/" rather than "/authorize" — using the legacy #authorize flow. This is a ` +
196
- `pre-2026 build; newer ones through release-2026-03-16 serve /authorize directly.`
197
- )
198
- }
199
- }
200
-
201
- /**
202
- * Returns an AuthClient without awaiting, or `undefined` when the auth
203
- * module has not been loaded yet.
204
- *
205
- * Callers that must stay inside a user gesture use this instead of
206
- * `ensureClient`; awaiting anything before opening the identity
207
- * provider window loses the gesture.
208
- */
209
- public getPreparedClient(
210
- options?: AuthenticationClientOptions
211
- ): AuthClientLike | undefined {
212
- return this.ensurePreparedClient(this.resolveClientOptions(options))
213
- }
214
-
215
- public authenticate = async (): Promise<Identity | undefined> => {
216
- if (this.authState.isAuthenticated) {
217
- // Returning on the cached flag alone meant a delegation that expired
218
- // mid-session was never re-observed: the UI kept rendering a signed-in
219
- // state while every update call failed, and only a reload recovered.
220
- // Re-ask the client, which reads the cached expiry rather than hitting
221
- // storage. A throw here is treated as "still valid" so a transient
222
- // failure cannot sign anyone out.
223
- if (!this.authClient) {
224
- return this.authState.identity || undefined
225
- }
226
- const stillValid = await Promise.resolve(
227
- this.authClient.isAuthenticated()
228
- ).catch(() => true)
229
- if (stillValid) {
230
- return this.authState.identity || undefined
231
- }
232
- // Expired. Re-deriving state from the client will not help: the v8
233
- // client keeps handing out the lapsed delegation from getIdentity()
234
- // until signOut() runs (only a fresh page load purges it), so the
235
- // expired identity would go straight back on the agent and every
236
- // refetch would be signed with it. End the session explicitly.
237
- await this.expireSession()
238
- return undefined
239
- }
240
- if (this.authPromise) {
241
- return this.authPromise
242
- }
243
- if (this.authModuleMissing) {
244
- return undefined
245
- }
246
-
247
- this.authPromise = (async () => {
248
- if (isDev() && typeof window !== "undefined") {
249
- console.info(
250
- `%cic-reactor:%c Authenticating...`,
251
- "color: #3b82f6; font-weight: bold",
252
- "color: inherit",
253
- {
254
- network: this.clientManager.network,
255
- authClient: this.authClient ? "Shared Instance" : "Dynamic Import",
256
- }
257
- )
258
- }
259
- this.updateState({ isAuthenticating: true })
260
- // Anything that changes auth state — a logout, notably — bumps this. If
261
- // it moves while the awaits below are in flight, the result we are
262
- // holding describes a session that has since ended, and installing it
263
- // would put the signed-out user's delegation back on the agent.
264
- const revision = this.authStateRevision
265
- try {
266
- if (!this.authClient) {
267
- const authClient = await this.initializeClient(
268
- this.resolveClientOptions()
269
- )
270
- if (!authClient) {
271
- this.updateState({ isAuthenticating: false })
272
- return undefined
273
- }
274
- }
275
- const clientIdentity = await this.authClient!.getIdentity()
276
- const isAuthenticated = await this.authClient!.isAuthenticated()
277
-
278
- if (revision !== this.authStateRevision) {
279
- // Superseded — leave whatever ran in the meantime in place.
280
- return this.authState.identity || undefined
281
- }
282
- // A client that says it is not authenticated but still hands out a
283
- // non-anonymous identity is holding a delegation it will no longer
284
- // vouch for (expired, mid-session). Nothing may be signed with it.
285
- const identity =
286
- isAuthenticated || clientIdentity.getPrincipal().isAnonymous()
287
- ? clientIdentity
288
- : new AnonymousIdentity()
289
- // Restoring an anonymous session is the common first-load case; pushing
290
- // it through updateAgent would invalidate the whole query cache on
291
- // every mount for nothing. It is only skipped while the agent is
292
- // anonymous too, so a lapsed delegation still gets replaced.
293
- if (
294
- isAuthenticated ||
295
- !identity.getPrincipal().isAnonymous() ||
296
- !this.agentIsAnonymous()
297
- ) {
298
- this.clientManager.updateAgent(identity)
299
- }
300
- this.updateState({
301
- identity,
302
- isAuthenticated,
303
- isAuthenticating: false,
304
- })
305
- return identity
306
- } catch (error) {
307
- this.updateState({ error: error as Error, isAuthenticating: false })
308
- console.error("Authentication failed:", error)
309
- throw error
310
- } finally {
311
- this.authPromise = undefined
312
- }
313
- })()
314
-
315
- return this.authPromise
316
- }
317
-
318
- public login = async (loginOptions?: AuthenticationSignInOptions) => {
319
- let didCompleteSignIn = false
320
-
321
- try {
322
- const authClientOptions = this.resolveClientOptions(loginOptions)
323
-
324
- // Stays synchronous when `prepareClient()` has already loaded the auth
325
- // module, so `signIn()` still runs inside the caller's click handler.
326
- if (!this.ensurePreparedClient(authClientOptions)) {
327
- await this.initializeClient(authClientOptions)
328
- }
329
-
330
- if (!this.authClient) {
331
- await this.authenticate()
332
- }
333
-
334
- if (!this.authClient) {
335
- throw new Error(
336
- "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(...) })"
337
- )
338
- }
339
-
340
- this.updateState({ isAuthenticating: true, error: undefined })
341
- const identity = await this.signInOrRecoverIdentity(
342
- getSignInOptions(loginOptions)
343
- )
344
-
345
- if (!this.clientManager.agentState.isInitialized) {
346
- await this.clientManager.initializeAgent()
347
- }
348
-
349
- this.clientManager.updateAgent(identity)
350
- this.updateState({
351
- identity,
352
- isAuthenticated: true,
353
- isAuthenticating: false,
354
- })
355
- didCompleteSignIn = true
356
-
357
- try {
358
- await loginOptions?.onSuccess?.()
359
- } catch (callbackError) {
360
- this.updateState({ error: callbackError as Error })
361
- await loginOptions?.onError?.((callbackError as Error).message)
362
- throw callbackError
363
- }
364
- } catch (error) {
365
- if (!didCompleteSignIn) {
366
- await loginOptions?.onError?.((error as Error).message)
367
- this.updateState({
368
- error: error as Error,
369
- isAuthenticating: false,
370
- })
371
- }
372
- throw error
373
- }
374
- }
375
-
376
- public logout = async (options?: { returnTo?: string }) => {
377
- if (!this.authClient) {
378
- throw new Error(
379
- "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
- )
381
- }
382
- this.updateState({ isAuthenticating: true, error: undefined })
383
- try {
384
- await this.authClient.signOut(options)
385
- const identity = await this.authClient.getIdentity()
386
- this.clientManager.updateAgent(identity)
387
- this.updateState({
388
- identity,
389
- isAuthenticated: false,
390
- isAuthenticating: false,
391
- })
392
- } catch (error) {
393
- // Without this the manager was left with `isAuthenticating: true` and no
394
- // recorded error, so a button disabled on `isAuthenticating` stayed stuck
395
- // and nothing told the app why.
396
- this.updateState({ error: error as Error, isAuthenticating: false })
397
- throw error
398
- }
399
- }
400
-
401
- private async initializeClient(
402
- options?: AuthenticationClientOptions
403
- ): Promise<AuthClientLike | undefined> {
404
- const AuthClient = await this.loadAuthClientConstructor()
405
-
406
- if (!AuthClient) {
407
- return undefined
408
- }
409
-
410
- this.authClient = new AuthClient(options)
411
- this.authClientOptions = options
412
- return this.authClient
413
- }
414
-
415
- /** @internal Used by IdentityAttributesManager. */
416
- public async signInOrRecoverIdentity(
417
- options?: AuthClientSignInOptions
418
- ): Promise<Identity> {
419
- if (!this.authClient) {
420
- throw new Error(
421
- "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
- )
423
- }
424
-
425
- try {
426
- return await this.authClient.signIn(options)
427
- } catch (error) {
428
- const identity = await Promise.resolve(
429
- this.authClient.getIdentity()
430
- ).catch(() => null)
431
- const isAuthenticated = await Promise.resolve(
432
- this.authClient.isAuthenticated()
433
- ).catch(() => false)
434
-
435
- if (identity && isAuthenticated) {
436
- return identity
437
- }
438
-
439
- throw error
440
- }
441
- }
442
-
443
- private ensurePreparedClient(
444
- options?: AuthenticationClientOptions
445
- ): AuthClientLike | undefined {
446
- if (this.authClient && !this.shouldRecreateClient(options)) {
447
- return this.authClient
448
- }
449
-
450
- const AuthClient = this.authClientConstructor
451
- if (!AuthClient || this.authClientWasProvided) {
452
- return undefined
453
- }
454
-
455
- this.authClient = new AuthClient(options)
456
- this.authClientOptions = options
457
- return this.authClient
458
- }
459
-
460
- /**
461
- * Only rebuild the client when the effective options actually changed.
462
- *
463
- * Recreating on every call would discard the client warmed up by
464
- * `prepareClient()` and register a duplicate sign-out callback on the
465
- * shared IdleManager singleton each time.
466
- */
467
- private shouldRecreateClient(options?: AuthenticationClientOptions): boolean {
468
- if (this.authClientWasProvided) {
469
- return false
470
- }
471
- return !isSameAuthClientOptions(this.authClientOptions, options)
472
- }
473
-
474
- /**
475
- * Merges constructor-level defaults with per-call overrides and fills in the
476
- * network-appropriate identity provider.
477
- */
478
- private resolveClientOptions(
479
- options?: AuthenticationClientOptions
480
- ): AuthenticationClientOptions {
481
- const merged = getAuthClientOptions({
482
- ...this.defaultClientOptions,
483
- ...options,
484
- })
485
-
486
- return {
487
- ...merged,
488
- identityProvider:
489
- merged?.identityProvider ?? this.getDefaultIdentityProvider(),
490
- }
491
- }
492
-
493
- /** Whether the shared agent currently signs as the anonymous principal. */
494
- private agentIsAnonymous(): boolean {
495
- const installed = this.clientManager.identity
496
- return installed === undefined || installed.getPrincipal().isAnonymous()
497
- }
498
-
499
- /**
500
- * End a session whose delegation has lapsed: ask the client to forget it,
501
- * put the anonymous identity on the agent -- which also sweeps the previous
502
- * user's caller-scoped cache entries and refetches the rest anonymously --
503
- * and publish the signed-out state. `signOut` failing changes nothing here:
504
- * the delegation is already unusable, and the agent must not keep it.
505
- */
506
- private async expireSession() {
507
- try {
508
- await this.authClient?.signOut()
509
- } catch {
510
- // Nothing to keep; fall through to anonymous either way.
511
- }
512
- const identity = new AnonymousIdentity()
513
- this.clientManager.updateAgent(identity)
514
- this.updateState({
515
- identity,
516
- isAuthenticated: false,
517
- isAuthenticating: false,
518
- error: undefined,
519
- })
520
- }
521
-
522
- private async syncStateFromClient(revision = this.authStateRevision) {
523
- if (!this.authClient) {
524
- return
525
- }
526
-
527
- const identity = await this.authClient.getIdentity()
528
- const isAuthenticated = await this.authClient.isAuthenticated()
529
- if (revision !== this.authStateRevision) {
530
- return
531
- }
532
- this.clientManager.updateAgent(identity)
533
- this.updateState({
534
- identity,
535
- isAuthenticated,
536
- isAuthenticating: false,
537
- error: undefined,
538
- })
539
- }
540
-
541
- /** @internal Used by IdentityAttributesManager. */
542
- public async ensureClient(options?: AuthenticationClientOptions) {
543
- const clientOptions = this.resolveClientOptions(options)
544
- if (!this.ensurePreparedClient(clientOptions)) {
545
- await this.initializeClient(clientOptions)
546
- }
547
- return this.authClient
548
- }
549
-
550
- /** @internal Used by IdentityAttributesManager. */
551
- public async commitIdentity(identity: Identity, isAuthenticated: boolean) {
552
- if (!this.clientManager.agentState.isInitialized) {
553
- await this.clientManager.initializeAgent()
554
- }
555
- this.clientManager.updateAgent(identity)
556
- this.updateState({ identity, isAuthenticated, isAuthenticating: false })
557
- }
558
-
559
- /** @internal Used by IdentityAttributesManager. */
560
- public setAuthenticating() {
561
- this.updateState({ isAuthenticating: true, error: undefined })
562
- }
563
-
564
- /** @internal Used by IdentityAttributesManager. */
565
- public setAuthenticationError(error: Error) {
566
- this.updateState({ error, isAuthenticating: false })
567
- }
568
-
569
- private getDefaultIdentityProvider(): string | URL {
570
- if (this.identityProvider) {
571
- return this.identityProvider
572
- }
573
- if (!this.clientManager.isLocal) {
574
- return IC_INTERNET_IDENTITY_PROVIDER
575
- }
576
-
577
- const canisterId =
578
- this.internetIdentityId ?? LOCAL_INTERNET_IDENTITY_CANISTER_ID
579
-
580
- // `null` is the probe's positive finding that this build serves no sign-in
581
- // UI. Throwing here surfaces an actionable message where the caller can see
582
- // it, instead of opening a popup onto the gateway's verification-error page
583
- // and leaving the app waiting until the user closes it.
584
- if (this.localAuthorizePath === null) {
585
- throw localInternetIdentityUnavailableError(canisterId)
586
- }
587
-
588
- return localInternetIdentityProvider(
589
- Number(this.clientManager.agentHost?.port) || 4943,
590
- this.internetIdentityId,
591
- this.localAuthorizePath
592
- )
593
- }
594
-
595
- private updateState(newState: Partial<AuthState>) {
596
- if (isDev()) console.debug("[ic-reactor] Updating Auth State:", newState)
597
- this.authStateRevision += 1
598
- this.authStateValue = { ...this.authStateValue, ...newState }
599
- this.authStateSubscribers.forEach((subscriber) =>
600
- subscriber(this.authStateValue)
601
- )
602
- }
603
-
604
- private async loadAuthClientConstructor() {
605
- if (this.authClientConstructor) {
606
- return this.authClientConstructor
607
- }
608
-
609
- if (!this.authClientConstructorPromise) {
610
- this.authClientConstructorPromise = importAuthClientModule()
611
- .then((authModule) => {
612
- const AuthClient = (
613
- authModule as { AuthClient?: AuthClientConstructor }
614
- ).AuthClient
615
-
616
- if (!AuthClient) {
617
- throw new Error("@icp-sdk/auth/client did not export AuthClient")
618
- }
619
-
620
- this.authClientConstructor = AuthClient
621
- return AuthClient
622
- })
623
- .catch((error) => {
624
- this.authModuleMissing = true
625
- this.authClientConstructorPromise = undefined
626
- if (
627
- error instanceof Error &&
628
- error.message.includes("did not export AuthClient")
629
- ) {
630
- throw error
631
- }
632
- return undefined
633
- })
634
- }
635
-
636
- return this.authClientConstructorPromise
637
- }
638
- }
639
-
640
- /**
641
- * Loads the optional `@icp-sdk/auth` peer.
642
- *
643
- * The `try` is load-bearing, not defensive. webpack flags a dynamic import
644
- * that sits lexically inside a `try` block as an *optional* dependency
645
- * (`ImportParserPlugin`: `dep.optional = Boolean(parser.scope.inTry)`), and
646
- * `Compilation` reports an unresolvable optional dependency as a build
647
- * *warning* instead of a fatal "Module not found" error, emitting a module
648
- * that rejects at runtime. Without the `try`, every webpack-based toolchain
649
- * (webpack, `next build --webpack`, Rspack) fails to build for consumers who
650
- * never touch authentication, because npm/pnpm do not install optional peers.
651
- * Do not hoist this import out of the `try`, and do not wrap it in a nested
652
- * function — webpack resets `inTry` at every function boundary.
653
- *
654
- * The specifier must also stay a literal: a variable specifier (or
655
- * `@vite-ignore`) makes bundlers skip this import entirely, so the bare
656
- * specifier survives into the browser, where it cannot be resolved without an
657
- * import map and every login path fails. A literal lets Vite/Rollup/webpack
658
- * code-split the optional peer, and lets them tree-shake it away for apps that
659
- * never reference this class.
660
- */
661
- function importAuthClientModule(): Promise<unknown> {
662
- try {
663
- return import("@icp-sdk/auth/client")
664
- } catch (error) {
665
- // Native ESM without an import map (and bundlers that neither resolve nor
666
- // stub the specifier) throw synchronously rather than rejecting.
667
- return Promise.reject(error)
668
- }
669
- }
670
-
671
- function getAuthClientOptions(
672
- options?: AuthenticationClientOptions
673
- ): AuthenticationClientOptions | undefined {
674
- if (!options) {
675
- return undefined
676
- }
677
-
678
- return {
679
- identityProvider: options.identityProvider,
680
- windowOpenerFeatures: options.windowOpenerFeatures,
681
- openIdProvider: getAuthClientOpenIdProvider(options.openIdProvider),
682
- derivationOrigin: options.derivationOrigin,
683
- storage: options.storage,
684
- keyType: options.keyType,
685
- idleOptions: options.idleOptions,
686
- identity: options.identity,
687
- transport: options.transport,
688
- }
689
- }
690
-
691
- function getAuthClientOpenIdProvider(
692
- openIdProvider?: AuthenticationClientOptions["openIdProvider"]
693
- ): AuthenticationClientOptions["openIdProvider"] | undefined {
694
- return openIdProvider === "google" ||
695
- openIdProvider === "apple" ||
696
- openIdProvider === "microsoft"
697
- ? openIdProvider
698
- : undefined
699
- }
700
-
701
- /**
702
- * Compares the options two AuthClients would be built from. Object-valued
703
- * options (`storage`, `identity`, `idleOptions`) are compared by reference,
704
- * which is what module-scoped configuration produces.
705
- */
706
- function isSameAuthClientOptions(
707
- current?: AuthenticationClientOptions,
708
- next?: AuthenticationClientOptions
709
- ): boolean {
710
- if (current === next) {
711
- return true
712
- }
713
- if (!current || !next) {
714
- return false
715
- }
716
-
717
- return (
718
- String(current.identityProvider ?? "") ===
719
- String(next.identityProvider ?? "") &&
720
- current.windowOpenerFeatures === next.windowOpenerFeatures &&
721
- current.openIdProvider === next.openIdProvider &&
722
- String(current.derivationOrigin ?? "") ===
723
- String(next.derivationOrigin ?? "") &&
724
- current.storage === next.storage &&
725
- current.keyType === next.keyType &&
726
- current.idleOptions === next.idleOptions &&
727
- current.identity === next.identity &&
728
- current.transport === next.transport
729
- )
730
- }
731
-
732
- /**
733
- * Decides whether an Internet Identity provider carried by the `ic_env` cookie
734
- * may be used.
735
- *
736
- * The cookie is written by the replica serving a local or testnet deployment,
737
- * and it is only authoritative in that setting: cookies are not origin-isolated
738
- * the way script-accessible storage is, so on a mainnet deployment the identity
739
- * provider is configuration that belongs in code rather than something read
740
- * back out of the environment at runtime. `AuthClient` treats `identityProvider`
741
- * as trusted developer configuration and does not re-check it.
742
- *
743
- * This mirrors the root-key guard in `ClientManager` (see `packages/core`):
744
- * both values arrive from the same cookie and get the same treatment — adopted
745
- * off-mainnet, ignored on mainnet in favour of the pinned default.
746
- *
747
- * A provider on the app's own origin is always accepted: it is exactly as
748
- * trustworthy as the page doing the asking. Callers that need a custom provider
749
- * on mainnet pass `identityProvider` explicitly.
750
- */
751
- function acceptEnvIdentityProvider(
752
- value: string | undefined,
753
- clientManager: ClientManager
754
- ): string | undefined {
755
- if (!value) return undefined
756
-
757
- const pageOrigin =
758
- typeof window !== "undefined" ? window.location?.origin : undefined
759
-
760
- let url: URL
761
- try {
762
- url = new URL(value, pageOrigin)
763
- } catch {
764
- return undefined
765
- }
766
-
767
- if (pageOrigin && url.origin === pageOrigin) {
768
- return url.toString()
769
- }
770
-
771
- // Positive allowlist, not `!isMainnetHost(...)`.
772
- //
773
- // The old comment here claimed this defaulted to true for an unknown host so
774
- // that "anything that looks like mainnet fails closed". It did not:
775
- // isMainnetHost defaults to true only for `undefined`, and returns false for
776
- // every unrecognised host — so a dapp on a custom domain accepted an identity
777
- // provider chosen by a cookie, and the ic_env cookie is writable by any
778
- // sibling subdomain. Redirecting the Internet Identity flow is as severe as
779
- // substituting the root key, so both now use the same test.
780
- //
781
- // That test is the ClientManager's single resolved decision rather than a
782
- // recomputed host test, which silently ignored a caller
783
- // who had opted in: `allowEnvConfig: true` accepted the cookie's root key and
784
- // then discarded its identity provider, from the same cookie, on the same
785
- // host.
786
- if (!clientManager.trustsEnvConfig) {
787
- console.warn(
788
- `[ic-reactor] Ignoring the Internet Identity provider from the ic_env cookie ` +
789
- `("${url.origin}") because this reactor does not target a local replica, where ` +
790
- `the provider is taken from configuration rather than the environment. Pass ` +
791
- `\`identityProvider\` to AuthenticationManager to use a custom provider.`
792
- )
793
- return undefined
794
- }
795
-
796
- return url.toString()
797
- }
798
-
799
- /**
800
- * Accepts an Internet Identity canister ID from `ic_env` only when it parses as
801
- * a principal.
802
- *
803
- * The value is interpolated into a provider URL
804
- * (`http://<id>.localhost:<port>/authorize`), so anything that is not a bare
805
- * principal can change the shape of that URL rather than just its subdomain.
806
- * Only reachable on local networks, since mainnet uses the pinned provider.
807
- */
808
- function acceptEnvCanisterId(value: string | undefined): string | undefined {
809
- if (!value) return undefined
810
- try {
811
- return Principal.fromText(value).toText()
812
- } catch {
813
- return undefined
814
- }
815
- }
816
-
817
- function getAuthenticationCanisterEnv(): Record<string, string> | undefined {
818
- const canisterEnv = safeGetCanisterEnv<Record<string, string>>()
819
- if (canisterEnv) {
820
- return canisterEnv as unknown as Record<string, string>
821
- }
822
-
823
- if (typeof document === "undefined") {
824
- return undefined
825
- }
826
-
827
- const cookie = document.cookie
828
- .split(";")
829
- .find((part) => part.trim().startsWith("ic_env="))
830
- const encodedValue = cookie?.split("=").slice(1).join("=")?.trim()
831
- if (!encodedValue) {
832
- return undefined
833
- }
834
-
835
- const env = Object.fromEntries(
836
- decodeURIComponent(encodedValue)
837
- .split("&")
838
- .map((entry) => {
839
- const separatorIndex = entry.indexOf("=")
840
- return separatorIndex === -1
841
- ? [entry, ""]
842
- : [entry.slice(0, separatorIndex), entry.slice(separatorIndex + 1)]
843
- })
844
- )
845
-
846
- return Object.keys(env).length ? env : undefined
847
- }
848
-
849
- function getSignInOptions(
850
- options?: AuthenticationSignInOptions
851
- ): AuthClientSignInOptions | undefined {
852
- if (!options) {
853
- return undefined
854
- }
855
-
856
- return {
857
- maxTimeToLive: options.maxTimeToLive,
858
- targets: options.targets,
859
- }
860
- }