@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.
- package/LICENSE +21 -0
- package/README.md +37 -2
- package/dist/native/AppleSignInButton.tsrx.js +63 -0
- package/dist/native/GoogleSignInButton.tsrx.js +35 -0
- package/dist/native/apple.js +63 -0
- package/dist/native/google.js +50 -0
- package/dist/native/index.js +5 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/AppleSignInButton.web.tsrx.js +71 -0
- package/dist/web/GoogleSignInButton.web.tsrx.js +47 -0
- package/dist/web/apple.web.js +86 -0
- package/dist/web/google.web.js +130 -0
- package/dist/web/index.js +5 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +77 -10
- package/src/AppleSignInButton.macos.tsrx +36 -0
- package/src/AppleSignInButton.tsrx +46 -0
- package/src/AppleSignInButton.web.tsrx +54 -0
- package/src/GoogleSignInButton.macos.tsrx +7 -0
- package/src/GoogleSignInButton.tsrx +31 -0
- package/src/GoogleSignInButton.web.tsrx +23 -0
- package/src/apple.macos.ts +320 -0
- package/src/apple.ts +95 -0
- package/src/apple.web.ts +114 -0
- package/src/google.macos.ts +18 -0
- package/src/google.ts +61 -0
- package/src/google.web.ts +167 -0
- package/src/index.macos.ts +19 -0
- package/src/index.ts +21 -0
- package/src/index.web.ts +21 -0
- package/src/native-elements.d.ts +10 -0
- package/src/types.ts +147 -0
- package/types/generated/.tsrx-typegen-manifest.json +16 -0
- package/types/generated/AppleSignInButton.d.ts +5 -0
- package/types/generated/AppleSignInButton.web.d.ts +6 -0
- package/types/generated/GoogleSignInButton.d.ts +5 -0
- package/types/generated/GoogleSignInButton.web.d.ts +6 -0
- package/types/generated/apple.d.ts +2 -0
- package/types/generated/apple.web.d.ts +2 -0
- package/types/generated/google.d.ts +2 -0
- package/types/generated/google.web.d.ts +8 -0
- package/types/generated/index.d.ts +5 -0
- package/types/generated/index.web.d.ts +5 -0
- package/types/generated/types.d.ts +133 -0
- package/types/index.macos.d.ts +36 -0
- package/index.d.ts +0 -1
- 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
|
|
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
|
|
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
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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
|
+
}
|