Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

copy and fix ts linting

+2 -2
src/lib/db.test.ts
···
// Now try to claim it with testDid1 - should fail
try {
await claimCustomDomain(testDid1, testDomain, hash3)
-
expect.fail('Should have thrown an error when trying to claim a verified domain')
+
expect('Should have thrown an error when trying to claim a verified domain').fail()
} catch (err) {
-
expect(err.message).toBe('conflict')
+
expect((err as Error).message).toBe('conflict')
}
// Verify the domain is still owned by testDid2 and verified
+1 -242
src/lib/db.ts
···
-
import { NodeOAuthClient, type ClientMetadata } from "@atproto/oauth-client-node";
import { SQL } from "bun";
-
import { JoseKey } from "@atproto/jwk-jose";
import { BASE_HOST } from "./constants";
export const db = new SQL(
···
return rows[0] ?? null;
};
-
export const getAllWispDomains = async (did: string) => {
+
export const getAllWispDomains = async (did: string): Promise<Array<{ domain: string; rkey: string | null }>> => {
const rows = await db`SELECT domain, rkey FROM domains WHERE did = ${did} ORDER BY created_at ASC`;
return rows;
};
···
export const deleteWispDomain = async (domain: string): Promise<void> => {
await db`DELETE FROM domains WHERE domain = ${domain}`;
-
};
-
-
// Session timeout configuration (30 days in seconds)
-
const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds
-
// OAuth state timeout (1 hour in seconds)
-
const STATE_TIMEOUT = 60 * 60; // 3600 seconds
-
-
const stateStore = {
-
async set(key: string, data: any) {
-
const expiresAt = Math.floor(Date.now() / 1000) + STATE_TIMEOUT;
-
await db`
-
INSERT INTO oauth_states (key, data, created_at, expires_at)
-
VALUES (${key}, ${JSON.stringify(data)}, EXTRACT(EPOCH FROM NOW()), ${expiresAt})
-
ON CONFLICT (key) DO UPDATE SET data = EXCLUDED.data, expires_at = ${expiresAt}
-
`;
-
},
-
async get(key: string) {
-
const now = Math.floor(Date.now() / 1000);
-
const result = await db`
-
SELECT data, expires_at
-
FROM oauth_states
-
WHERE key = ${key}
-
`;
-
if (!result[0]) return undefined;
-
-
// Check if expired
-
const expiresAt = Number(result[0].expires_at);
-
if (expiresAt && now > expiresAt) {
-
await db`DELETE FROM oauth_states WHERE key = ${key}`;
-
return undefined;
-
}
-
-
return JSON.parse(result[0].data);
-
},
-
async del(key: string) {
-
await db`DELETE FROM oauth_states WHERE key = ${key}`;
-
}
-
};
-
-
const sessionStore = {
-
async set(sub: string, data: any) {
-
const expiresAt = Math.floor(Date.now() / 1000) + SESSION_TIMEOUT;
-
await db`
-
INSERT INTO oauth_sessions (sub, data, updated_at, expires_at)
-
VALUES (${sub}, ${JSON.stringify(data)}, EXTRACT(EPOCH FROM NOW()), ${expiresAt})
-
ON CONFLICT (sub) DO UPDATE SET
-
data = EXCLUDED.data,
-
updated_at = EXTRACT(EPOCH FROM NOW()),
-
expires_at = ${expiresAt}
-
`;
-
},
-
async get(sub: string) {
-
const now = Math.floor(Date.now() / 1000);
-
const result = await db`
-
SELECT data, expires_at
-
FROM oauth_sessions
-
WHERE sub = ${sub}
-
`;
-
if (!result[0]) return undefined;
-
-
// Check if expired
-
const expiresAt = Number(result[0].expires_at);
-
if (expiresAt && now > expiresAt) {
-
console.log('[sessionStore] Session expired, deleting', sub);
-
await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
-
return undefined;
-
}
-
-
return JSON.parse(result[0].data);
-
},
-
async del(sub: string) {
-
await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
-
}
-
};
-
-
export { sessionStore };
-
-
// Cleanup expired sessions and states
-
export const cleanupExpiredSessions = async () => {
-
const now = Math.floor(Date.now() / 1000);
-
try {
-
const sessionsDeleted = await db`
-
DELETE FROM oauth_sessions WHERE expires_at < ${now}
-
`;
-
const statesDeleted = await db`
-
DELETE FROM oauth_states WHERE expires_at IS NOT NULL AND expires_at < ${now}
-
`;
-
console.log(`[Cleanup] Deleted ${sessionsDeleted.length} expired sessions and ${statesDeleted.length} expired states`);
-
return { sessions: sessionsDeleted.length, states: statesDeleted.length };
-
} catch (err) {
-
console.error('[Cleanup] Failed to cleanup expired data:', err);
-
return { sessions: 0, states: 0 };
-
}
-
};
-
-
export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => {
-
const isLocalDev = process.env.LOCAL_DEV === 'true';
-
-
if (isLocalDev) {
-
// Loopback client for local development
-
// For loopback, scopes and redirect_uri must be in client_id query string
-
const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
-
const scope = 'atproto transition:generic';
-
const params = new URLSearchParams();
-
params.append('redirect_uri', redirectUri);
-
params.append('scope', scope);
-
-
return {
-
client_id: `http://localhost?${params.toString()}`,
-
client_name: config.clientName,
-
client_uri: config.domain,
-
redirect_uris: [redirectUri],
-
grant_types: ['authorization_code', 'refresh_token'],
-
response_types: ['code'],
-
application_type: 'web',
-
token_endpoint_auth_method: 'none',
-
scope: scope,
-
dpop_bound_access_tokens: false,
-
subject_type: 'public'
-
};
-
}
-
-
// Production client with private_key_jwt
-
return {
-
client_id: `${config.domain}/client-metadata.json`,
-
client_name: config.clientName,
-
client_uri: config.domain,
-
logo_uri: `${config.domain}/logo.png`,
-
tos_uri: `${config.domain}/tos`,
-
policy_uri: `${config.domain}/policy`,
-
redirect_uris: [`${config.domain}/api/auth/callback`],
-
grant_types: ['authorization_code', 'refresh_token'],
-
response_types: ['code'],
-
application_type: 'web',
-
token_endpoint_auth_method: 'private_key_jwt',
-
token_endpoint_auth_signing_alg: "ES256",
-
scope: "atproto transition:generic",
-
dpop_bound_access_tokens: true,
-
jwks_uri: `${config.domain}/jwks.json`,
-
subject_type: 'public',
-
authorization_signed_response_alg: 'ES256'
-
};
-
};
-
-
const persistKey = async (key: JoseKey) => {
-
const priv = key.privateJwk;
-
if (!priv) return;
-
const kid = key.kid ?? crypto.randomUUID();
-
await db`
-
INSERT INTO oauth_keys (kid, jwk, created_at)
-
VALUES (${kid}, ${JSON.stringify(priv)}, EXTRACT(EPOCH FROM NOW()))
-
ON CONFLICT (kid) DO UPDATE SET jwk = EXCLUDED.jwk
-
`;
-
};
-
-
const loadPersistedKeys = async (): Promise<JoseKey[]> => {
-
const rows = await db`SELECT kid, jwk, created_at FROM oauth_keys ORDER BY kid`;
-
const keys: JoseKey[] = [];
-
for (const row of rows) {
-
try {
-
const obj = JSON.parse(row.jwk);
-
const key = await JoseKey.fromImportable(obj as any, (obj as any).kid);
-
keys.push(key);
-
} catch (err) {
-
console.error('Could not parse stored JWK', err);
-
}
-
}
-
return keys;
-
};
-
-
const ensureKeys = async (): Promise<JoseKey[]> => {
-
let keys = await loadPersistedKeys();
-
const needed: string[] = [];
-
for (let i = 1; i <= 3; i++) {
-
const kid = `key${i}`;
-
if (!keys.some(k => k.kid === kid)) needed.push(kid);
-
}
-
for (const kid of needed) {
-
const newKey = await JoseKey.generate(['ES256'], kid);
-
await persistKey(newKey);
-
keys.push(newKey);
-
}
-
keys.sort((a, b) => (a.kid ?? '').localeCompare(b.kid ?? ''));
-
return keys;
-
};
-
-
// Load keys from database every time (stateless - safe for horizontal scaling)
-
export const getCurrentKeys = async (): Promise<JoseKey[]> => {
-
return await loadPersistedKeys();
-
};
-
-
// Key rotation - rotate keys older than 30 days (monthly rotation)
-
const KEY_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
-
-
export const rotateKeysIfNeeded = async (): Promise<boolean> => {
-
const now = Math.floor(Date.now() / 1000);
-
const cutoffTime = now - KEY_MAX_AGE;
-
-
try {
-
// Find keys older than 30 days
-
const oldKeys = await db`
-
SELECT kid, created_at FROM oauth_keys
-
WHERE created_at IS NOT NULL AND created_at < ${cutoffTime}
-
ORDER BY created_at ASC
-
`;
-
-
if (oldKeys.length === 0) {
-
console.log('[KeyRotation] No keys need rotation');
-
return false;
-
}
-
-
console.log(`[KeyRotation] Found ${oldKeys.length} key(s) older than 30 days, rotating oldest key`);
-
-
// Rotate the oldest key
-
const oldestKey = oldKeys[0];
-
const oldKid = oldestKey.kid;
-
-
// Generate new key with same kid
-
const newKey = await JoseKey.generate(['ES256'], oldKid);
-
await persistKey(newKey);
-
-
console.log(`[KeyRotation] Rotated key ${oldKid}`);
-
-
return true;
-
} catch (err) {
-
console.error('[KeyRotation] Failed to rotate keys:', err);
-
return false;
-
}
-
};
-
-
export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => {
-
const keys = await ensureKeys();
-
-
return new NodeOAuthClient({
-
clientMetadata: createClientMetadata(config),
-
keyset: keys,
-
stateStore,
-
sessionStore
-
});
};
export const getCustomDomainsByDid = async (did: string) => {
+4 -3
src/lib/oauth-client.ts
···
token_endpoint_auth_method: 'none',
scope: scope,
dpop_bound_access_tokens: false,
-
subject_type: 'public'
-
};
+
subject_type: 'public',
+
authorization_signed_response_alg: 'ES256'
+
} as ClientMetadata;
}
// Production client with private_key_jwt
···
jwks_uri: `${config.domain}/jwks.json`,
subject_type: 'public',
authorization_signed_response_alg: 'ES256'
-
};
+
} as ClientMetadata;
};
const persistKey = async (key: JoseKey) => {
+12 -12
src/lib/wisp-utils.test.ts
···
function createMockBlobRef(mimeType: string, size: number): BlobRef {
// Create a properly formatted CID
const cid = CID.parse(TEST_CID_STRING)
-
return new BlobRef(cid, mimeType, size)
+
return new BlobRef(cid as any, mimeType, size)
}
describe('shouldCompressFile', () => {
···
describe('extractBlobMap', () => {
test('should extract blob map from flat directory structure', () => {
const mockCid = CID.parse(TEST_CID_STRING)
-
const mockBlob = new BlobRef(mockCid, 'text/html', 100)
+
const mockBlob = new BlobRef(mockCid as any, 'text/html', 100)
const directory: Directory = {
$type: 'place.wisp.fs#directory',
···
const mockCid1 = CID.parse(TEST_CID_STRING)
const mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')
-
const mockBlob1 = new BlobRef(mockCid1, 'text/html', 100)
-
const mockBlob2 = new BlobRef(mockCid2, 'text/css', 50)
+
const mockBlob1 = new BlobRef(mockCid1 as any, 'text/html', 100)
+
const mockBlob2 = new BlobRef(mockCid2 as any, 'text/css', 50)
const directory: Directory = {
$type: 'place.wisp.fs#directory',
···
test('should handle deeply nested directory structures', () => {
const mockCid = CID.parse(TEST_CID_STRING)
-
const mockBlob = new BlobRef(mockCid, 'text/javascript', 200)
+
const mockBlob = new BlobRef(mockCid as any, 'text/javascript', 200)
const directory: Directory = {
$type: 'place.wisp.fs#directory',
···
// This test verifies the fix: AT Protocol SDK returns BlobRef instances,
// not plain objects with $type and $link properties
const mockCid = CID.parse(TEST_CID_STRING)
-
const mockBlob = new BlobRef(mockCid, 'application/octet-stream', 500)
+
const mockBlob = new BlobRef(mockCid as any, 'application/octet-stream', 500)
const directory: Directory = {
$type: 'place.wisp.fs#directory',
···
const mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')
const mockCid3 = CID.parse('bafkreieb3ixgchss44kw7xiavnkns47emdfsqbhcdfluo3p6n3o53fl3vq')
-
const mockBlob1 = new BlobRef(mockCid1, 'image/png', 1000)
-
const mockBlob2 = new BlobRef(mockCid2, 'image/png', 2000)
-
const mockBlob3 = new BlobRef(mockCid3, 'image/png', 3000)
+
const mockBlob1 = new BlobRef(mockCid1 as any, 'image/png', 1000)
+
const mockBlob2 = new BlobRef(mockCid2 as any, 'image/png', 2000)
+
const mockBlob3 = new BlobRef(mockCid3 as any, 'image/png', 3000)
const directory: Directory = {
$type: 'place.wisp.fs#directory',
···
node: {
$type: 'place.wisp.fs#file',
type: 'file',
-
blob: new BlobRef(mockCid1, 'text/html', 100),
+
blob: new BlobRef(mockCid1 as any, 'text/html', 100),
},
},
{
···
node: {
$type: 'place.wisp.fs#file',
type: 'file',
-
blob: new BlobRef(mockCid2, 'text/css', 50),
+
blob: new BlobRef(mockCid2 as any, 'text/css', 50),
},
},
],
···
node: {
$type: 'place.wisp.fs#file',
type: 'file',
-
blob: new BlobRef(mockCid3, 'text/markdown', 200),
+
blob: new BlobRef(mockCid3 as any, 'text/markdown', 200),
},
},
],
+4 -1
src/lib/wisp-utils.ts
···
const remainingPath = pathParts.slice(1).join('/');
return {
name: entry.name,
-
node: replaceDirectoryWithSubfs(entry.node as Directory, remainingPath, subfsUri)
+
node: {
+
...replaceDirectoryWithSubfs(entry.node as Directory, remainingPath, subfsUri),
+
$type: 'place.wisp.fs#directory' as const
+
}
};
}
}
+2 -2
src/routes/auth.ts
···
})
// Sync sites from PDS to database cache
-
logger.debug('[Auth] Syncing sites from PDS for', session.did)
+
logger.debug('[Auth] Syncing sites from PDS for', session.did as any)
try {
const syncResult = await syncSitesFromPDS(session.did, session)
logger.debug(`[Auth] Sync complete: ${syncResult.synced} sites synced`)
···
if (did && typeof did === 'string') {
try {
await client.revoke(did)
-
logger.debug('[Auth] Revoked OAuth session for', did)
+
logger.debug('[Auth] Revoked OAuth session for', did as any)
} catch (err) {
logger.error('[Auth] Failed to revoke session', err)
// Continue with logout even if revoke fails
+1 -1
src/routes/domain.ts
···
});
} catch (err) {
// Record might not exist in PDS, continue anyway
-
logger.warn('[Domain] Could not delete wisp domain from PDS', err);
+
logger.warn('[Domain] Could not delete wisp domain from PDS', err as any);
}
return { success: true };