@ic-reactor/react 3.12.5 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +410 -44
- package/dist/auth/auth-client-compat.d.ts +122 -0
- package/dist/auth/auth-client-compat.d.ts.map +1 -0
- package/dist/auth/auth-client-compat.js +162 -0
- package/dist/auth/auth-client-compat.js.map +1 -0
- package/dist/auth/authentication-manager.d.ts +287 -5
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +920 -150
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.js +36 -20
- package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
- package/dist/auth/identity-attributes-manager.d.ts +2 -1
- package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.js +90 -6
- package/dist/auth/identity-attributes-manager.js.map +1 -1
- package/dist/auth/identity-attributes.d.ts.map +1 -1
- package/dist/auth/identity-attributes.js +57 -0
- package/dist/auth/identity-attributes.js.map +1 -1
- package/dist/auth/local-ii-probe.d.ts +12 -1
- package/dist/auth/local-ii-probe.d.ts.map +1 -1
- package/dist/auth/local-ii-probe.js +22 -3
- package/dist/auth/local-ii-probe.js.map +1 -1
- package/dist/auth/types.d.ts +48 -5
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/createActorHooks.d.ts +7 -18
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +51 -10
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +39 -15
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts +3 -0
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +87 -80
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts +35 -2
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +104 -17
- package/dist/createQuery.js.map +1 -1
- package/dist/createReactorProvider.d.ts +158 -0
- package/dist/createReactorProvider.d.ts.map +1 -0
- package/dist/createReactorProvider.js +256 -0
- package/dist/createReactorProvider.js.map +1 -0
- package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +59 -27
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts +23 -2
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +68 -21
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineDisplayReactor.d.ts +43 -0
- package/dist/defineDisplayReactor.d.ts.map +1 -0
- package/dist/defineDisplayReactor.js +42 -0
- package/dist/defineDisplayReactor.js.map +1 -0
- package/dist/defineReactor.d.ts +46 -72
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +11 -176
- package/dist/defineReactor.js.map +1 -1
- package/dist/defineReactorShared.d.ts +84 -0
- package/dist/defineReactorShared.d.ts.map +1 -0
- package/dist/defineReactorShared.js +139 -0
- package/dist/defineReactorShared.js.map +1 -0
- package/dist/hooks/createAuthHooks.d.ts +7 -0
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +180 -20
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +34 -6
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +54 -21
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +37 -4
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +201 -57
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +10 -7
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +14 -13
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorQuery.d.ts +17 -4
- package/dist/hooks/useActorQuery.d.ts.map +1 -1
- package/dist/hooks/useActorQuery.js +30 -9
- package/dist/hooks/useActorQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +15 -3
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
- package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.js +20 -9
- package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/ownedAuthentication.d.ts +52 -0
- package/dist/ownedAuthentication.d.ts.map +1 -0
- package/dist/ownedAuthentication.js +49 -0
- package/dist/ownedAuthentication.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +23 -0
- package/dist/server.js.map +1 -0
- package/dist/testing.d.ts +19 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +416 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +159 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +301 -1
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +12 -7
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +34 -15
- package/dist/validation.js.map +1 -1
- package/llms.txt +259 -33
- package/package.json +17 -5
- package/src/auth/auth-client-compat.ts +273 -0
- package/src/auth/authentication-manager.ts +918 -96
- package/src/auth/createIdentityAttributeHooks.ts +47 -21
- package/src/auth/identity-attributes-manager.ts +100 -5
- package/src/auth/identity-attributes.ts +75 -0
- package/src/auth/local-ii-probe.ts +29 -3
- package/src/auth/types.ts +49 -6
- package/src/createActorHooks.ts +20 -32
- package/src/createInfiniteQuery.ts +120 -28
- package/src/createMutation.ts +161 -178
- package/src/createQuery.ts +164 -32
- package/src/createReactorProvider.ts +365 -0
- package/src/createSuspenseInfiniteQuery.ts +93 -43
- package/src/createSuspenseQuery.ts +102 -32
- package/src/defineDisplayReactor.ts +62 -0
- package/src/defineReactor.ts +81 -263
- package/src/defineReactorShared.ts +268 -0
- package/src/hooks/createAuthHooks.ts +206 -24
- package/src/hooks/useActorInfiniteQuery.ts +122 -49
- package/src/hooks/useActorMethod.ts +295 -92
- package/src/hooks/useActorMutation.ts +23 -23
- package/src/hooks/useActorQuery.ts +43 -10
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +93 -50
- package/src/hooks/useActorSuspenseQuery.ts +30 -15
- package/src/index.ts +8 -0
- package/src/ownedAuthentication.ts +81 -0
- package/src/server.ts +23 -0
- package/src/testing.ts +18 -0
- package/src/types.ts +463 -14
- package/src/utils.ts +387 -3
- 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
|
|
44
|
-
* principal
|
|
45
|
-
*
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
55
|
-
(
|
|
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
|
-
|
|
60
|
-
|
|
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 (
|
|
63
|
-
return
|
|
81
|
+
if (requestedFor !== current && !(signsIn && current !== undefined)) {
|
|
82
|
+
return
|
|
64
83
|
}
|
|
65
|
-
|
|
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(
|
|
96
|
+
publishIfCurrent(result)
|
|
79
97
|
return result
|
|
80
98
|
} catch (error) {
|
|
81
|
-
|
|
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(
|
|
119
|
+
publishIfCurrent(result)
|
|
98
120
|
return result
|
|
99
121
|
} catch (error) {
|
|
100
|
-
|
|
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
|
-
|
|
70
|
+
signInPromise = signIn
|
|
63
71
|
? this.authentication.signInOrRecoverIdentity({
|
|
64
72
|
maxTimeToLive,
|
|
73
|
+
maxTimeToIdle,
|
|
65
74
|
targets,
|
|
66
75
|
})
|
|
67
|
-
:
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
151
|
+
const problem =
|
|
140
152
|
`[ic-reactor] The Internet Identity canister ${canisterId} does not serve a sign-in UI: ` +
|
|
141
|
-
|
|
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`
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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`.
|
|
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
|
-
/**
|
|
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`
|
|
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>
|
package/src/createActorHooks.ts
CHANGED
|
@@ -44,59 +44,47 @@ import {
|
|
|
44
44
|
useActorMethod,
|
|
45
45
|
UseActorMethodParameters,
|
|
46
46
|
} from "./hooks/useActorMethod.js"
|
|
47
|
-
import
|
|
48
|
-
import { InfiniteQueryConfig } from "./createInfiniteQuery.js"
|
|
47
|
+
import { SkippableInfiniteQueryConfig } from "./createInfiniteQuery.js"
|
|
49
48
|
import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery.js"
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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:
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
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>
|
|
@@ -113,7 +101,7 @@ export type ActorHooks<Service, Transform extends TransformKey> = {
|
|
|
113
101
|
TPageParam
|
|
114
102
|
>,
|
|
115
103
|
>(
|
|
116
|
-
config:
|
|
104
|
+
config: SkippableInfiniteQueryConfig<
|
|
117
105
|
Service,
|
|
118
106
|
Method,
|
|
119
107
|
Transform,
|