@octane-xplat/auth 0.9.0 → 0.10.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 CHANGED
@@ -1,24 +1,26 @@
1
1
  # `@octane-xplat/auth`
2
2
 
3
+ ```sh
4
+ pnpm add @octane-xplat/auth
5
+ ```
6
+
3
7
  Sign in with Apple and Google Sign-In for Octane xplat apps — the native
4
8
  SDKs (`@nativescript/apple-sign-in`, `@nativescript/google-signin`) on
5
9
  iOS/Android, the providers' web SDKs (Apple JS / Google Identity Services)
6
10
  in the browser, and AuthenticationServices through the ObjC bridge plus a
7
- Google flow on the macOS AppKit host. One `AuthUser`/`AuthCredential`
11
+ app-configured hosted Google flow on the macOS AppKit host. One `AuthUser`/`AuthCredential`
8
12
  contract everywhere.
9
13
 
10
- ```sh
11
- pnpm add @octane-xplat/auth
12
- ```
13
-
14
- ```tsx
15
- import { appleAuth, googleAuth, AppleSignInButton } from '@octane-xplat/auth'
16
-
17
- appleAuth.configure({ clientId: 'com.example.app.web' }) // web-only fields
14
+ ```ts
15
+ import { appleAuth } from '@octane-xplat/auth'
18
16
 
19
- const result = await appleAuth.signIn({ scopes: ['email', 'name'] })
20
- if (result.status === 'success') {
21
- const { idToken, user } = result.credential // hand idToken to your backend
17
+ appleAuth.configure({ clientId: 'com.example.app.web' })
18
+ if (appleAuth.supported) {
19
+ const result = await appleAuth.signIn({ scopes: ['email', 'name'] })
20
+ if (result.status === 'success') {
21
+ // Send this credential to your backend for verification.
22
+ const { idToken, user } = result.credential
23
+ }
22
24
  }
23
25
  ```
24
26
 
@@ -26,13 +28,46 @@ Each provider object reports `supported` — Apple is `false` on Android —
26
28
  and exposes `configure`, `signIn`, and provider extras
27
29
  (`appleAuth.getCredentialState`, `googleAuth.signOut`). `SignInResult`
28
30
  distinguishes `success` / `cancelled` / `error`; a dismissed sheet is
29
- `cancelled`, not a throw. The sign-in buttons are platform-authentic
30
- components, so render them from platform-suffixed files or accept each
31
+ `cancelled`, not a throw. Apple uses the official ASAuthorizationAppleIDButton on AppKit; Google
32
+ uses a text trigger for the hosted flow there. The other sign-in buttons are
33
+ platform-authentic components, so render them from platform-suffixed files or accept each
31
34
  target's native styling.
32
35
 
36
+ The following component belongs in an `.ios.tsx` file.
37
+
38
+ ```tsx
39
+ import { AppleSignInButton, appleAuth, googleAuth } from '@octane-xplat/auth'
40
+
41
+ export function SignIn() {
42
+ return (
43
+ <AppleSignInButton
44
+ onResult={(result) => {
45
+ if (result.status === 'success') {
46
+ void appleAuth.getCredentialState(result.credential.user.id)
47
+ }
48
+ }}
49
+ />
50
+ )
51
+ }
52
+ // When ending a Google session:
53
+ await googleAuth.signOut()
54
+ ```
55
+
33
56
  Web needs registered client IDs; iOS needs the `applesignin` entitlement
34
57
  and Google's `GoogleService-Info.plist`/reversed-client-id URL scheme;
35
58
  Android needs `google-services.json` and a `serverClientId` for
36
59
  `serverAuthCode`. Guide: [Using device
37
- features](../../docs/platform-services.md). Exercised by
60
+ features](../../docs/platform/platform-services.md). Exercised by
38
61
  [`AuthDemo`](../demos/src/AuthDemo.tsrx).
62
+
63
+ On macOS, `supported` reports AuthenticationServices availability, not completed
64
+ provider registration. Apple needs a signed app ID with Sign in with Apple
65
+ enabled and a matching provisioning profile and entitlement. Google needs
66
+ `googleAuth.configure({ hostedFlow })`, where `createRequest` starts a fresh
67
+ backend attempt and `complete` validates/redeems the captured callback on that
68
+ backend, returning `AuthCredential`. A client ID alone is insufficient on
69
+ AppKit. The adapter receives the configured client IDs, scopes, hosted domain,
70
+ and per-attempt nonce; the backend must apply and verify them.
71
+
72
+ See [macOS provider setup](../../docs/platform/platform-services.md#macos-provider-sign-in)
73
+ and the [maintained hosted adapter](../../examples/auth/google-hosted.macos.ts).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "2924c745-ebe0-4012-9aea-c8c26eeb7458",
3
+ "buildId": "fc764ad7-51bc-48e0-b465-8a53d74ea14e",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "dc0dd9f6-f1fb-4b15-a5c3-95ddbe1709aa",
3
+ "buildId": "500fb75c-1e7a-46fe-8b9a-4a439f2dfd7e",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/auth",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Sign in with Apple + Google Sign-In for Octane xplat — native SDKs on iOS/Android, platform web SDKs in the browser",
5
5
  "keywords": [
6
6
  "android",
@@ -52,15 +52,17 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@nativescript/apple-sign-in": "2.0.0",
55
- "@nativescript/google-signin": "2.1.1"
55
+ "@nativescript/google-signin": "2.1.1",
56
+ "@octane-xplat/platform": "0.10.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@nativescript-community/octane": "0.2.4",
59
60
  "@nativescript/core": "9.1.2",
61
+ "@octane-xplat/macos-renderer": "0.10.0",
60
62
  "@octanejs/vite-plugin": "0.1.61",
61
63
  "@tsrx/typescript-plugin": "0.4.11",
62
64
  "octane": "0.6.3",
63
- "tsrx-typegen": "0.2.1",
65
+ "tsrx-typegen": "0.2.2",
64
66
  "typescript": "5.9.3",
65
67
  "vite": "8.3.0"
66
68
  },
@@ -81,6 +83,8 @@
81
83
  "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
82
84
  "test:packed": "node tests/packed-consumer.mjs",
83
85
  "typegen": "tsrx-typegen --target octane",
84
- "pack:check": "tsrx-typegen --pack-check"
86
+ "pack:check": "tsrx-typegen --pack-check",
87
+ "test": "node --test tests/*.test.mjs",
88
+ "typecheck:macos": "tsrx-tsc --noEmit -p tsconfig.macos.json"
85
89
  }
86
90
  }
@@ -1,36 +1,34 @@
1
- /** @jsxImportSource @xplat/macos/renderer */
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import { useEffect, useRef } from 'octane'
2
3
  import { appleAuth } from './apple.macos'
3
- import type { AppleSignInButtonProps, SignInResult } from './types'
4
+ import { attachAppleButton } from './apple-button.macos'
5
+ import type { AppleSignInButtonProps } from './types'
4
6
 
5
- const LABELS = {
6
- default: 'Sign in with Apple',
7
- signUp: 'Sign up with Apple',
8
- continue: 'Continue with Apple',
9
- } as const
10
-
11
- /** Sign in with Apple button — macOS leaf. The AppKit renderer has no
12
- * ASAuthorizationAppleIDButton in its vocabulary, so this is a flexbox row
13
- * with a click-gesture tap (the same shape as Pressable/controls.macos)
14
- * that runs the AuthenticationServices flow and reports through `onResult`.
15
- * onPress on the intrinsic <button> is not used — the universal compile
16
- * emits DOM-only delegateEvents for it. */
7
+ /** Official ASAuthorizationAppleIDButton, embedded in the leaf's host view. */
17
8
  export function AppleSignInButton(props: AppleSignInButtonProps) @{
9
+ const anchor = useRef<any>(null)
10
+ const handle = useRef<ReturnType<typeof attachAppleButton> | null>(null)
11
+ const current = useRef(props)
12
+ current.current = props
13
+
14
+ useEffect(() => {
15
+ if (!anchor.current) return
16
+ handle.current = attachAppleButton(anchor.current, current.current, () => {
17
+ const latest = current.current
18
+ if (latest.disabled) return
19
+ latest.onPress?.()
20
+ void appleAuth.signIn().then((result) => current.current.onResult?.(result))
21
+ })
22
+
23
+ return () => { handle.current?.detach(); handle.current = null }
24
+ }, [props.type, props.theme])
25
+
26
+ useEffect(() => { handle.current?.update(!!props.disabled) })
27
+
18
28
  <flexboxlayout
29
+ ref={(view: any) => { anchor.current = view }}
19
30
  id={props.id}
20
- className={['vx-button', props.className]}
21
- style={props.style}
22
- accessibilityLabel={LABELS[props.type ?? 'default']}
23
- onTap={
24
- props.disabled === true
25
- ? undefined
26
- : () => {
27
- props.onPress?.()
28
- if (props.onResult) {
29
- void appleAuth.signIn().then((result: SignInResult) => props.onResult?.(result))
30
- }
31
- }
32
- }
33
- >
34
- <label text={LABELS[props.type ?? 'default']} />
35
- </flexboxlayout>
31
+ className={props.className}
32
+ style={{ width: 240, height: 44, ...props.style }}
33
+ />;
36
34
  }
@@ -1,7 +1,21 @@
1
- /** @jsxImportSource @xplat/macos/renderer */
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import { googleAuth } from './google.macos'
2
3
  import type { GoogleSignInButtonProps } from './types'
3
4
 
4
- /** Explicit placeholder — the AppKit host has no Google sign-in button. */
5
- export function GoogleSignInButton(_props: GoogleSignInButtonProps) @{
6
- <label className="vx-unsupported">Google Sign-In is unsupported by the AppKit host</label>
5
+ /** Hosted Google sign-in trigger; Google does not provide an AppKit button. */
6
+ export function GoogleSignInButton(props: GoogleSignInButtonProps) @{
7
+ <flexboxlayout
8
+ id={props.id}
9
+ className={['vx-button', props.className]}
10
+ style={props.style}
11
+ accessibilityRole="button"
12
+ accessibilityLabel="Sign in with Google"
13
+ accessibilityState={{ disabled: !!props.disabled }}
14
+ onTap={props.disabled ? undefined : () => {
15
+ props.onPress?.()
16
+ void googleAuth.signIn().then((result) => props.onResult?.(result))
17
+ }}
18
+ >
19
+ <label text="Sign in with Google" />
20
+ </flexboxlayout>;
7
21
  }
@@ -0,0 +1,75 @@
1
+ // Leaf-owned attachment: no renderer vocabulary or shared host change needed.
2
+ import type { AppleSignInButtonProps } from './types'
3
+
4
+ declare const objc: any
5
+ declare const NSObject: any
6
+ declare const interop: any
7
+ declare const ASAuthorizationAppleIDButton: any
8
+
9
+ let ActionTarget: any
10
+
11
+ export function attachAppleButton(view: any, props: AppleSignInButtonProps, press: () => void) {
12
+ try {
13
+ objc.import('AuthenticationServices')
14
+ } catch {}
15
+
16
+ if (typeof ASAuthorizationAppleIDButton === 'undefined') {
17
+ throw new Error('The official Sign in with Apple button is unavailable on this host')
18
+ }
19
+
20
+ if (!ActionTarget) {
21
+ ActionTarget = NSObject.extend(
22
+ {
23
+ press(this: any, _sender: any) {
24
+ this.callback?.()
25
+ },
26
+ },
27
+ { exposedMethods: { 'press:': { params: [interop.types.id], returns: interop.types.void } } },
28
+ )
29
+ }
30
+
31
+ const target = ActionTarget.new()
32
+ target.callback = press
33
+ // Values from ASAuthorizationAppleIDButton.h (macOS 10.15+).
34
+ const type = props.type === 'continue' ? 1 : props.type === 'signUp' ? 2 : 0
35
+ const theme = props.theme === 'white' ? 0 : props.theme === 'whiteOutline' ? 1 : 2
36
+ const button =
37
+ ASAuthorizationAppleIDButton.alloc().initWithAuthorizationButtonTypeAuthorizationButtonStyle(
38
+ type,
39
+ theme,
40
+ )
41
+
42
+ button.target = target
43
+ button.action = 'press:'
44
+ button.enabled = !props.disabled
45
+ button.translatesAutoresizingMaskIntoConstraints = false
46
+ view.addSubview(button)
47
+ const constraints = [
48
+ button.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor),
49
+ button.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor),
50
+ button.topAnchor.constraintEqualToAnchor(view.topAnchor),
51
+ button.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor),
52
+ ]
53
+
54
+ for (const constraint of constraints) {
55
+ constraint.active = true
56
+ }
57
+
58
+ return {
59
+ // Target is weak in AppKit: retain its wrapper with the attachment handle.
60
+ target,
61
+ button,
62
+ update(disabled: boolean) {
63
+ button.enabled = !disabled
64
+ },
65
+ detach() {
66
+ target.callback = null
67
+ button.target = null
68
+ for (const constraint of constraints) {
69
+ constraint.active = false
70
+ }
71
+
72
+ button.removeFromSuperview()
73
+ },
74
+ }
75
+ }
@@ -41,8 +41,8 @@ const SCOPE_MAP: Record<string, () => any> = {
41
41
  function isCancel(error: unknown): boolean {
42
42
  // ASAuthorizationError.canceled = 1001 (NSError.code).
43
43
  return (
44
- (error as any)?.code === 1001 ||
45
- /cancel/i.test(String((error as any)?.localizedDescription ?? error))
44
+ (error as any)?.domain === 'com.apple.AuthenticationServices.AuthorizationError' &&
45
+ Number((error as any)?.code) === 1001
46
46
  )
47
47
  }
48
48
 
@@ -189,15 +189,24 @@ function ensureClasses() {
189
189
 
190
190
  AppleAuthDelegate = NSObject.extend(
191
191
  {
192
- authorizationControllerDidCompleteWithAuthorization(_controller: any, authorization: any) {
193
- const finish = activeFinish
194
- activeFinish = null
195
- finish?.({ status: 'success', credential: toCredential(authorization.credential) })
192
+ authorizationControllerDidCompleteWithAuthorization(
193
+ this: any,
194
+ _controller: any,
195
+ authorization: any,
196
+ ) {
197
+ try {
198
+ const credential = toCredential(authorization.credential)
199
+ if (!credential.user.id || !credential.idToken) {
200
+ throw new Error('Sign in with Apple returned an incomplete credential')
201
+ }
202
+
203
+ this.finish?.({ status: 'success', credential })
204
+ } catch (error) {
205
+ this.finish?.({ status: 'error', message: String((error as any)?.message ?? error) })
206
+ }
196
207
  },
197
- authorizationControllerDidCompleteWithError(_controller: any, error: any) {
198
- const finish = activeFinish
199
- activeFinish = null
200
- finish?.(
208
+ authorizationControllerDidCompleteWithError(this: any, _controller: any, error: any) {
209
+ this.finish?.(
201
210
  isCancel(error)
202
211
  ? { status: 'cancelled' }
203
212
  : { status: 'error', message: String((error as any)?.localizedDescription ?? error) },
@@ -243,7 +252,7 @@ function ensureClasses() {
243
252
  let activeController: any = null
244
253
  let activeDelegate: any = null
245
254
  let activeAnchor: any = null
246
- let activeFinish: ((result: SignInResult) => void) | null = null
255
+ let active = false
247
256
 
248
257
  export const appleAuth: AppleAuth = {
249
258
  get supported() {
@@ -257,37 +266,59 @@ export const appleAuth: AppleAuth = {
257
266
  return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
258
267
  }
259
268
 
260
- if (activeController) {
269
+ if (active) {
261
270
  return { status: 'error', message: 'a Sign in with Apple flow is already active' }
262
271
  }
263
272
 
273
+ active = true
264
274
  return new Promise<SignInResult>((resolve) => {
275
+ let settled = false
265
276
  const finish = (result: SignInResult) => {
277
+ if (settled) {
278
+ return
279
+ }
280
+
281
+ settled = true
282
+ active = false
266
283
  activeController = null
267
284
  activeDelegate = null
268
285
  activeAnchor = null
269
- activeFinish = null
270
286
  resolve(result)
271
287
  }
272
288
 
273
- ensureClasses()
274
- activeDelegate = AppleAuthDelegate.new()
275
- activeFinish = finish
276
- const provider = ASAuthorizationAppleIDProvider.new()
277
- const request = provider.createRequest()
278
- if (options?.scopes?.length) {
279
- request.requestedScopes = options.scopes.map((s) => SCOPE_MAP[s]?.()).filter(Boolean)
280
- }
281
-
282
- if (options?.nonce !== undefined) {
283
- request.nonce = sha256Hex(options.nonce)
289
+ try {
290
+ const app = NSApplication.sharedApplication
291
+ if (!(app.keyWindow ?? app.mainWindow ?? app.windows?.firstObject)) {
292
+ throw new Error('Sign in with Apple requires an AppKit presentation window')
293
+ }
294
+
295
+ ensureClasses()
296
+ activeDelegate = AppleAuthDelegate.new()
297
+ activeDelegate.finish = finish
298
+ const provider = ASAuthorizationAppleIDProvider.new()
299
+ const request = provider.createRequest()
300
+ if (options?.scopes?.length) {
301
+ request.requestedScopes = options.scopes.map((s) => SCOPE_MAP[s]?.()).filter(Boolean)
302
+ }
303
+
304
+ if (options?.nonce !== undefined) {
305
+ request.nonce = sha256Hex(options.nonce)
306
+ }
307
+
308
+ activeAnchor = AppleAuthAnchor.new()
309
+ activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([
310
+ request,
311
+ ])
312
+
313
+ activeController.delegate = activeDelegate
314
+ activeController.presentationContextProvider = activeAnchor
315
+ activeController.performRequests()
316
+ } catch (error) {
317
+ finish({
318
+ status: 'error',
319
+ message: String((error as any)?.localizedDescription ?? (error as any)?.message ?? error),
320
+ })
284
321
  }
285
-
286
- activeAnchor = AppleAuthAnchor.new()
287
- activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([request])
288
- activeController.delegate = activeDelegate
289
- activeController.presentationContextProvider = activeAnchor
290
- activeController.performRequests()
291
322
  })
292
323
  },
293
324
  async getCredentialState(userId: string): Promise<AppleCredentialState> {
@@ -296,25 +327,31 @@ export const appleAuth: AppleAuth = {
296
327
  }
297
328
 
298
329
  return new Promise<AppleCredentialState>((resolve) => {
299
- ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
300
- userId,
301
- (state: number, error: any) => {
302
- if (error) {
303
- resolve('unknown')
304
- return
305
- }
306
-
307
- resolve(
308
- state === 1
309
- ? 'authorized'
310
- : state === 0
311
- ? 'revoked'
312
- : state === 3
313
- ? 'transferred'
314
- : 'notFound',
315
- )
316
- },
317
- )
330
+ try {
331
+ ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
332
+ userId,
333
+ (state: number, error: any) => {
334
+ if (error) {
335
+ resolve('unknown')
336
+ return
337
+ }
338
+
339
+ resolve(
340
+ state === 1
341
+ ? 'authorized'
342
+ : state === 0
343
+ ? 'revoked'
344
+ : state === 3
345
+ ? 'transferred'
346
+ : state === 2
347
+ ? 'notFound'
348
+ : 'unknown',
349
+ )
350
+ },
351
+ )
352
+ } catch {
353
+ resolve('unknown')
354
+ }
318
355
  })
319
356
  },
320
357
  }
@@ -1,18 +1,85 @@
1
- // Google Sign-In — macOS leaf. The AppKit host has no NativeScript plugin
2
- // runtime; apps that need Google sign-in on macOS run the hosted ceremony
3
- // through `@octane-xplat/platform`'s `authSession` instead.
4
- import type { GoogleAuth, SignInResult } from './types'
5
-
6
- const UNSUPPORTED: SignInResult = {
7
- status: 'error',
8
- message: 'Google Sign-In is unsupported by the AppKit host — use a hosted authSession flow',
9
- }
1
+ // Google credentials on AppKit come from the app's hosted OAuth backend.
2
+ // The shared methods/result stay identical to the SDK leaves.
3
+ import { authSession } from '@octane-xplat/platform'
4
+ import type { GoogleAuth, GoogleAuthConfig, SignInResult } from './types'
5
+
6
+ let configuration: GoogleAuthConfig | undefined
7
+ let active = false
10
8
 
11
9
  export const googleAuth: GoogleAuth = {
12
- supported: false,
13
- async configure() {},
14
- async signIn(): Promise<SignInResult> {
15
- return UNSUPPORTED
10
+ get supported() {
11
+ return authSession.supported
12
+ },
13
+ async configure(config) {
14
+ configuration = config ? { ...config, scopes: config.scopes?.slice() } : undefined
15
+ },
16
+ async signIn(options = {}): Promise<SignInResult> {
17
+ const config = configuration
18
+ const flow = config?.hostedFlow
19
+ const session = authSession.impl
20
+ if (!session) {
21
+ return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
22
+ }
23
+
24
+ if (!flow) {
25
+ return {
26
+ status: 'error',
27
+ message: 'Google Sign-In on macOS requires configure({ hostedFlow })',
28
+ }
29
+ }
30
+
31
+ if (active) {
32
+ return { status: 'error', message: 'a Google Sign-In flow is already active' }
33
+ }
34
+
35
+ active = true
36
+ try {
37
+ const request = await flow.createRequest({
38
+ ...options,
39
+ clientId: config?.clientId,
40
+ serverClientId: config?.serverClientId,
41
+ scopes: config?.scopes?.slice(),
42
+ hostedDomain: config?.hostedDomain,
43
+ })
44
+
45
+ // Backend URLs must use TLS; the backend owns Google registration,
46
+ // state/nonce binding and single-use callback-code redemption.
47
+ if (!/^https:\/\//i.test(request.url)) {
48
+ throw new Error('Google hosted sign-in requires an HTTPS URL')
49
+ }
50
+
51
+ const result = await session.open(request.url, {
52
+ callbackScheme: request.callbackScheme,
53
+ prefersEphemeralSession: true,
54
+ })
55
+
56
+ if (result.type === 'cancel') {
57
+ return { status: 'cancelled' }
58
+ }
59
+
60
+ if (result.type === 'error') {
61
+ return { status: 'error', message: result.message }
62
+ }
63
+
64
+ const credential = await flow.complete(result.url)
65
+ if (
66
+ credential?.provider !== 'google' ||
67
+ typeof credential.user?.id !== 'string' ||
68
+ !credential.user.id ||
69
+ !Array.isArray(credential.scopes) ||
70
+ credential.scopes.some((scope) => typeof scope !== 'string')
71
+ ) {
72
+ throw new Error('Google hosted sign-in returned an invalid credential')
73
+ }
74
+
75
+ return { status: 'success', credential }
76
+ } catch (error) {
77
+ return { status: 'error', message: String((error as any)?.message ?? error) }
78
+ } finally {
79
+ active = false
80
+ }
81
+ },
82
+ async signOut() {
83
+ await configuration?.hostedFlow?.signOut?.()
16
84
  },
17
- async signOut() {},
18
85
  }
@@ -13,6 +13,7 @@ export type {
13
13
  AuthUser,
14
14
  GoogleAuth,
15
15
  GoogleAuthConfig,
16
+ GoogleHostedAuthFlow,
16
17
  GoogleSignInButtonProps,
17
18
  GoogleSignInOptions,
18
19
  SignInResult,
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export type {
15
15
  AuthUser,
16
16
  GoogleAuth,
17
17
  GoogleAuthConfig,
18
+ GoogleHostedAuthFlow,
18
19
  GoogleSignInButtonProps,
19
20
  GoogleSignInOptions,
20
21
  SignInResult,
package/src/index.web.ts CHANGED
@@ -15,6 +15,7 @@ export type {
15
15
  AuthUser,
16
16
  GoogleAuth,
17
17
  GoogleAuthConfig,
18
+ GoogleHostedAuthFlow,
18
19
  GoogleSignInButtonProps,
19
20
  GoogleSignInOptions,
20
21
  SignInResult,
package/src/types.ts CHANGED
@@ -87,6 +87,19 @@ export interface AppleAuth {
87
87
  getCredentialState(userId: string): Promise<AppleCredentialState>
88
88
  }
89
89
 
90
+ /** App-owned Google OAuth ceremony for the macOS system browser. */
91
+ export interface GoogleHostedAuthFlow {
92
+ /** Issue a fresh backend attempt, binding state, nonce, scopes and callback destination. */
93
+ createRequest(
94
+ options: GoogleSignInOptions &
95
+ Pick<GoogleAuthConfig, 'clientId' | 'serverClientId' | 'scopes' | 'hostedDomain'>,
96
+ ): Promise<{ url: string; callbackScheme: string }>
97
+ /** Validate/redeem the callback against that attempt on your backend; return its Google credential. */
98
+ complete(callbackURL: string): Promise<AuthCredential>
99
+ /** Optional backend logout. macOS requests ephemeral browser sessions. */
100
+ signOut?(): Promise<void>
101
+ }
102
+
90
103
  export interface GoogleAuthConfig {
91
104
  /**
92
105
  * OAuth client id — the web client id on web (required there). On iOS it
@@ -99,11 +112,13 @@ export interface GoogleAuthConfig {
99
112
  scopes?: string[]
100
113
  /** Restrict to a Google Workspace domain. */
101
114
  hostedDomain?: string
115
+ /** macOS only — hosted OAuth adapter; other targets keep using their provider SDK. */
116
+ hostedFlow?: GoogleHostedAuthFlow
102
117
  }
103
118
 
104
119
  export interface GoogleSignInOptions {
105
120
  /**
106
- * Web only — nonce bound into the id token. Scope requests go through
121
+ * Web/macOS — nonce bound into the id token. Scope requests go through
107
122
  * `GoogleAuthConfig.scopes`; neither provider SDK takes per-call scopes.
108
123
  */
109
124
  nonce?: string
@@ -111,14 +126,14 @@ export interface GoogleSignInOptions {
111
126
 
112
127
  export interface GoogleAuth {
113
128
  /**
114
- * Whether the provider SDK can run here — iOS/Android + browsers. False on
115
- * macOS (use a hosted `authSession` flow instead). Android devices without
129
+ * Whether the provider can run here — iOS/Android + browsers, or macOS
130
+ * AuthenticationServices (requires `configure({ hostedFlow })`). Android devices without
116
131
  * Play services still report true — the sign-in call surfaces the failure.
117
132
  */
118
133
  readonly supported: boolean
119
134
  configure(config?: GoogleAuthConfig): Promise<void>
120
135
  signIn(options?: GoogleSignInOptions): Promise<SignInResult>
121
- /** Clear the SDK's account selection so the next `signIn` re-prompts. */
136
+ /** Clear SDK account selection; macOS calls hostedFlow.signOut and requests ephemeral sessions. */
122
137
  signOut(): Promise<void>
123
138
  }
124
139
 
@@ -2,4 +2,4 @@ export { appleAuth } from './apple.js';
2
2
  export { googleAuth } from './google.js';
3
3
  export { AppleSignInButton } from './AppleSignInButton.js';
4
4
  export { GoogleSignInButton } from './GoogleSignInButton.js';
5
- export type { AppleAuth, AppleAuthConfig, AppleCredentialState, AppleScope, AppleSignInButtonProps, AppleSignInOptions, AuthCredential, AuthUser, GoogleAuth, GoogleAuthConfig, GoogleSignInButtonProps, GoogleSignInOptions, SignInResult, } from './types.js';
5
+ export type { AppleAuth, AppleAuthConfig, AppleCredentialState, AppleScope, AppleSignInButtonProps, AppleSignInOptions, AuthCredential, AuthUser, GoogleAuth, GoogleAuthConfig, GoogleHostedAuthFlow, GoogleSignInButtonProps, GoogleSignInOptions, SignInResult, } from './types.js';
@@ -2,4 +2,4 @@ export { appleAuth } from './apple.web.js';
2
2
  export { googleAuth } from './google.web.js';
3
3
  export { AppleSignInButton } from './AppleSignInButton.web.js';
4
4
  export { GoogleSignInButton } from './GoogleSignInButton.web.js';
5
- export type { AppleAuth, AppleAuthConfig, AppleCredentialState, AppleScope, AppleSignInButtonProps, AppleSignInOptions, AuthCredential, AuthUser, GoogleAuth, GoogleAuthConfig, GoogleSignInButtonProps, GoogleSignInOptions, SignInResult, } from './types.js';
5
+ export type { AppleAuth, AppleAuthConfig, AppleCredentialState, AppleScope, AppleSignInButtonProps, AppleSignInOptions, AuthCredential, AuthUser, GoogleAuth, GoogleAuthConfig, GoogleHostedAuthFlow, GoogleSignInButtonProps, GoogleSignInOptions, SignInResult, } from './types.js';
@@ -77,6 +77,18 @@ export interface AppleAuth {
77
77
  /** iOS and macOS — credential state for a prior `user.id`. */
78
78
  getCredentialState(userId: string): Promise<AppleCredentialState>;
79
79
  }
80
+ /** App-owned Google OAuth ceremony for the macOS system browser. */
81
+ export interface GoogleHostedAuthFlow {
82
+ /** Issue a fresh backend attempt, binding state, nonce, scopes and callback destination. */
83
+ createRequest(options: GoogleSignInOptions & Pick<GoogleAuthConfig, 'clientId' | 'serverClientId' | 'scopes' | 'hostedDomain'>): Promise<{
84
+ url: string;
85
+ callbackScheme: string;
86
+ }>;
87
+ /** Validate/redeem the callback against that attempt on your backend; return its Google credential. */
88
+ complete(callbackURL: string): Promise<AuthCredential>;
89
+ /** Optional backend logout. macOS requests ephemeral browser sessions. */
90
+ signOut?(): Promise<void>;
91
+ }
80
92
  export interface GoogleAuthConfig {
81
93
  /**
82
94
  * OAuth client id — the web client id on web (required there). On iOS it
@@ -89,24 +101,26 @@ export interface GoogleAuthConfig {
89
101
  scopes?: string[];
90
102
  /** Restrict to a Google Workspace domain. */
91
103
  hostedDomain?: string;
104
+ /** macOS only — hosted OAuth adapter; other targets keep using their provider SDK. */
105
+ hostedFlow?: GoogleHostedAuthFlow;
92
106
  }
93
107
  export interface GoogleSignInOptions {
94
108
  /**
95
- * Web only — nonce bound into the id token. Scope requests go through
109
+ * Web/macOS — nonce bound into the id token. Scope requests go through
96
110
  * `GoogleAuthConfig.scopes`; neither provider SDK takes per-call scopes.
97
111
  */
98
112
  nonce?: string;
99
113
  }
100
114
  export interface GoogleAuth {
101
115
  /**
102
- * Whether the provider SDK can run here — iOS/Android + browsers. False on
103
- * macOS (use a hosted `authSession` flow instead). Android devices without
116
+ * Whether the provider can run here — iOS/Android + browsers, or macOS
117
+ * AuthenticationServices (requires `configure({ hostedFlow })`). Android devices without
104
118
  * Play services still report true — the sign-in call surfaces the failure.
105
119
  */
106
120
  readonly supported: boolean;
107
121
  configure(config?: GoogleAuthConfig): Promise<void>;
108
122
  signIn(options?: GoogleSignInOptions): Promise<SignInResult>;
109
- /** Clear the SDK's account selection so the next `signIn` re-prompts. */
123
+ /** Clear SDK account selection; macOS calls hostedFlow.signOut and requests ephemeral sessions. */
110
124
  signOut(): Promise<void>;
111
125
  }
112
126
  interface SignInButtonBaseProps {
@@ -9,6 +9,7 @@ import type {
9
9
  AuthUser,
10
10
  GoogleAuth,
11
11
  GoogleAuthConfig,
12
+ GoogleHostedAuthFlow,
12
13
  GoogleSignInButtonProps,
13
14
  GoogleSignInOptions,
14
15
  SignInResult,
@@ -25,6 +26,7 @@ export type {
25
26
  AuthUser,
26
27
  GoogleAuth,
27
28
  GoogleAuthConfig,
29
+ GoogleHostedAuthFlow,
28
30
  GoogleSignInButtonProps,
29
31
  GoogleSignInOptions,
30
32
  SignInResult,