@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 +50 -15
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +8 -4
- package/src/AppleSignInButton.macos.tsrx +28 -30
- package/src/GoogleSignInButton.macos.tsrx +18 -4
- package/src/apple-button.macos.ts +75 -0
- package/src/apple.macos.ts +86 -49
- package/src/google.macos.ts +81 -14
- package/src/index.macos.ts +1 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/types.ts +19 -4
- package/types/generated/index.d.ts +1 -1
- package/types/generated/index.web.d.ts +1 -1
- package/types/generated/types.d.ts +18 -4
- package/types/index.macos.d.ts +2 -0
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
|
-
```
|
|
11
|
-
|
|
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
|
-
|
|
20
|
-
if (
|
|
21
|
-
const
|
|
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.
|
|
30
|
-
|
|
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/auth",
|
|
3
|
-
"version": "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.
|
|
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
|
|
1
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
2
|
+
import { useEffect, useRef } from 'octane'
|
|
2
3
|
import { appleAuth } from './apple.macos'
|
|
3
|
-
import
|
|
4
|
+
import { attachAppleButton } from './apple-button.macos'
|
|
5
|
+
import type { AppleSignInButtonProps } from './types'
|
|
4
6
|
|
|
5
|
-
|
|
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={
|
|
21
|
-
style={props.style}
|
|
22
|
-
|
|
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
|
|
1
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
2
|
+
import { googleAuth } from './google.macos'
|
|
2
3
|
import type { GoogleSignInButtonProps } from './types'
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
export function GoogleSignInButton(
|
|
6
|
-
<
|
|
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
|
+
}
|
package/src/apple.macos.ts
CHANGED
|
@@ -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)?.
|
|
45
|
-
|
|
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(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
request
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
}
|
package/src/google.macos.ts
CHANGED
|
@@ -1,18 +1,85 @@
|
|
|
1
|
-
// Google
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import type { GoogleAuth, SignInResult } from './types'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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/index.macos.ts
CHANGED
package/src/index.ts
CHANGED
package/src/index.web.ts
CHANGED
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
|
|
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
|
|
115
|
-
*
|
|
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
|
|
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
|
|
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
|
|
103
|
-
*
|
|
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
|
|
123
|
+
/** Clear SDK account selection; macOS calls hostedFlow.signOut and requests ephemeral sessions. */
|
|
110
124
|
signOut(): Promise<void>;
|
|
111
125
|
}
|
|
112
126
|
interface SignInButtonBaseProps {
|
package/types/index.macos.d.ts
CHANGED
|
@@ -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,
|