@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bod.ee/db",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 });
@@ -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(msg.accountFingerprint, msg.password, msg.devicePublicKey, msg.deviceName);
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
- if (accounts.length > 0) return error('Authentication required', Errors.AUTH_REQUIRED);
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);
@@ -796,13 +796,40 @@ describe('KeyAuth', () => {
796
796
  expect(result.data.isRoot).toBe(true);
797
797
  expect(result.data.deviceFingerprint).toBeTruthy();
798
798
 
799
- // Second unauthenticated create should be rejected
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 () => {