@octane-xplat/auth 0.10.0 → 0.11.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 (72) hide show
  1. package/README.md +65 -1
  2. package/dist/native/auth/src/base64.js +17 -0
  3. package/dist/native/auth/src/hosted.js +252 -0
  4. package/dist/native/auth/src/sha256.js +141 -0
  5. package/dist/native/index.js +6 -5
  6. package/dist/native/octane-client-build.json +1 -1
  7. package/dist/native/platform/src/a11y.js +1 -0
  8. package/dist/native/platform/src/app-info.js +1 -0
  9. package/dist/native/platform/src/auth-session.js +138 -0
  10. package/dist/native/platform/src/breakpoints.tsrx.js +5 -0
  11. package/dist/native/platform/src/clipboard.js +1 -0
  12. package/dist/native/platform/src/connectivity.js +1 -0
  13. package/dist/native/platform/src/deep-links.js +1 -0
  14. package/dist/native/platform/src/device.js +3 -0
  15. package/dist/native/platform/src/index.js +17 -0
  16. package/dist/native/platform/src/lifecycle.tsrx.js +7 -0
  17. package/dist/native/platform/src/locale.js +3 -0
  18. package/dist/native/platform/src/open-url.js +1 -0
  19. package/dist/native/platform/src/random.js +38 -0
  20. package/dist/native/platform/src/safe-area.tsrx.js +4 -0
  21. package/dist/native/platform/src/screen.tsrx.js +4 -0
  22. package/dist/native/platform/src/storage.js +1 -0
  23. package/dist/native/platform/src/system-bars.js +1 -0
  24. package/dist/native/secure-storage/src/index.js +2 -0
  25. package/dist/native/secure-storage/src/secure-storage.js +17 -0
  26. package/dist/web/auth/src/base64.js +17 -0
  27. package/dist/web/auth/src/hosted.js +251 -0
  28. package/dist/web/auth/src/sha256.js +141 -0
  29. package/dist/web/index.js +6 -5
  30. package/dist/web/octane-client-build.json +1 -1
  31. package/dist/web/platform/src/app-info.web.js +3 -0
  32. package/dist/web/platform/src/auth-session.web.js +10 -0
  33. package/dist/web/platform/src/breakpoints.web.tsrx.js +7 -0
  34. package/dist/web/platform/src/clipboard.web.js +3 -0
  35. package/dist/web/platform/src/deep-links.web.js +3 -0
  36. package/dist/web/platform/src/device.web.js +2 -0
  37. package/dist/web/platform/src/host-bridge.web.js +18 -0
  38. package/dist/web/platform/src/host-protocol.js +73 -0
  39. package/dist/web/platform/src/host-runtime.web.js +76 -0
  40. package/dist/web/platform/src/index.web.js +13 -0
  41. package/dist/web/platform/src/lifecycle.web.tsrx.js +3 -0
  42. package/dist/web/platform/src/locale.web.js +10 -0
  43. package/dist/web/platform/src/random.web.js +21 -0
  44. package/dist/web/platform/src/safe-area.web.tsrx.js +3 -0
  45. package/dist/web/platform/src/screen.web.tsrx.js +3 -0
  46. package/dist/web/platform/src/webauthn.web.js +2 -0
  47. package/dist/web/secure-storage/src/secure-storage.web.js +21 -0
  48. package/package.json +6 -4
  49. package/src/apple.macos.ts +1 -81
  50. package/src/base64.ts +25 -0
  51. package/src/hosted.ts +392 -0
  52. package/src/index.macos.ts +16 -0
  53. package/src/index.ts +16 -0
  54. package/src/index.web.ts +16 -0
  55. package/src/sha256.ts +118 -0
  56. package/src/types.ts +197 -0
  57. package/types/generated/.tsrx-typegen-manifest.json +3 -0
  58. package/types/generated/base64.d.ts +1 -0
  59. package/types/generated/hosted.d.ts +6 -0
  60. package/types/generated/index.d.ts +2 -1
  61. package/types/generated/index.web.d.ts +2 -1
  62. package/types/generated/sha256.d.ts +4 -0
  63. package/types/generated/types.d.ts +182 -0
  64. package/types/index.macos.d.ts +38 -0
  65. /package/dist/native/{AppleSignInButton.tsrx.js → auth/src/AppleSignInButton.tsrx.js} +0 -0
  66. /package/dist/native/{GoogleSignInButton.tsrx.js → auth/src/GoogleSignInButton.tsrx.js} +0 -0
  67. /package/dist/native/{apple.js → auth/src/apple.js} +0 -0
  68. /package/dist/native/{google.js → auth/src/google.js} +0 -0
  69. /package/dist/web/{AppleSignInButton.web.tsrx.js → auth/src/AppleSignInButton.web.tsrx.js} +0 -0
  70. /package/dist/web/{GoogleSignInButton.web.tsrx.js → auth/src/GoogleSignInButton.web.tsrx.js} +0 -0
  71. /package/dist/web/{apple.web.js → auth/src/apple.web.js} +0 -0
  72. /package/dist/web/{google.web.js → auth/src/google.web.js} +0 -0
package/src/hosted.ts ADDED
@@ -0,0 +1,392 @@
1
+ // Hosted-auth client — the session transport for native apps that sign in
2
+ // through a hosted page. Sign-in is a PKCE-bound system-browser ceremony
3
+ // (authSession): the flow's `begin` issues a backend attempt and returns
4
+ // the hosted URL, the browser returns on a custom scheme, and `complete`
5
+ // redeems the callback into a short-lived access token (Bearer on API
6
+ // calls) plus a durable session credential (refresh + sign-out).
7
+ //
8
+ // The client owns the ceremony lifecycle and credential storage; the
9
+ // `HostedAuthFlow` adapter owns where requests go and what they carry —
10
+ // no backend endpoints or field names are baked in here.
11
+ //
12
+ // Everything is string plumbing — no DOM globals (URL/Headers/Request types
13
+ // are not declared in the native typecheck program, invariant 4).
14
+ import { authSession, random } from '@octane-xplat/platform'
15
+ import { secureStorage } from '@octane-xplat/secure-storage'
16
+ import { bytesToBase64Url } from './base64'
17
+ import { sha256, utf8Bytes } from './sha256'
18
+ import type {
19
+ HostedAuth,
20
+ HostedAuthAttempt,
21
+ HostedAuthConfig,
22
+ HostedAuthCredentialStore,
23
+ HostedAuthCredentials,
24
+ HostedAuthFetch,
25
+ HostedAuthRequestInit,
26
+ HostedAuthSignInResult,
27
+ } from './types'
28
+
29
+ const DEFAULT_STORAGE_KEY = 'hosted-auth'
30
+ // Re-mint the access token this far ahead of its server-side expiry.
31
+ const REFRESH_SKEW_MS = 30_000
32
+
33
+ export function createHostedAuth(config: HostedAuthConfig): HostedAuth {
34
+ const apiOrigin = normalizeOrigin(config.apiOrigin)
35
+ const authorizePath = config.authorizePath ?? defaultAuthorizePath
36
+ const storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY
37
+ const now = config.now ?? Date.now
38
+ const transport: HostedAuthFetch =
39
+ config.fetch ??
40
+ ((url, init) => {
41
+ const impl = (globalThis as { fetch?: HostedAuthFetch }).fetch
42
+ if (!impl) {
43
+ return Promise.reject(new Error('hostedAuth: fetch is unavailable on this target'))
44
+ }
45
+
46
+ return impl(url, init)
47
+ })
48
+
49
+ let credentials: HostedAuthCredentials | null | undefined
50
+ let refreshPromise: Promise<string | null> | null = null
51
+
52
+ function store(): HostedAuthCredentialStore | null {
53
+ return config.storage ?? secureStorage.impl
54
+ }
55
+
56
+ async function persist(creds: HostedAuthCredentials | null) {
57
+ credentials = creds
58
+ const storage = store()
59
+ if (!storage) {
60
+ return
61
+ }
62
+
63
+ try {
64
+ if (creds) {
65
+ await storage.set(storageKey, JSON.stringify(creds))
66
+ } else {
67
+ await storage.remove(storageKey)
68
+ }
69
+ } catch {
70
+ // Persistence is best-effort — the session still lives in memory.
71
+ }
72
+ }
73
+
74
+ async function load(): Promise<HostedAuthCredentials | null> {
75
+ if (credentials !== undefined) {
76
+ return credentials
77
+ }
78
+
79
+ const storage = store()
80
+ if (!storage) {
81
+ credentials = null
82
+ return null
83
+ }
84
+
85
+ try {
86
+ const raw = await storage.get(storageKey)
87
+ const parsed = raw ? parseCredentials(raw) : null
88
+ // Assign only if still unrestored — a signIn/persist landing mid-read wins.
89
+ if (credentials === undefined) {
90
+ credentials = parsed
91
+ }
92
+
93
+ if (!parsed && raw) {
94
+ await storage.remove(storageKey)
95
+ }
96
+ } catch {
97
+ // Unreadable storage is treated as empty.
98
+ credentials ??= null
99
+ }
100
+
101
+ return credentials ?? null
102
+ }
103
+
104
+ async function refresh(): Promise<string | null> {
105
+ const creds = await load()
106
+ if (!creds || !config.flow.refresh) {
107
+ return null
108
+ }
109
+
110
+ const next = await config.flow.refresh(creds, { fetch: transport })
111
+ if (!isCredentials(next)) {
112
+ // The durable session is expired or revoked — local state is dead.
113
+ await persist(null)
114
+ return null
115
+ }
116
+
117
+ await persist(next)
118
+ return next.token
119
+ }
120
+
121
+ function refreshedAccessToken(): Promise<string | null> {
122
+ refreshPromise ??= refresh().finally(() => {
123
+ refreshPromise = null
124
+ })
125
+
126
+ return refreshPromise
127
+ }
128
+
129
+ async function getAccessToken(): Promise<string | null> {
130
+ const creds = await load()
131
+ if (!creds) {
132
+ return null
133
+ }
134
+
135
+ if (creds.expiresAt - now() > REFRESH_SKEW_MS) {
136
+ return creds.token
137
+ }
138
+
139
+ return refreshedAccessToken()
140
+ }
141
+
142
+ async function signIn(): Promise<HostedAuthSignInResult> {
143
+ const session = config.authSession ?? {
144
+ supported: authSession.supported,
145
+ open: (url, options) => {
146
+ const impl = authSession.impl
147
+ if (!impl) {
148
+ return Promise.resolve({
149
+ type: 'error' as const,
150
+ message: 'hostedAuth: hosted sign-in is not supported on this target',
151
+ })
152
+ }
153
+
154
+ return impl.open(url, options)
155
+ },
156
+ }
157
+
158
+ if (!session.supported || !random.supported) {
159
+ return {
160
+ status: 'error',
161
+ message: 'hostedAuth: hosted sign-in is not supported on this target',
162
+ }
163
+ }
164
+
165
+ const verifier = bytesToBase64Url(random.bytes(32))
166
+ const pkce = { challenge: bytesToBase64Url(sha256(utf8Bytes(verifier))), verifier }
167
+
168
+ let attempt: HostedAuthAttempt
169
+ try {
170
+ attempt = await config.flow.begin({ fetch: transport, pkce })
171
+ } catch (error) {
172
+ return { status: 'error', message: errorMessage(error) }
173
+ }
174
+
175
+ const result = await session.open(attempt.url, { callbackScheme: attempt.callbackScheme })
176
+ if (result.type === 'cancel') {
177
+ return { status: 'cancelled' }
178
+ }
179
+
180
+ if (result.type === 'error') {
181
+ return { status: 'error', message: result.message }
182
+ }
183
+
184
+ if (attempt.state !== undefined && queryParam(result.url, 'state') !== attempt.state) {
185
+ return {
186
+ status: 'error',
187
+ message: 'hostedAuth: callback did not carry the expected state',
188
+ }
189
+ }
190
+
191
+ try {
192
+ const creds = await config.flow.complete(result.url, attempt, {
193
+ fetch: transport,
194
+ pkce,
195
+ })
196
+
197
+ if (!isCredentials(creds)) {
198
+ return {
199
+ status: 'error',
200
+ message: 'hostedAuth: sign-in did not produce credentials',
201
+ }
202
+ }
203
+
204
+ await persist(creds)
205
+ return { status: 'success' }
206
+ } catch (error) {
207
+ return { status: 'error', message: errorMessage(error) }
208
+ }
209
+ }
210
+
211
+ async function fetchWithAuth(url: string, init?: HostedAuthRequestInit) {
212
+ const pathname = apiPath(url, apiOrigin)
213
+
214
+ if (pathname === null || !authorizePath(pathname)) {
215
+ return transport(url, init)
216
+ }
217
+
218
+ const target = url.startsWith('/') ? `${apiOrigin}${url}` : url
219
+ const token = await getAccessToken()
220
+ const response = await transport(target, token === null ? init : withBearer(init, token))
221
+
222
+ if (!isInvalidAccessTokenChallenge(response.status, response.headers)) {
223
+ return response
224
+ }
225
+
226
+ const refreshed = await refreshedAccessToken()
227
+ if (refreshed === null) {
228
+ return response
229
+ }
230
+
231
+ return transport(target, withBearer(init, refreshed))
232
+ }
233
+
234
+ async function signOut() {
235
+ const creds = await load()
236
+ await persist(null)
237
+ if (!creds || !config.flow.revoke) {
238
+ return
239
+ }
240
+
241
+ try {
242
+ await config.flow.revoke(creds, { fetch: transport })
243
+ } catch {
244
+ // Revocation is best-effort — local credentials are already cleared.
245
+ }
246
+ }
247
+
248
+ return {
249
+ get supported() {
250
+ return (config.authSession?.supported ?? authSession.supported) && random.supported
251
+ },
252
+ restore: async () => ((await load()) ? 'authenticated' : 'unauthenticated'),
253
+ signIn,
254
+ getAccessToken,
255
+ fetch: fetchWithAuth,
256
+ signOut,
257
+ }
258
+ }
259
+
260
+ function normalizeOrigin(value: string): string {
261
+ const trimmed = value.trim().replace(/\/+$/u, '')
262
+ if (!/^https?:\/\/[^\s/?#]+$/u.test(trimmed)) {
263
+ throw new TypeError(
264
+ `hostedAuth: apiOrigin must be an HTTP(S) origin without path or parameters, got ${JSON.stringify(value)}`,
265
+ )
266
+ }
267
+
268
+ return trimmed
269
+ }
270
+
271
+ /** `POST` a JSON body through the flow's transport. */
272
+ export function postJson(transport: HostedAuthFetch, url: string, body: Record<string, string>) {
273
+ return transport(url, {
274
+ body: JSON.stringify(body),
275
+ headers: { 'content-type': 'application/json' },
276
+ method: 'POST',
277
+ })
278
+ }
279
+
280
+ /** `?name=value` extraction without URLSearchParams (not in the native program). */
281
+ export function queryParam(url: string, name: string): string | null {
282
+ const start = url.indexOf('?')
283
+ if (start === -1) {
284
+ return null
285
+ }
286
+
287
+ const query = url.slice(start + 1).split('#', 1)[0]
288
+ for (const pair of query.split('&')) {
289
+ const eq = pair.indexOf('=')
290
+ const key = eq === -1 ? pair : pair.slice(0, eq)
291
+ if (key === name) {
292
+ return eq === -1 ? '' : decodeURIComponent(pair.slice(eq + 1).replaceAll('+', ' '))
293
+ }
294
+ }
295
+
296
+ return null
297
+ }
298
+
299
+ function isCredentials(value: unknown): value is HostedAuthCredentials {
300
+ const creds = value as HostedAuthCredentials | null
301
+ return (
302
+ !!creds &&
303
+ typeof creds.token === 'string' &&
304
+ typeof creds.expiresAt === 'number' &&
305
+ typeof creds.sessionToken === 'string'
306
+ )
307
+ }
308
+
309
+ function parseCredentials(raw: string): HostedAuthCredentials | null {
310
+ try {
311
+ const parsed = JSON.parse(raw) as Partial<HostedAuthCredentials>
312
+ if (isCredentials(parsed)) {
313
+ return { expiresAt: parsed.expiresAt, sessionToken: parsed.sessionToken, token: parsed.token }
314
+ }
315
+ } catch {
316
+ // fall through
317
+ }
318
+
319
+ return null
320
+ }
321
+
322
+ /**
323
+ * Pathname of `url` when it belongs to `apiOrigin` — `null` for other
324
+ * origins. Relative (`/path`) and origin-prefixed inputs both resolve.
325
+ */
326
+ function apiPath(url: string, apiOrigin: string): string | null {
327
+ if (url.startsWith('//')) {
328
+ return null
329
+ }
330
+
331
+ let path: string
332
+ if (url.startsWith('/')) {
333
+ path = url
334
+ } else if (url.startsWith(apiOrigin + '/') || url === apiOrigin) {
335
+ path = url.slice(apiOrigin.length) || '/'
336
+ } else {
337
+ return null
338
+ }
339
+
340
+ const end = path.search(/[?#]/u)
341
+ return end === -1 ? path : path.slice(0, end)
342
+ }
343
+
344
+ /**
345
+ * Default `authorizePath` — every same-origin path under `/api/` except the
346
+ * `/api/auth` mount hosted backends reserve for auth endpoints.
347
+ */
348
+ function defaultAuthorizePath(pathname: string): boolean {
349
+ return (
350
+ pathname.startsWith('/api/') && !(pathname === '/api/auth' || pathname.startsWith('/api/auth/'))
351
+ )
352
+ }
353
+
354
+ function withBearer(init: HostedAuthRequestInit | undefined, token: string): HostedAuthRequestInit {
355
+ const headers: Record<string, string> = {}
356
+ const incoming = init?.headers
357
+ if (incoming) {
358
+ if (Symbol.iterator in incoming) {
359
+ for (const [name, value] of incoming) {
360
+ headers[name.toLowerCase()] = value
361
+ }
362
+ } else {
363
+ for (const name of Object.keys(incoming)) {
364
+ headers[name.toLowerCase()] = incoming[name]
365
+ }
366
+ }
367
+ }
368
+
369
+ headers.authorization = `Bearer ${token}`
370
+ return { ...init, headers }
371
+ }
372
+
373
+ /**
374
+ * RFC 6750 `401 + WWW-Authenticate: Bearer error="invalid_token"` — the
375
+ * challenge that distinguishes a dead access token from an ordinary
376
+ * authorization failure.
377
+ */
378
+ function isInvalidAccessTokenChallenge(
379
+ status: number,
380
+ headers: { get(name: string): string | null },
381
+ ): boolean {
382
+ if (status !== 401) {
383
+ return false
384
+ }
385
+
386
+ const challenge = headers.get('www-authenticate')
387
+ return !!challenge && /error\s*=\s*"invalid_token"/iu.test(challenge)
388
+ }
389
+
390
+ function errorMessage(error: unknown): string {
391
+ return error instanceof Error ? error.message : 'hostedAuth: the hosted ceremony failed'
392
+ }
@@ -2,6 +2,7 @@ export { appleAuth } from './apple.macos'
2
2
  export { googleAuth } from './google.macos'
3
3
  export { AppleSignInButton } from './AppleSignInButton.macos.tsrx'
4
4
  export { GoogleSignInButton } from './GoogleSignInButton.macos.tsrx'
5
+ export { createHostedAuth, postJson, queryParam } from './hosted'
5
6
  export type {
6
7
  AppleAuth,
7
8
  AppleAuthConfig,
@@ -11,6 +12,21 @@ export type {
11
12
  AppleSignInOptions,
12
13
  AuthCredential,
13
14
  AuthUser,
15
+ HostedAuth,
16
+ HostedAuthAttempt,
17
+ HostedAuthConfig,
18
+ HostedAuthCredentialStore,
19
+ HostedAuthCredentials,
20
+ HostedAuthFetch,
21
+ HostedAuthFlow,
22
+ HostedAuthFlowContext,
23
+ HostedAuthPkce,
24
+ HostedAuthRequestInit,
25
+ HostedAuthResponse,
26
+ HostedAuthSession,
27
+ HostedAuthSessionResult,
28
+ HostedAuthSignInResult,
29
+ HostedAuthStatus,
14
30
  GoogleAuth,
15
31
  GoogleAuthConfig,
16
32
  GoogleHostedAuthFlow,
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export { appleAuth } from './apple'
4
4
  export { googleAuth } from './google'
5
5
  export { AppleSignInButton } from './AppleSignInButton.tsrx'
6
6
  export { GoogleSignInButton } from './GoogleSignInButton.tsrx'
7
+ export { createHostedAuth, postJson, queryParam } from './hosted'
7
8
  export type {
8
9
  AppleAuth,
9
10
  AppleAuthConfig,
@@ -13,6 +14,21 @@ export type {
13
14
  AppleSignInOptions,
14
15
  AuthCredential,
15
16
  AuthUser,
17
+ HostedAuth,
18
+ HostedAuthAttempt,
19
+ HostedAuthConfig,
20
+ HostedAuthCredentialStore,
21
+ HostedAuthCredentials,
22
+ HostedAuthFetch,
23
+ HostedAuthFlow,
24
+ HostedAuthFlowContext,
25
+ HostedAuthPkce,
26
+ HostedAuthRequestInit,
27
+ HostedAuthResponse,
28
+ HostedAuthSession,
29
+ HostedAuthSessionResult,
30
+ HostedAuthSignInResult,
31
+ HostedAuthStatus,
16
32
  GoogleAuth,
17
33
  GoogleAuthConfig,
18
34
  GoogleHostedAuthFlow,
package/src/index.web.ts CHANGED
@@ -4,6 +4,7 @@ export { appleAuth } from './apple.web'
4
4
  export { googleAuth } from './google.web'
5
5
  export { AppleSignInButton } from './AppleSignInButton.web.tsrx'
6
6
  export { GoogleSignInButton } from './GoogleSignInButton.web.tsrx'
7
+ export { createHostedAuth, postJson, queryParam } from './hosted'
7
8
  export type {
8
9
  AppleAuth,
9
10
  AppleAuthConfig,
@@ -13,6 +14,21 @@ export type {
13
14
  AppleSignInOptions,
14
15
  AuthCredential,
15
16
  AuthUser,
17
+ HostedAuth,
18
+ HostedAuthAttempt,
19
+ HostedAuthConfig,
20
+ HostedAuthCredentialStore,
21
+ HostedAuthCredentials,
22
+ HostedAuthFetch,
23
+ HostedAuthFlow,
24
+ HostedAuthFlowContext,
25
+ HostedAuthPkce,
26
+ HostedAuthRequestInit,
27
+ HostedAuthResponse,
28
+ HostedAuthSession,
29
+ HostedAuthSessionResult,
30
+ HostedAuthSignInResult,
31
+ HostedAuthStatus,
16
32
  GoogleAuth,
17
33
  GoogleAuthConfig,
18
34
  GoogleHostedAuthFlow,
package/src/sha256.ts ADDED
@@ -0,0 +1,118 @@
1
+ // FIPS 180-4 SHA-256 — pure TypeScript so every target hashes identically.
2
+ // The native runtime has no crypto.subtle, and TextEncoder is not declared
3
+ // cross-target, so UTF-8 encoding is manual. Kept dependency-free on purpose
4
+ // (same rationale as the apple.macos.ts copy this was extracted from).
5
+
6
+ export function utf8Bytes(input: string): Uint8Array {
7
+ const bytes: number[] = []
8
+ for (let i = 0; i < input.length; i++) {
9
+ let cp = input.charCodeAt(i)
10
+ if (cp >= 0xd800 && cp <= 0xdbff && i + 1 < input.length) {
11
+ const low = input.charCodeAt(i + 1)
12
+ if (low >= 0xdc00 && low <= 0xdfff) {
13
+ cp = 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00)
14
+ i++
15
+ }
16
+ }
17
+
18
+ if (cp < 0x80) {
19
+ bytes.push(cp)
20
+ } else if (cp < 0x800) {
21
+ bytes.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f))
22
+ } else if (cp < 0x10000) {
23
+ bytes.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f))
24
+ } else {
25
+ bytes.push(
26
+ 0xf0 | (cp >> 18),
27
+ 0x80 | ((cp >> 12) & 0x3f),
28
+ 0x80 | ((cp >> 6) & 0x3f),
29
+ 0x80 | (cp & 0x3f),
30
+ )
31
+ }
32
+ }
33
+
34
+ return new Uint8Array(bytes)
35
+ }
36
+
37
+ export function sha256(bytes: Uint8Array): Uint8Array {
38
+ const K = new Uint32Array([
39
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
40
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
41
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
42
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
43
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
44
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
45
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
46
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
47
+ ])
48
+
49
+ const H = new Uint32Array([
50
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
51
+ ])
52
+
53
+ const bitLen = bytes.length * 8
54
+ const padded = new Uint8Array((bytes.length + 9 + 63) & ~63)
55
+ padded.set(bytes)
56
+ padded[bytes.length] = 0x80
57
+ const dv = new DataView(padded.buffer)
58
+ dv.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000))
59
+ dv.setUint32(padded.length - 4, bitLen >>> 0)
60
+ const w = new Uint32Array(64)
61
+ for (let block = 0; block < padded.length; block += 64) {
62
+ for (let t = 0; t < 16; t++) {
63
+ w[t] = dv.getUint32(block + t * 4)
64
+ }
65
+
66
+ for (let t = 16; t < 64; t++) {
67
+ const s0 =
68
+ ((w[t - 15] >>> 7) | (w[t - 15] << 25)) ^
69
+ ((w[t - 15] >>> 18) | (w[t - 15] << 14)) ^
70
+ (w[t - 15] >>> 3)
71
+
72
+ const s1 =
73
+ ((w[t - 2] >>> 17) | (w[t - 2] << 15)) ^
74
+ ((w[t - 2] >>> 19) | (w[t - 2] << 13)) ^
75
+ (w[t - 2] >>> 10)
76
+
77
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0
78
+ }
79
+
80
+ let [a, b, c, d, e, f, g, h] = H
81
+ for (let t = 0; t < 64; t++) {
82
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7))
83
+ const t1 = (h + S1 + ((e & f) ^ (~e & g)) + K[t] + w[t]) >>> 0
84
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10))
85
+ const t2 = (S0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 0
86
+ h = g
87
+ g = f
88
+ f = e
89
+ e = (d + t1) >>> 0
90
+ d = c
91
+ c = b
92
+ b = a
93
+ a = (t1 + t2) >>> 0
94
+ }
95
+
96
+ H[0] = (H[0] + a) >>> 0
97
+ H[1] = (H[1] + b) >>> 0
98
+ H[2] = (H[2] + c) >>> 0
99
+ H[3] = (H[3] + d) >>> 0
100
+ H[4] = (H[4] + e) >>> 0
101
+ H[5] = (H[5] + f) >>> 0
102
+ H[6] = (H[6] + g) >>> 0
103
+ H[7] = (H[7] + h) >>> 0
104
+ }
105
+
106
+ const digest = new Uint8Array(32)
107
+ const out = new DataView(digest.buffer)
108
+ for (let i = 0; i < 8; i++) {
109
+ out.setUint32(i * 4, H[i])
110
+ }
111
+
112
+ return digest
113
+ }
114
+
115
+ /** Hex SHA-256 of a UTF-8 string — e.g. the Apple request nonce binding. */
116
+ export function sha256Hex(input: string): string {
117
+ return [...sha256(utf8Bytes(input))].map((x) => x.toString(16).padStart(2, '0')).join('')
118
+ }