nid-webauthn-emulator 0.2.7 → 0.2.9

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.ja.md CHANGED
@@ -201,13 +201,32 @@ test.describe("Passkeys Tests", { tag: ["@daily"] }, () => {
201
201
  await page.evaluate(BrowserInjection.HookWebAuthnApis);
202
202
  ```
203
203
 
204
- `BrowserInjection.HookWebAuthnApis` は JavaScript 関数のシリアライズされた文字列であり、評価すると下記のような処理を行います。
204
+ ## ユニットテストでの WebAuthn API フック
205
205
 
206
- - `navigator.credentials.get` の定義を上書きし、`window.webAuthnEmulatorGet` を呼び出す
207
- - `navigator.credentials.create` の定義を上書きし、`window.webAuthnEmulatorCreate` を呼び出す
208
- - `PublicKeyCredential.signalUnknownCredential` の定義を追加し、`window.webAuthnEmulatorSignalUnknownCredential` を呼び出す
206
+ Vitest/Jest などで WebAuthn API を差し替えたい場合は、`src/test-utils/unit-test.ts` の `createPasskeysEmulator` を使うと `PublicKeyCredential` / `navigator.credentials` を簡単にモックできます。
209
207
 
210
- これにより先ほどの `exposeFunction` で定義した `WebAuthnEmulator` のメソッドが、`navigator.credentials.get` および `navigator.credentials.create` 呼び出し時に実行されるようになります。これらの処理中にはテストコンテキストと Playwright のコンテキスト間の通信のためにデータのシリアライズおよびデシリアライズが行われるため、そのための処理も含まれています。
208
+ ```TypeScript
209
+ import { createPasskeysEmulator } from "nid-webauthn-emulator";
210
+
211
+ const { interface: methods, addPasskey } = createPasskeysEmulator({
212
+ origin: "http://localhost",
213
+ rpId: "localhost",
214
+ autofill: true, // false にすると isConditionalMediationAvailable が false を返す
215
+ // creationException / requestException を指定すると DOMException が発生する挙動を注入できます
216
+ });
217
+
218
+ params?.existingPasskeys?.forEach((passkey) => {
219
+ addPasskey(passkey); // テスト開始前に登録済みパスキーを追加
220
+ });
221
+
222
+ // 既存の実装を消し、エミュレータを差し込む
223
+ const undefinedProperty = { value: undefined, configurable: true };
224
+ Object.defineProperty(window, "PublicKeyCredential", undefinedProperty);
225
+ Object.defineProperty(window.navigator, "credentials", undefinedProperty);
226
+
227
+ vi.spyOn(window, "PublicKeyCredential", "get").mockReturnValue(methods.publicKeyCredentials);
228
+ vi.spyOn(window.navigator, "credentials", "get").mockReturnValue(methods.credentialsContainer);
229
+ ```
211
230
 
212
231
  ## ライセンス
213
232
 
package/README.md CHANGED
@@ -210,8 +210,35 @@ await page.evaluate(BrowserInjection.HookWebAuthnApis);
210
210
 
211
211
  This ensures that the `WebAuthnEmulator` methods defined earlier with `exposeFunction` are executed when `navigator.credentials.get` and `navigator.credentials.create` are called. These processes include serialization and deserialization of data for communication between the test context and Playwright context.
212
212
 
213
+ ## Hooking WebAuthn APIs in Unit Tests
214
+
215
+ If you want to swap out WebAuthn APIs in Vitest/Jest, you can mock `PublicKeyCredential` / `navigator.credentials` using `createPasskeysEmulator` from `src/test-utils/unit-test.ts`.
216
+
217
+ ```TypeScript
218
+ import { createPasskeysEmulator } from "nid-webauthn-emulator";
219
+
220
+ const { interface: methods, addPasskey } = createPasskeysEmulator({
221
+ origin: "http://localhost",
222
+ rpId: "localhost",
223
+ autofill: true, // returns false from isConditionalMediationAvailable when set to false
224
+ // specify creationException / requestException to inject a DOMException
225
+ });
226
+
227
+ params?.existingPasskeys?.forEach((passkey) => {
228
+ addPasskey(passkey); // add pre-registered passkeys before the test starts
229
+ });
230
+
231
+ // remove existing implementations and inject the emulator
232
+ const undefinedProperty = { value: undefined, configurable: true };
233
+ Object.defineProperty(window, "PublicKeyCredential", undefinedProperty);
234
+ Object.defineProperty(window.navigator, "credentials", undefinedProperty);
235
+
236
+ vi.spyOn(window, "PublicKeyCredential", "get").mockReturnValue(methods.publicKeyCredentials);
237
+ vi.spyOn(window.navigator, "credentials", "get").mockReturnValue(methods.credentialsContainer);
238
+ ```
239
+
213
240
  ## License
214
241
 
215
242
  MIT License
216
243
 
217
- Copyright (C) 2024 Nikkei Inc.
244
+ Copyright (C) 2024 Nikkei Inc.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const globals_1 = require("@jest/globals");
7
+ const authenticator_emulator_1 = require("../../src/authenticator/authenticator-emulator");
8
+ const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
9
+ const unit_test_1 = require("../../src/test-utils/unit-test");
10
+ const webauthn_model_json_1 = require("../../src/webauthn/webauthn-model-json");
11
+ const globalScope = globalThis;
12
+ const originalLocation = globalScope.location;
13
+ const originalBtoa = globalScope.btoa;
14
+ const originalAtob = globalScope.atob;
15
+ (0, globals_1.beforeAll)(() => {
16
+ if (!globalScope.btoa) {
17
+ globalScope.btoa = (data) => Buffer.from(data, "binary").toString("base64");
18
+ }
19
+ if (!globalScope.atob) {
20
+ globalScope.atob = (base64) => Buffer.from(base64, "base64").toString("binary");
21
+ }
22
+ });
23
+ const clearDefaultRepository = () => {
24
+ const repository = new authenticator_emulator_1.AuthenticatorEmulator().params.credentialsRepository;
25
+ repository?.loadCredentials().forEach((credential) => {
26
+ repository.deleteCredential(credential);
27
+ });
28
+ };
29
+ (0, globals_1.beforeEach)(() => {
30
+ clearDefaultRepository();
31
+ });
32
+ (0, globals_1.afterEach)(() => {
33
+ globalScope.location = originalLocation;
34
+ clearDefaultRepository();
35
+ });
36
+ (0, globals_1.afterAll)(() => {
37
+ globalScope.location = originalLocation;
38
+ globalScope.btoa = originalBtoa;
39
+ globalScope.atob = originalAtob;
40
+ });
41
+ (0, globals_1.describe)("createPasskeysEmulator", () => {
42
+ (0, globals_1.test)("exposes default passkeys-like interfaces", async () => {
43
+ const emulator = (0, unit_test_1.createPasskeysEmulator)();
44
+ (0, globals_1.expect)(await emulator.methods.publicKeyCredentials.isConditionalMediationAvailable?.()).toBe(true);
45
+ (0, globals_1.expect)(await emulator.methods.publicKeyCredentials.getClientCapabilities()).toEqual({ conditionalGet: true });
46
+ (0, globals_1.expect)(await emulator.methods.publicKeyCredentials.isUserVerifyingPlatformAuthenticatorAvailable()).toBe(true);
47
+ (0, globals_1.expect)(emulator.methods.publicKeyCredentials.parseCreationOptionsFromJSON).toBe(webauthn_model_json_1.parseCreationOptionsFromJSON);
48
+ (0, globals_1.expect)(emulator.methods.publicKeyCredentials.parseRequestOptionsFromJSON).toBe(webauthn_model_json_1.parseRequestOptionsFromJSON);
49
+ });
50
+ (0, globals_1.test)("addPasskey stores credentials for custom rpId and allows authentication", async () => {
51
+ globalScope.location = { origin: "https://example.com" };
52
+ const emulator = (0, unit_test_1.createPasskeysEmulator)({ origin: "https://example.com", rpId: "example.com" });
53
+ emulator.addPasskey("user-123");
54
+ const repository = emulator.instance.authenticator.params.credentialsRepository;
55
+ const credentials = repository?.loadCredentials() ?? [];
56
+ (0, globals_1.expect)(credentials).toHaveLength(1);
57
+ (0, globals_1.expect)(credentials[0].publicKeyCredentialSource.rpId.value).toBe("example.com");
58
+ const credentialId = encode_utils_1.default.encodeBase64Url(credentials[0].publicKeyCredentialSource.id);
59
+ const requestOptions = emulator.methods.publicKeyCredentials.parseRequestOptionsFromJSON({
60
+ challenge: encode_utils_1.default.encodeBase64Url(Buffer.from("challenge")),
61
+ rpId: "example.com",
62
+ allowCredentials: [{ type: "public-key", id: credentialId }],
63
+ });
64
+ const assertion = (await emulator.methods.credentialsContainer.get({
65
+ publicKey: requestOptions,
66
+ }));
67
+ (0, globals_1.expect)(assertion.id).toBe(credentialId);
68
+ const userHandle = assertion.response.userHandle;
69
+ (0, globals_1.expect)(encode_utils_1.default.encodeBase64Url(new Uint8Array(userHandle ?? new ArrayBuffer(0)))).toBe(encode_utils_1.default.encodeBase64Url(Buffer.from("user-123")));
70
+ });
71
+ (0, globals_1.test)("creationException bubbles up as DOMException", async () => {
72
+ globalScope.location = { origin: "http://localhost" };
73
+ const emulator = (0, unit_test_1.createPasskeysEmulator)({ creationException: "AbortError" });
74
+ const creationOptions = emulator.methods.publicKeyCredentials.parseCreationOptionsFromJSON({
75
+ challenge: encode_utils_1.default.encodeBase64Url(Buffer.from("challenge")),
76
+ rp: { name: "Test RP", id: "localhost" },
77
+ user: {
78
+ id: encode_utils_1.default.encodeBase64Url(Buffer.from("creation-user")),
79
+ name: "creation-user",
80
+ displayName: "",
81
+ },
82
+ pubKeyCredParams: [{ alg: -7, type: "public-key" }],
83
+ });
84
+ const createCall = emulator.methods.credentialsContainer.create({ publicKey: creationOptions });
85
+ await (0, globals_1.expect)(createCall).rejects.toThrow(DOMException);
86
+ await (0, globals_1.expect)(createCall).rejects.toMatchObject({ name: "AbortError" });
87
+ });
88
+ (0, globals_1.test)("requestException is thrown when fetching assertions", async () => {
89
+ globalScope.location = { origin: "http://localhost" };
90
+ const emulator = (0, unit_test_1.createPasskeysEmulator)({ requestException: "InvalidStateError" });
91
+ emulator.addPasskey("auth-user");
92
+ const repository = emulator.instance.authenticator.params.credentialsRepository;
93
+ const credentials = repository?.loadCredentials() ?? [];
94
+ (0, globals_1.expect)(credentials).toHaveLength(1);
95
+ const credentialId = encode_utils_1.default.encodeBase64Url(credentials[0].publicKeyCredentialSource.id);
96
+ const requestOptions = emulator.methods.publicKeyCredentials.parseRequestOptionsFromJSON({
97
+ challenge: encode_utils_1.default.encodeBase64Url(Buffer.from("request")),
98
+ rpId: "localhost",
99
+ allowCredentials: [{ type: "public-key", id: credentialId }],
100
+ });
101
+ const parsedId = encode_utils_1.default.encodeBase64Url(new Uint8Array(requestOptions.allowCredentials?.[0].id ?? new ArrayBuffer(0)));
102
+ (0, globals_1.expect)(parsedId).toBe(credentialId);
103
+ try {
104
+ await emulator.methods.credentialsContainer.get({ publicKey: requestOptions });
105
+ throw new Error("Expected DOMException");
106
+ }
107
+ catch (error) {
108
+ (0, globals_1.expect)(error).toBeInstanceOf(DOMException);
109
+ (0, globals_1.expect)(error.name).toBe("InvalidStateError");
110
+ }
111
+ });
112
+ (0, globals_1.test)("disables conditional mediation when autofill flag is false", async () => {
113
+ const emulator = (0, unit_test_1.createPasskeysEmulator)({ autofill: false });
114
+ await (0, globals_1.expect)(emulator.methods.publicKeyCredentials.isConditionalMediationAvailable?.()).resolves.toBe(false);
115
+ });
116
+ });
@@ -5,6 +5,7 @@ export * from "./authenticator/ctap-model";
5
5
  export * from "./repository/credentials-file-repository";
6
6
  export * from "./repository/credentials-memory-repository";
7
7
  export * from "./repository/credentials-repository";
8
+ export * from "./test-utils/unit-test";
8
9
  export * from "./webauthn/cose-key";
9
10
  export * from "./webauthn/webauthn-emulator";
10
11
  export * from "./webauthn/webauthn-model";
package/dist/src/index.js CHANGED
@@ -23,6 +23,7 @@ __exportStar(require("./authenticator/ctap-model"), exports);
23
23
  __exportStar(require("./repository/credentials-file-repository"), exports);
24
24
  __exportStar(require("./repository/credentials-memory-repository"), exports);
25
25
  __exportStar(require("./repository/credentials-repository"), exports);
26
+ __exportStar(require("./test-utils/unit-test"), exports);
26
27
  __exportStar(require("./webauthn/cose-key"), exports);
27
28
  __exportStar(require("./webauthn/webauthn-emulator"), exports);
28
29
  __exportStar(require("./webauthn/webauthn-model"), exports);
@@ -46,7 +46,7 @@ function encodeCbor(data) {
46
46
  if (typeof value === "object" && value !== null) {
47
47
  const encodedData = new Map();
48
48
  for (const [k, v] of Object.entries(value)) {
49
- const ki = Number.parseInt(k);
49
+ const ki = Number.parseInt(k, 10);
50
50
  if (Number.isNaN(ki)) {
51
51
  encodedData.set(k, encoder(v));
52
52
  }
@@ -0,0 +1,31 @@
1
+ import { WebAuthnEmulator } from "../webauthn/webauthn-emulator";
2
+ import { type AllAcceptedCredentialsOptionsJSON, type CurrentUserDetailsOptionsJSON, type UnknownCredentialOptionsJSON } from "../webauthn/webauthn-model-json";
3
+ type PasskeysEmulatorParams = {
4
+ origin?: string;
5
+ autofill?: boolean;
6
+ creationException?: string;
7
+ requestException?: string;
8
+ rpId?: string;
9
+ };
10
+ export declare const createPasskeysEmulator: (params?: PasskeysEmulatorParams) => {
11
+ instance: WebAuthnEmulator;
12
+ addPasskey: (userId: string) => import("../webauthn/webauthn-model-json").RegistrationResponseJSON;
13
+ methods: {
14
+ credentialsContainer: CredentialsContainer;
15
+ publicKeyCredentials: {
16
+ new (): PublicKeyCredential;
17
+ prototype: PublicKeyCredential;
18
+ getClientCapabilities(): Promise<PublicKeyCredentialClientCapabilities>;
19
+ isConditionalMediationAvailable(): Promise<boolean>;
20
+ isUserVerifyingPlatformAuthenticatorAvailable(): Promise<boolean>;
21
+ parseCreationOptionsFromJSON(options: PublicKeyCredentialCreationOptionsJSON): PublicKeyCredentialCreationOptions;
22
+ parseRequestOptionsFromJSON(options: PublicKeyCredentialRequestOptionsJSON): PublicKeyCredentialRequestOptions;
23
+ } & {
24
+ signalUnknownCredential(options: UnknownCredentialOptionsJSON): Promise<void>;
25
+ signalAllAcceptedCredentials(options: AllAcceptedCredentialsOptionsJSON): Promise<void>;
26
+ signalCurrentUserDetails(options: CurrentUserDetailsOptionsJSON): Promise<void>;
27
+ isConditionalMediationAvailable?(): Promise<boolean>;
28
+ };
29
+ };
30
+ };
31
+ export {};
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPasskeysEmulator = void 0;
4
+ const authenticator_emulator_1 = require("../authenticator/authenticator-emulator");
5
+ const webauthn_emulator_1 = require("../webauthn/webauthn-emulator");
6
+ const webauthn_model_json_1 = require("../webauthn/webauthn-model-json");
7
+ const createInteraction = (exception) => exception
8
+ ? () => {
9
+ throw new DOMException("test", exception);
10
+ }
11
+ : undefined;
12
+ const addTestPasskey = (emulator, origin, userId, rpId = "localhost") => {
13
+ const creationOption = {
14
+ challenge: "EA3d-yrkJAfNWICZ7It5ErO0XngxTe32L0t8IjEj9r8",
15
+ rp: { name: "Test RP", id: rpId },
16
+ user: {
17
+ id: (0, webauthn_model_json_1.encodeBase64Url)(Buffer.from(userId)),
18
+ name: "test",
19
+ displayName: "",
20
+ },
21
+ pubKeyCredParams: [
22
+ { alg: -8, type: "public-key" },
23
+ { alg: -7, type: "public-key" },
24
+ { alg: -257, type: "public-key" },
25
+ ],
26
+ };
27
+ return emulator.createJSON(origin, creationOption);
28
+ };
29
+ const createPasskeysEmulator = (params) => {
30
+ const authenticator = new authenticator_emulator_1.AuthenticatorEmulator({
31
+ userMakeCredentialInteraction: createInteraction(params?.creationException),
32
+ userGetAssertionInteraction: createInteraction(params?.requestException),
33
+ });
34
+ const instance = new webauthn_emulator_1.WebAuthnEmulator(authenticator);
35
+ const origin = params?.origin ?? "http://localhost";
36
+ const publicKeyCredentials = {
37
+ isConditionalMediationAvailable: async () => Promise.resolve(params?.autofill ?? true),
38
+ signalUnknownCredential: async (options) => instance.signalUnknownCredential(options),
39
+ signalAllAcceptedCredentials: async (options) => instance.signalAllAcceptedCredentials(options),
40
+ signalCurrentUserDetails: async (options) => instance.signalCurrentUserDetails(options),
41
+ getClientCapabilities: async () => ({ conditionalGet: true }),
42
+ isUserVerifyingPlatformAuthenticatorAvailable: async () => true,
43
+ parseCreationOptionsFromJSON: webauthn_model_json_1.parseCreationOptionsFromJSON,
44
+ parseRequestOptionsFromJSON: webauthn_model_json_1.parseRequestOptionsFromJSON,
45
+ };
46
+ const credentialsContainer = {
47
+ get: async (options) => instance.get(origin, options ?? {}),
48
+ create: async (options) => instance.create(origin, options ?? {}),
49
+ };
50
+ const addPasskey = (userId) => addTestPasskey(instance, origin, userId, params?.rpId);
51
+ return {
52
+ instance,
53
+ addPasskey,
54
+ methods: {
55
+ credentialsContainer,
56
+ publicKeyCredentials,
57
+ },
58
+ };
59
+ };
60
+ exports.createPasskeysEmulator = createPasskeysEmulator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nid-webauthn-emulator",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "keywords": [
5
5
  "test",
6
6
  "e2e",
@@ -15,19 +15,19 @@
15
15
  "main": "dist/src/index.js",
16
16
  "types": "dist/src/index.d.ts",
17
17
  "devDependencies": {
18
- "@biomejs/biome": "^2.2.4",
18
+ "@biomejs/biome": "^2.3.8",
19
19
  "@jest/globals": "^30.2.0",
20
- "@simplewebauthn/server": "^13.2.1",
21
- "@tsconfig/recommended": "^1.0.10",
22
- "@types/node": "^24.5.2",
20
+ "@simplewebauthn/server": "^13.2.2",
21
+ "@tsconfig/recommended": "^1.0.13",
22
+ "@types/node": "^24.10.1",
23
23
  "jest": "^30.2.0",
24
- "ts-jest": "^29.4.4",
24
+ "ts-jest": "^29.4.5",
25
25
  "ts-node": "^10.9.2",
26
- "typescript": "^5.9.2"
26
+ "typescript": "^5.9.3"
27
27
  },
28
28
  "dependencies": {
29
29
  "cbor": "^10.0.11",
30
- "tldts": "^7.0.16"
30
+ "tldts": "^7.0.19"
31
31
  },
32
32
  "scripts": {
33
33
  "check": "biome check .",