@octane-xplat/auth 0.0.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -2
  3. package/dist/native/AppleSignInButton.tsrx.js +63 -0
  4. package/dist/native/GoogleSignInButton.tsrx.js +35 -0
  5. package/dist/native/apple.js +63 -0
  6. package/dist/native/google.js +50 -0
  7. package/dist/native/index.js +5 -0
  8. package/dist/native/octane-client-build.json +8 -0
  9. package/dist/web/AppleSignInButton.web.tsrx.js +71 -0
  10. package/dist/web/GoogleSignInButton.web.tsrx.js +47 -0
  11. package/dist/web/apple.web.js +86 -0
  12. package/dist/web/google.web.js +130 -0
  13. package/dist/web/index.js +5 -0
  14. package/dist/web/octane-client-build.json +8 -0
  15. package/package.json +77 -10
  16. package/src/AppleSignInButton.macos.tsrx +36 -0
  17. package/src/AppleSignInButton.tsrx +46 -0
  18. package/src/AppleSignInButton.web.tsrx +54 -0
  19. package/src/GoogleSignInButton.macos.tsrx +7 -0
  20. package/src/GoogleSignInButton.tsrx +31 -0
  21. package/src/GoogleSignInButton.web.tsrx +23 -0
  22. package/src/apple.macos.ts +320 -0
  23. package/src/apple.ts +95 -0
  24. package/src/apple.web.ts +114 -0
  25. package/src/google.macos.ts +18 -0
  26. package/src/google.ts +61 -0
  27. package/src/google.web.ts +167 -0
  28. package/src/index.macos.ts +19 -0
  29. package/src/index.ts +21 -0
  30. package/src/index.web.ts +21 -0
  31. package/src/native-elements.d.ts +10 -0
  32. package/src/types.ts +147 -0
  33. package/types/generated/.tsrx-typegen-manifest.json +16 -0
  34. package/types/generated/AppleSignInButton.d.ts +5 -0
  35. package/types/generated/AppleSignInButton.web.d.ts +6 -0
  36. package/types/generated/GoogleSignInButton.d.ts +5 -0
  37. package/types/generated/GoogleSignInButton.web.d.ts +6 -0
  38. package/types/generated/apple.d.ts +2 -0
  39. package/types/generated/apple.web.d.ts +2 -0
  40. package/types/generated/google.d.ts +2 -0
  41. package/types/generated/google.web.d.ts +8 -0
  42. package/types/generated/index.d.ts +5 -0
  43. package/types/generated/index.web.d.ts +5 -0
  44. package/types/generated/types.d.ts +133 -0
  45. package/types/index.macos.d.ts +36 -0
  46. package/index.d.ts +0 -1
  47. package/index.js +0 -2
package/package.json CHANGED
@@ -1,19 +1,86 @@
1
1
  {
2
2
  "name": "@octane-xplat/auth",
3
- "version": "0.0.1",
3
+ "version": "0.9.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
+ "keywords": [
6
+ "android",
7
+ "apple",
8
+ "auth",
9
+ "cross-platform",
10
+ "google",
11
+ "ios",
12
+ "nativescript",
13
+ "oauth",
14
+ "octane",
15
+ "sign-in",
16
+ "web",
17
+ "xplat"
18
+ ],
19
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/auth",
5
20
  "license": "MIT",
6
21
  "repository": {
7
22
  "type": "git",
8
- "url": "https://github.com/octane-xplat/octane-xplat.git",
23
+ "url": "https://github.com/octane-xplat/octane-xplat",
9
24
  "directory": "packages/auth"
10
25
  },
11
- "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/auth",
12
- "main": "./index.js",
13
- "types": "./index.d.ts",
14
26
  "files": [
15
- "README.md",
16
- "index.js",
17
- "index.d.ts"
18
- ]
19
- }
27
+ "dist",
28
+ "types",
29
+ "src"
30
+ ],
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "macos": {
35
+ "types": "./types/index.macos.d.ts",
36
+ "default": "./src/index.macos.ts"
37
+ },
38
+ "web": {
39
+ "types": "./types/generated/index.web.d.ts",
40
+ "default": "./dist/web/index.js"
41
+ },
42
+ "native": {
43
+ "types": "./types/generated/index.d.ts",
44
+ "default": "./dist/native/index.js"
45
+ },
46
+ "types": "./types/generated/index.d.ts",
47
+ "default": "./dist/native/index.js"
48
+ }
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@nativescript/apple-sign-in": "2.0.0",
55
+ "@nativescript/google-signin": "2.1.1"
56
+ },
57
+ "devDependencies": {
58
+ "@nativescript-community/octane": "0.2.4",
59
+ "@nativescript/core": "9.1.2",
60
+ "@octanejs/vite-plugin": "0.1.61",
61
+ "@tsrx/typescript-plugin": "0.4.11",
62
+ "octane": "0.6.3",
63
+ "tsrx-typegen": "0.2.1",
64
+ "typescript": "5.9.3",
65
+ "vite": "8.3.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@nativescript-community/octane": ">=0.2.1 <1",
69
+ "@nativescript/core": ">=9.1.0 <10",
70
+ "octane": ">=0.6.3 <1"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@nativescript-community/octane": {
74
+ "optional": true
75
+ },
76
+ "@nativescript/core": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "scripts": {
81
+ "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
82
+ "test:packed": "node tests/packed-consumer.mjs",
83
+ "typegen": "tsrx-typegen --target octane",
84
+ "pack:check": "tsrx-typegen --pack-check"
85
+ }
86
+ }
@@ -0,0 +1,36 @@
1
+ /** @jsxImportSource @xplat/macos/renderer */
2
+ import { appleAuth } from './apple.macos'
3
+ import type { AppleSignInButtonProps, SignInResult } from './types'
4
+
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. */
17
+ export function AppleSignInButton(props: AppleSignInButtonProps) @{
18
+ <flexboxlayout
19
+ 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>
36
+ }
@@ -0,0 +1,46 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane';
3
+ import * as applePlugin from '@nativescript/apple-sign-in';
4
+ import { appleAuth } from './apple';
5
+ import type { AppleSignInButtonProps, SignInResult } from './types';
6
+
7
+ // The plugin's SignInButton wraps ASAuthorizationAppleIDButton — iOS only
8
+ // (the class is absent from the plugin's Android module), so register it
9
+ // conditionally and fall back to a plain button everywhere else.
10
+ const PluginButton = (applePlugin as any).SignInButton;
11
+ if (PluginButton) {
12
+ registerElement('applesigninbutton', PluginButton);
13
+ }
14
+
15
+ /** Sign in with Apple button — the platform-authentic
16
+ * ASAuthorizationAppleIDButton on iOS, a plain fallback button elsewhere.
17
+ * A tap starts `appleAuth.signIn()`; `onResult` reports the outcome. */
18
+ export function AppleSignInButton(props: AppleSignInButtonProps) @{
19
+ const run = () => {
20
+ props.onPress?.();
21
+ if (props.onResult) {
22
+ appleAuth.signIn().then((result: SignInResult) => props.onResult?.(result));
23
+ }
24
+ };
25
+
26
+ @if (PluginButton && appleAuth.supported) {
27
+ <applesigninbutton
28
+ id={props.id}
29
+ className={props.className}
30
+ style={props.style}
31
+ type={props.type ?? 'default'}
32
+ buttonStyle={props.theme ?? 'black'}
33
+ isEnabled={props.disabled !== true}
34
+ onTap={run}
35
+ />
36
+ } @else {
37
+ <button
38
+ id={props.id}
39
+ className={props.className}
40
+ style={props.style}
41
+ text={props.type === 'signUp' ? 'Sign up with Apple' : props.type === 'continue' ? 'Continue with Apple' : 'Sign in with Apple'}
42
+ isEnabled={props.disabled !== true}
43
+ onTap={run}
44
+ />
45
+ }
46
+ }
@@ -0,0 +1,54 @@
1
+ import { appleAuth } from './apple.web';
2
+ import type { AppleSignInButtonProps, SignInResult } from './types';
3
+
4
+ const LABELS = {
5
+ default: 'Sign in with Apple',
6
+ signUp: 'Sign up with Apple',
7
+ continue: 'Continue with Apple',
8
+ } as const;
9
+
10
+ const THEMES = {
11
+ black: { background: '#000', color: '#fff', border: '1px solid #000' },
12
+ white: { background: '#fff', color: '#000', border: '1px solid #fff' },
13
+ whiteOutline: { background: '#fff', color: '#000', border: '1px solid #000' },
14
+ } as const;
15
+
16
+ /** Sign in with Apple button — web leaf. Apple's JS API renders official
17
+ * markup only through its own stylesheet hooks, so this is a styled fallback
18
+ * matching Apple's published button spec; the click runs the popup flow via
19
+ * `appleAuth.signIn()` and reports through `onResult`. */
20
+ export function AppleSignInButton(props: AppleSignInButtonProps) @{
21
+ const theme = THEMES[props.theme ?? 'black'];
22
+
23
+ <button
24
+ type="button"
25
+ id={props.id}
26
+ className={props.className}
27
+ disabled={props.disabled}
28
+ style={{
29
+ display: 'inline-flex',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ gap: 6,
33
+ minHeight: 40,
34
+ padding: '0 16px',
35
+ borderRadius: 6,
36
+ fontFamily: "-apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif",
37
+ fontSize: 15,
38
+ cursor: props.disabled ? 'default' : 'pointer',
39
+ ...theme,
40
+ ...props.style,
41
+ }}
42
+ onClick={() => {
43
+ props.onPress?.();
44
+ if (props.onResult) {
45
+ void appleAuth.signIn().then((result: SignInResult) => props.onResult?.(result));
46
+ }
47
+ }}
48
+ >
49
+ <svg width="15" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
50
+ <path d="M15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09z" />
51
+ </svg>
52
+ {LABELS[props.type ?? 'default']}
53
+ </button>
54
+ }
@@ -0,0 +1,7 @@
1
+ /** @jsxImportSource @xplat/macos/renderer */
2
+ import type { GoogleSignInButtonProps } from './types'
3
+
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>
7
+ }
@@ -0,0 +1,31 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane';
3
+ import { GoogleSignInButton as NSGoogleSignInButton } from '@nativescript/google-signin';
4
+ import { googleAuth } from './google';
5
+ import type { GoogleSignInButtonProps, SignInResult } from './types';
6
+
7
+ // GIDSignInButton (iOS) / the plugin's Google button (Android). The plugin
8
+ // emits the standard `tap` event — the driver's onTap alias wires it.
9
+ registerElement('googlesigninbutton', NSGoogleSignInButton);
10
+
11
+ /** Google Sign-In button — the provider's button view on iOS/Android. A tap
12
+ * starts `googleAuth.signIn()`; `onResult` reports the outcome. Requires a
13
+ * `googleAuth.configure()` call first where the platform needs it. */
14
+ export function GoogleSignInButton(props: GoogleSignInButtonProps) @{
15
+ const run = () => {
16
+ props.onPress?.();
17
+ if (props.onResult) {
18
+ googleAuth.signIn().then((result: SignInResult) => props.onResult?.(result));
19
+ }
20
+ };
21
+
22
+ <googlesigninbutton
23
+ id={props.id}
24
+ className={props.className}
25
+ style={props.style}
26
+ colorScheme={props.theme ?? 'auto'}
27
+ colorStyle={props.variant ?? 'standard'}
28
+ isEnabled={props.disabled !== true}
29
+ onTap={run}
30
+ />
31
+ }
@@ -0,0 +1,23 @@
1
+ import { renderGoogleButton } from './google.web';
2
+ import type { GoogleSignInButtonProps, SignInResult } from './types';
3
+
4
+ /** Google Sign-In button — web leaf. Renders the official GIS button via
5
+ * `renderButton` when a `clientId` is configured; without one it falls back
6
+ * to a plain button that runs `googleAuth.signIn()` (One Tap). `onResult`
7
+ * reports the outcome either way. */
8
+ export function GoogleSignInButton(props: GoogleSignInButtonProps) @{
9
+ <div
10
+ id={props.id}
11
+ className={props.className}
12
+ style={props.style}
13
+ ref={(el: HTMLElement | null) => {
14
+ if (el && !props.disabled) {
15
+ void renderGoogleButton(
16
+ el,
17
+ { theme: props.theme, variant: props.variant },
18
+ (result: SignInResult) => props.onResult?.(result),
19
+ );
20
+ }
21
+ }}
22
+ />
23
+ }
@@ -0,0 +1,320 @@
1
+ // Sign in with Apple — macOS leaf over AuthenticationServices via the
2
+ // appkit-node-api ObjC bridge (the same framework the iOS plugin wraps,
3
+ // available since macOS 10.15). The `com.apple.developer.applesignin`
4
+ // entitlement must sit on the packaged app for the sheet to present;
5
+ // hosts without it still load the framework and surface the OS error.
6
+ import type {
7
+ AppleAuth,
8
+ AppleAuthConfig,
9
+ AppleCredentialState,
10
+ AppleSignInOptions,
11
+ AuthCredential,
12
+ SignInResult,
13
+ } from './types'
14
+
15
+ declare const ASAuthorizationAppleIDProvider: any
16
+ declare const ASAuthorizationController: any
17
+ declare const ASAuthorizationControllerDelegate: any
18
+ declare const ASAuthorizationControllerPresentationContextProviding: any
19
+ declare const ASAuthorizationScopeEmail: any
20
+ declare const ASAuthorizationScopeFullName: any
21
+ declare const NSApplication: any
22
+ declare const NSString: any
23
+ declare const NSObject: any
24
+ declare const interop: any
25
+ declare const objc: any
26
+
27
+ // Import before the NSObject.extend classes below are built — their
28
+ // protocol/exposed-method constants resolve at extend() time, so the
29
+ // framework's metadata has to be loaded first.
30
+ try {
31
+ objc.import('AuthenticationServices')
32
+ } catch {
33
+ // Symbols may already be bound — failures surface via `supported: false`.
34
+ }
35
+
36
+ const SCOPE_MAP: Record<string, () => any> = {
37
+ email: () => ASAuthorizationScopeEmail,
38
+ name: () => ASAuthorizationScopeFullName,
39
+ }
40
+
41
+ function isCancel(error: unknown): boolean {
42
+ // ASAuthorizationError.canceled = 1001 (NSError.code).
43
+ return (
44
+ (error as any)?.code === 1001 ||
45
+ /cancel/i.test(String((error as any)?.localizedDescription ?? error))
46
+ )
47
+ }
48
+
49
+ function utf8Bytes(input: string): Uint8Array {
50
+ const data = NSString.stringWithString(input).dataUsingEncoding(4 /* NSUTF8StringEncoding */)
51
+ return new Uint8Array(interop.bufferFromData(data))
52
+ }
53
+
54
+ // FIPS 180-4 — the request nonce must be the hex SHA-256 of the app's nonce
55
+ // (same transform the iOS plugin applies via CommonCrypto, which has no
56
+ // node-api metadata). Kept local to stay dependency-free.
57
+ function sha256Hex(input: string): string {
58
+ const K = new Uint32Array([
59
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
60
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
61
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
62
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
63
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
64
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
65
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
66
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
67
+ ])
68
+
69
+ const H = new Uint32Array([
70
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
71
+ ])
72
+
73
+ const bytes = utf8Bytes(input)
74
+ const bitLen = bytes.length * 8
75
+ const padded = new Uint8Array((bytes.length + 9 + 63) & ~63)
76
+ padded.set(bytes)
77
+ padded[bytes.length] = 0x80
78
+ const dv = new DataView(padded.buffer)
79
+ dv.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000))
80
+ dv.setUint32(padded.length - 4, bitLen >>> 0)
81
+ const w = new Uint32Array(64)
82
+ for (let block = 0; block < padded.length; block += 64) {
83
+ for (let t = 0; t < 16; t++) {
84
+ w[t] = dv.getUint32(block + t * 4)
85
+ }
86
+
87
+ for (let t = 16; t < 64; t++) {
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
+
98
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0
99
+ }
100
+
101
+ let [a, b, c, d, e, f, g, h] = H
102
+ for (let t = 0; t < 64; t++) {
103
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7))
104
+ const t1 = (h + S1 + ((e & f) ^ (~e & g)) + K[t] + w[t]) >>> 0
105
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10))
106
+ const t2 = (S0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 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
115
+ }
116
+
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
125
+ }
126
+
127
+ return [...H].map((x) => x.toString(16).padStart(8, '0')).join('')
128
+ }
129
+
130
+ function toCredential(credential: any): AuthCredential {
131
+ const tokenData = credential.valueForKey('identityToken')
132
+ const codeData = credential.valueForKey('authorizationCode')
133
+ const fullName = credential.fullName
134
+ const name = [fullName?.givenName, fullName?.familyName].filter(Boolean).join(' ') || undefined
135
+
136
+ return {
137
+ provider: 'apple',
138
+ idToken: tokenData ? String(NSString.alloc().initWithDataEncoding(tokenData, 4)) : undefined,
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),
149
+ ),
150
+ user: {
151
+ id: String(credential.user ?? ''),
152
+ email: credential.email ? String(credential.email) : undefined,
153
+ name,
154
+ givenName: fullName?.givenName ? String(fullName.givenName) : undefined,
155
+ familyName: fullName?.familyName ? String(fullName.familyName) : undefined,
156
+ },
157
+ }
158
+ }
159
+
160
+ let frameworkReady: boolean | null = null
161
+
162
+ function frameworkAvailable(): boolean {
163
+ if (frameworkReady !== null) {
164
+ return frameworkReady
165
+ }
166
+
167
+ try {
168
+ objc.import('AuthenticationServices')
169
+ } catch {
170
+ // The framework ships in every macOS ≥ 10.15 SDK — a failed import just
171
+ // means the symbols still resolve lazily below.
172
+ }
173
+
174
+ frameworkReady = typeof ASAuthorizationAppleIDProvider !== 'undefined'
175
+ return frameworkReady
176
+ }
177
+
178
+ // NSObject.extend is the registration shape this node-api runtime binds
179
+ // (class syntax + NativeClass registers the class but not the methods —
180
+ // verified by probe: respondsToSelector is only true for .extend). Classes
181
+ // are built lazily so the protocol constants resolve after import.
182
+ let AppleAuthDelegate: any = null
183
+ let AppleAuthAnchor: any = null
184
+
185
+ function ensureClasses() {
186
+ if (AppleAuthDelegate !== null) {
187
+ return
188
+ }
189
+
190
+ AppleAuthDelegate = NSObject.extend(
191
+ {
192
+ authorizationControllerDidCompleteWithAuthorization(_controller: any, authorization: any) {
193
+ const finish = activeFinish
194
+ activeFinish = null
195
+ finish?.({ status: 'success', credential: toCredential(authorization.credential) })
196
+ },
197
+ authorizationControllerDidCompleteWithError(_controller: any, error: any) {
198
+ const finish = activeFinish
199
+ activeFinish = null
200
+ finish?.(
201
+ isCancel(error)
202
+ ? { status: 'cancelled' }
203
+ : { status: 'error', message: String((error as any)?.localizedDescription ?? error) },
204
+ )
205
+ },
206
+ },
207
+ {
208
+ protocols: [ASAuthorizationControllerDelegate],
209
+ exposedMethods: {
210
+ 'authorizationController:didCompleteWithAuthorization:': {
211
+ params: [interop.types.id, interop.types.id],
212
+ returns: interop.types.void,
213
+ },
214
+ 'authorizationController:didCompleteWithError:': {
215
+ params: [interop.types.id, interop.types.id],
216
+ returns: interop.types.void,
217
+ },
218
+ },
219
+ },
220
+ )
221
+
222
+ AppleAuthAnchor = NSObject.extend(
223
+ {
224
+ presentationAnchorForAuthorizationController(_controller: any): any {
225
+ const app = NSApplication.sharedApplication
226
+ return app.keyWindow ?? app.mainWindow ?? app.windows?.firstObject
227
+ },
228
+ },
229
+ {
230
+ protocols: [ASAuthorizationControllerPresentationContextProviding],
231
+ exposedMethods: {
232
+ 'presentationAnchorForAuthorizationController:': {
233
+ params: [interop.types.id],
234
+ returns: interop.types.id,
235
+ },
236
+ },
237
+ },
238
+ )
239
+ }
240
+
241
+ // Retain the ObjC objects + resolver for the ceremony's lifetime — the
242
+ // bridge sees no JS-side reference after signIn returns.
243
+ let activeController: any = null
244
+ let activeDelegate: any = null
245
+ let activeAnchor: any = null
246
+ let activeFinish: ((result: SignInResult) => void) | null = null
247
+
248
+ export const appleAuth: AppleAuth = {
249
+ get supported() {
250
+ return frameworkAvailable()
251
+ },
252
+ configure(_config: AppleAuthConfig) {
253
+ // No native configuration — the flow is bound to the app id entitlement.
254
+ },
255
+ async signIn(options?: AppleSignInOptions): Promise<SignInResult> {
256
+ if (!frameworkAvailable()) {
257
+ return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
258
+ }
259
+
260
+ if (activeController) {
261
+ return { status: 'error', message: 'a Sign in with Apple flow is already active' }
262
+ }
263
+
264
+ return new Promise<SignInResult>((resolve) => {
265
+ const finish = (result: SignInResult) => {
266
+ activeController = null
267
+ activeDelegate = null
268
+ activeAnchor = null
269
+ activeFinish = null
270
+ resolve(result)
271
+ }
272
+
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)
284
+ }
285
+
286
+ activeAnchor = AppleAuthAnchor.new()
287
+ activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([request])
288
+ activeController.delegate = activeDelegate
289
+ activeController.presentationContextProvider = activeAnchor
290
+ activeController.performRequests()
291
+ })
292
+ },
293
+ async getCredentialState(userId: string): Promise<AppleCredentialState> {
294
+ if (!frameworkAvailable()) {
295
+ return 'unknown'
296
+ }
297
+
298
+ 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
+ )
318
+ })
319
+ },
320
+ }