import Database from 'better-sqlite3'; import { beforeEach, describe, expect, it } from 'vitest'; import { createSharedStorageSql as createCloudflareStorage } from '../packages/cloudflare/src/index.js'; import { createSharedStorage as createSqliteStorage } from '../packages/storage-sqlite/src/index.js'; // The two adapters answer the same contract from mirrored schemas and share no // code: one runs on better-sqlite3, the other on Durable Object SQLite. Running // one suite against both is what keeps them honest about the same behaviour. const DID = 'did:plc:alice'; const OTHER_DID = 'did:plc:bob'; /** * A Durable Object `state.storage.sql` over better-sqlite3. The dialects differ * in places, so a pass here is not a promise the query runs on the real thing. * @param {Database.Database} db */ function createDoSql(db) { return { /** @param {string} query @param {...unknown} params */ exec(query, ...params) { let statement; try { statement = db.prepare(query); } catch (err) { // A schema string holds several statements; prepare() takes one. if (!/more than one statement/.test(String(err))) throw err; db.exec(query); return { toArray: () => [], rowsRead: 0, rowsWritten: 0 }; } if (statement.reader) { const rows = statement.all(...params); return { toArray: () => rows, rowsRead: rows.length, rowsWritten: 0 }; } const info = statement.run(...params); return { toArray: () => [], rowsRead: 0, rowsWritten: info.changes }; }, }; } const adapters = { 'storage-sqlite': () => createSqliteStorage(new Database(':memory:')), 'cloudflare-do-sqlite': () => createCloudflareStorage( /** @type {any} */ (createDoSql(new Database(':memory:'))), ), }; for (const [name, create] of Object.entries(adapters)) { describe(`app password storage (${name})`, () => { /** @type {ReturnType} */ let storage; beforeEach(() => { storage = create(); }); it('round-trips a password by hash', async () => { const stored = await storage.putAppPassword( DID, 'client', 'hash-a', false, '2026-01-01T00:00:00.000Z', ); expect(stored).toBe(true); expect(await storage.getAppPasswordByHash(DID, 'hash-a')).toEqual({ name: 'client', privileged: false, createdAt: '2026-01-01T00:00:00.000Z', }); }); it('preserves the privileged flag', async () => { await storage.putAppPassword( DID, 'trusted', 'hash-p', true, '2026-01-01T00:00:00.000Z', ); const found = await storage.getAppPasswordByHash(DID, 'hash-p'); expect(found?.privileged).toBe(true); }); it('returns null for an unknown hash', async () => { expect(await storage.getAppPasswordByHash(DID, 'nope')).toBeNull(); }); it('refuses a duplicate name without disturbing the original', async () => { await storage.putAppPassword( DID, 'client', 'hash-first', false, '2026-01-01T00:00:00.000Z', ); const second = await storage.putAppPassword( DID, 'client', 'hash-second', true, '2026-01-02T00:00:00.000Z', ); expect(second).toBe(false); // The first password must still authenticate, and the rejected one must not. expect( await storage.getAppPasswordByHash(DID, 'hash-first'), ).not.toBeNull(); expect(await storage.getAppPasswordByHash(DID, 'hash-second')).toBeNull(); }); it('scopes passwords to their account', async () => { await storage.putAppPassword( DID, 'client', 'hash-shared', false, '2026-01-01T00:00:00.000Z', ); // Same hash, different DID: must not authenticate. In practice the hash // is salted per-DID so this cannot collide, but the query must not rely // on that. expect( await storage.getAppPasswordByHash(OTHER_DID, 'hash-shared'), ).toBeNull(); expect(await storage.listAppPasswords(OTHER_DID)).toEqual([]); }); it('lists passwords oldest first and never exposes a hash', async () => { await storage.putAppPassword( DID, 'second', 'h2', false, '2026-02-01T00:00:00.000Z', ); await storage.putAppPassword( DID, 'first', 'h1', true, '2026-01-01T00:00:00.000Z', ); const listed = await storage.listAppPasswords(DID); expect(listed.map((p) => p.name)).toEqual(['first', 'second']); expect(listed[0]).toEqual({ name: 'first', privileged: true, createdAt: '2026-01-01T00:00:00.000Z', }); for (const entry of listed) { expect(Object.keys(entry).sort()).toEqual([ 'createdAt', 'name', 'privileged', ]); } }); it('deletes by name and stops the hash authenticating', async () => { await storage.putAppPassword( DID, 'client', 'hash-x', false, '2026-01-01T00:00:00.000Z', ); expect(await storage.deleteAppPassword(DID, 'client')).toBe(true); expect(await storage.getAppPasswordByHash(DID, 'hash-x')).toBeNull(); expect(await storage.listAppPasswords(DID)).toEqual([]); }); it('reports deleting a name that is not there', async () => { expect(await storage.deleteAppPassword(DID, 'ghost')).toBe(false); }); it('frees the name once revoked', async () => { await storage.putAppPassword( DID, 'client', 'hash-old', false, '2026-01-01T00:00:00.000Z', ); await storage.deleteAppPassword(DID, 'client'); const reused = await storage.putAppPassword( DID, 'client', 'hash-new', false, '2026-01-03T00:00:00.000Z', ); expect(reused).toBe(true); expect( await storage.getAppPasswordByHash(DID, 'hash-new'), ).not.toBeNull(); expect(await storage.getAppPasswordByHash(DID, 'hash-old')).toBeNull(); }); }); describe(`oauth token storage (${name})`, () => { /** @type {ReturnType} */ let storage; beforeEach(() => { storage = create(); }); it("lists a DID's tokens with the key needed to revoke one", async () => { await storage.putOAuthToken('refresh-a', { did: DID, clientId: 'https://app.example', scope: 'atproto', createdAt: 1, }); await storage.putOAuthToken('refresh-b', { did: OTHER_DID, clientId: 'https://other.example', createdAt: 2, }); const listed = await storage.listOAuthTokensByDid(DID); expect(listed).toHaveLength(1); expect(listed[0].clientId).toBe('https://app.example'); expect(listed[0].tokenId).toBe('refresh-a'); }); it('deletes the token a listing pointed at', async () => { await storage.putOAuthToken('refresh-a', { did: DID, createdAt: 1 }); await storage.putOAuthToken('refresh-c', { did: DID, createdAt: 2 }); const [first] = await storage.listOAuthTokensByDid(DID); await storage.deleteOAuthToken(/** @type {string} */ (first.tokenId)); const remaining = await storage.listOAuthTokensByDid(DID); expect(remaining).toHaveLength(1); expect(remaining[0].tokenId).not.toBe(first.tokenId); }); }); describe(`passkey storage (${name})`, () => { /** @type {ReturnType} */ let storage; /** @param {string} credentialId @param {string} did */ const record = (credentialId, did = DID) => ({ credentialId, did, name: 'laptop', publicKey: 'spki-bytes', algorithm: -7, signCount: 0, createdAt: '2026-01-01T00:00:00.000Z', lastUsedAt: null, }); beforeEach(() => { storage = create(); }); it('round-trips a credential by its id alone', async () => { expect(await storage.putPasskey(record('cred-a'))).toBe(true); // A sign-in knows the credential and not the account const found = await storage.getPasskey('cred-a'); expect(found?.did).toBe(DID); expect(found?.publicKey).toBe('spki-bytes'); expect(found?.algorithm).toBe(-7); }); it("lists only the account's own credentials", async () => { await storage.putPasskey(record('cred-a')); await storage.putPasskey(record('cred-b', OTHER_DID)); const listed = await storage.listPasskeys(DID); expect(listed).toHaveLength(1); expect(listed[0].credentialId).toBe('cred-a'); }); it('refuses to register the same credential twice', async () => { await storage.putPasskey(record('cred-a')); expect(await storage.putPasskey(record('cred-a'))).toBe(false); }); it('records the counter and last use', async () => { await storage.putPasskey(record('cred-a')); await storage.touchPasskey('cred-a', 42, '2026-02-02T00:00:00.000Z'); const found = await storage.getPasskey('cred-a'); expect(found?.signCount).toBe(42); expect(found?.lastUsedAt).toBe('2026-02-02T00:00:00.000Z'); }); it('deletes only on a matching account', async () => { await storage.putPasskey(record('cred-a')); expect(await storage.deletePasskey(OTHER_DID, 'cred-a')).toBe(false); expect(await storage.getPasskey('cred-a')).not.toBeNull(); expect(await storage.deletePasskey(DID, 'cred-a')).toBe(true); expect(await storage.getPasskey('cred-a')).toBeNull(); expect(await storage.listPasskeys(DID)).toHaveLength(0); }); }); }