@ic-reactor/core 3.3.1 → 3.5.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.
@@ -0,0 +1,256 @@
1
+ import { IDL } from "@icp-sdk/core/candid"
2
+
3
+ import type {
4
+ IdentityAttributeOpenIdProvider,
5
+ IdentityAttributeValues,
6
+ SignedIdentityAttributes,
7
+ } from "./types/client"
8
+
9
+ export const IDENTITY_ATTRIBUTES_BETA_PROVIDER = "https://beta.id.ai/authorize"
10
+
11
+ const OPEN_ID_PROVIDER_URLS = {
12
+ apple: "https://appleid.apple.com",
13
+ google: "https://accounts.google.com",
14
+ microsoft: "https://login.microsoftonline.com/{tid}/v2.0",
15
+ } as const
16
+
17
+ export function identityAttributeKeys({
18
+ openIdProvider,
19
+ keys,
20
+ }: {
21
+ openIdProvider: IdentityAttributeOpenIdProvider
22
+ keys: string[]
23
+ }): string[] {
24
+ const provider = normalizeOpenIdProvider(openIdProvider)
25
+ return keys.map((key) => `openid:${provider}:${key}`)
26
+ }
27
+
28
+ export function normalizeOpenIdProvider(
29
+ openIdProvider: IdentityAttributeOpenIdProvider
30
+ ): string {
31
+ return (
32
+ OPEN_ID_PROVIDER_URLS[
33
+ openIdProvider as keyof typeof OPEN_ID_PROVIDER_URLS
34
+ ] ?? openIdProvider
35
+ )
36
+ }
37
+
38
+ export async function resolveIdentityAttributeKeys({
39
+ openIdProvider,
40
+ keys,
41
+ }: {
42
+ openIdProvider: IdentityAttributeOpenIdProvider
43
+ keys: string[]
44
+ }): Promise<string[]> {
45
+ return identityAttributeKeys({ openIdProvider, keys })
46
+ }
47
+
48
+ export function decodeIdentityAttributeValues(
49
+ data: Uint8Array,
50
+ requestedKeys: string[]
51
+ ): IdentityAttributeValues {
52
+ const requestedKeyMap = requestedKeys.reduce<Record<string, string>>(
53
+ (acc, key) => {
54
+ acc[key] = identityAttributeDisplayKey(key)
55
+ acc[identityAttributeDisplayKey(key)] = identityAttributeDisplayKey(key)
56
+ return acc
57
+ },
58
+ {}
59
+ )
60
+
61
+ const decodedValues = decodeCandidAttributeValues(data, requestedKeyMap)
62
+ if (Object.keys(decodedValues).length > 0) {
63
+ return decodedValues
64
+ }
65
+
66
+ return extractPrintableAttributeValues(data, requestedKeys)
67
+ }
68
+
69
+ function decodeCandidAttributeValues(
70
+ data: Uint8Array,
71
+ requestedKeyMap: Record<string, string>
72
+ ): IdentityAttributeValues {
73
+ const textPairs = IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))
74
+ const recordPairs = IDL.Vec(
75
+ IDL.Record({
76
+ key: IDL.Text,
77
+ value: IDL.Text,
78
+ })
79
+ )
80
+
81
+ const candidates = [
82
+ [textPairs],
83
+ [recordPairs],
84
+ [IDL.Record({ attributes: textPairs })],
85
+ [IDL.Record({ values: textPairs })],
86
+ [IDL.Record({ attributes: recordPairs })],
87
+ [IDL.Record({ values: recordPairs })],
88
+ ]
89
+
90
+ for (const candidate of candidates) {
91
+ try {
92
+ const decoded = IDL.decode(candidate, data)
93
+ const values = collectDecodedAttributeValues(decoded, requestedKeyMap)
94
+ if (Object.keys(values).length > 0) {
95
+ return values
96
+ }
97
+ } catch {
98
+ // Try the next known beta payload shape, then fall back to text extraction.
99
+ }
100
+ }
101
+
102
+ return {}
103
+ }
104
+
105
+ function collectDecodedAttributeValues(
106
+ value: unknown,
107
+ requestedKeyMap: Record<string, string>,
108
+ activeKey?: string
109
+ ): IdentityAttributeValues {
110
+ const values: IdentityAttributeValues = {}
111
+
112
+ if (typeof value === "string") {
113
+ const displayKey = requestedKeyMap[value]
114
+ if (displayKey) {
115
+ return values
116
+ }
117
+ if (activeKey && isPrintableTextValue(value)) {
118
+ values[activeKey] = value
119
+ }
120
+ return values
121
+ }
122
+
123
+ if (Array.isArray(value)) {
124
+ if (value.length === 2 && typeof value[0] === "string") {
125
+ const displayKey = requestedKeyMap[value[0]]
126
+ if (
127
+ displayKey &&
128
+ typeof value[1] === "string" &&
129
+ isPrintableTextValue(value[1])
130
+ ) {
131
+ values[displayKey] = value[1]
132
+ return values
133
+ }
134
+ }
135
+
136
+ for (const item of value) {
137
+ Object.assign(
138
+ values,
139
+ collectDecodedAttributeValues(item, requestedKeyMap, activeKey)
140
+ )
141
+ }
142
+ return values
143
+ }
144
+
145
+ if (value && typeof value === "object") {
146
+ const record = value as Record<string, unknown>
147
+ if (typeof record.key === "string" && typeof record.value === "string") {
148
+ const displayKey = requestedKeyMap[record.key]
149
+ if (displayKey && isPrintableTextValue(record.value)) {
150
+ values[displayKey] = record.value
151
+ return values
152
+ }
153
+ }
154
+
155
+ for (const [key, nested] of Object.entries(record)) {
156
+ const displayKey = requestedKeyMap[key] ?? activeKey
157
+ if (
158
+ displayKey &&
159
+ typeof nested === "string" &&
160
+ isPrintableTextValue(nested)
161
+ ) {
162
+ values[displayKey] = nested
163
+ continue
164
+ }
165
+ Object.assign(
166
+ values,
167
+ collectDecodedAttributeValues(nested, requestedKeyMap, displayKey)
168
+ )
169
+ }
170
+ }
171
+
172
+ return values
173
+ }
174
+
175
+ function extractPrintableAttributeValues(
176
+ data: Uint8Array,
177
+ requestedKeys: string[]
178
+ ): IdentityAttributeValues {
179
+ const text = new TextDecoder().decode(data)
180
+ const values: IdentityAttributeValues = {}
181
+
182
+ for (const requestedKey of requestedKeys) {
183
+ const start = text.indexOf(requestedKey)
184
+ if (start === -1) {
185
+ continue
186
+ }
187
+
188
+ const tail = text.slice(start + requestedKey.length)
189
+ const printableRuns = tail.match(/[\x20-\x7E]{2,512}/g) ?? []
190
+ const value = printableRuns
191
+ .map((candidate) =>
192
+ cleanPrintableAttributeValue(candidate, requestedKeys)
193
+ )
194
+ .find(
195
+ (candidate) =>
196
+ isPrintableTextValue(candidate) &&
197
+ candidate !== requestedKey &&
198
+ !candidate.startsWith("openid:")
199
+ )
200
+
201
+ if (value) {
202
+ values[identityAttributeDisplayKey(requestedKey)] = value.trim()
203
+ }
204
+ }
205
+
206
+ return values
207
+ }
208
+
209
+ function cleanPrintableAttributeValue(
210
+ value: string,
211
+ requestedKeys: string[]
212
+ ): string {
213
+ let cleaned = value
214
+
215
+ for (const requestedKey of requestedKeys) {
216
+ const nextKeyIndex = cleaned.indexOf(requestedKey)
217
+ if (nextKeyIndex > 0) {
218
+ cleaned = cleaned.slice(
219
+ 0,
220
+ trimCandidTextLengthPrefix(cleaned, nextKeyIndex)
221
+ )
222
+ }
223
+ }
224
+
225
+ const nextOpenIdKeyMatch = /\d*openid:/.exec(cleaned)
226
+ if (nextOpenIdKeyMatch?.index && nextOpenIdKeyMatch.index > 0) {
227
+ cleaned = cleaned.slice(0, nextOpenIdKeyMatch.index)
228
+ }
229
+
230
+ return cleaned.trim()
231
+ }
232
+
233
+ function trimCandidTextLengthPrefix(value: string, index: number): number {
234
+ let trimmedIndex = index
235
+ while (trimmedIndex > 0 && /\d/.test(value[trimmedIndex - 1])) {
236
+ trimmedIndex -= 1
237
+ }
238
+ return trimmedIndex
239
+ }
240
+
241
+ function identityAttributeDisplayKey(key: string): string {
242
+ return key.split(":").pop() || key
243
+ }
244
+
245
+ function isPrintableTextValue(value: string): boolean {
246
+ return value.length > 0 && value.length <= 512 && /^[\x20-\x7E]+$/.test(value)
247
+ }
248
+
249
+ export function normalizeSignedIdentityAttributes(
250
+ attributes: SignedIdentityAttributes
251
+ ): SignedIdentityAttributes {
252
+ return {
253
+ data: new Uint8Array(attributes.data),
254
+ signature: new Uint8Array(attributes.signature),
255
+ }
256
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./reactor"
2
2
  export * from "./client"
3
3
  export * from "./utils"
4
+ export * from "./identity-attributes"
4
5
  export * from "./display"
5
6
  export * from "./display-reactor"
6
7
  export * from "./types"
@@ -1,5 +1,5 @@
1
1
  import type { HttpAgent, HttpAgentOptions, Identity } from "@icp-sdk/core/agent"
2
- import type { AuthClient } from "@icp-sdk/auth/client"
2
+ import type { Principal } from "@icp-sdk/core/principal"
3
3
  import type { QueryClient } from "@tanstack/query-core"
4
4
 
5
5
  /**
@@ -11,6 +11,86 @@ import type { QueryClient } from "@tanstack/query-core"
11
11
  * @property {boolean} [withLocalEnv] - If true, configures the agent for a local environment.
12
12
  * @property {boolean} [withProcessEnv] - If true, auto-configures the agent based on process.env settings.
13
13
  */
14
+ export interface SignedIdentityAttributes {
15
+ data: Uint8Array
16
+ signature: Uint8Array
17
+ }
18
+
19
+ export interface IdentityAttributeRequest {
20
+ keys: string[]
21
+ nonce: Uint8Array
22
+ }
23
+
24
+ export interface IdentityAttributeValues {
25
+ email?: string
26
+ name?: string
27
+ verified_email?: string
28
+ [key: string]: string | undefined
29
+ }
30
+
31
+ export interface IdentityAttributeResult {
32
+ principal: string
33
+ requestedKeys: string[]
34
+ signedAttributes: SignedIdentityAttributes
35
+ decodedAttributes: IdentityAttributeValues
36
+ completedAt: string
37
+ }
38
+
39
+ type IdentityAttributeOpenIdProviderAlias = "google" | "apple" | "microsoft"
40
+
41
+ export type IdentityAttributeOpenIdProvider =
42
+ | IdentityAttributeOpenIdProviderAlias
43
+ | (string & {})
44
+
45
+ export interface ClientManagerAuthClientOptions {
46
+ identityProvider?: string | URL
47
+ windowOpenerFeatures?: string
48
+ openIdProvider?: IdentityAttributeOpenIdProvider
49
+ }
50
+
51
+ export interface AuthClientSignInOptions {
52
+ maxTimeToLive?: bigint
53
+ targets?: Principal[]
54
+ }
55
+
56
+ export interface ClientManagerSignInOptions
57
+ extends AuthClientSignInOptions, ClientManagerAuthClientOptions {
58
+ onSuccess?: () => void | Promise<void>
59
+ onError?: (error?: string) => void | Promise<void>
60
+ }
61
+
62
+ export interface RequestIdentityAttributesParameters {
63
+ keys: string[]
64
+ nonce: Uint8Array
65
+ identityProvider?: string | URL
66
+ openIdProvider?: IdentityAttributeOpenIdProvider
67
+ windowOpenerFeatures?: string
68
+ signIn?: boolean
69
+ maxTimeToLive?: bigint
70
+ targets?: Principal[]
71
+ }
72
+
73
+ export interface RequestOpenIdIdentityAttributesParameters {
74
+ nonce: Uint8Array
75
+ openIdProvider: IdentityAttributeOpenIdProvider
76
+ keys: string[]
77
+ identityProvider?: string | URL
78
+ windowOpenerFeatures?: string
79
+ signIn?: boolean
80
+ maxTimeToLive?: bigint
81
+ targets?: Principal[]
82
+ }
83
+
84
+ export interface AuthClientLike {
85
+ getIdentity(): Promise<Identity> | Identity
86
+ isAuthenticated(): Promise<boolean> | boolean
87
+ signIn(options?: AuthClientSignInOptions): Promise<Identity>
88
+ logout(options?: { returnTo?: string }): Promise<void>
89
+ requestAttributes(
90
+ params: IdentityAttributeRequest
91
+ ): Promise<SignedIdentityAttributes>
92
+ }
93
+
14
94
  export interface ClientManagerParameters {
15
95
  /**
16
96
  * The TanStack QueryClient used for caching and state management.
@@ -39,7 +119,7 @@ export interface ClientManagerParameters {
39
119
  * This is useful for environments where dynamic imports are not supported or
40
120
  * when you want to share an AuthClient instance across multiple managers.
41
121
  */
42
- authClient?: AuthClient
122
+ authClient?: AuthClientLike
43
123
  /**
44
124
  * **EXPERIMENTAL** - If true, uses the canister environment from `@icp-sdk/core/agent/canister-env`
45
125
  * to automatically configure the agent host and root key based on the `ic_env` cookie.
@@ -6,7 +6,7 @@ export const IC_HOST_NETWORK_URI = "https://ic0.app"
6
6
 
7
7
  export const LOCAL_HOST_NETWORK_URI = "http://127.0.0.1:4943"
8
8
 
9
- export const IC_INTERNET_IDENTITY_PROVIDER = "https://id.ai"
9
+ export const IC_INTERNET_IDENTITY_PROVIDER = "https://id.ai/authorize"
10
10
 
11
11
  export const LOCAL_INTERNET_IDENTITY_PROVIDER =
12
12
  "http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943"