Files
eden/scripts/smoke-convex.ts

43 lines
3.8 KiB
TypeScript

import { ConvexHttpClient } from 'convex/browser';
import { makeFunctionReference } from 'convex/server';
import { ConvexStore } from '../src/lib/server/store';
import { applicationJwt, hashSecret, randomSecret } from '../src/lib/server/security';
import assert from 'node:assert/strict';
import { SignJWT, importPKCS8 } from 'jose';
if (process.env.SMOKE_ALLOW_WRITE !== '1') throw new Error('This smoke test writes synthetic records. Set SMOKE_ALLOW_WRITE=1 only for a disposable deployment.');
const url = process.env.CONVEX_SELF_HOSTED_URL!;
const store = new ConvexStore(url, process.env.CONVEX_BRIDGE_SECRET!);
const now = Date.now(); const hash = hashSecret(randomSecret());
const characterId = 90_000_000 + Math.floor(Math.random() * 1_000_000);
const user = await store.completeLogin({ characterId, name: 'Disposable Test Pilot', ownerHash: hashSecret(randomSecret()), scopes: ['esi-assets.read_assets.v1'], intent: 'initial_login', capability: 'asset_search', requestedScopes: ['esi-assets.read_assets.v1'], approvedExistingScopes: [], encryptedTokens: 'test-only-encrypted-placeholder', tokenExpiresAt: now + 60_000, sessionHash: hash, sessionExpiresAt: now + 60_000 });
assert.equal((await store.session(hash))?.userId, user.userId);
const generation = await store.beginSync(user.userId, characterId);
await store.stageAssets(user.userId, characterId, generation, 0, [{ itemId: '9223372036854775806', characterId, typeId: 34, quantity: 50, locationId: '60003760', locationType: 'station', locationFlag: 'Hangar' }]);
await store.finishSync(user.userId, characterId, generation, 1, now, now + 3_600_000);
assert.equal((await store.assets(user.userId, characterId))[0]!.quantity, 50);
let isolated = false;
try { isolated = (await store.assets('other-user', characterId)).length === 0; } catch { isolated = true; }
assert(isolated);
const failure = await store.beginSync(user.userId, characterId);
await store.stageAssets(user.userId, characterId, failure, 0, []);
await store.failSync(user.userId, characterId, failure, 'test failure');
assert.equal((await store.assets(user.userId, characterId))[0]!.quantity, 50);
const client = new ConvexHttpClient(url, { skipConvexDeploymentUrlCheck: true, logger: false });
client.setAuth(await applicationJwt({ ...user, issuer: process.env.APP_AUTH_ISSUER!, privateKey: process.env.APP_AUTH_PRIVATE_KEY! }));
const rows = await client.query(makeFunctionReference<'query', Record<string, never>, unknown[] | null>('status:characters'), {});
assert.equal(rows?.length, 1);
assert(!JSON.stringify(rows).includes('encryptedTokens'));
const wrongAudience = new ConvexHttpClient(url, { skipConvexDeploymentUrlCheck: true, logger: false });
const key = await importPKCS8(Buffer.from(process.env.APP_AUTH_PRIVATE_KEY!, 'base64').toString('utf8'), 'RS256');
wrongAudience.setAuth(await new SignJWT({ sid: user.sessionId }).setProtectedHeader({ alg: 'RS256', typ: 'JWT', kid: 'app-v1' }).setIssuer(process.env.APP_AUTH_ISSUER!).setSubject(user.userId).setAudience('not-eve-intelligence').setIssuedAt().setExpirationTime('5m').sign(key));
let wrongAudienceDenied = false;
try { const rows = await wrongAudience.query(makeFunctionReference<'query', Record<string, never>, unknown[] | null>('status:characters'), {}); wrongAudienceDenied = rows === null; } catch { wrongAudienceDenied = true; }
assert(wrongAudienceDenied, 'Convex must reject a correctly signed token for another audience');
await store.deleteSession(hash);
assert.equal(await store.session(hash), null);
let revoked = false;
try { const result = await client.query(makeFunctionReference<'query', Record<string, never>, unknown[] | null>('status:characters'), {}); revoked = result === null || result.length === 0; } catch { revoked = true; }
assert(revoked);
console.log('Real self-hosted Convex smoke passed: login/session, snapshot promotion, failure preservation, tenant isolation, JWT-safe projection and revocation.');