feat: release authentication diagnostics 0.2.0

This commit is contained in:
2026-08-19 14:13:48 +02:00
parent 603559c540
commit 53cc91f2a5
41 changed files with 3472 additions and 154 deletions
+53 -1
View File
@@ -2,10 +2,11 @@ import { describe, expect, it } from "vitest";
import {
exportCsv,
importCsv,
importGoogleMigrationBatch,
importOtpAuthList,
importPlainPskc,
} from "../../src/otp/migration";
import { utf8ToBytes } from "../../src/crypto/encoding";
import { bytesToBase64Url, utf8ToBytes } from "../../src/crypto/encoding";
import type { OtpProfile } from "../../src/otp/profile";
const profile: OtpProfile = {
@@ -20,6 +21,43 @@ const profile: OtpProfile = {
extensions: new Map(),
};
function varint(value: number): number[] {
const output: number[] = [];
let remaining = value;
do {
let byte = remaining & 0x7f;
remaining >>>= 7;
if (remaining) byte |= 0x80;
output.push(byte);
} while (remaining);
return output;
}
function field(number: number, value: number | Uint8Array): number[] {
return typeof value === "number"
? [...varint(number << 3), ...varint(value)]
: [...varint((number << 3) | 2), ...varint(value.length), ...value];
}
function googlePart(index: number, size = 2, id = 73): string {
const credential = Uint8Array.from([
...field(1, utf8ToBytes("12345678901234567890")),
...field(2, utf8ToBytes(`user-${index}`)),
...field(3, utf8ToBytes("Example")),
...field(4, 1),
...field(5, 1),
...field(6, 2),
]);
const payload = Uint8Array.from([
...field(1, credential),
...field(2, 1),
...field(3, size),
...field(4, index),
...field(5, id),
]);
return `otpauth-migration://offline?data=${bytesToBase64Url(payload)}`;
}
describe("OTP migrations", () => {
it("round-trips the documented CSV including quoting", () => {
const result = importCsv(exportCsv([profile]));
@@ -63,4 +101,18 @@ describe("OTP migrations", () => {
),
).toThrow(/encrypted/iu);
});
it("assembles Google multi-QR batches in index order", () => {
const result = importGoogleMigrationBatch([googlePart(1), googlePart(0)]);
expect(result.profiles.map((item) => item.account)).toEqual([
"user-0",
"user-1",
]);
expect(() => importGoogleMigrationBatch([googlePart(0)])).toThrow(
/missing part 2/iu,
);
expect(() =>
importGoogleMigrationBatch([googlePart(0), googlePart(1, 2, 99)]),
).toThrow(/different batches/iu);
});
});