@bod.ee/db 0.10.1 → 0.10.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/client/BodClient.ts +5 -0
- package/src/server/Transport.ts +14 -4
- package/tests/keyauth.test.ts +28 -1
package/package.json
CHANGED
package/src/client/BodClient.ts
CHANGED
|
@@ -546,6 +546,11 @@ export class AuthFacade {
|
|
|
546
546
|
return this.client._send('auth-link-device', { accountFingerprint, password, devicePublicKey, deviceName }) as Promise<{ fingerprint: string }>;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
/** Link device by account display name (resolves fingerprint server-side) */
|
|
550
|
+
async linkDeviceByName(displayName: string, password: string, devicePublicKey: string, deviceName?: string): Promise<{ fingerprint: string }> {
|
|
551
|
+
return this.client._send('auth-link-device', { displayName, password, devicePublicKey, deviceName }) as Promise<{ fingerprint: string }>;
|
|
552
|
+
}
|
|
553
|
+
|
|
549
554
|
/** Revoke a session */
|
|
550
555
|
async revokeSession(sid: string): Promise<void> {
|
|
551
556
|
await this.client._send('auth-revoke-session', { sid });
|
package/src/server/Transport.ts
CHANGED
|
@@ -716,8 +716,18 @@ export class Transport {
|
|
|
716
716
|
}
|
|
717
717
|
case 'auth-link-device': {
|
|
718
718
|
if (!self.options.keyAuth) return error('KeyAuth not configured', Errors.INTERNAL);
|
|
719
|
+
// Resolve account: by fingerprint or displayName
|
|
720
|
+
let accountFp = msg.accountFingerprint;
|
|
721
|
+
if (!accountFp && msg.displayName) {
|
|
722
|
+
const match = self.options.keyAuth.listAccounts().find(
|
|
723
|
+
(a: any) => a.displayName?.toLowerCase() === msg.displayName.toLowerCase()
|
|
724
|
+
);
|
|
725
|
+
if (!match) return error('Account not found', Errors.AUTH_REQUIRED);
|
|
726
|
+
accountFp = match.fingerprint;
|
|
727
|
+
}
|
|
728
|
+
if (!accountFp) return error('accountFingerprint or displayName required', Errors.INVALID);
|
|
719
729
|
// Password is the auth — engine.linkDevice verifies it
|
|
720
|
-
const linked = self.options.keyAuth.linkDevice(
|
|
730
|
+
const linked = self.options.keyAuth.linkDevice(accountFp, msg.password, msg.devicePublicKey, msg.deviceName);
|
|
721
731
|
if (!linked) return error('Link failed (wrong password or account not found)', Errors.AUTH_REQUIRED);
|
|
722
732
|
return reply(linked);
|
|
723
733
|
}
|
|
@@ -737,14 +747,14 @@ export class Transport {
|
|
|
737
747
|
}
|
|
738
748
|
case 'auth-create-account': {
|
|
739
749
|
if (!self.options.keyAuth) return error('KeyAuth not configured', Errors.INTERNAL);
|
|
740
|
-
// Root-only, or allowed when zero accounts exist (bootstrap)
|
|
750
|
+
// Root-only, or allowed when zero accounts exist (bootstrap), or open registration
|
|
741
751
|
if (ws.data.auth) {
|
|
742
752
|
const ctx = ws.data.auth as { isRoot?: boolean };
|
|
743
753
|
if (!ctx.isRoot) return error('Root only', Errors.PERMISSION_DENIED);
|
|
744
754
|
} else {
|
|
745
|
-
// Only allow unauthenticated if no accounts exist yet
|
|
746
755
|
const accounts = self.options.keyAuth.listAccounts();
|
|
747
|
-
|
|
756
|
+
const allowOpen = self.options.keyAuth.options.allowOpenRegistration;
|
|
757
|
+
if (accounts.length > 0 && !allowOpen) return error('Authentication required', Errors.AUTH_REQUIRED);
|
|
748
758
|
}
|
|
749
759
|
const result = self.options.keyAuth.createAccount(msg.password, msg.roles, msg.displayName, msg.devicePublicKey);
|
|
750
760
|
return reply(result);
|
package/tests/keyauth.test.ts
CHANGED
|
@@ -796,13 +796,40 @@ describe('KeyAuth', () => {
|
|
|
796
796
|
expect(result.data.isRoot).toBe(true);
|
|
797
797
|
expect(result.data.deviceFingerprint).toBeTruthy();
|
|
798
798
|
|
|
799
|
-
//
|
|
799
|
+
// With open registration (default), second create is allowed
|
|
800
800
|
const ws2 = await openWs();
|
|
801
|
+
const second = await wsSend(ws2, { op: 'auth-create-account', password: 'user2' });
|
|
802
|
+
expect(second.ok).toBe(true);
|
|
803
|
+
expect(second.data.isRoot).toBe(false);
|
|
804
|
+
ws.close();
|
|
805
|
+
ws2.close();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('auth-create-account closed registration: second account rejected', async () => {
|
|
809
|
+
// Use a separate DB with closed registration
|
|
810
|
+
const closedPort = 14400 + Math.floor(Math.random() * 1000);
|
|
811
|
+
const closedDb = new BodDB({ path: ':memory:', sweepInterval: 0, keyAuth: { allowOpenRegistration: false } });
|
|
812
|
+
closedDb.serve({ port: closedPort });
|
|
813
|
+
const openClosedWs = () => new Promise<WebSocket>((res, rej) => {
|
|
814
|
+
const w = new WebSocket(`ws://localhost:${closedPort}`);
|
|
815
|
+
w.addEventListener('open', () => res(w));
|
|
816
|
+
w.addEventListener('error', rej);
|
|
817
|
+
});
|
|
818
|
+
const deviceKp = await browserGenerateKeyPair();
|
|
819
|
+
const ws = await openClosedWs();
|
|
820
|
+
const result = await wsSend(ws, {
|
|
821
|
+
op: 'auth-create-account', password: 'root-pw', displayName: 'Root',
|
|
822
|
+
devicePublicKey: deviceKp.publicKeyBase64,
|
|
823
|
+
});
|
|
824
|
+
expect(result.ok).toBe(true);
|
|
825
|
+
|
|
826
|
+
const ws2 = await openClosedWs();
|
|
801
827
|
const reject = await wsSend(ws2, { op: 'auth-create-account', password: 'hacker' });
|
|
802
828
|
expect(reject.ok).toBe(false);
|
|
803
829
|
expect(reject.code).toBe('AUTH_REQUIRED');
|
|
804
830
|
ws.close();
|
|
805
831
|
ws2.close();
|
|
832
|
+
closedDb.close();
|
|
806
833
|
});
|
|
807
834
|
|
|
808
835
|
it('auth-register-device via WS', async () => {
|