chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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<Record<string, unknown>>} [options.oauthTokens]
* @param {Array<Record<string, unknown>>} [options.sessions]
* @param {Array<Record<string, unknown>>} [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<string, string>} [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),
}),
);
}