@ic-reactor/react 3.12.4 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +410 -44
  2. package/dist/auth/auth-client-compat.d.ts +122 -0
  3. package/dist/auth/auth-client-compat.d.ts.map +1 -0
  4. package/dist/auth/auth-client-compat.js +162 -0
  5. package/dist/auth/auth-client-compat.js.map +1 -0
  6. package/dist/auth/authentication-manager.d.ts +287 -5
  7. package/dist/auth/authentication-manager.d.ts.map +1 -1
  8. package/dist/auth/authentication-manager.js +920 -150
  9. package/dist/auth/authentication-manager.js.map +1 -1
  10. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  11. package/dist/auth/createIdentityAttributeHooks.js +36 -20
  12. package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
  13. package/dist/auth/identity-attributes-manager.d.ts +2 -1
  14. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  15. package/dist/auth/identity-attributes-manager.js +90 -6
  16. package/dist/auth/identity-attributes-manager.js.map +1 -1
  17. package/dist/auth/identity-attributes.d.ts.map +1 -1
  18. package/dist/auth/identity-attributes.js +57 -0
  19. package/dist/auth/identity-attributes.js.map +1 -1
  20. package/dist/auth/local-ii-probe.d.ts +12 -1
  21. package/dist/auth/local-ii-probe.d.ts.map +1 -1
  22. package/dist/auth/local-ii-probe.js +22 -3
  23. package/dist/auth/local-ii-probe.js.map +1 -1
  24. package/dist/auth/types.d.ts +48 -5
  25. package/dist/auth/types.d.ts.map +1 -1
  26. package/dist/createActorHooks.d.ts +9 -20
  27. package/dist/createActorHooks.d.ts.map +1 -1
  28. package/dist/createActorHooks.js.map +1 -1
  29. package/dist/createInfiniteQuery.d.ts +51 -10
  30. package/dist/createInfiniteQuery.d.ts.map +1 -1
  31. package/dist/createInfiniteQuery.js +39 -15
  32. package/dist/createInfiniteQuery.js.map +1 -1
  33. package/dist/createMutation.d.ts +4 -1
  34. package/dist/createMutation.d.ts.map +1 -1
  35. package/dist/createMutation.js +121 -84
  36. package/dist/createMutation.js.map +1 -1
  37. package/dist/createQuery.d.ts +35 -2
  38. package/dist/createQuery.d.ts.map +1 -1
  39. package/dist/createQuery.js +104 -17
  40. package/dist/createQuery.js.map +1 -1
  41. package/dist/createReactorProvider.d.ts +158 -0
  42. package/dist/createReactorProvider.d.ts.map +1 -0
  43. package/dist/createReactorProvider.js +256 -0
  44. package/dist/createReactorProvider.js.map +1 -0
  45. package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
  46. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  47. package/dist/createSuspenseInfiniteQuery.js +59 -27
  48. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  49. package/dist/createSuspenseQuery.d.ts +23 -2
  50. package/dist/createSuspenseQuery.d.ts.map +1 -1
  51. package/dist/createSuspenseQuery.js +68 -21
  52. package/dist/createSuspenseQuery.js.map +1 -1
  53. package/dist/defineDisplayReactor.d.ts +43 -0
  54. package/dist/defineDisplayReactor.d.ts.map +1 -0
  55. package/dist/defineDisplayReactor.js +42 -0
  56. package/dist/defineDisplayReactor.js.map +1 -0
  57. package/dist/defineReactor.d.ts +46 -72
  58. package/dist/defineReactor.d.ts.map +1 -1
  59. package/dist/defineReactor.js +11 -176
  60. package/dist/defineReactor.js.map +1 -1
  61. package/dist/defineReactorShared.d.ts +84 -0
  62. package/dist/defineReactorShared.d.ts.map +1 -0
  63. package/dist/defineReactorShared.js +139 -0
  64. package/dist/defineReactorShared.js.map +1 -0
  65. package/dist/hooks/createAuthHooks.d.ts +9 -2
  66. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  67. package/dist/hooks/createAuthHooks.js +184 -24
  68. package/dist/hooks/createAuthHooks.js.map +1 -1
  69. package/dist/hooks/useActorInfiniteQuery.d.ts +36 -8
  70. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  71. package/dist/hooks/useActorInfiniteQuery.js +54 -21
  72. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  73. package/dist/hooks/useActorMethod.d.ts +37 -4
  74. package/dist/hooks/useActorMethod.d.ts.map +1 -1
  75. package/dist/hooks/useActorMethod.js +201 -57
  76. package/dist/hooks/useActorMethod.js.map +1 -1
  77. package/dist/hooks/useActorMutation.d.ts +15 -12
  78. package/dist/hooks/useActorMutation.d.ts.map +1 -1
  79. package/dist/hooks/useActorMutation.js +14 -13
  80. package/dist/hooks/useActorMutation.js.map +1 -1
  81. package/dist/hooks/useActorQuery.d.ts +17 -4
  82. package/dist/hooks/useActorQuery.d.ts.map +1 -1
  83. package/dist/hooks/useActorQuery.js +30 -9
  84. package/dist/hooks/useActorQuery.js.map +1 -1
  85. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +17 -5
  86. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  87. package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
  88. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  89. package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
  90. package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
  91. package/dist/hooks/useActorSuspenseQuery.js +20 -9
  92. package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
  93. package/dist/index.d.ts +4 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +6 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/ownedAuthentication.d.ts +52 -0
  98. package/dist/ownedAuthentication.d.ts.map +1 -0
  99. package/dist/ownedAuthentication.js +49 -0
  100. package/dist/ownedAuthentication.js.map +1 -0
  101. package/dist/server.d.ts +21 -0
  102. package/dist/server.d.ts.map +1 -0
  103. package/dist/server.js +23 -0
  104. package/dist/server.js.map +1 -0
  105. package/dist/testing.d.ts +19 -0
  106. package/dist/testing.d.ts.map +1 -0
  107. package/dist/testing.js +19 -0
  108. package/dist/testing.js.map +1 -0
  109. package/dist/types.d.ts +428 -21
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +1 -1
  112. package/dist/utils.d.ts +159 -3
  113. package/dist/utils.d.ts.map +1 -1
  114. package/dist/utils.js +301 -1
  115. package/dist/utils.js.map +1 -1
  116. package/dist/validation.d.ts +12 -7
  117. package/dist/validation.d.ts.map +1 -1
  118. package/dist/validation.js +34 -15
  119. package/dist/validation.js.map +1 -1
  120. package/llms.txt +259 -33
  121. package/package.json +17 -5
  122. package/src/auth/auth-client-compat.ts +273 -0
  123. package/src/auth/authentication-manager.ts +918 -96
  124. package/src/auth/createIdentityAttributeHooks.ts +47 -21
  125. package/src/auth/identity-attributes-manager.ts +100 -5
  126. package/src/auth/identity-attributes.ts +75 -0
  127. package/src/auth/local-ii-probe.ts +29 -3
  128. package/src/auth/types.ts +49 -6
  129. package/src/createActorHooks.ts +50 -42
  130. package/src/createInfiniteQuery.ts +120 -28
  131. package/src/createMutation.ts +213 -132
  132. package/src/createQuery.ts +164 -32
  133. package/src/createReactorProvider.ts +365 -0
  134. package/src/createSuspenseInfiniteQuery.ts +93 -43
  135. package/src/createSuspenseQuery.ts +102 -32
  136. package/src/defineDisplayReactor.ts +62 -0
  137. package/src/defineReactor.ts +81 -263
  138. package/src/defineReactorShared.ts +268 -0
  139. package/src/hooks/createAuthHooks.ts +210 -28
  140. package/src/hooks/useActorInfiniteQuery.ts +156 -55
  141. package/src/hooks/useActorMethod.ts +295 -92
  142. package/src/hooks/useActorMutation.ts +42 -30
  143. package/src/hooks/useActorQuery.ts +43 -10
  144. package/src/hooks/useActorSuspenseInfiniteQuery.ts +110 -54
  145. package/src/hooks/useActorSuspenseQuery.ts +30 -15
  146. package/src/index.ts +8 -0
  147. package/src/ownedAuthentication.ts +81 -0
  148. package/src/server.ts +23 -0
  149. package/src/testing.ts +18 -0
  150. package/src/types.ts +492 -22
  151. package/src/utils.ts +387 -3
  152. package/src/validation.ts +43 -19
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react"
2
+ import { Principal } from "@icp-sdk/core/principal"
2
3
  import type { IdentityAttributesManager } from "./identity-attributes-manager.js"
3
4
  import type {
4
5
  IdentityAttributeResult,
@@ -19,6 +20,9 @@ export interface UseIdentityAttributesReturn {
19
20
  clearAttributes: () => void
20
21
  }
21
22
 
23
+ /** What `request()` reports as the principal of a signed-out session. */
24
+ const ANONYMOUS_PRINCIPAL = Principal.anonymous().toText()
25
+
22
26
  export function createIdentityAttributeHooks(
23
27
  identityAttributes: IdentityAttributesManager
24
28
  ) {
@@ -40,30 +44,44 @@ export function createIdentityAttributeHooks(
40
44
  * A request started before a sign-out or an account switch resolves after
41
45
  * it, and publishing that result would put the previous user's decoded PII
42
46
  * back on screen with no further auth event to clear it again. So a result
43
- * is committed only when it is the signed-in user's own: it carries the
44
- * principal that is current when it resolves, or the session has not
45
- * changed since it was asked for.
47
+ * is committed only when it is the current session's own: it carries the
48
+ * principal signed in when it resolves or, while nobody is, the anonymous
49
+ * principal that a `signIn: false` request made while signed out reports.
46
50
  *
47
- * The first test is the one the default flow needs. With `signIn: true`
48
- * the request itself signs the user in, so the principal seen before the
49
- * request (none, for a first sign-in; the old account, for a switch made
50
- * inside the provider window) is never the one the result belongs to.
51
- * Comparing against the pre-request principal alone left the hook's
52
- * `attributes` null after every successful first-time sign-in.
51
+ * With `signIn: true` the request itself signs the user in, so the
52
+ * principal seen before the request (none, for a first sign-in; the old
53
+ * account, for a switch made inside the provider window) is never the one
54
+ * the result belongs to. Comparing against it left `attributes` null after
55
+ * every successful first-time sign-in. Nor does "the same principal before
56
+ * and after" mean the session never changed: the request's own sign-in is
57
+ * published only when the whole request ends, so a user who signed in
58
+ * through it and signed out before it ended is nobody on both sides. Their
59
+ * name and email then went up on the signed-out screen.
53
60
  */
54
- const isCurrent = useCallback(
55
- (requestedFor: string | undefined) => requestedFor === currentPrincipal(),
61
+ const publishIfCurrent = useCallback(
62
+ (result: IdentityAttributeResult) => {
63
+ const session = currentPrincipal() ?? ANONYMOUS_PRINCIPAL
64
+ if (result.principal !== session) return false
65
+ setAttributes(result)
66
+ return true
67
+ },
56
68
  [currentPrincipal]
57
69
  )
58
70
 
59
- const publishIfCurrent = useCallback(
60
- (requestedFor: string | undefined, result: IdentityAttributeResult) => {
71
+ /**
72
+ * The failure counterpart of `publishIfCurrent`. A request that signs in
73
+ * can fail on its attribute side after its own sign-in went through, and
74
+ * the manager keeps that sign-in before it reports the failure, so the
75
+ * session is now the one this request opened and the failure is that
76
+ * user's to see. A failure that lands after a sign-out is still dropped.
77
+ */
78
+ const publishErrorIfCurrent = useCallback(
79
+ (requestedFor: string | undefined, signsIn: boolean, error: Error) => {
61
80
  const current = currentPrincipal()
62
- if (result.principal !== current && requestedFor !== current) {
63
- return false
81
+ if (requestedFor !== current && !(signsIn && current !== undefined)) {
82
+ return
64
83
  }
65
- setAttributes(result)
66
- return true
84
+ setAttributeError(error)
67
85
  },
68
86
  [currentPrincipal]
69
87
  )
@@ -75,10 +93,14 @@ export function createIdentityAttributeHooks(
75
93
  const requestedFor = currentPrincipal()
76
94
  try {
77
95
  const result = await identityAttributes.request(params)
78
- publishIfCurrent(requestedFor, result)
96
+ publishIfCurrent(result)
79
97
  return result
80
98
  } catch (error) {
81
- if (isCurrent(requestedFor)) setAttributeError(error as Error)
99
+ publishErrorIfCurrent(
100
+ requestedFor,
101
+ params.signIn !== false,
102
+ error as Error
103
+ )
82
104
  throw error
83
105
  } finally {
84
106
  setIsRequestingAttributes(false)
@@ -94,10 +116,14 @@ export function createIdentityAttributeHooks(
94
116
  const requestedFor = currentPrincipal()
95
117
  try {
96
118
  const result = await identityAttributes.requestOpenId(params)
97
- publishIfCurrent(requestedFor, result)
119
+ publishIfCurrent(result)
98
120
  return result
99
121
  } catch (error) {
100
- if (isCurrent(requestedFor)) setAttributeError(error as Error)
122
+ publishErrorIfCurrent(
123
+ requestedFor,
124
+ params.signIn !== false,
125
+ error as Error
126
+ )
101
127
  throw error
102
128
  } finally {
103
129
  setIsRequestingAttributes(false)
@@ -1,4 +1,6 @@
1
+ import type { Identity } from "@icp-sdk/core/agent"
1
2
  import type {
3
+ AuthClientLike,
2
4
  IdentityAttributeNonce,
3
5
  IdentityAttributeResult,
4
6
  RequestIdentityAttributesParameters,
@@ -37,6 +39,7 @@ export class IdentityAttributesManager {
37
39
  nonce,
38
40
  signIn = true,
39
41
  maxTimeToLive,
42
+ maxTimeToIdle,
40
43
  targets,
41
44
  ...clientOptions
42
45
  }: RequestIdentityAttributesParameters): Promise<IdentityAttributeResult> => {
@@ -50,21 +53,29 @@ export class IdentityAttributesManager {
50
53
 
51
54
  if (!authClient) {
52
55
  throw new Error(
53
- "Authentication module is missing or failed to initialize. To request identity attributes, please install @icp-sdk/auth v8, or provide a compatible authClient."
56
+ "Authentication module is missing or failed to initialize. To request identity attributes, please install @icp-sdk/auth (v8 or v10), or provide a compatible authClient."
54
57
  )
55
58
  }
56
59
 
57
60
  this.authentication.setAuthenticating()
61
+ // Replaced by anything else that changes auth state while this request
62
+ // runs; see where the result is committed.
63
+ const pendingState = this.authentication.authState
58
64
 
65
+ let signInPromise: Promise<Identity> | undefined
66
+ let committed = false
59
67
  try {
60
68
  // `signIn` first: it opens the transport channel synchronously, and the
61
69
  // attribute request then reuses that same channel and window.
62
- const identityPromise = signIn
70
+ signInPromise = signIn
63
71
  ? this.authentication.signInOrRecoverIdentity({
64
72
  maxTimeToLive,
73
+ maxTimeToIdle,
65
74
  targets,
66
75
  })
67
- : Promise.resolve(authClient.getIdentity())
76
+ : undefined
77
+ const identityPromise =
78
+ signInPromise ?? Promise.resolve(authClient.getIdentity())
68
79
  const requestPromise = authClient.requestAttributes({
69
80
  keys,
70
81
  nonce: toAuthClientNonce(nonce),
@@ -76,8 +87,39 @@ export class IdentityAttributesManager {
76
87
  ])
77
88
 
78
89
  const finalIdentity = identity ?? (await authClient.getIdentity())
79
- const isAuthenticated = await authClient.isAuthenticated()
80
- await this.authentication.commitIdentity(finalIdentity, isAuthenticated)
90
+ // A v8 client can say it is authenticated for a delegation that lapsed
91
+ // in this tab, or while it holds no session, once another tab has
92
+ // signed in; see `AuthenticationManager.vouchesFor`.
93
+ const isAuthenticated = this.authentication.vouchesFor(
94
+ finalIdentity,
95
+ await authClient.isAuthenticated()
96
+ )
97
+ committed = true
98
+ // A sign-out, or another account's sign-in, can finish while the
99
+ // attribute side is still pending: neither ends it. `finalIdentity` then
100
+ // belongs to a session the client has left, and committing it put the
101
+ // signed-out user's delegation back on the agent while the app showed
102
+ // them signed out. So commit only what the client still holds and
103
+ // vouches for, the rule `authenticate()` applies. Whatever moved the
104
+ // client through the manager has published its own state.
105
+ //
106
+ // When nothing has published since this request began, the session
107
+ // changed under the manager and this request is the first to find out.
108
+ // A client that vouches for no session any more has lost it, as when
109
+ // another tab signed out, while it still hands out the identity it held:
110
+ // the manager signs out too, or it goes on reporting the user signed in
111
+ // with their delegation on the agent. A client that still vouches for a
112
+ // session gives no grounds to sign anyone out, so only the
113
+ // `isAuthenticating` this request started is ended.
114
+ if (await clientHolds(authClient, finalIdentity, isAuthenticated)) {
115
+ await this.authentication.commitIdentity(finalIdentity, isAuthenticated)
116
+ } else if (this.authentication.authState === pendingState) {
117
+ if (isAuthenticated) {
118
+ this.authentication.settleAuthenticating()
119
+ } else {
120
+ this.authentication.commitSignedOut()
121
+ }
122
+ }
81
123
 
82
124
  const normalizedSignedAttributes =
83
125
  normalizeSignedIdentityAttributes(signedAttributes)
@@ -93,11 +135,47 @@ export class IdentityAttributesManager {
93
135
  completedAt: new Date().toISOString(),
94
136
  }
95
137
  } catch (error) {
138
+ // The sign-in and the attribute request share one provider window and
139
+ // can end differently. When the attribute side failed (the app's nonce
140
+ // call rejected, or the provider could not certify a key) but the user
141
+ // went on to finish signing in, the client held a session nothing had
142
+ // committed: the app showed the user signed out until a reload restored
143
+ // it. So a sign-in this request started is waited for, and kept when the
144
+ // client vouches for it, before the failure is reported.
145
+ if (signInPromise && !committed) {
146
+ await this.keepCompletedSignIn(signInPromise, authClient)
147
+ }
96
148
  this.authentication.setAuthenticationError(error as Error)
97
149
  throw error
98
150
  }
99
151
  }
100
152
 
153
+ private async keepCompletedSignIn(
154
+ signInPromise: Promise<Identity>,
155
+ authClient: AuthClientLike
156
+ ) {
157
+ const identity = await signInPromise.catch(() => undefined)
158
+ if (!identity) return
159
+ const isAuthenticated = await Promise.resolve()
160
+ .then(() => authClient.isAuthenticated())
161
+ .catch(() => false)
162
+ if (!isAuthenticated) return
163
+ // The session the client holds now must be the one this request opened.
164
+ // Another login can switch the shared client to a different account while
165
+ // the attribute side is still pending, and committing this identity then
166
+ // would put back the account the user switched away from.
167
+ const current = await Promise.resolve()
168
+ .then(() => authClient.getIdentity())
169
+ .catch(() => undefined)
170
+ if (current?.getPrincipal().toText() !== identity.getPrincipal().toText()) {
171
+ return
172
+ }
173
+ // The attribute failure is the one to report, so a failure here is not.
174
+ await this.authentication
175
+ .commitIdentity(identity, true)
176
+ .catch(() => undefined)
177
+ }
178
+
101
179
  public requestOpenId = ({
102
180
  keys,
103
181
  openIdProvider,
@@ -112,6 +190,23 @@ export class IdentityAttributesManager {
112
190
  }
113
191
  }
114
192
 
193
+ /**
194
+ * Whether `identity` is the session `authClient` holds now, and one it still
195
+ * vouches for. The anonymous identity needs no vouching: it signs no one in.
196
+ */
197
+ async function clientHolds(
198
+ authClient: AuthClientLike,
199
+ identity: Identity,
200
+ isAuthenticated: boolean
201
+ ): Promise<boolean> {
202
+ const held = await Promise.resolve()
203
+ .then(() => authClient.getIdentity())
204
+ .catch(() => undefined)
205
+ const principal = identity.getPrincipal()
206
+ if (held?.getPrincipal().toText() !== principal.toText()) return false
207
+ return isAuthenticated || principal.isAnonymous()
208
+ }
209
+
115
210
  /**
116
211
  * Adapts our nonce input to the `@icp-sdk/auth` nonce contract.
117
212
  *
@@ -65,6 +65,14 @@ export function decodeIdentityAttributeValues(
65
65
  {}
66
66
  )
67
67
 
68
+ // Internet Identity's own format, read by its type. Once a message is known
69
+ // to be one, a key it does not hold is absent: the text fallback below
70
+ // cannot add anything true, only a neighbouring value under the wrong name.
71
+ const icrc3Entries = decodeIcrc3AttributeMap(data)
72
+ if (icrc3Entries) {
73
+ return collectIcrc3TextValues(icrc3Entries, requestedKeyMap)
74
+ }
75
+
68
76
  const decodedValues = decodeCandidAttributeValues(data, requestedKeyMap)
69
77
  if (Object.keys(decodedValues).length > 0) {
70
78
  return decodedValues
@@ -73,6 +81,73 @@ export function decodeIdentityAttributeValues(
73
81
  return extractPrintableAttributeValues(data, requestedKeys)
74
82
  }
75
83
 
84
+ /** The ICRC-3 `Value` an Internet Identity attribute message is encoded as. */
85
+ type Icrc3Value =
86
+ | { Nat: bigint }
87
+ | { Int: bigint }
88
+ | { Blob: Uint8Array | number[] }
89
+ | { Text: string }
90
+ | { Array: Icrc3Value[] }
91
+ | { Map: Array<[string, Icrc3Value]> }
92
+
93
+ /**
94
+ * The entries of an Internet Identity attribute message, or `undefined` when
95
+ * `data` is not one.
96
+ *
97
+ * The canister certifies `Encode!(&Icrc3Value::Map(..))`: the requested
98
+ * attributes under the keys they were requested by (bare, such as `email`, for
99
+ * an unscoped request), plus `implicit:nonce`, `implicit:origin` and
100
+ * `implicit:issued_at_timestamp_ns`. None of the shapes
101
+ * {@link decodeCandidAttributeValues} tries match it, so without this every
102
+ * real response was read by scraping printable text, which returned the
103
+ * neighbouring length byte as part of a value, cut names at the first
104
+ * non-ASCII character, and matched keys inside other values.
105
+ *
106
+ * @see https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-3/README.md
107
+ */
108
+ function decodeIcrc3AttributeMap(
109
+ data: Uint8Array
110
+ ): Array<[string, Icrc3Value]> | undefined {
111
+ const value = IDL.Rec()
112
+ value.fill(
113
+ IDL.Variant({
114
+ Nat: IDL.Nat,
115
+ Int: IDL.Int,
116
+ Blob: IDL.Vec(IDL.Nat8),
117
+ Text: IDL.Text,
118
+ Array: IDL.Vec(value),
119
+ Map: IDL.Vec(IDL.Tuple(IDL.Text, value)),
120
+ })
121
+ )
122
+
123
+ try {
124
+ const [decoded] = IDL.decode([value], data) as [Icrc3Value]
125
+ return "Map" in decoded ? decoded.Map : undefined
126
+ } catch {
127
+ return undefined
128
+ }
129
+ }
130
+
131
+ /**
132
+ * The requested attributes in an ICRC-3 map, as certified.
133
+ *
134
+ * Only `Text` entries are attribute values; the `implicit:` entries are there
135
+ * for the verifier and are returned only if asked for by name.
136
+ */
137
+ function collectIcrc3TextValues(
138
+ entries: Array<[string, Icrc3Value]>,
139
+ requestedKeyMap: Record<string, string>
140
+ ): IdentityAttributeValues {
141
+ const values: IdentityAttributeValues = {}
142
+ for (const [key, value] of entries) {
143
+ if (!Object.prototype.hasOwnProperty.call(requestedKeyMap, key)) continue
144
+ if ("Text" in value) {
145
+ values[requestedKeyMap[key]] = value.Text
146
+ }
147
+ }
148
+ return values
149
+ }
150
+
76
151
  function decodeCandidAttributeValues(
77
152
  data: Uint8Array,
78
153
  requestedKeyMap: Record<string, string>
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { Actor, type HttpAgent } from "@icp-sdk/core/agent"
23
23
  import { IDL } from "@icp-sdk/core/candid"
24
+ import type { AuthClientFlavor } from "./auth-client-compat.js"
24
25
 
25
26
  /** The `http_request` query every asset-serving canister exposes. */
26
27
  const httpInterface: IDL.InterfaceFactory = ({ IDL }) => {
@@ -132,13 +133,38 @@ export async function probeLocalInternetIdentity(
132
133
  /**
133
134
  * The error a login gets when the installed build serves no sign-in UI, in
134
135
  * place of a 503 page inside a popup the app cannot see.
136
+ *
137
+ * The fix depends on the installed `@icp-sdk/auth` major. v8 signs in through
138
+ * the page the canister serves, so an older build that still serves one works.
139
+ * v10 signs in through `ii_session_delegation` and mints with
140
+ * `app_prepare_delegation`, which Internet Identity gained only after its
141
+ * frontend left the canister (#561). No build serves both, so a v10 app needs
142
+ * the frontend from somewhere else, and older builds cannot help it.
143
+ *
144
+ * @param flavor - The installed client's contract. Defaults to `legacy` (v8),
145
+ * whose advice this error gave before it knew about v10.
135
146
  */
136
147
  export function localInternetIdentityUnavailableError(
137
- canisterId: string
148
+ canisterId: string,
149
+ flavor: AuthClientFlavor = "legacy"
138
150
  ): Error {
139
- return new Error(
151
+ const problem =
140
152
  `[ic-reactor] The Internet Identity canister ${canisterId} does not serve a sign-in UI: ` +
141
- `neither /authorize nor / returned a page. Internet Identity builds from ` +
153
+ `neither /authorize nor / returned a page. `
154
+
155
+ if (flavor === "session") {
156
+ return new Error(
157
+ problem +
158
+ `With @icp-sdk/auth v10, local sign-in needs an Internet Identity frontend served ` +
159
+ `separately, because no Internet Identity canister build both serves /authorize and ` +
160
+ `supports v10 sessions. Set \`identityProvider\` to that frontend's authorize URL and ` +
161
+ `\`internetIdentityId\` to the local Internet Identity canister that mints its delegations.`
162
+ )
163
+ }
164
+
165
+ return new Error(
166
+ problem +
167
+ `Internet Identity builds from ` +
142
168
  `release-2026-03-23 onward moved their frontend out of the canister (the id.ai split) ` +
143
169
  `and cannot be used locally. Install internet_identity_dev.wasm from release-2026-03-16, ` +
144
170
  `the newest build that still self-serves /authorize, or point \`identityProvider\` at a ` +
package/src/auth/types.ts CHANGED
@@ -12,7 +12,7 @@ export interface SignedIdentityAttributes {
12
12
  *
13
13
  * A function (or promise) lets the Internet Identity window open while the
14
14
  * nonce is still being fetched, so the popup is opened inside the user gesture
15
- * instead of after it. `@icp-sdk/auth` v8 requires the function form, and
15
+ * instead of after it. Every supported `@icp-sdk/auth` requires the function form, and
16
16
  * IC Reactor normalizes whichever of these you pass into that thunk.
17
17
  */
18
18
  export type IdentityAttributeNonce =
@@ -72,7 +72,10 @@ export interface AuthClientStorageLike {
72
72
  remove(key: string): Promise<void>
73
73
  }
74
74
 
75
- /** Idle-session configuration forwarded to the underlying AuthClient. */
75
+ /**
76
+ * Idle-session configuration forwarded to the underlying AuthClient.
77
+ * `@icp-sdk/auth` v8 only; see {@link AuthenticationClientOptions.idleOptions}.
78
+ */
76
79
  export interface AuthClientIdleOptions {
77
80
  /** Called once the user has been idle for `idleTimeout` ms. */
78
81
  onIdle?: () => unknown
@@ -114,19 +117,31 @@ export interface AuthenticationClientOptions {
114
117
  * @see https://github.com/dfinity/internet-identity/blob/main/docs/internet-identity-spec.adoc
115
118
  */
116
119
  derivationOrigin?: string | URL
117
- /** Persistent storage backend. @default IndexedDB */
120
+ /**
121
+ * Persistent storage backend. `@icp-sdk/auth` v8 only: v10 drops it with a
122
+ * one-time warning. @default IndexedDB
123
+ */
118
124
  storage?: AuthClientStorageLike
119
125
  /**
120
126
  * Session key algorithm. Use `"Ed25519"` when the storage backend cannot
121
- * hold a `CryptoKey`. @default "ECDSA"
127
+ * hold a `CryptoKey`. `@icp-sdk/auth` v8 only: v10 drops it with a one-time
128
+ * warning. @default "ECDSA"
122
129
  */
123
130
  keyType?: AuthClientKeyType
124
131
  /**
125
132
  * Idle timeout configuration. Pass `{ disableIdle: true }` to opt out of the
126
133
  * default behaviour, which signs the user out and reloads after 10 minutes.
134
+ *
135
+ * `@icp-sdk/auth` v8 only. v10 has no equivalent, so IC Reactor drops it
136
+ * there with a one-time warning: on v10 the identity provider enforces the
137
+ * idle limit, so pass `maxTimeToIdle` to `login()` and set
138
+ * `disableBrowserActivity` instead.
127
139
  */
128
140
  idleOptions?: AuthClientIdleOptions
129
- /** An existing identity to authenticate via delegation. */
141
+ /**
142
+ * An existing identity to authenticate via delegation. `@icp-sdk/auth` v8
143
+ * only: v10 drops it with a one-time warning.
144
+ */
130
145
  identity?: SignIdentity | PartialIdentity
131
146
  /**
132
147
  * How the client talks to the identity provider. `"redirect"` requires
@@ -134,10 +149,32 @@ export interface AuthenticationClientOptions {
134
149
  * @default "window"
135
150
  */
136
151
  transport?: "window" | "redirect"
152
+ /**
153
+ * Stops a v10 client from watching the page for signs of activity, so only
154
+ * requests keep the session in use.
155
+ *
156
+ * `@icp-sdk/auth` v10 only. v8 has no equivalent, so IC Reactor drops it
157
+ * there with a one-time warning. On v8, `idleOptions` controls idle handling.
158
+ * @default false
159
+ */
160
+ disableBrowserActivity?: boolean
137
161
  }
138
162
 
139
163
  export interface AuthClientSignInOptions {
164
+ /** The longest the delegation may last, in nanoseconds. */
140
165
  maxTimeToLive?: bigint
166
+ /**
167
+ * How long a signed-in user may be idle before the sign-in ends, in
168
+ * nanoseconds.
169
+ *
170
+ * `@icp-sdk/auth` v10 only. v8's sign-in has no idle limit, so IC Reactor
171
+ * drops it there with a one-time warning. On v8, set `idleOptions` instead.
172
+ */
173
+ maxTimeToIdle?: bigint
174
+ /**
175
+ * Canisters the delegation is restricted to. `@icp-sdk/auth` v8 only: v10
176
+ * ignores it, and IC Reactor warns when it is set for a v10 client.
177
+ */
141
178
  targets?: Principal[]
142
179
  }
143
180
 
@@ -156,6 +193,7 @@ export interface RequestIdentityAttributesParameters extends AuthenticationClien
156
193
  */
157
194
  signIn?: boolean
158
195
  maxTimeToLive?: bigint
196
+ maxTimeToIdle?: bigint
159
197
  targets?: Principal[]
160
198
  }
161
199
 
@@ -178,6 +216,11 @@ export interface RequestOpenIdIdentityAttributesParameters extends Omit<
178
216
  /**
179
217
  * Structural subset of `@icp-sdk/auth`'s `AuthClient` that IC Reactor relies
180
218
  * on. Declared locally so `@icp-sdk/auth` stays an optional peer dependency.
219
+ *
220
+ * Every method here is identical across the supported majors (v8 and v10) --
221
+ * only the option objects handed to the constructor and to `signIn` diverge,
222
+ * and `auth-client-compat.ts` translates those. That is what lets one interface
223
+ * describe both.
181
224
  */
182
225
  export interface AuthClientLike {
183
226
  getIdentity(): Promise<Identity> | Identity
@@ -186,7 +229,7 @@ export interface AuthClientLike {
186
229
  signOut(options?: { returnTo?: string }): Promise<void>
187
230
  requestAttributes(params: {
188
231
  keys: string[]
189
- // `@icp-sdk/auth` v8 takes a thunk, so it can journal the value and replay
232
+ // Every supported `@icp-sdk/auth` takes a thunk, so it can journal the value and replay
190
233
  // the same bytes through a redirect flow.
191
234
  nonce: () => Promise<Uint8Array>
192
235
  }): Promise<SignedIdentityAttributes>
@@ -44,99 +44,107 @@ import {
44
44
  useActorMethod,
45
45
  UseActorMethodParameters,
46
46
  } from "./hooks/useActorMethod.js"
47
- import type { CallConfig } from "@icp-sdk/core/agent"
48
- import { InfiniteQueryConfig } from "./createInfiniteQuery.js"
47
+ import { SkippableInfiniteQueryConfig } from "./createInfiniteQuery.js"
49
48
  import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery.js"
50
- import { QueryConfig, SuspenseQueryConfig, MutationConfig } from "./types.js"
51
-
52
- /**
53
- * The bound query hooks forward their config to `useActorQuery` and
54
- * `useActorSuspenseQuery`, which take `callConfig`. `QueryConfig` is shared with
55
- * `createQuery`, which does not use it, so it is added here and not there.
56
- */
57
- type WithCallConfig<Config> = Config & {
58
- /** Agent call configuration (canisterId override, effectiveCanisterId, etc.) */
59
- callConfig?: CallConfig
60
- }
49
+ import {
50
+ SkippableQueryConfig,
51
+ SuspenseQueryConfig,
52
+ MutationConfig,
53
+ } from "./types.js"
61
54
 
62
55
  export type ActorHooks<Service, Transform extends TransformKey> = {
56
+ // `args` may be `skipToken` here, and not in the suspense hooks below.
63
57
  useActorQuery: {
64
58
  <Method extends FunctionName<Service>>(
65
- config: WithCallConfig<
66
- QueryConfig<
67
- Service,
68
- Method,
69
- Transform,
70
- ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>
71
- >
59
+ config: SkippableQueryConfig<
60
+ Service,
61
+ Method,
62
+ Transform,
63
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>
72
64
  >
73
65
  ): UseQueryResult<
74
66
  ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
75
67
  ReactorReturnErr<Service, Method, Transform>
76
68
  >
77
69
  <Method extends FunctionName<Service>, TData>(
78
- config: WithCallConfig<QueryConfig<Service, Method, Transform, TData>>
70
+ config: SkippableQueryConfig<Service, Method, Transform, TData>
79
71
  ): UseQueryResult<TData, ReactorReturnErr<Service, Method, Transform>>
80
72
  }
81
73
 
82
74
  useActorSuspenseQuery: {
83
75
  <Method extends FunctionName<Service>>(
84
- config: WithCallConfig<
85
- SuspenseQueryConfig<
86
- Service,
87
- Method,
88
- Transform,
89
- ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>
90
- >
76
+ config: SuspenseQueryConfig<
77
+ Service,
78
+ Method,
79
+ Transform,
80
+ ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>
91
81
  >
92
82
  ): UseSuspenseQueryResult<
93
83
  ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
94
84
  ReactorReturnErr<Service, Method, Transform>
95
85
  >
96
86
  <Method extends FunctionName<Service>, TData>(
97
- config: WithCallConfig<
98
- SuspenseQueryConfig<Service, Method, Transform, TData>
99
- >
87
+ config: SuspenseQueryConfig<Service, Method, Transform, TData>
100
88
  ): UseSuspenseQueryResult<
101
89
  TData,
102
90
  ReactorReturnErr<Service, Method, Transform>
103
91
  >
104
92
  }
105
93
 
94
+ // `Selected` is what `select` returns, so `select` can reshape the pages the
95
+ // way `createInfiniteQuery` allows. Without `select` it stays `InfiniteData`.
106
96
  useActorInfiniteQuery: <
107
97
  Method extends FunctionName<Service>,
108
98
  TPageParam = unknown,
109
- >(
110
- config: InfiniteQueryConfig<Service, Method, Transform, TPageParam>
111
- ) => UseInfiniteQueryResult<
112
- InfiniteData<
99
+ Selected = InfiniteData<
113
100
  ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
114
101
  TPageParam
115
102
  >,
103
+ >(
104
+ config: SkippableInfiniteQueryConfig<
105
+ Service,
106
+ Method,
107
+ Transform,
108
+ TPageParam,
109
+ Selected
110
+ >
111
+ ) => UseInfiniteQueryResult<
112
+ Selected,
116
113
  ReactorReturnErr<Service, Method, Transform>
117
114
  >
118
115
 
119
116
  useActorSuspenseInfiniteQuery: <
120
117
  Method extends FunctionName<Service>,
121
118
  TPageParam = unknown,
122
- >(
123
- config: SuspenseInfiniteQueryConfig<Service, Method, Transform, TPageParam>
124
- ) => UseSuspenseInfiniteQueryResult<
125
- InfiniteData<
119
+ Selected = InfiniteData<
126
120
  ReactorQueryData<ReactorReturnOk<Service, Method, Transform>>,
127
121
  TPageParam
128
122
  >,
123
+ >(
124
+ config: SuspenseInfiniteQueryConfig<
125
+ Service,
126
+ Method,
127
+ Transform,
128
+ TPageParam,
129
+ Selected
130
+ >
131
+ ) => UseSuspenseInfiniteQueryResult<
132
+ Selected,
129
133
  ReactorReturnErr<Service, Method, Transform>
130
134
  >
131
135
 
132
- useActorMutation: <Method extends FunctionName<Service>>(
133
- config: MutationConfig<Service, Method, Transform>
136
+ useActorMutation: <
137
+ Method extends FunctionName<Service>,
138
+ TOnMutateResult = unknown,
139
+ >(
140
+ config: MutationConfig<Service, Method, Transform, TOnMutateResult>
134
141
  ) => UseMutationResult<
135
142
  ReactorReturnOk<Service, Method, Transform>,
136
143
  ReactorReturnErr<Service, Method, Transform>,
137
144
  // Without this TVariables defaults to `unknown` and mutate/mutateAsync
138
145
  // accept anything; the standalone hook and createMutation both pass it.
139
- ReactorArgs<Service, Method, Transform>
146
+ ReactorArgs<Service, Method, Transform>,
147
+ TOnMutateResult
140
148
  >
141
149
 
142
150
  useActorMethod: <Method extends FunctionName<Service>>(