@frockbot/plugin-auth 0.3.13 → 0.3.15
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@better-auth/electron": "1.7.2",
|
|
24
|
-
"@
|
|
25
|
-
"@frockbot/
|
|
24
|
+
"@capacitor/core": "8.5.0",
|
|
25
|
+
"@frockbot/client-core": "0.3.15",
|
|
26
|
+
"@frockbot/kernel-contracts": "0.3.15",
|
|
26
27
|
"better-auth": "1.7.2",
|
|
27
28
|
"cordis": "4.0.0-rc.8",
|
|
28
29
|
"vue": "3.5.41"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"@frockbot/plugin-testkit": "0.3.
|
|
32
|
+
"@frockbot/plugin-testkit": "0.3.15",
|
|
32
33
|
"@types/bun": "1.4.0",
|
|
33
34
|
"@types/node": "26.2.0",
|
|
34
35
|
"typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
|
package/src/client/AuthGate.vue
CHANGED
|
@@ -3,6 +3,10 @@ import { authSessionClientKey } from "../shared.js";
|
|
|
3
3
|
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue";
|
|
4
4
|
import { hostedAuthClient } from "./browser.js";
|
|
5
5
|
import { developmentLoginUrl, isLoopbackHost } from "./development-login";
|
|
6
|
+
import {
|
|
7
|
+
isAndroidNativeShell,
|
|
8
|
+
requestNativeGoogleCredential,
|
|
9
|
+
} from "./native-google.js";
|
|
6
10
|
|
|
7
11
|
const providedSession = inject(authSessionClientKey);
|
|
8
12
|
if (!providedSession) throw new Error("auth session client was not provided");
|
|
@@ -17,6 +21,7 @@ const user = computed(() =>
|
|
|
17
21
|
);
|
|
18
22
|
const loading = computed(() => session.projection.value.status === "loading");
|
|
19
23
|
const isDesktop = computed(() => Boolean(window.frockbotDesktop));
|
|
24
|
+
const isAndroid = isAndroidNativeShell();
|
|
20
25
|
const isLocalDevelopment = computed(() =>
|
|
21
26
|
isLoopbackHost(window.location.hostname),
|
|
22
27
|
);
|
|
@@ -77,6 +82,23 @@ async function signIn(): Promise<void> {
|
|
|
77
82
|
await window.requestAuth();
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
85
|
+
if (isAndroid) {
|
|
86
|
+
const credential = await requestNativeGoogleCredential();
|
|
87
|
+
const result = await hostedAuthClient.signIn.social({
|
|
88
|
+
provider: "google",
|
|
89
|
+
idToken: {
|
|
90
|
+
token: credential.idToken,
|
|
91
|
+
nonce: credential.nonce,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (result.error) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"FrockBot could not verify the Google sign-in. Please try again.",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
window.location.reload();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
80
102
|
const query = electronAuthQuery();
|
|
81
103
|
const callbackURL = new URL("/", window.location.origin);
|
|
82
104
|
if (query) {
|
|
@@ -138,7 +160,13 @@ onBeforeUnmount(() => {
|
|
|
138
160
|
<div class="auth-mark" aria-hidden="true">⌁</div>
|
|
139
161
|
<p class="auth-eyebrow">FrockBot</p>
|
|
140
162
|
<h1 id="auth-title">Welcome back</h1>
|
|
141
|
-
<p class="auth-copy">
|
|
163
|
+
<p class="auth-copy">
|
|
164
|
+
{{
|
|
165
|
+
isAndroid
|
|
166
|
+
? "Sign in to continue."
|
|
167
|
+
: "Sign in with your browser to continue."
|
|
168
|
+
}}
|
|
169
|
+
</p>
|
|
142
170
|
<div v-if="loading" class="auth-loading" aria-live="polite">
|
|
143
171
|
Checking your session…
|
|
144
172
|
</div>
|
|
@@ -159,7 +187,13 @@ onBeforeUnmount(() => {
|
|
|
159
187
|
@click="signIn"
|
|
160
188
|
>
|
|
161
189
|
<span class="google-g" aria-hidden="true">G</span>
|
|
162
|
-
{{
|
|
190
|
+
{{
|
|
191
|
+
signingIn
|
|
192
|
+
? isAndroid
|
|
193
|
+
? "Signing in…"
|
|
194
|
+
: "Waiting for browser…"
|
|
195
|
+
: "Continue with Google"
|
|
196
|
+
}}
|
|
163
197
|
</button>
|
|
164
198
|
</div>
|
|
165
199
|
<p v-if="error" class="auth-error" role="alert">{{ error }}</p>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isAndroidNativeShell,
|
|
4
|
+
requestNativeGoogleCredential,
|
|
5
|
+
} from "./native-google.ts";
|
|
6
|
+
|
|
7
|
+
describe("native Google sign-in", () => {
|
|
8
|
+
test("detects only the native Android bridge", () => {
|
|
9
|
+
expect(
|
|
10
|
+
isAndroidNativeShell({
|
|
11
|
+
isNativePlatform: () => true,
|
|
12
|
+
getPlatform: () => "android",
|
|
13
|
+
}),
|
|
14
|
+
).toBe(true);
|
|
15
|
+
expect(
|
|
16
|
+
isAndroidNativeShell({
|
|
17
|
+
isNativePlatform: () => true,
|
|
18
|
+
getPlatform: () => "ios",
|
|
19
|
+
}),
|
|
20
|
+
).toBe(false);
|
|
21
|
+
expect(
|
|
22
|
+
isAndroidNativeShell({
|
|
23
|
+
isNativePlatform: () => false,
|
|
24
|
+
getPlatform: () => "web",
|
|
25
|
+
}),
|
|
26
|
+
).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("decodes the bounded native credential", async () => {
|
|
30
|
+
await expect(
|
|
31
|
+
requestNativeGoogleCredential({
|
|
32
|
+
signIn: () =>
|
|
33
|
+
Promise.resolve({ idToken: "header.payload.signature", nonce: "n" }),
|
|
34
|
+
}),
|
|
35
|
+
).resolves.toEqual({
|
|
36
|
+
idToken: "header.payload.signature",
|
|
37
|
+
nonce: "n",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("rejects malformed native responses", async () => {
|
|
42
|
+
await expect(
|
|
43
|
+
requestNativeGoogleCredential({
|
|
44
|
+
signIn: () => Promise.resolve({ idToken: "token" }),
|
|
45
|
+
}),
|
|
46
|
+
).rejects.toThrow("Google returned an invalid sign-in response.");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Capacitor, registerPlugin } from "@capacitor/core";
|
|
2
|
+
|
|
3
|
+
const MAX_ID_TOKEN_LENGTH = 16_384;
|
|
4
|
+
const MAX_NONCE_LENGTH = 512;
|
|
5
|
+
|
|
6
|
+
interface NativePlatform {
|
|
7
|
+
isNativePlatform(): boolean;
|
|
8
|
+
getPlatform(): string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface NativeGoogleCredential {
|
|
12
|
+
idToken: string;
|
|
13
|
+
nonce: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface NativeGoogleAuthPlugin {
|
|
17
|
+
signIn(): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const nativeGoogleAuth =
|
|
21
|
+
registerPlugin<NativeGoogleAuthPlugin>("FrockBotGoogleAuth");
|
|
22
|
+
|
|
23
|
+
export function isAndroidNativeShell(
|
|
24
|
+
platform: NativePlatform = Capacitor,
|
|
25
|
+
): boolean {
|
|
26
|
+
return platform.isNativePlatform() && platform.getPlatform() === "android";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function requestNativeGoogleCredential(
|
|
30
|
+
plugin: NativeGoogleAuthPlugin = nativeGoogleAuth,
|
|
31
|
+
): Promise<NativeGoogleCredential> {
|
|
32
|
+
const result = await plugin.signIn();
|
|
33
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) {
|
|
34
|
+
throw new Error("Google returned an invalid sign-in response.");
|
|
35
|
+
}
|
|
36
|
+
const keys = Object.keys(result);
|
|
37
|
+
const value = result as Record<string, unknown>;
|
|
38
|
+
if (
|
|
39
|
+
keys.length !== 2 ||
|
|
40
|
+
!keys.includes("idToken") ||
|
|
41
|
+
!keys.includes("nonce") ||
|
|
42
|
+
typeof value.idToken !== "string" ||
|
|
43
|
+
!value.idToken ||
|
|
44
|
+
value.idToken.length > MAX_ID_TOKEN_LENGTH ||
|
|
45
|
+
typeof value.nonce !== "string" ||
|
|
46
|
+
!value.nonce ||
|
|
47
|
+
value.nonce.length > MAX_NONCE_LENGTH
|
|
48
|
+
) {
|
|
49
|
+
throw new Error("Google returned an invalid sign-in response.");
|
|
50
|
+
}
|
|
51
|
+
return { idToken: value.idToken, nonce: value.nonce };
|
|
52
|
+
}
|