@celestia-island/hikari 0.50.0 → 0.50.2
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 +1 -1
- package/src/components/HkPasswordInput.tsx +2 -1
- package/src/components/HkSignInCard.tsx +8 -5
- package/src/components/HkStepFlow.tsx +0 -1
- package/src/runtime/credentialAutofill.test.ts +90 -0
- package/src/runtime/credentialAutofill.ts +38 -0
- package/src/runtime/index.ts +1 -0
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "vue";
|
|
10
10
|
|
|
11
11
|
import { useI18n } from "../i18n/context";
|
|
12
|
+
import { credentialAutocomplete } from "../runtime/credentialAutofill";
|
|
12
13
|
|
|
13
14
|
import { onFrame, onceFrame, type AnimationHandle } from "../runtime/animationBus";
|
|
14
15
|
import { scheduleCronAfter, type CronHandle } from "../runtime/cronBus";
|
|
@@ -763,7 +764,7 @@ export default defineComponent({
|
|
|
763
764
|
type="password"
|
|
764
765
|
value={props.modelValue}
|
|
765
766
|
name={props.name}
|
|
766
|
-
autocomplete={props.autocomplete ?? "off"}
|
|
767
|
+
autocomplete={props.autocomplete ?? credentialAutocomplete("password", "off")}
|
|
767
768
|
data-1p-ignore
|
|
768
769
|
data-lpignore="true"
|
|
769
770
|
disabled={props.disabled}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { credentialAutocomplete } from "../runtime/credentialAutofill";
|
|
1
2
|
import { defineComponent, ref } from "vue";
|
|
2
3
|
import { useI18n } from "../i18n/context";
|
|
3
4
|
|
|
@@ -56,9 +57,11 @@ export const HkSignInCard = defineComponent({
|
|
|
56
57
|
loading: { type: Boolean, default: false },
|
|
57
58
|
/** Extra guard on top of the built-in empty-field check. */
|
|
58
59
|
disabled: { type: Boolean, default: false },
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/** Autocomplete hints. Undefined (the default) resolves through
|
|
61
|
+
* the runtime credential policy: browser = standard tokens, Tauri
|
|
62
|
+
* webview = suppression (no native credential chrome). */
|
|
63
|
+
usernameAutocomplete: { type: String, default: undefined },
|
|
64
|
+
passwordAutocomplete: { type: String, default: undefined },
|
|
62
65
|
/** Submit label; defaults to the hikari::signIn.submit locale. */
|
|
63
66
|
submitLabel: { type: String, default: undefined },
|
|
64
67
|
/** Username-field type; switch to "email" for identifier logins. */
|
|
@@ -100,7 +103,7 @@ export const HkSignInCard = defineComponent({
|
|
|
100
103
|
onUpdate:modelValue={(v: string) => (username.value = v)}
|
|
101
104
|
type={props.usernameType}
|
|
102
105
|
name="signin-username"
|
|
103
|
-
autocomplete={props.usernameAutocomplete}
|
|
106
|
+
autocomplete={props.usernameAutocomplete ?? credentialAutocomplete("username", "username")}
|
|
104
107
|
placeholder={
|
|
105
108
|
props.usernamePlaceholder ??
|
|
106
109
|
t("hikari::signIn.usernamePlaceholder", "Username")
|
|
@@ -136,7 +139,7 @@ export const HkSignInCard = defineComponent({
|
|
|
136
139
|
modelValue={password.value}
|
|
137
140
|
onUpdate:modelValue={(v: string) => (password.value = v)}
|
|
138
141
|
name="signin-password"
|
|
139
|
-
autocomplete={props.passwordAutocomplete}
|
|
142
|
+
autocomplete={props.passwordAutocomplete ?? credentialAutocomplete("password", "current-password")}
|
|
140
143
|
placeholder={t("hikari::signIn.passwordPlaceholder", "Password")}
|
|
141
144
|
disabled={props.loading || props.disabled}
|
|
142
145
|
submitOnEnter={attemptSubmit}
|
|
@@ -135,7 +135,6 @@ export default defineComponent({
|
|
|
135
135
|
<Transition
|
|
136
136
|
name={dir === "back" ? "hk-stepflow-back" : "hk-stepflow-fwd"}
|
|
137
137
|
mode="out-in"
|
|
138
|
-
appear
|
|
139
138
|
>
|
|
140
139
|
<div key={props.modelValue} class="hk-stepflow-body">
|
|
141
140
|
{slotFn?.({ key: props.modelValue, index, direction: dir })}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import { credentialAutocomplete } from "./credentialAutofill";
|
|
5
|
+
import HkPasswordInput from "../components/HkPasswordInput";
|
|
6
|
+
import { HkSignInCard } from "../components/HkSignInCard";
|
|
7
|
+
|
|
8
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
9
|
+
|
|
10
|
+
function mount(node: ReturnType<typeof h>) {
|
|
11
|
+
const container = document.createElement("div");
|
|
12
|
+
document.body.appendChild(container);
|
|
13
|
+
const app = createApp({ render: () => node });
|
|
14
|
+
app.mount(container);
|
|
15
|
+
mounts.push({ app, container });
|
|
16
|
+
return container;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
21
|
+
app.unmount();
|
|
22
|
+
container.remove();
|
|
23
|
+
}
|
|
24
|
+
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function setTauri(on: boolean) {
|
|
28
|
+
const w = window as unknown as Record<string, unknown>;
|
|
29
|
+
if (on) w.__TAURI_INTERNALS__ = { invoke: () => {} };
|
|
30
|
+
else delete w.__TAURI_INTERNALS__;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("credentialAutocomplete policy", () => {
|
|
34
|
+
it("keeps the browser defaults outside a Tauri webview", () => {
|
|
35
|
+
setTauri(false);
|
|
36
|
+
expect(credentialAutocomplete("username", "username")).toBe("username");
|
|
37
|
+
expect(credentialAutocomplete("password", "current-password")).toBe("current-password");
|
|
38
|
+
expect(credentialAutocomplete("password", "off")).toBe("off");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("degrades to suppression tokens inside a Tauri webview", () => {
|
|
42
|
+
setTauri(true);
|
|
43
|
+
expect(credentialAutocomplete("username", "username")).toBe("off");
|
|
44
|
+
// new-password is the one token the Chromium family respects for
|
|
45
|
+
// suppressing saved-credential fill; plain "off" is ignored on login forms.
|
|
46
|
+
expect(credentialAutocomplete("password", "current-password")).toBe("new-password");
|
|
47
|
+
expect(credentialAutocomplete("password", "off")).toBe("new-password");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("HkSignInCard credential fields", () => {
|
|
52
|
+
it("renders standard tokens in the browser", () => {
|
|
53
|
+
setTauri(false);
|
|
54
|
+
const c = mount(h(HkSignInCard, { title: "t", onSubmit: () => {} }));
|
|
55
|
+
const inputs = Array.from(c.querySelectorAll("input"));
|
|
56
|
+
const username = inputs[0] as HTMLInputElement;
|
|
57
|
+
const password = inputs.find((i) => i.type === "password") as HTMLInputElement;
|
|
58
|
+
expect(username.autocomplete).toBe("username");
|
|
59
|
+
expect(password.autocomplete).toBe("current-password");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders suppression tokens inside a Tauri webview", () => {
|
|
63
|
+
setTauri(true);
|
|
64
|
+
const c = mount(h(HkSignInCard, { title: "t", onSubmit: () => {} }));
|
|
65
|
+
const inputs = Array.from(c.querySelectorAll("input"));
|
|
66
|
+
const username = inputs[0] as HTMLInputElement;
|
|
67
|
+
const password = inputs.find((i) => i.type === "password") as HTMLInputElement;
|
|
68
|
+
expect(username.autocomplete).toBe("off");
|
|
69
|
+
expect(password.autocomplete).toBe("new-password");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("lets an explicit prop override the policy in Tauri too", () => {
|
|
73
|
+
setTauri(true);
|
|
74
|
+
const c = mount(h(HkSignInCard, { title: "t", onSubmit: () => {}, usernameAutocomplete: "email" }));
|
|
75
|
+
const username = c.querySelector("input") as HTMLInputElement;
|
|
76
|
+
expect(username.autocomplete).toBe("email");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("HkPasswordInput credential field", () => {
|
|
81
|
+
it("keeps the off default in the browser and degrades in Tauri", () => {
|
|
82
|
+
setTauri(false);
|
|
83
|
+
const browser = mount(h(HkPasswordInput, { modelValue: "" }));
|
|
84
|
+
expect((browser.querySelector(".hk-pwd-input") as HTMLInputElement).autocomplete).toBe("off");
|
|
85
|
+
|
|
86
|
+
setTauri(true);
|
|
87
|
+
const tauri = mount(h(HkPasswordInput, { modelValue: "" }));
|
|
88
|
+
expect((tauri.querySelector(".hk-pwd-input") as HTMLInputElement).autocomplete).toBe("new-password");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isTauri } from "./env";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Credential-field autocomplete policy (user direction 2026-09-14): no
|
|
5
|
+
* Tauri app in the group may surface the webview's native credential
|
|
6
|
+
* machinery — the stored-password dropdown or the save-password bubble.
|
|
7
|
+
*
|
|
8
|
+
* The policy is *runtime Tauri-aware*, not absolute: the same hikari app
|
|
9
|
+
* runs in plain browsers too, where password managers are welcome and the
|
|
10
|
+
* standard tokens (`username` / `current-password`) are the correct UX.
|
|
11
|
+
*
|
|
12
|
+
* Inside a Tauri webview we degrade to the strongest suppression tokens:
|
|
13
|
+
*
|
|
14
|
+
* - password: `new-password` — the one autocomplete token Chromium-family
|
|
15
|
+
* engines (WebView2 included) respect for suppressing saved-credential
|
|
16
|
+
* fill; plain `off` is explicitly ignored by them for login forms.
|
|
17
|
+
* - username: `off` — WebView2's fill store is additionally emptied by the
|
|
18
|
+
* native hardening layer each app ships (`webview_hardening.rs` flips
|
|
19
|
+
* `ICoreWebView2Settings4.IsPasswordAutosaveEnabled` and
|
|
20
|
+
* `IsGeneralAutofillEnabled` off), so there is nothing to offer; the
|
|
21
|
+
* token here keeps the DOM contract aligned on engines without that
|
|
22
|
+
* native layer.
|
|
23
|
+
*
|
|
24
|
+
* Extensions such as 1Password/LastPass are signaled separately by the
|
|
25
|
+
* components (`data-1p-ignore` / `data-lpignore`).
|
|
26
|
+
*
|
|
27
|
+
* @param kind which credential field the token is for.
|
|
28
|
+
* @param browserDefault the token this component uses in a plain browser.
|
|
29
|
+
*/
|
|
30
|
+
export function credentialAutocomplete(
|
|
31
|
+
kind: "username" | "password",
|
|
32
|
+
browserDefault: string,
|
|
33
|
+
): string {
|
|
34
|
+
if (isTauri()) {
|
|
35
|
+
return kind === "password" ? "new-password" : "off";
|
|
36
|
+
}
|
|
37
|
+
return browserDefault;
|
|
38
|
+
}
|
package/src/runtime/index.ts
CHANGED