@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.
- package/README.md +65 -1
- package/dist/native/auth/src/base64.js +17 -0
- package/dist/native/auth/src/hosted.js +252 -0
- package/dist/native/auth/src/sha256.js +141 -0
- package/dist/native/index.js +6 -5
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/platform/src/a11y.js +1 -0
- package/dist/native/platform/src/app-info.js +1 -0
- package/dist/native/platform/src/auth-session.js +138 -0
- package/dist/native/platform/src/breakpoints.tsrx.js +5 -0
- package/dist/native/platform/src/clipboard.js +1 -0
- package/dist/native/platform/src/connectivity.js +1 -0
- package/dist/native/platform/src/deep-links.js +1 -0
- package/dist/native/platform/src/device.js +3 -0
- package/dist/native/platform/src/index.js +17 -0
- package/dist/native/platform/src/lifecycle.tsrx.js +7 -0
- package/dist/native/platform/src/locale.js +3 -0
- package/dist/native/platform/src/open-url.js +1 -0
- package/dist/native/platform/src/random.js +38 -0
- package/dist/native/platform/src/safe-area.tsrx.js +4 -0
- package/dist/native/platform/src/screen.tsrx.js +4 -0
- package/dist/native/platform/src/storage.js +1 -0
- package/dist/native/platform/src/system-bars.js +1 -0
- package/dist/native/secure-storage/src/index.js +2 -0
- package/dist/native/secure-storage/src/secure-storage.js +17 -0
- package/dist/web/auth/src/base64.js +17 -0
- package/dist/web/auth/src/hosted.js +251 -0
- package/dist/web/auth/src/sha256.js +141 -0
- package/dist/web/index.js +6 -5
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/platform/src/app-info.web.js +3 -0
- package/dist/web/platform/src/auth-session.web.js +10 -0
- package/dist/web/platform/src/breakpoints.web.tsrx.js +7 -0
- package/dist/web/platform/src/clipboard.web.js +3 -0
- package/dist/web/platform/src/deep-links.web.js +3 -0
- package/dist/web/platform/src/device.web.js +2 -0
- package/dist/web/platform/src/host-bridge.web.js +18 -0
- package/dist/web/platform/src/host-protocol.js +73 -0
- package/dist/web/platform/src/host-runtime.web.js +76 -0
- package/dist/web/platform/src/index.web.js +13 -0
- package/dist/web/platform/src/lifecycle.web.tsrx.js +3 -0
- package/dist/web/platform/src/locale.web.js +10 -0
- package/dist/web/platform/src/random.web.js +21 -0
- package/dist/web/platform/src/safe-area.web.tsrx.js +3 -0
- package/dist/web/platform/src/screen.web.tsrx.js +3 -0
- package/dist/web/platform/src/webauthn.web.js +2 -0
- package/dist/web/secure-storage/src/secure-storage.web.js +21 -0
- package/package.json +6 -4
- package/src/apple.macos.ts +1 -81
- package/src/base64.ts +25 -0
- package/src/hosted.ts +392 -0
- package/src/index.macos.ts +16 -0
- package/src/index.ts +16 -0
- package/src/index.web.ts +16 -0
- package/src/sha256.ts +118 -0
- package/src/types.ts +197 -0
- package/types/generated/.tsrx-typegen-manifest.json +3 -0
- package/types/generated/base64.d.ts +1 -0
- package/types/generated/hosted.d.ts +6 -0
- package/types/generated/index.d.ts +2 -1
- package/types/generated/index.web.d.ts +2 -1
- package/types/generated/sha256.d.ts +4 -0
- package/types/generated/types.d.ts +182 -0
- package/types/index.macos.d.ts +38 -0
- /package/dist/native/{AppleSignInButton.tsrx.js → auth/src/AppleSignInButton.tsrx.js} +0 -0
- /package/dist/native/{GoogleSignInButton.tsrx.js → auth/src/GoogleSignInButton.tsrx.js} +0 -0
- /package/dist/native/{apple.js → auth/src/apple.js} +0 -0
- /package/dist/native/{google.js → auth/src/google.js} +0 -0
- /package/dist/web/{AppleSignInButton.web.tsrx.js → auth/src/AppleSignInButton.web.tsrx.js} +0 -0
- /package/dist/web/{GoogleSignInButton.web.tsrx.js → auth/src/GoogleSignInButton.web.tsrx.js} +0 -0
- /package/dist/web/{apple.web.js → auth/src/apple.web.js} +0 -0
- /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
|
+
}
|
package/src/index.macos.ts
CHANGED
|
@@ -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
|
+
}
|