@octane-xplat/auth 0.8.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 ADDED
@@ -0,0 +1,73 @@
1
+ # `@octane-xplat/auth`
2
+
3
+ ```sh
4
+ pnpm add @octane-xplat/auth
5
+ ```
6
+
7
+ Sign in with Apple and Google Sign-In for Octane xplat apps — the native
8
+ SDKs (`@nativescript/apple-sign-in`, `@nativescript/google-signin`) on
9
+ iOS/Android, the providers' web SDKs (Apple JS / Google Identity Services)
10
+ in the browser, and AuthenticationServices through the ObjC bridge plus a
11
+ app-configured hosted Google flow on the macOS AppKit host. One `AuthUser`/`AuthCredential`
12
+ contract everywhere.
13
+
14
+ ```ts
15
+ import { appleAuth } from '@octane-xplat/auth'
16
+
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
+ }
24
+ }
25
+ ```
26
+
27
+ Each provider object reports `supported` — Apple is `false` on Android —
28
+ and exposes `configure`, `signIn`, and provider extras
29
+ (`appleAuth.getCredentialState`, `googleAuth.signOut`). `SignInResult`
30
+ distinguishes `success` / `cancelled` / `error`; a dismissed sheet is
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
34
+ target's native styling.
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
+
56
+ Web needs registered client IDs; iOS needs the `applesignin` entitlement
57
+ and Google's `GoogleService-Info.plist`/reversed-client-id URL scheme;
58
+ Android needs `google-services.json` and a `serverClientId` for
59
+ `serverAuthCode`. Guide: [Using device
60
+ features](../../docs/platform/platform-services.md). Exercised by
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": "b22aa5b8-e210-4f27-babd-a9c30bb0a8dd",
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": "321142a6-1005-48c0-850a-256122cdddf2",
3
+ "buildId": "500fb75c-1e7a-46fe-8b9a-4a439f2dfd7e",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "name": "@octane-xplat/auth",
3
- "version": "0.8.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
- "octane",
7
- "xplat",
8
- "cross-platform",
9
- "auth",
10
- "sign-in",
6
+ "android",
11
7
  "apple",
8
+ "auth",
9
+ "cross-platform",
12
10
  "google",
13
- "oauth",
14
- "nativescript",
15
11
  "ios",
16
- "android",
17
- "web"
12
+ "nativescript",
13
+ "oauth",
14
+ "octane",
15
+ "sign-in",
16
+ "web",
17
+ "xplat"
18
18
  ],
19
- "license": "MIT",
20
19
  "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/auth",
20
+ "license": "MIT",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/octane-xplat/octane-xplat.git",
23
+ "url": "https://github.com/octane-xplat/octane-xplat",
24
24
  "directory": "packages/auth"
25
25
  },
26
26
  "files": [
@@ -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.0",
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
+ }
@@ -40,7 +40,10 @@ const SCOPE_MAP: Record<string, () => any> = {
40
40
 
41
41
  function isCancel(error: unknown): boolean {
42
42
  // ASAuthorizationError.canceled = 1001 (NSError.code).
43
- return (error as any)?.code === 1001 || /cancel/i.test(String((error as any)?.localizedDescription ?? error))
43
+ return (
44
+ (error as any)?.domain === 'com.apple.AuthenticationServices.AuthorizationError' &&
45
+ Number((error as any)?.code) === 1001
46
+ )
44
47
  }
45
48
 
46
49
  function utf8Bytes(input: string): Uint8Array {
@@ -63,7 +66,10 @@ function sha256Hex(input: string): string {
63
66
  0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
64
67
  ])
65
68
 
66
- const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19])
69
+ const H = new Uint32Array([
70
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
71
+ ])
72
+
67
73
  const bytes = utf8Bytes(input)
68
74
  const bitLen = bytes.length * 8
69
75
  const padded = new Uint8Array((bytes.length + 9 + 63) & ~63)
@@ -74,25 +80,48 @@ function sha256Hex(input: string): string {
74
80
  dv.setUint32(padded.length - 4, bitLen >>> 0)
75
81
  const w = new Uint32Array(64)
76
82
  for (let block = 0; block < padded.length; block += 64) {
77
- for (let t = 0; t < 16; t++) {w[t] = dv.getUint32(block + t * 4)}
83
+ for (let t = 0; t < 16; t++) {
84
+ w[t] = dv.getUint32(block + t * 4)
85
+ }
86
+
78
87
  for (let t = 16; t < 64; t++) {
79
- const s0 = (w[t - 15] >>> 7 | w[t - 15] << 25) ^ (w[t - 15] >>> 18 | w[t - 15] << 14) ^ (w[t - 15] >>> 3)
80
- const s1 = (w[t - 2] >>> 17 | w[t - 2] << 15) ^ (w[t - 2] >>> 19 | w[t - 2] << 13) ^ (w[t - 2] >>> 10)
88
+ const s0 =
89
+ ((w[t - 15] >>> 7) | (w[t - 15] << 25)) ^
90
+ ((w[t - 15] >>> 18) | (w[t - 15] << 14)) ^
91
+ (w[t - 15] >>> 3)
92
+
93
+ const s1 =
94
+ ((w[t - 2] >>> 17) | (w[t - 2] << 15)) ^
95
+ ((w[t - 2] >>> 19) | (w[t - 2] << 13)) ^
96
+ (w[t - 2] >>> 10)
97
+
81
98
  w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0
82
99
  }
83
100
 
84
101
  let [a, b, c, d, e, f, g, h] = H
85
102
  for (let t = 0; t < 64; t++) {
86
- const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)
103
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7))
87
104
  const t1 = (h + S1 + ((e & f) ^ (~e & g)) + K[t] + w[t]) >>> 0
88
- const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)
105
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10))
89
106
  const t2 = (S0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 0
90
- h = g; g = f; f = e; e = (d + t1) >>> 0
91
- d = c; c = b; b = a; a = (t1 + t2) >>> 0
107
+ h = g
108
+ g = f
109
+ f = e
110
+ e = (d + t1) >>> 0
111
+ d = c
112
+ c = b
113
+ b = a
114
+ a = (t1 + t2) >>> 0
92
115
  }
93
116
 
94
- H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0
95
- H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0
117
+ H[0] = (H[0] + a) >>> 0
118
+ H[1] = (H[1] + b) >>> 0
119
+ H[2] = (H[2] + c) >>> 0
120
+ H[3] = (H[3] + d) >>> 0
121
+ H[4] = (H[4] + e) >>> 0
122
+ H[5] = (H[5] + f) >>> 0
123
+ H[6] = (H[6] + g) >>> 0
124
+ H[7] = (H[7] + h) >>> 0
96
125
  }
97
126
 
98
127
  return [...H].map((x) => x.toString(16).padStart(8, '0')).join('')
@@ -102,15 +131,21 @@ function toCredential(credential: any): AuthCredential {
102
131
  const tokenData = credential.valueForKey('identityToken')
103
132
  const codeData = credential.valueForKey('authorizationCode')
104
133
  const fullName = credential.fullName
105
- const name =
106
- [fullName?.givenName, fullName?.familyName].filter(Boolean).join(' ') || undefined
134
+ const name = [fullName?.givenName, fullName?.familyName].filter(Boolean).join(' ') || undefined
107
135
 
108
136
  return {
109
137
  provider: 'apple',
110
138
  idToken: tokenData ? String(NSString.alloc().initWithDataEncoding(tokenData, 4)) : undefined,
111
- authorizationCode: codeData ? String(NSString.alloc().initWithDataEncoding(codeData, 4)) : undefined,
112
- scopes: (credential.authorizedScopes ? Array.from(credential.authorizedScopes as any) : []).map((s: any) =>
113
- s === ASAuthorizationScopeEmail ? 'email' : s === ASAuthorizationScopeFullName ? 'name' : String(s),
139
+ authorizationCode: codeData
140
+ ? String(NSString.alloc().initWithDataEncoding(codeData, 4))
141
+ : undefined,
142
+ scopes: (credential.authorizedScopes ? Array.from(credential.authorizedScopes as any) : []).map(
143
+ (s: any) =>
144
+ s === ASAuthorizationScopeEmail
145
+ ? 'email'
146
+ : s === ASAuthorizationScopeFullName
147
+ ? 'name'
148
+ : String(s),
114
149
  ),
115
150
  user: {
116
151
  id: String(credential.user ?? ''),
@@ -154,15 +189,24 @@ function ensureClasses() {
154
189
 
155
190
  AppleAuthDelegate = NSObject.extend(
156
191
  {
157
- authorizationControllerDidCompleteWithAuthorization(_controller: any, authorization: any) {
158
- const finish = activeFinish
159
- activeFinish = null
160
- 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
+ }
161
207
  },
162
- authorizationControllerDidCompleteWithError(_controller: any, error: any) {
163
- const finish = activeFinish
164
- activeFinish = null
165
- finish?.(
208
+ authorizationControllerDidCompleteWithError(this: any, _controller: any, error: any) {
209
+ this.finish?.(
166
210
  isCancel(error)
167
211
  ? { status: 'cancelled' }
168
212
  : { status: 'error', message: String((error as any)?.localizedDescription ?? error) },
@@ -208,7 +252,7 @@ function ensureClasses() {
208
252
  let activeController: any = null
209
253
  let activeDelegate: any = null
210
254
  let activeAnchor: any = null
211
- let activeFinish: ((result: SignInResult) => void) | null = null
255
+ let active = false
212
256
 
213
257
  export const appleAuth: AppleAuth = {
214
258
  get supported() {
@@ -222,37 +266,59 @@ export const appleAuth: AppleAuth = {
222
266
  return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
223
267
  }
224
268
 
225
- if (activeController) {
269
+ if (active) {
226
270
  return { status: 'error', message: 'a Sign in with Apple flow is already active' }
227
271
  }
228
272
 
273
+ active = true
229
274
  return new Promise<SignInResult>((resolve) => {
275
+ let settled = false
230
276
  const finish = (result: SignInResult) => {
277
+ if (settled) {
278
+ return
279
+ }
280
+
281
+ settled = true
282
+ active = false
231
283
  activeController = null
232
284
  activeDelegate = null
233
285
  activeAnchor = null
234
- activeFinish = null
235
286
  resolve(result)
236
287
  }
237
288
 
238
- ensureClasses()
239
- activeDelegate = AppleAuthDelegate.new()
240
- activeFinish = finish
241
- const provider = ASAuthorizationAppleIDProvider.new()
242
- const request = provider.createRequest()
243
- if (options?.scopes?.length) {
244
- request.requestedScopes = options.scopes.map((s) => SCOPE_MAP[s]?.()).filter(Boolean)
245
- }
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
+ }
246
294
 
247
- if (options?.nonce !== undefined) {
248
- request.nonce = sha256Hex(options.nonce)
249
- }
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
+ ])
250
312
 
251
- activeAnchor = AppleAuthAnchor.new()
252
- activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([request])
253
- activeController.delegate = activeDelegate
254
- activeController.presentationContextProvider = activeAnchor
255
- activeController.performRequests()
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
+ })
321
+ }
256
322
  })
257
323
  },
258
324
  async getCredentialState(userId: string): Promise<AppleCredentialState> {
@@ -261,17 +327,31 @@ export const appleAuth: AppleAuth = {
261
327
  }
262
328
 
263
329
  return new Promise<AppleCredentialState>((resolve) => {
264
- ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
265
- userId,
266
- (state: number, error: any) => {
267
- if (error) {
268
- resolve('unknown')
269
- return
270
- }
330
+ try {
331
+ ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
332
+ userId,
333
+ (state: number, error: any) => {
334
+ if (error) {
335
+ resolve('unknown')
336
+ return
337
+ }
271
338
 
272
- resolve(state === 1 ? 'authorized' : state === 0 ? 'revoked' : state === 3 ? 'transferred' : 'notFound')
273
- },
274
- )
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
+ }
275
355
  })
276
356
  },
277
357
  }
package/src/apple.web.ts CHANGED
@@ -47,9 +47,9 @@ function decodeJwt(token: string): Record<string, any> {
47
47
  function toCredential(response: any): AuthCredential {
48
48
  const auth = response?.authorization ?? {}
49
49
  const claims = auth.id_token ? decodeJwt(auth.id_token) : {}
50
- const name = [response?.user?.name?.firstName, response?.user?.name?.lastName]
51
- .filter(Boolean)
52
- .join(' ') || undefined
50
+ const name =
51
+ [response?.user?.name?.firstName, response?.user?.name?.lastName].filter(Boolean).join(' ') ||
52
+ undefined
53
53
 
54
54
  return {
55
55
  provider: 'apple',
@@ -101,7 +101,10 @@ export const appleAuth: AppleAuth = {
101
101
  } catch (error) {
102
102
  return isCancel(error)
103
103
  ? { status: 'cancelled' }
104
- : { status: 'error', message: String((error as any)?.message ?? (error as any)?.error ?? error) }
104
+ : {
105
+ status: 'error',
106
+ message: String((error as any)?.message ?? (error as any)?.error ?? error),
107
+ }
105
108
  }
106
109
  },
107
110
  async getCredentialState(): Promise<AppleCredentialState> {
@@ -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
  }
package/src/google.web.ts CHANGED
@@ -78,7 +78,11 @@ function initialize(google: any) {
78
78
  /** Internal — the button leaf renders the official GIS button into `host`. */
79
79
  export async function renderGoogleButton(
80
80
  host: HTMLElement,
81
- options: { theme?: 'dark' | 'light' | 'auto'; variant?: 'standard' | 'wide' | 'icon'; width?: number },
81
+ options: {
82
+ theme?: 'dark' | 'light' | 'auto'
83
+ variant?: 'standard' | 'wide' | 'icon'
84
+ width?: number
85
+ },
82
86
  onResult: (result: SignInResult) => void,
83
87
  ): Promise<void> {
84
88
  if (!config.clientId) {
@@ -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
@@ -1,9 +1,9 @@
1
1
  // Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
2
2
  // reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
3
- export { appleAuth } from './apple';
4
- export { googleAuth } from './google';
5
- export { AppleSignInButton } from './AppleSignInButton.tsrx';
6
- export { GoogleSignInButton } from './GoogleSignInButton.tsrx';
3
+ export { appleAuth } from './apple'
4
+ export { googleAuth } from './google'
5
+ export { AppleSignInButton } from './AppleSignInButton.tsrx'
6
+ export { GoogleSignInButton } from './GoogleSignInButton.tsrx'
7
7
  export type {
8
8
  AppleAuth,
9
9
  AppleAuthConfig,
@@ -15,7 +15,8 @@ export type {
15
15
  AuthUser,
16
16
  GoogleAuth,
17
17
  GoogleAuthConfig,
18
+ GoogleHostedAuthFlow,
18
19
  GoogleSignInButtonProps,
19
20
  GoogleSignInOptions,
20
21
  SignInResult,
21
- } from './types';
22
+ } from './types'
package/src/index.web.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
2
2
  // reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
3
- export { appleAuth } from './apple.web';
4
- export { googleAuth } from './google.web';
5
- export { AppleSignInButton } from './AppleSignInButton.web.tsrx';
6
- export { GoogleSignInButton } from './GoogleSignInButton.web.tsrx';
3
+ export { appleAuth } from './apple.web'
4
+ export { googleAuth } from './google.web'
5
+ export { AppleSignInButton } from './AppleSignInButton.web.tsrx'
6
+ export { GoogleSignInButton } from './GoogleSignInButton.web.tsrx'
7
7
  export type {
8
8
  AppleAuth,
9
9
  AppleAuthConfig,
@@ -15,7 +15,8 @@ export type {
15
15
  AuthUser,
16
16
  GoogleAuth,
17
17
  GoogleAuthConfig,
18
+ GoogleHostedAuthFlow,
18
19
  GoogleSignInButtonProps,
19
20
  GoogleSignInOptions,
20
21
  SignInResult,
21
- } from './types';
22
+ } from './types'
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,