// The /account/api test harness: fake actor and shared storage with the // account's own fields, a server over them, and the cookie-session calls // the section tests make. Each package's account-API suite composes its // extension over createPds. import { createAccountJwt } from '../../packages/core/src/auth.js'; import { PersonalDataServer } from '../../packages/core/src/pds.js'; export const DID = 'did:plc:abcdefghij234567abcdefgh'; export const HANDLE = 'test.handle'; export const SECRET = 'test-secret'; export const BASE = 'https://pds.example.com'; /** * @param {Object} [overrides] - Port methods that replace the defaults */ export function createActorStorage(overrides = {}) { /** @type {import('../../packages/core/src/ports.js').EmailState|null} */ let email = null; /** @type {import('../../packages/core/src/ports.js').AccountUiState|null} */ let uiState = null; let currentStatus = 'active'; /** @type {Array<{seq: number, evt: Uint8Array}>} */ const events = []; return { events, async getDid() { return DID; }, async getHandle() { return HANDLE; }, async getAccountStatus() { return currentStatus; }, async setAccountStatus(/** @type {string} */ next) { currentStatus = next; }, async getEmail() { return email; }, async setEmail(/** @type {any} */ state) { email = state; }, async getAccountUiState() { return uiState; }, async setAccountUiState(/** @type {any} */ state) { uiState = state; }, async getPrivateKey() { return null; }, async getLatestCommit() { return { seq: 7, cid: 'bafycommit', rev: '3lrev' }; }, async getLatestSeq() { return events.length ? Math.max(...events.map((e) => e.seq)) : 0; }, async putEvent( /** @type {number} */ seq, /** @type {string} */ _did, /** @type {string} */ _prev, /** @type {Uint8Array} */ evt, ) { events.push({ seq, evt }); }, async getEventsBefore( /** @type {number} */ before, /** @type {number} */ limit, ) { return events .filter((e) => e.seq < before) .sort((a, b) => b.seq - a.seq) .slice(0, limit); }, async getRecord() { return null; }, async listAllRecords() { return []; }, async listRecords() { return { records: [], cursor: null }; }, async listBlobs() { return { cids: [], cursor: null }; }, ...overrides, }; } /** * @param {Object} [options] * @param {Array>} [options.oauthTokens] * @param {Array>} [options.sessions] * @param {Array>} [options.passkeys] */ export function createSharedStorage({ oauthTokens = [], sessions = [], passkeys = [], } = {}) { /** @type {any[]} */ const passwords = []; /** @type {any[]} */ const sessionRows = [...sessions]; /** @type {any[]} */ const passkeyRows = [...passkeys]; let tokens = [...oauthTokens]; return { get sessionRows() { return sessionRows; }, get tokens() { return tokens; }, async listOAuthTokensByDid() { return tokens; }, async deleteOAuthToken(/** @type {string} */ tokenId) { tokens = tokens.filter((token) => token.tokenId !== tokenId); }, async listAppPasswords() { return passwords.map(({ hash: _hash, ...entry }) => entry); }, async putAppPassword( /** @type {string} */ _did, /** @type {string} */ name, /** @type {string} */ hash, /** @type {boolean} */ privileged, /** @type {string} */ createdAt, ) { if (passwords.some((entry) => entry.name === name)) return false; passwords.push({ name, privileged, createdAt, hash }); return true; }, async deleteAppPassword( /** @type {string} */ _did, /** @type {string} */ name, ) { const index = passwords.findIndex((entry) => entry.name === name); if (index === -1) return false; passwords.splice(index, 1); return true; }, async putSession(/** @type {any} */ session) { const index = sessionRows.findIndex((s) => s.jti === session.jti); if (index === -1) sessionRows.push(session); else sessionRows[index] = session; }, async getSession(/** @type {string} */ jti) { return sessionRows.find((s) => s.jti === jti) || null; }, async listSessions() { return sessionRows; }, async deleteSession(/** @type {string} */ jti) { const index = sessionRows.findIndex((s) => s.jti === jti); if (index === -1) return false; sessionRows.splice(index, 1); return true; }, async putPasskey(/** @type {any} */ record) { passkeyRows.push(record); return true; }, async getPasskey(/** @type {string} */ credentialId) { return passkeyRows.find((p) => p.credentialId === credentialId) || null; }, async listPasskeys() { return passkeyRows; }, async deletePasskey( /** @type {string} */ _did, /** @type {string} */ credentialId, ) { const index = passkeyRows.findIndex( (p) => p.credentialId === credentialId, ); if (index === -1) return false; passkeyRows.splice(index, 1); return true; }, async touchPasskey() {}, }; } /** An email port that records what it sends and can be told to fail. */ export function createEmailer() { /** @type {Array<{to: string, subject: string, text: string}>} */ const sent = []; let failing = false; return { sent, setFailing(/** @type {boolean} */ value) { failing = value; }, async send(/** @type {any} */ message) { if (failing) throw new Error('smtp down'); sent.push(message); }, }; } /** * @param {Object} [options] * @param {Object} [options.actorStorage] * @param {Object} [options.sharedStorage] * @param {Object} [options.emailer] * @param {Object} [options.spaceStorage] * @param {Object} [options.lexiconResolver] * @param {boolean} [options.readOnly] * @param {import('../../packages/core/src/ports.js').ExtensionFactory[]} [options.extensions] */ export function createPds({ actorStorage = createActorStorage(), sharedStorage = createSharedStorage(), emailer, spaceStorage, lexiconResolver, readOnly = false, extensions, } = {}) { return new PersonalDataServer({ actorStorage: /** @type {any} */ (actorStorage), sharedStorage: /** @type {any} */ (sharedStorage), blobs: /** @type {any} */ ({}), jwtSecret: SECRET, hostname: 'pds.example.com', password: 'account-password', emailer: /** @type {any} */ (emailer), spaceStorage: /** @type {any} */ (spaceStorage), lexiconResolver: /** @type {any} */ (lexiconResolver), readOnly, extensions, }); } export async function sessionCookie() { const jwt = await createAccountJwt(DID, SECRET); return `pdsjs_account=${encodeURIComponent(jwt)}`; } /** * @param {PersonalDataServer} pds * @param {string} path * @param {string} cookie */ export function apiGet(pds, path, cookie) { return pds.fetch(new Request(BASE + path, { headers: { cookie } })); } /** * @param {PersonalDataServer} pds * @param {string} path * @param {unknown} body * @param {string} cookie * @param {Record} [headers] */ export function apiPost(pds, path, body, cookie, headers = {}) { return pds.fetch( new Request(BASE + path, { method: 'POST', headers: { cookie, 'content-type': 'application/json', ...headers }, body: JSON.stringify(body), }), ); }