feat(hololake): add mobile server account and capability proxy

This commit is contained in:
冰朔 2026-08-02 21:49:53 +08:00
commit 19bd38c6e3
11 changed files with 1436 additions and 1 deletions

View file

@ -0,0 +1,211 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const {
HoloLakeSessionManager,
} = require("./hololake-session");
function fixture(overrides = {}) {
let now = 1_800_000_000_000;
const mail = [];
const manager = new HoloLakeSessionManager({
registeredEmails: ["owner@example.invalid"],
pepper: "test-only-pepper-with-enough-entropy",
stateFile: "",
now: () => now,
sendEmail: async message => {
mail.push(message);
return true;
},
...overrides,
});
return {
manager,
mail,
advance(milliseconds) {
now += milliseconds;
},
};
}
test("OTP request is non-enumerating and stores no plaintext code", async () => {
const known = fixture();
const requested = await known.manager.requestOtp({
email: "Owner@Example.Invalid",
deviceId: "ios-device-001",
});
assert.equal(requested.accepted, true);
assert.match(requested.request_id, /^[0-9a-f-]{36}$/);
assert.equal(known.mail.length, 1);
assert.match(known.mail[0].text, /\b\d{6}\b/);
const otp = known.mail[0].text.match(/\b\d{6}\b/)[0];
assert.doesNotMatch(JSON.stringify(known.manager.inspectState()), new RegExp(otp));
const unknown = fixture();
const decoy = await unknown.manager.requestOtp({
email: "nobody@example.invalid",
deviceId: "ios-device-001",
});
assert.equal(decoy.accepted, requested.accepted);
assert.match(decoy.request_id, /^[0-9a-f-]{36}$/);
assert.equal(unknown.mail.length, 0);
});
test("OTP verification is device-bound, attempt-limited, and returns a one-time session token", async () => {
const state = fixture();
const requested = await state.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
});
const otp = state.mail[0].text.match(/\b\d{6}\b/)[0];
const wrongDevice = state.manager.verifyOtp({
requestId: requested.request_id,
code: otp,
deviceId: "ios-device-002",
});
assert.equal(wrongDevice.ok, false);
assert.equal(wrongDevice.error, "invalid_or_expired_code");
const verified = state.manager.verifyOtp({
requestId: requested.request_id,
code: otp,
deviceId: "ios-device-001",
});
assert.equal(verified.ok, true);
assert.match(verified.session_token, /^[A-Za-z0-9_-]{40,}$/);
assert.doesNotMatch(
JSON.stringify(state.manager.inspectState()),
new RegExp(verified.session_token),
);
const reused = state.manager.verifyOtp({
requestId: requested.request_id,
code: otp,
deviceId: "ios-device-001",
});
assert.equal(reused.ok, false);
assert.equal(reused.error, "invalid_or_expired_code");
});
test("session authentication, expiry, and revocation never return the stored token", async () => {
const state = fixture({ sessionTtlSeconds: 60 });
const requested = await state.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
});
const otp = state.mail[0].text.match(/\b\d{6}\b/)[0];
const verified = state.manager.verifyOtp({
requestId: requested.request_id,
code: otp,
deviceId: "ios-device-001",
});
const active = state.manager.authenticate(
verified.session_token,
"ios-device-001",
);
assert.equal(active.ok, true);
assert.equal(active.session.device_id, "ios-device-001");
assert.equal(Object.hasOwn(active.session, "token"), false);
assert.equal(
state.manager.authenticate(verified.session_token, "ios-device-002").error,
"session_device_mismatch",
);
assert.equal(
state.manager.revoke(verified.session_token, "ios-device-002").error,
"session_device_mismatch",
);
assert.equal(
state.manager.revoke(verified.session_token, "ios-device-001").ok,
true,
);
assert.equal(
state.manager.authenticate(verified.session_token, "ios-device-001").error,
"session_invalid",
);
const second = await state.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
});
const secondOtp = state.mail[1].text.match(/\b\d{6}\b/)[0];
const secondSession = state.manager.verifyOtp({
requestId: second.request_id,
code: secondOtp,
deviceId: "ios-device-001",
});
state.advance(61_000);
assert.equal(
state.manager.authenticate(
secondSession.session_token,
"ios-device-001",
).error,
"session_expired",
);
});
test("OTP expires, locks after five failed attempts, and request rate is bounded", async () => {
const expired = fixture({ otpTtlSeconds: 30 });
const requested = await expired.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
});
const otp = expired.mail[0].text.match(/\b\d{6}\b/)[0];
expired.advance(31_000);
assert.equal(
expired.manager.verifyOtp({
requestId: requested.request_id,
code: otp,
deviceId: "ios-device-001",
}).error,
"invalid_or_expired_code",
);
const locked = fixture({ maxOtpAttempts: 5 });
const lockRequest = await locked.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
});
const realOtp = locked.mail[0].text.match(/\b\d{6}\b/)[0];
for (let attempt = 0; attempt < 5; attempt += 1) {
assert.equal(
locked.manager.verifyOtp({
requestId: lockRequest.request_id,
code: "000000",
deviceId: "ios-device-001",
}).ok,
false,
);
}
assert.equal(
locked.manager.verifyOtp({
requestId: lockRequest.request_id,
code: realOtp,
deviceId: "ios-device-001",
}).error,
"invalid_or_expired_code",
);
const limited = fixture({ requestLimit: 2 });
await limited.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
networkKey: "198.51.100.7",
});
await limited.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
networkKey: "198.51.100.7",
});
const denied = await limited.manager.requestOtp({
email: "owner@example.invalid",
deviceId: "ios-device-001",
networkKey: "198.51.100.7",
});
assert.equal(denied.accepted, false);
assert.equal(denied.error, "rate_limited");
assert.equal(limited.mail.length, 2);
});