feat: integrate platform backend and application interfaces

This commit is contained in:
Qiufeng
2026-09-02 21:09:47 +08:00
parent cd9ac1af70
commit b16390dd41
648 changed files with 102488 additions and 17737 deletions
+41
View File
@@ -0,0 +1,41 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createHttpServer } from "../src/app/http.ts";
import { createStore } from "../src/store.ts";
test("ordinary-user registration and login work with verification enhancements disabled", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "account-policy-test-secret", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: false, userMfaRequired: false, userVerificationRequired: false, captchaRequired: false });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const policy = await fetch(`${base}/api/v1/auth/policy`); assert.deepEqual((await policy.json() as { data: unknown }).data, { captchaRequired: false, userVerificationRequired: false, userMfaRequired: false });
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "policy-off@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; verificationRequired: boolean } }; assert.equal(register.status, 201); assert.equal(registered.data.verificationRequired, false);
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "policy-off@example.com", password: "password123" }) });
const session = await login.json() as { data: { accessToken?: string; mfaRequired?: boolean } }; assert.equal(login.status, 200); assert.ok(session.data.accessToken); assert.equal(session.data.mfaRequired, false);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("authenticated password change revokes other sessions and close anonymizes asynchronously", async () => {
const store = createStore();
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "account-security-test-secret", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: false, userMfaRequired: false, captchaRequired: false });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "account-security@example.com", password: "old-password-123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const firstLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "account-security@example.com", password: "old-password-123" }) });
const firstPayload = await firstLogin.json() as { data: { accessToken: string } }; assert.ok(firstPayload.data.accessToken);
const secondLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "account-security@example.com", password: "old-password-123" }) });
const secondPayload = await secondLogin.json() as { data: { accessToken: string } }; assert.ok(secondPayload.data.accessToken);
const changed = await fetch(`${base}/api/v1/auth/password/change`, { method: "POST", headers: { Authorization: `Bearer ${firstPayload.data.accessToken}`, "content-type": "application/json" }, body: JSON.stringify({ currentPassword: "old-password-123", newPassword: "new-password-456" }) });
assert.equal(changed.status, 200);
const oldSession = await fetch(`${base}/api/v1/me`, { headers: { Authorization: `Bearer ${secondPayload.data.accessToken}` } }); assert.equal(oldSession.status, 401);
const newLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "account-security@example.com", password: "new-password-456" }) });
const newPayload = await newLogin.json() as { data: { accessToken: string } }; assert.ok(newPayload.data.accessToken);
const closed = await fetch(`${base}/api/v1/me/close`, { method: "POST", headers: { Authorization: `Bearer ${newPayload.data.accessToken}` } }); assert.equal(closed.status, 200);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(store.users.get(registered.data.userId)?.status, "closing"); assert.equal(store.users.get(registered.data.userId)?.email, undefined);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
+282
View File
@@ -0,0 +1,282 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createHash, createHmac } from "node:crypto";
import { createHttpServer } from "../src/app/http.ts";
import { createStore } from "../src/store.ts";
function totpCode(secret: string, now = Date.now()) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let buffer = 0; let bits = 0; const bytes: number[] = [];
for (const character of secret) {
buffer = (buffer << 5) | alphabet.indexOf(character); bits += 5;
if (bits >= 8) { bytes.push((buffer >>> (bits - 8)) & 255); bits -= 8; }
}
const message = Buffer.alloc(8); message.writeBigInt64BE(BigInt(Math.floor(now / 30_000)));
const digest = createHmac("sha1", Buffer.from(bytes)).update(message).digest();
const offset = digest[digest.length - 1] & 15;
const value = ((digest[offset] & 127) << 24) | (digest[offset + 1] << 16) | (digest[offset + 2] << 8) | digest[offset + 3];
return String(value % 1_000_000).padStart(6, "0");
}
test("registration, verification, recharge and task idempotency keep one charge", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "test@example.com", password: "password123" }) });
const registered = (await register.json()) as { data: { userId: string; devVerificationCode: string } };
const verify = await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
assert.equal(verify.status, 200);
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "test@example.com", password: "password123" }) });
const session = (await login.json()) as { data: { accessToken: string } };
const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json" };
const recharge = await fetch(`${base}/api/v1/recharge/orders`, { method: "POST", headers: { ...auth, "Idempotency-Key": "recharge" }, body: JSON.stringify({ amount: 100 }) });
const rechargePayload = await recharge.json() as { data: Record<string, unknown> };
assert.equal(recharge.status, 201);
assert.equal(Object.prototype.hasOwnProperty.call(rechargePayload.data, "provider"), false);
const rechargeOrders = await fetch(`${base}/api/v1/recharge/orders`, { headers: auth });
const rechargeOrdersPayload = await rechargeOrders.json() as { data: { items: Array<Record<string, unknown>> } };
assert.equal(Object.prototype.hasOwnProperty.call(rechargeOrdersPayload.data.items[0], "provider"), false);
const first = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: { ...auth, "Idempotency-Key": "task" }, body: JSON.stringify({ modelProductId: "basic-image", prompt: "fixture" }) });
const firstPayload = (await first.json()) as { data: { id: string } };
const second = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: { ...auth, "Idempotency-Key": "task" }, body: JSON.stringify({ modelProductId: "basic-image", prompt: "fixture" }) });
const secondPayload = (await second.json()) as { data: { id: string } };
assert.equal(firstPayload.data.id, secondPayload.data.id);
await new Promise((resolve) => setTimeout(resolve, 500));
const balance = await fetch(`${base}/api/v1/balance`, { headers: auth });
const balancePayload = (await balance.json()) as { data: { available: number; reserved: number } };
assert.deepEqual(balancePayload.data, { available: 90, reserved: 0, unit: { name: "金币", shortCode: "COIN", icon: "coins", precision: 0, version: 1 } });
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
});
test("adapter payment failure does not credit a recharge order", async () => {
const previous = process.env.NODE_ENV; process.env.NODE_ENV = "development";
const store = createStore(); store.paymentProviders.clear(); store.paymentProviders.set("adapter", { id: "adapter", enabled: true, mode: "live" });
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true, paymentWebhookSecret: "webhook-secret" });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "adapter-failure@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "adapter-failure@example.com", password: "password123" }) });
const session = await login.json() as { data: { accessToken: string } }; const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json", "Idempotency-Key": "adapter-recharge" };
const orderResponse = await fetch(`${base}/api/v1/recharge/orders`, { method: "POST", headers: auth, body: JSON.stringify({ amount: 100 }) }); const order = await orderResponse.json() as { data: { orderId: string } };
const payload = { orderId: order.data.orderId, eventId: "adapter-failure-event", status: "failed", amount: 100, currency: "CNY" };
const signature = createHmac("sha256", "webhook-secret").update(createHash("sha256").update(JSON.stringify(payload)).digest("hex")).digest("hex");
const webhook = await fetch(`${base}/api/v1/payment/webhooks/adapter`, { method: "POST", headers: { "content-type": "application/json", "x-miragenflow-signature": signature }, body: JSON.stringify(payload) });
assert.equal(webhook.status, 200); const balance = await fetch(`${base}/api/v1/balance`, { headers: auth }); const balancePayload = await balance.json() as { data: { available: number } }; assert.equal(balancePayload.data.available, 0);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); if (previous === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = previous; }
});
test("adapter refund reverses recharge credit once", async () => {
const previous = process.env.NODE_ENV; process.env.NODE_ENV = "development";
const store = createStore(); store.paymentProviders.clear(); store.paymentProviders.set("adapter", { id: "adapter", enabled: true, mode: "live" });
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true, paymentWebhookSecret: "webhook-secret" });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "adapter-refund@example.com", password: "password123" }) }); const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "adapter-refund@example.com", password: "password123" }) }); const session = await login.json() as { data: { accessToken: string } }; const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json", "Idempotency-Key": "adapter-refund-order" };
const orderResponse = await fetch(`${base}/api/v1/recharge/orders`, { method: "POST", headers: auth, body: JSON.stringify({ amount: 100 }) }); const order = await orderResponse.json() as { data: { orderId: string } };
const paidBody = { orderId: order.data.orderId, eventId: "adapter-paid-event", status: "paid", amount: 100, currency: "CNY" }; const sign = (body: unknown) => createHmac("sha256", "webhook-secret").update(createHash("sha256").update(JSON.stringify(body)).digest("hex")).digest("hex");
assert.equal((await fetch(`${base}/api/v1/payment/webhooks/adapter`, { method: "POST", headers: { "content-type": "application/json", "x-miragenflow-signature": sign(paidBody) }, body: JSON.stringify(paidBody) })).status, 200);
const refundBody = { ...paidBody, eventId: "adapter-refund-event", status: "refunded" };
const orderRecord = store.rechargeOrders.get(order.data.orderId)!; const bucket = (store.buckets.get(registered.data.userId) || []).find((item) => item.id === orderRecord.creditBucketId)!; const account = store.balances.get(registered.data.userId)!;
bucket.remaining -= 1; account.available -= 1;
assert.equal((await fetch(`${base}/api/v1/payment/webhooks/adapter`, { method: "POST", headers: { "content-type": "application/json", "x-miragenflow-signature": sign(refundBody) }, body: JSON.stringify(refundBody) })).status, 409);
assert.equal(orderRecord.status, "paid"); assert.equal(store.ledger.some((entry) => entry.referenceId === order.data.orderId && entry.type === "refund"), false);
bucket.remaining += 1; account.available += 1;
assert.equal((await fetch(`${base}/api/v1/payment/webhooks/adapter`, { method: "POST", headers: { "content-type": "application/json", "x-miragenflow-signature": sign(refundBody) }, body: JSON.stringify(refundBody) })).status, 200);
const repeated = await fetch(`${base}/api/v1/payment/webhooks/adapter`, { method: "POST", headers: { "content-type": "application/json", "x-miragenflow-signature": sign(refundBody) }, body: JSON.stringify(refundBody) }); assert.equal(repeated.status, 200);
const balance = await fetch(`${base}/api/v1/balance`, { headers: auth }); const balancePayload = await balance.json() as { data: { available: number } }; assert.equal(balancePayload.data.available, 0);
const ledger = await fetch(`${base}/api/v1/balance/ledger`, { headers: auth }); const ledgerPayload = await ledger.json() as { data: { items: Array<{ type: string; referenceId?: string; bucketId?: string }> } }; const orderEntries = ledgerPayload.data.items.filter((item) => item.referenceId === order.data.orderId); assert.equal(orderEntries.filter((item) => item.type === "refund").length, 1); assert.ok(orderEntries.every((item) => Boolean(item.bucketId)));
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); if (previous === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = previous; }
});
test("channel failures move through the ordered group and release on exhaustion", async () => {
const previous = process.env.MIRAGENFLOW_FIXTURE_FAIL_CHANNELS;
process.env.MIRAGENFLOW_FIXTURE_FAIL_CHANNELS = "channel-a,channel-b,channel-c";
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "failover@example.com", password: "password123" }) });
const registered = (await register.json()) as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "failover@example.com", password: "password123" }) });
const session = (await login.json()) as { data: { accessToken: string } };
const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json" };
await fetch(`${base}/api/v1/recharge/orders`, { method: "POST", headers: { ...auth, "Idempotency-Key": "recharge" }, body: JSON.stringify({ amount: 100 }) });
const created = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: { ...auth, "Idempotency-Key": "failover" }, body: JSON.stringify({ modelProductId: "basic-image", prompt: "fixture" }) });
const task = (await created.json()) as { data: { id: string } };
await new Promise((resolve) => setTimeout(resolve, 300));
const events = await fetch(`${base}/api/v1/tasks/${task.data.id}/events`, { headers: auth });
const eventPayload = (await events.json()) as { data: { items: Array<{ type: string; payload: Record<string, unknown> }> } };
const attempts = eventPayload.data.items.filter((event) => event.type === "task.attempt");
assert.equal(attempts.length, 4);
const terminal = await fetch(`${base}/api/v1/tasks/${task.data.id}`, { headers: auth });
const terminalPayload = (await terminal.json()) as { data: { status: string; attempts: Array<{ status: string }> } };
assert.equal(terminalPayload.data.status, "succeeded");
assert.equal(terminalPayload.data.attempts.filter((attempt) => attempt.status === "failed").length, 3);
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
if (previous === undefined) delete process.env.MIRAGENFLOW_FIXTURE_FAIL_CHANNELS;
else process.env.MIRAGENFLOW_FIXTURE_FAIL_CHANNELS = previous;
});
test("plan purchase mock webhook credits once and hides provider details", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "plan-purchase@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "plan-purchase@example.com", password: "password123" }) });
const session = await login.json() as { data: { accessToken: string } }; const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json", "Idempotency-Key": "plan-purchase" };
const purchase = await fetch(`${base}/api/v1/plans/starter/purchase`, { method: "POST", headers: auth, body: "{}" }); const payload = await purchase.json() as { data: Record<string, unknown> };
assert.equal(purchase.status, 202); assert.equal(payload.data.status, "paid");
for (const field of ["userId", "provider", "idempotencyKey", "requestFingerprint", "creditBucketId", "entitlementId", "planSnapshot"]) assert.equal(Object.prototype.hasOwnProperty.call(payload.data, field), false);
const plan = await fetch(`${base}/api/v1/catalog/plans/starter`); const planPayload = await plan.json() as { data: { item: { allowedModelProductIds: string[] } } }; assert.equal(plan.status, 200); assert.ok(planPayload.data.item.allowedModelProductIds.every((value) => value.endsWith("-v1")));
const entitlements = await fetch(`${base}/api/v1/me/entitlements`, { headers: auth }); const entitlementPayload = await entitlements.json() as { data: { items: Array<{ allowedModelProductIds: string[] }> } }; assert.equal(entitlements.status, 200); assert.ok(entitlementPayload.data.items[0].allowedModelProductIds.every((value) => value.endsWith("-v1")));
const balance = await fetch(`${base}/api/v1/balance`, { headers: auth }); const balancePayload = await balance.json() as { data: { available: number; reserved: number } }; assert.equal(balancePayload.data.available, 100); assert.equal(balancePayload.data.reserved, 0);
const repeated = await fetch(`${base}/api/v1/plans/starter/purchase`, { method: "POST", headers: auth, body: "{}" }); const repeatedPayload = await repeated.json() as { data: { id: string } }; assert.equal(repeatedPayload.data.id, payload.data.id);
const ledger = await fetch(`${base}/api/v1/balance/ledger`, { headers: auth }); const ledgerPayload = await ledger.json() as { data: { items: Array<{ referenceId?: string; bucketId?: string }> } }; const purchaseEntries = ledgerPayload.data.items.filter((item) => item.referenceId === payload.data.id); assert.equal(purchaseEntries.length, 1); assert.ok(purchaseEntries.every((item) => Boolean(item.bucketId)));
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("queued cancellation releases the reserved balance exactly once", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "cancel@example.com", password: "password123" }) });
const registered = (await register.json()) as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "cancel@example.com", password: "password123" }) });
const session = (await login.json()) as { data: { accessToken: string } };
const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json" };
await fetch(`${base}/api/v1/recharge/orders`, { method: "POST", headers: { ...auth, "Idempotency-Key": "recharge" }, body: JSON.stringify({ amount: 100 }) });
const created = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: { ...auth, "Idempotency-Key": "cancel" }, body: JSON.stringify({ modelProductId: "basic-image", prompt: "fixture" }) });
const task = (await created.json()) as { data: { id: string } };
const canceled = await fetch(`${base}/api/v1/tasks/${task.data.id}/cancel`, { method: "POST", headers: auth });
assert.equal(canceled.status, 200);
await new Promise((resolve) => setTimeout(resolve, 100));
const balance = await fetch(`${base}/api/v1/balance`, { headers: auth });
const balancePayload = (await balance.json()) as { data: { available: number; reserved: number } };
assert.equal(balancePayload.data.available, 100);
assert.equal(balancePayload.data.reserved, 0);
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
});
test("public task responses do not expose provider route fields and 3D remains disabled", async () => {
const store = createStore();
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "public-contract@example.com", password: "password123" }) });
const registered = (await register.json()) as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "public-contract@example.com", password: "password123" }) });
const session = (await login.json()) as { data: { accessToken: string } }; const auth = { Authorization: `Bearer ${session.data.accessToken}` };
const catalog = await fetch(`${base}/api/v1/catalog/models`, { headers: auth }); const catalogPayload = await catalog.json() as { data: { items: Array<Record<string, unknown>> } };
assert.equal(Object.prototype.hasOwnProperty.call(catalogPayload.data.items[0], "channelGroupId"), false);
const task = "provider-task"; const now = new Date().toISOString(); store.tasks.set(task, { id: task, ownerId: registered.data.userId, taskType: "image", modelProductId: "basic-image", status: "queued", estimatedCost: 1, reservedCost: 0, createdAt: now, updatedAt: now, eventSequence: 0, channelGroupId: "image-default", routeSnapshotVersion: 1, routeSnapshot: { groupId: "image-default", version: 1, channelIds: ["channel-a"] }, pricingSnapshot: { basePrice: 1, multiplier: 1, unitVersion: 1 }, planSnapshot: { queuePriority: 0, maxConcurrent: 1 }, reserveExpiresAt: now, idempotencyKey: "private-task-key", requestFingerprint: "private-fingerprint", attempts: [{ id: "attempt", channelId: "channel-a", sequence: 1, status: "started", providerRequestId: "provider-secret", platformIdempotencyKey: "provider-idempotency", leaseExpiresAt: now, startedAt: now }], outputs: [] });
const hidden = JSON.stringify(store.tasks.get(task)); assert.equal(hidden.includes("provider-secret"), true);
const taskResponse = await fetch(`${base}/api/v1/tasks/${task}`, { headers: auth }); const taskPayload = await taskResponse.json() as { data: Record<string, unknown> & { attempts?: Array<Record<string, unknown>> } };
assert.equal(taskResponse.status, 200);
for (const field of ["ownerId", "channelGroupId", "routeSnapshotVersion", "routeSnapshot", "pricingSnapshot", "planSnapshot", "leaseExpiresAt", "leaseToken", "providerSubmitted", "preferredReserveBucketId", "idempotencyKey", "requestFingerprint", "reserveExpiresAt"]) assert.equal(Object.prototype.hasOwnProperty.call(taskPayload.data, field), false);
for (const field of ["channelId", "providerRequestId", "errorCode", "platformIdempotencyKey", "leaseExpiresAt"]) assert.equal(Object.prototype.hasOwnProperty.call(taskPayload.data.attempts?.[0] || {}, field), false);
const disabled3d = await fetch(`${base}/api/v1/tasks/3d`, { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ modelProductId: "basic-image", prompt: "fixture" }) }); const disabledPayload = await disabled3d.json() as { error?: { code?: string } }; assert.ok([400, 422].includes(disabled3d.status)); assert.equal(disabledPayload.error?.code, "CAPABILITY_NOT_ENABLED");
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("task creation accepts the published public model id without exposing route details", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "public-id@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "public-id@example.com", password: "password123" }) });
const session = await login.json() as { data: { accessToken: string } }; const auth = { Authorization: `Bearer ${session.data.accessToken}`, "Content-Type": "application/json", "Idempotency-Key": "public-model-id" };
const response = await fetch(`${base}/api/v1/recharge/orders`, { method: "POST", headers: { ...auth, "Idempotency-Key": "public-id-recharge" }, body: JSON.stringify({ amount: 100 }) });
assert.equal(response.status, 201);
const task = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: auth, body: JSON.stringify({ modelProductId: "basic-image-v1", prompt: "fixture" }) });
assert.equal(task.status, 202);
const payload = await task.json() as { data: { modelSnapshot?: { publicModelId?: string }; modelProductId: string } };
assert.equal(payload.data.modelSnapshot?.publicModelId, "basic-image-v1");
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("published catalog never exposes a 3D capability in V1", async () => {
const store = createStore();
store.products[0].capabilities = [...store.products[0].capabilities, "3d"];
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string");
const response = await fetch(`http://127.0.0.1:${address.port}/api/v1/catalog/models`);
const payload = await response.json() as { data: { items: Array<{ capabilities: string[] }> } };
assert.equal(response.status, 200);
assert.equal(payload.data.items.some((item) => item.capabilities.includes("3d")), false);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("user MFA recovery codes are one-time credentials", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true, userMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-recovery@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const enrollment = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-recovery@example.com", password: "password123" }) });
const enrollmentPayload = await enrollment.json() as { data: { challengeId: string; challengeType: string; mfaEnrollment: { secret: string; recoveryCodes: string[] } } };
assert.equal(enrollmentPayload.data.challengeType, "setup");
assert.ok(enrollmentPayload.data.mfaEnrollment.recoveryCodes.length > 0);
const confirmed = await fetch(`${base}/api/v1/auth/mfa/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: enrollmentPayload.data.challengeId, code: totpCode(enrollmentPayload.data.mfaEnrollment.secret) }) });
assert.equal(confirmed.status, 200);
const firstLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-recovery@example.com", password: "password123" }) });
const firstChallenge = await firstLogin.json() as { data: { challengeId: string; challengeType: string } };
assert.equal(firstChallenge.data.challengeType, "verify");
const recoveryCode = enrollmentPayload.data.mfaEnrollment.recoveryCodes[0];
const recovered = await fetch(`${base}/api/v1/auth/mfa/recovery`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: firstChallenge.data.challengeId, recoveryCode }) });
assert.equal(recovered.status, 200);
const secondLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-recovery@example.com", password: "password123" }) });
const secondChallenge = await secondLogin.json() as { data: { challengeId: string } };
const repeated = await fetch(`${base}/api/v1/auth/mfa/recovery`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: secondChallenge.data.challengeId, recoveryCode }) });
assert.equal(repeated.status, 401);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("voluntary user MFA remains active when the global policy is disabled", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true, userMfaRequired: false });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`;
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-policy@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const firstLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-policy@example.com", password: "password123" }) });
const firstSession = await firstLogin.json() as { data: { accessToken: string } };
const setup = await fetch(`${base}/api/v1/auth/mfa/setup`, { method: "POST", headers: { Authorization: `Bearer ${firstSession.data.accessToken}`, "content-type": "application/json" }, body: "{}" });
const setupPayload = await setup.json() as { data: { challengeId: string; mfaEnrollment: { secret: string; recoveryCodes: string[] } } };
assert.ok(setupPayload.data.mfaEnrollment.recoveryCodes.length > 0);
const confirmed = await fetch(`${base}/api/v1/auth/mfa/verify`, { method: "POST", headers: { Authorization: `Bearer ${firstSession.data.accessToken}`, "content-type": "application/json" }, body: JSON.stringify({ challengeId: setupPayload.data.challengeId, code: totpCode(setupPayload.data.mfaEnrollment.secret) }) });
assert.equal(confirmed.status, 200);
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "mfa-policy@example.com", password: "password123" }) });
const loginPayload = await login.json() as { data?: { accessToken?: string; mfaRequired?: boolean; challengeType?: string } };
assert.equal(login.status, 200); assert.equal(loginPayload.data?.accessToken, undefined); assert.equal(loginPayload.data?.mfaRequired, true); assert.equal(loginPayload.data?.challengeType, "verify");
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
+63
View File
@@ -0,0 +1,63 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createHttpServer } from "../src/app/http.ts";
import { createStore } from "../src/store.ts";
test("health endpoint exposes the service contract", async () => {
const server = createHttpServer(createStore(), { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
const response = await fetch(`http://127.0.0.1:${address.port}/api/health`);
assert.equal(response.status, 200);
const payload = (await response.json()) as { ok: boolean; data: { service: string } };
assert.equal(payload.ok, true);
assert.equal(payload.data.service, "miragenflow-server");
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
});
test("readiness returns 503 when a dependency is unavailable and unknown API routes return 404", async () => {
const store = createStore();
store.health = async () => ({ persistence: { adapter: "memory", status: "unavailable", detail: "test failure" }, queue: { adapter: "memory", status: "ready" } });
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "test", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address(); assert.ok(address && typeof address !== "string");
try {
const base = `http://127.0.0.1:${address.port}`;
assert.equal((await fetch(`${base}/api/ready`)).status, 503);
assert.equal((await fetch(`${base}/api/v1/not-a-real-route`)).status, 404);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("fixture profiles keep mock payment out of normal development and production", async () => {
const previousNodeEnv = process.env.NODE_ENV;
const previousTestMode = process.env.MIRAGENFLOW_TEST_MODE;
let server: ReturnType<typeof createHttpServer> | undefined;
try {
process.env.NODE_ENV = "development";
delete process.env.MIRAGENFLOW_TEST_MODE;
assert.equal(createStore().paymentProviders.size, 0);
process.env.MIRAGENFLOW_TEST_MODE = "true";
assert.equal(createStore().paymentProviders.size, 1);
process.env.NODE_ENV = "production";
delete process.env.MIRAGENFLOW_TEST_MODE;
const store = createStore();
const serverInstance = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: "production-test-secret", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "production-password", adminMfaRequired: false });
server = serverInstance;
assert.equal(store.products.length, 0);
assert.equal(store.paymentProviders.size, 0);
await new Promise<void>((resolve) => serverInstance.listen(0, "127.0.0.1", resolve));
const address = serverInstance.address(); assert.ok(address && typeof address !== "string");
const response = await fetch(`http://127.0.0.1:${address.port}/api/v1/payment/webhooks/mock`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
assert.equal(response.status, 404);
} finally {
const activeServer = server;
if (activeServer) await new Promise<void>((resolve, reject) => activeServer.close((error) => error ? reject(error) : resolve()));
if (previousNodeEnv === undefined) delete process.env.NODE_ENV;
else process.env.NODE_ENV = previousNodeEnv;
if (previousTestMode === undefined) delete process.env.MIRAGENFLOW_TEST_MODE;
else process.env.MIRAGENFLOW_TEST_MODE = previousTestMode;
}
});
+47
View File
@@ -0,0 +1,47 @@
import test from "node:test";
import assert from "node:assert/strict";
import { imageModelFor, imageSizeFor, normalizeImageParams, OPENAI_IMAGE_RATIOS, OPENAI_IMAGE_RESOLUTIONS } from "../src/image-options.ts";
test("OpenAI image ratio matrix produces aligned sizes for every resolution tier", () => {
for (const resolution of OPENAI_IMAGE_RESOLUTIONS) {
for (const ratio of OPENAI_IMAGE_RATIOS) {
const size = imageSizeFor(ratio, resolution);
if (ratio === "auto") {
assert.equal(size, "auto");
continue;
}
const match = /^(\d+)x(\d+)$/.exec(size);
assert.ok(match, `${resolution} ${ratio}`);
const width = Number(match[1]);
const height = Number(match[2]);
assert.equal(width % 16, 0);
assert.equal(height % 16, 0);
assert.ok(width >= 16 && height >= 16);
assert.ok(width * height <= 8_294_400);
assert.ok(Math.max(width, height) / Math.min(width, height) <= 3);
}
}
});
test("OpenAI image parameter defaults are explicit and transparent JPEG is coerced to PNG", () => {
const params = normalizeImageParams({ ratio: "16:9", resolution: "2K", background: "transparent", output_format: "jpeg" });
assert.equal(params.size, "2048x1152");
assert.equal(params.background, "transparent");
assert.equal(params.output_format, "png");
assert.equal(params.quality, "auto");
assert.equal(params.moderation, "auto");
assert.equal(params.ratio, undefined);
assert.equal(params.resolution, undefined);
});
test("OpenAI image model selection falls back from a missing tier mapping to the base model", () => {
assert.equal(imageModelFor("configured-image-model", "4K", { "1K": "mapped-1k" }), "configured-image-model");
assert.equal(imageModelFor(undefined, "2K", {}), "gpt-image-2");
assert.equal(imageModelFor("configured-image-model", "2K", { "2K": "mapped-2k" }), "mapped-2k");
});
test("invalid explicit image sizes are replaced by the selected ratio and resolution", () => {
assert.equal(normalizeImageParams({ size: "0x0", ratio: "1:1", resolution: "2K" }).size, "2048x2048");
assert.equal(normalizeImageParams({ size: "1000x1000", ratio: "1:1", resolution: "1K" }).size, "1024x1024");
assert.equal(normalizeImageParams({ size: "2048x1152", ratio: "16:9", resolution: "2K" }).size, "2048x1152");
});
+375
View File
@@ -0,0 +1,375 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { FileRepository } from "../src/infra/repository.ts";
import { MemoryTaskQueue } from "../src/infra/queue.ts";
import { ensureStagingDir, gcStaging, putStagingObject, safeStagingPath } from "../src/infra/staging.ts";
import { appendEvent, createStore, refundBalance, releaseBalance, reserveBalance, settleBalance } from "../src/store.ts";
import { recoverExpiredReservations, recoverExpiredTaskLeases } from "../src/jobs/task-worker.ts";
import { enqueueMessage, processMessageOutbox } from "../src/adapters/messaging.ts";
import { enqueueExpiredWebDavRetention } from "../src/adapters/webdav.ts";
test("file repository writes an atomic, private snapshot", async () => {
const root = await mkdtemp(path.join(tmpdir(), "miragenflow-repo-"));
try {
const repository = new FileRepository<{ marker: string }>(path.join(root, "nested", "snapshot.json"));
await repository.save({ marker: "persisted" });
assert.deepEqual(await repository.load(), { marker: "persisted" });
assert.equal((await repository.health()).status, "ready");
assert.equal(JSON.parse(await readFile(path.join(root, "nested", "snapshot.json"), "utf8")).marker, "persisted");
} finally { await rm(root, { recursive: true, force: true }); }
});
test("file repository tolerates concurrent atomic saves", async () => {
const root = await mkdtemp(path.join(tmpdir(), "miragenflow-repo-race-"));
try {
const repository = new FileRepository<{ marker: string }>(path.join(root, "snapshot.json"));
await Promise.all(Array.from({ length: 8 }, (_, index) => repository.save({ marker: String(index) })));
assert.match((await repository.load())?.marker || "", /^[0-7]$/);
} finally { await rm(root, { recursive: true, force: true }); }
});
test("postgres reload waits for an in-flight snapshot write", async () => {
const store = createStore();
let persisted: unknown = { users: [] };
let loadCalled = false;
let releaseSave!: () => void;
let resolveSaveStarted!: () => void;
const saveStarted = new Promise<void>((resolve) => { resolveSaveStarted = resolve; });
const saveRelease = new Promise<void>((resolve) => { releaseSave = resolve; });
let firstSave = true;
store.repository = {
adapter: "postgres",
async load() { loadCalled = true; return persisted; },
async save(value: unknown) {
if (firstSave) {
firstSave = false;
resolveSaveStarted();
await saveRelease;
}
persisted = value;
},
getRevision() { return 0; },
async health() { return { adapter: "postgres", status: "ready" }; },
};
store.users.set("reload-race-user", { id: "reload-race-user", version: 1, email: "reload@example.com", password: "hash", verified: true, status: "active", mfaRequired: false, mfaEnabled: false, roles: ["user"], failedLoginCount: 0, createdAt: new Date().toISOString() });
store.persist();
await saveStarted;
const reload = store.loadPersisted();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(loadCalled, false);
assert.equal(store.users.has("reload-race-user"), true);
releaseSave();
await reload;
assert.equal(loadCalled, true);
assert.equal(store.users.has("reload-race-user"), true);
await store.persistAsync();
const users = (persisted as { users?: Array<[string, unknown]> }).users || [];
assert.equal(users.some(([id]) => id === "reload-race-user"), true);
});
test("postgres reload retries when a write starts during the database read", async () => {
const store = createStore();
let persisted: unknown = { users: [] };
let firstLoad = true;
let releaseLoad!: () => void;
let resolveLoadStarted!: () => void;
const loadStarted = new Promise<void>((resolve) => { resolveLoadStarted = resolve; });
const loadRelease = new Promise<void>((resolve) => { releaseLoad = resolve; });
store.repository = {
adapter: "postgres",
async load() {
if (firstLoad) {
firstLoad = false;
resolveLoadStarted();
await loadRelease;
}
return persisted;
},
async save(value: unknown) { persisted = value; },
getRevision() { return 0; },
async health() { return { adapter: "postgres", status: "ready" }; },
};
const reload = store.loadPersisted();
await loadStarted;
store.users.set("reload-during-read-user", { id: "reload-during-read-user", version: 1, email: "reload-read@example.com", password: "hash", verified: true, status: "active", mfaRequired: false, mfaEnabled: false, roles: ["user"], failedLoginCount: 0, createdAt: new Date().toISOString() });
store.persist();
releaseLoad();
await reload;
assert.equal(store.users.has("reload-during-read-user"), true);
});
test("store transaction rolls back in-memory mutations and defers events until commit", async () => {
const store = createStore();
const taskId = "transaction-task";
store.tasks.set(taskId, { id: taskId, ownerId: "transaction-user", taskType: "image", modelProductId: "basic-image-v1", status: "queued", estimatedCost: 1, reservedCost: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), eventSequence: 0, channelGroupId: "image-default", routeSnapshotVersion: 1, attempts: [], outputs: [] });
let observed = 0;
store.taskSubscribers.set(taskId, new Set([() => { observed += 1; }]));
await assert.rejects(store.transact(() => { const task = store.tasks.get(taskId)!; task.status = "running"; appendEvent(store, taskId, { taskId, type: "task.running", payload: {} }); throw new Error("rollback"); }), /rollback/);
assert.equal(store.tasks.get(taskId)?.status, "queued");
assert.equal(store.events.get(taskId)?.length || 0, 0);
assert.equal(observed, 0);
await store.transact(() => { const task = store.tasks.get(taskId)!; task.status = "running"; appendEvent(store, taskId, { taskId, type: "task.running", payload: {} }); });
assert.equal(observed, 1);
});
test("memory queue leases a task once and allows ack", async () => {
const queue = new MemoryTaskQueue();
await queue.enqueue("task-1");
const claim = await queue.claim("worker-1", 1_000);
assert.equal(claim?.taskId, "task-1");
assert.equal((await queue.claim("worker-2", 1_000)), undefined);
await queue.ack("task-1", claim!.leaseToken);
assert.equal((await queue.health()).status, "ready");
});
test("staging objects stay below the private root and expired files are collected", async () => {
const root = await mkdtemp(path.join(tmpdir(), "miragenflow-staging-"));
try {
assert.equal((await ensureStagingDir(root)).status, "ready");
assert.throws(() => safeStagingPath(root, "../escape"), /escapes root|invalid/);
const object = await putStagingObject(root, "owner-1", new Uint8Array([1, 2, 3]));
assert.equal(object.path.startsWith(path.resolve(root)), true);
const protectedCount = await gcStaging(root, 0, Date.now() + 1_000, (key) => key === object.key);
assert.equal(protectedCount, 0);
const removed = await gcStaging(root, 0, Date.now() + 1_000);
assert.equal(removed, 1);
} finally { await rm(root, { recursive: true, force: true }); }
});
test("expired worker lease moves a running task to unknown without releasing reserve", () => {
const store = createStore();
const now = new Date(Date.now() - 10_000).toISOString();
store.tasks.set("lease-task", { id: "lease-task", ownerId: "lease-user", taskType: "image", modelProductId: "basic-image", status: "running", estimatedCost: 10, reservedCost: 10, createdAt: now, updatedAt: now, eventSequence: 0, channelGroupId: "image-default", routeSnapshotVersion: 1, leaseExpiresAt: now, attempts: [{ id: "attempt-1", channelId: "channel-a", sequence: 1, status: "started", startedAt: now, leaseExpiresAt: now }], outputs: [] });
store.balances.set("lease-user", { available: 0, reserved: 10 });
assert.equal(recoverExpiredTaskLeases(store), 1);
const task = store.tasks.get("lease-task")!;
assert.equal(task.status, "unknown");
assert.equal(task.reservedCost, 10);
assert.equal(task.attempts[0].reconciliationStatus, "pending");
assert.equal(store.balances.get("lease-user")?.reserved, 10);
});
test("bucket allocations release only the requested amount and settled refunds create a new bucket", () => {
const store = createStore();
const userId = "bucket-user";
store.balances.set(userId, { available: 20, reserved: 0 });
store.buckets.set(userId, [
{ id: "bucket-a", userId, source: "recharge", remaining: 10, priority: 0 },
{ id: "bucket-b", userId, source: "plan", remaining: 10, priority: 1 },
]);
assert.equal(reserveBalance(store, userId, 10, "task-bucket"), true);
assert.deepEqual(store.buckets.get(userId)?.map((bucket) => bucket.remaining), [0, 10]);
assert.equal(releaseBalance(store, userId, 4, "task-bucket"), true);
assert.equal(store.balances.get(userId)?.available, 14);
assert.equal(store.balances.get(userId)?.reserved, 6);
assert.deepEqual(store.buckets.get(userId)?.map((bucket) => bucket.remaining), [4, 10]);
assert.equal(settleBalance(store, userId, 6, 6, "task-bucket"), true);
assert.equal(store.balances.get(userId)?.reserved, 0);
assert.equal(refundBalance(store, userId, 3, "task-bucket-refund"), true);
assert.equal(store.balances.get(userId)?.available, 17);
assert.equal(store.ledger.filter((entry) => entry.userId === userId).length, 4);
});
test("partial settlement records the unused reserve as an explicit release", () => {
const store = createStore();
const userId = "partial-settle-user";
store.balances.set(userId, { available: 10, reserved: 0 });
store.buckets.set(userId, [{ id: "partial-bucket", userId, source: "recharge", remaining: 10, priority: 0 }]);
assert.equal(reserveBalance(store, userId, 10, "partial-task"), true);
assert.equal(settleBalance(store, userId, 10, 6, "partial-task"), true);
assert.equal(store.ledger.filter((entry) => entry.referenceId === "partial-task" && entry.type === "release").length, 1);
assert.equal(store.ledger.find((entry) => entry.referenceId === "partial-task" && entry.type === "release")?.amount, 4);
assert.equal(store.ledger.find((entry) => entry.referenceId === "partial-task" && entry.type === "release")?.bucketId, "partial-bucket");
assert.equal(store.ledger.find((entry) => entry.referenceId === "partial-task" && entry.type === "settle")?.bucketId, "partial-bucket");
assert.equal(store.balances.get(userId)?.available, 4);
});
test("legacy aggregate gaps materialize as UUID buckets even with a preferred bucket", () => {
const store = createStore();
const userId = "legacy-gap-user";
store.balances.set(userId, { available: 20, reserved: 0 });
store.buckets.set(userId, [{ id: "existing-bucket", userId, source: "recharge", remaining: 5, priority: 0 }]);
assert.equal(reserveBalance(store, userId, 10, "legacy-gap-task", { preferredBucketIds: new Set(["existing-bucket"]) }), true);
const buckets = store.buckets.get(userId) || [];
assert.equal(buckets.length, 2);
assert.match(buckets[1].id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
assert.equal(buckets.reduce((total, bucket) => total + bucket.remaining, 0), 10);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 10, reserved: 10 });
assert.equal(store.balances.get(userId)?.version, 2);
assert.equal((store.reserveAllocations.get(`${userId}:legacy-gap-task`) || []).reduce((total, allocation) => total + allocation.amount, 0), 10);
});
test("cross-bucket settlement keeps per-bucket release and charge attribution", () => {
const store = createStore();
const userId = "cross-bucket-settle-user";
store.balances.set(userId, { available: 20, reserved: 0 });
store.buckets.set(userId, [
{ id: "cross-bucket-a", userId, source: "recharge", remaining: 6, priority: 0 },
{ id: "cross-bucket-b", userId, source: "plan", remaining: 14, priority: 1 },
]);
assert.equal(reserveBalance(store, userId, 15, "cross-bucket-task"), true);
assert.equal(settleBalance(store, userId, 15, 8, "cross-bucket-task"), true);
const entries = store.ledger.filter((entry) => entry.referenceId === "cross-bucket-task");
assert.equal(entries.filter((entry) => entry.type === "settle").reduce((total, entry) => total - entry.amount, 0), 8);
assert.equal(entries.filter((entry) => entry.type === "release").reduce((total, entry) => total + entry.amount, 0), 7);
assert.ok(entries.filter((entry) => entry.type === "settle" || entry.type === "release").every((entry) => Boolean(entry.bucketId)));
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 12, reserved: 0 });
assert.equal((store.buckets.get(userId) || []).reduce((total, bucket) => total + bucket.remaining, 0), 12);
assert.equal(store.reserveAllocations.has(`${userId}:cross-bucket-task`), false);
});
test("release and settlement validation failures leave all balance state untouched", () => {
const releaseStore = createStore();
releaseStore.balances.set("missing-release-user", { available: 0, reserved: 5 });
releaseStore.reserveAllocations.set("missing-release-user:missing-release-task", [{ bucketId: "missing-bucket", amount: 5 }]);
const releaseBefore = JSON.stringify({ balances: [...releaseStore.balances], buckets: [...releaseStore.buckets], allocations: [...releaseStore.reserveAllocations], ledger: releaseStore.ledger });
assert.equal(releaseBalance(releaseStore, "missing-release-user", 5, "missing-release-task"), false);
assert.equal(JSON.stringify({ balances: [...releaseStore.balances], buckets: [...releaseStore.buckets], allocations: [...releaseStore.reserveAllocations], ledger: releaseStore.ledger }), releaseBefore);
const settleStore = createStore();
settleStore.balances.set("missing-settle-user", { available: 0, reserved: 5 });
settleStore.reserveAllocations.set("missing-settle-user:missing-settle-task", [{ bucketId: "missing-bucket", amount: 5 }]);
const settleBefore = JSON.stringify({ balances: [...settleStore.balances], buckets: [...settleStore.buckets], allocations: [...settleStore.reserveAllocations], ledger: settleStore.ledger });
assert.equal(settleBalance(settleStore, "missing-settle-user", 5, 3, "missing-settle-task"), false);
assert.equal(JSON.stringify({ balances: [...settleStore.balances], buckets: [...settleStore.buckets], allocations: [...settleStore.reserveAllocations], ledger: settleStore.ledger }), settleBefore);
});
test("zero-charge settlement writes an idempotent marker without losing the bucket", () => {
const store = createStore();
const userId = "zero-charge-user";
store.balances.set(userId, { available: 5, reserved: 0 });
store.buckets.set(userId, [{ id: "zero-charge-bucket", userId, source: "recharge", remaining: 5, priority: 0 }]);
assert.equal(reserveBalance(store, userId, 5, "zero-charge-task"), true);
assert.equal(settleBalance(store, userId, 5, 0, "zero-charge-task"), true);
assert.equal(store.ledger.filter((entry) => entry.idempotencyKey === `settle:${userId}:zero-charge-task` && entry.amount === 0).length, 1);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 5, reserved: 0 });
assert.equal(store.buckets.get(userId)?.[0].remaining, 5);
});
test("reserved plan balance that expires before release is not restored", () => {
const store = createStore();
const userId = "expired-reserved-release-user";
const referenceId = "expired-reserved-release-task";
store.balances.set(userId, { available: 10, reserved: 0 });
store.buckets.set(userId, [{ id: "expired-reserved-release-bucket", userId, source: "plan", remaining: 10, priority: 0, expiresAt: new Date(Date.now() + 60_000).toISOString() }]);
assert.equal(reserveBalance(store, userId, 10, referenceId), true);
store.buckets.get(userId)![0].expiresAt = new Date(Date.now() - 1_000).toISOString();
assert.equal(releaseBalance(store, userId, 4, referenceId), true);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 0, reserved: 6 });
assert.equal(store.buckets.get(userId)?.[0].remaining, 0);
assert.deepEqual(store.reserveAllocations.get(`${userId}:${referenceId}`), [{ bucketId: "expired-reserved-release-bucket", amount: 6 }]);
assert.equal(store.ledger.find((entry) => entry.idempotencyKey === `release:${userId}:${referenceId}:4:expire:expired-reserved-release-bucket`)?.amount, -4);
assert.equal(settleBalance(store, userId, 6, 6, referenceId), true);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 0, reserved: 0 });
});
test("unused part of an expired reservation is expired during settlement", () => {
const store = createStore();
const userId = "expired-reserved-settle-user";
const referenceId = "expired-reserved-settle-task";
store.balances.set(userId, { available: 10, reserved: 0 });
store.buckets.set(userId, [{ id: "expired-reserved-settle-bucket", userId, source: "plan", remaining: 10, priority: 0, expiresAt: new Date(Date.now() + 60_000).toISOString() }]);
assert.equal(reserveBalance(store, userId, 10, referenceId), true);
store.buckets.get(userId)![0].expiresAt = new Date(Date.now() - 1_000).toISOString();
assert.equal(settleBalance(store, userId, 10, 6, referenceId), true);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 0, reserved: 0 });
assert.equal(store.buckets.get(userId)?.[0].remaining, 0);
assert.equal(store.reserveAllocations.has(`${userId}:${referenceId}`), false);
assert.equal(store.ledger.find((entry) => entry.idempotencyKey === `settle:${userId}:${referenceId}:expire:expired-reserved-settle-bucket`)?.amount, -4);
assert.equal(store.ledger.find((entry) => entry.idempotencyKey === `settle:${userId}:${referenceId}:expired-reserved-settle-bucket`)?.amount, -6);
assert.equal(store.ledger.some((entry) => entry.referenceId === referenceId && entry.type === "release"), false);
});
test("expired unknown tasks without a provider request id remain held for reconciliation", () => {
const store = createStore();
const userId = "unknown-expiry-user";
store.balances.set(userId, { available: 10, reserved: 0 });
store.buckets.set(userId, [{ id: "bucket", userId, source: "recharge", remaining: 10, priority: 0 }]);
assert.equal(reserveBalance(store, userId, 10, "unknown-expiry-task"), true);
const expired = new Date(Date.now() - 1_000).toISOString();
store.tasks.set("unknown-expiry-task", { id: "unknown-expiry-task", ownerId: userId, taskType: "image", modelProductId: "basic-image", status: "unknown", estimatedCost: 10, reservedCost: 10, reserveExpiresAt: expired, createdAt: expired, updatedAt: expired, eventSequence: 0, channelGroupId: "image-default", routeSnapshotVersion: 1, attempts: [{ id: "unknown-attempt", channelId: "channel-a", sequence: 1, status: "unknown", startedAt: expired, finishedAt: expired, reconciliationStatus: "pending" }], outputs: [] });
assert.equal(recoverExpiredReservations(store), 0);
assert.equal(store.tasks.get("unknown-expiry-task")?.status, "unknown");
assert.equal(store.tasks.get("unknown-expiry-task")?.reservedCost, 10);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 0, reserved: 10 });
});
test("file snapshots use a version marker and reject corrupt state", async () => {
const root = await mkdtemp(path.join(tmpdir(), "miragenflow-snapshot-"));
try {
const file = path.join(root, "store.json");
const store = createStore(file);
store.persist();
const persisted = JSON.parse(await readFile(file, "utf8")) as { snapshotVersion?: number };
assert.equal(persisted.snapshotVersion, 1);
await writeFile(file, "{not-json", "utf8");
assert.throws(() => createStore(file), /persistence snapshot rejected/);
} finally { await rm(root, { recursive: true, force: true }); }
});
test("message outbox fails over to the next enabled provider", async () => {
const previous = process.env.MIRAGENFLOW_MESSAGE_FAIL;
process.env.MIRAGENFLOW_MESSAGE_FAIL = "email-primary";
const store = createStore();
store.messageProviders.set("email-primary", { id: "email-primary", channel: "email", enabled: true, priority: 0 });
store.messageProviders.set("email-secondary", { id: "email-secondary", channel: "email", enabled: true, priority: 1 });
const record = enqueueMessage(store, { channel: "email", target: "test@example.com", purpose: "register", templateData: { code: "123456" } });
await processMessageOutbox(store);
assert.equal(record.status, "sent");
assert.ok(record.providerMessageId?.startsWith("email-secondary-"));
if (previous === undefined) delete process.env.MIRAGENFLOW_MESSAGE_FAIL;
else process.env.MIRAGENFLOW_MESSAGE_FAIL = previous;
});
test("message outbox reuses a supplied idempotency key", () => {
const store = createStore();
const first = enqueueMessage(store, { channel: "email", target: "same@example.com", purpose: "notification", templateData: { message: "hello" }, idempotencyKey: "welcome:user-1" });
const second = enqueueMessage(store, { channel: "email", target: "same@example.com", purpose: "notification", templateData: { message: "changed" }, idempotencyKey: "welcome:user-1" });
assert.equal(second.id, first.id);
assert.equal(store.messageOutbox.size, 1);
});
test("expired WebDAV retention queues only tracked remote files", () => {
const store = createStore();
const userId = "webdav-retention-user";
store.webdav.set(userId, { userId, configured: true, directory: "miragenflow", state: "ready", retentionDays: 30, manifestRetentionExpiresAt: new Date(Date.now() - 1_000).toISOString() });
store.webdavFiles.set(`${userId}:canvas/manifest.json`, { userId, path: "canvas/manifest.json", mimeType: "application/json", data: "eA==", checksum: "a".repeat(64), etag: '"v1"', version: 1, updatedAt: new Date().toISOString() });
store.webdavFiles.set(`${userId}:assets/orphan.png`, { userId, path: "assets/orphan.png", mimeType: "image/png", data: "eA==", checksum: "b".repeat(64), etag: '"v1"', version: 1, updatedAt: new Date().toISOString() });
assert.equal(enqueueExpiredWebDavRetention(store), 2);
assert.equal(store.webdav.get(userId)?.retentionState, "deleting");
assert.deepEqual([...store.webdavJobs.values()].map((job) => job.intent), ["retention-delete", "retention-delete"]);
assert.equal(enqueueExpiredWebDavRetention(store), 0);
});
test("memory queue renews a lease and moves exhausted work to dead letters", async () => {
const queue = new MemoryTaskQueue();
await queue.enqueue("lease-task", { maxAttempts: 1 });
const lease = await queue.claim("worker", 20);
assert.ok(lease);
assert.equal(await queue.renew!("lease-task", lease!.leaseToken, 1000), true);
await queue.nack!("lease-task", lease!.leaseToken, { retry: true, maxAttempts: 1 });
assert.deepEqual(await queue.deadLetters!(), ["lease-task"]);
assert.equal(await queue.renew!("lease-task", lease!.leaseToken, 1000), false);
});
test("memory queue rejects stale ack/nack and makes the expired lease claimable again", async () => {
const queue = new MemoryTaskQueue();
await queue.enqueue("stale-lease-task");
const lease = await queue.claim("worker", 10);
assert.ok(lease);
await new Promise((resolve) => setTimeout(resolve, 25));
await queue.ack("stale-lease-task", lease!.leaseToken);
const reclaimed = await queue.claim("worker-2", 1_000);
assert.equal(reclaimed?.taskId, "stale-lease-task");
await queue.enqueue("stale-nack-task");
const staleNack = await queue.claim("worker-3", 10);
assert.ok(staleNack);
await new Promise((resolve) => setTimeout(resolve, 25));
assert.equal(await queue.nack!("stale-nack-task", staleNack!.leaseToken, { retry: true }), false);
});
+30
View File
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isPublicAddress, resolvePublicHttpsUrl } from "../src/infra/outbound-url.ts";
test("outbound URL validation rejects private, loopback, and reserved addresses", async () => {
assert.equal(isPublicAddress("10.0.0.8"), false);
assert.equal(isPublicAddress("192.168.1.10"), false);
assert.equal(isPublicAddress("127.0.0.1"), false);
assert.equal(isPublicAddress("::1"), false);
assert.equal(isPublicAddress("1.1.1.1"), true);
await assert.rejects(
resolvePublicHttpsUrl("https://provider.example.test/v1", async () => [{ address: "10.0.0.8", family: 4 }]),
/不允许指向本机、私有或保留网络/,
);
});
test("outbound URL validation pins a resolved public address", async () => {
const target = await resolvePublicHttpsUrl("https://provider.example.test/api", async () => [{ address: "1.1.1.1", family: 4 }]);
assert.equal(target.address, "1.1.1.1");
assert.equal(target.family, 4);
assert.equal(target.url.pathname, "/api");
});
test("local provider URLs require an explicit private-network opt-in", async () => {
const resolver = async () => [{ address: "127.0.0.1", family: 4 as const }];
await assert.rejects(resolvePublicHttpsUrl("http://localhost:3100/v1", resolver), /必须是无内嵌凭证的 HTTPS URL/);
const target = await resolvePublicHttpsUrl("http://localhost:3100/v1", resolver, { allowPrivateNetwork: true });
assert.equal(target.address, "127.0.0.1");
assert.equal(target.url.protocol, "http:");
});
+379
View File
@@ -0,0 +1,379 @@
import test from "node:test";
import assert from "node:assert/strict";
import { encryptSecret } from "../src/shared/auth.ts";
import { createStore } from "../src/store.ts";
import { invokeProvider, probeProviderModels, queryProvider } from "../src/adapters/provider.ts";
const png = Buffer.from("89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489", "hex");
function channel(store: ReturnType<typeof createStore>) {
return {
...store.channels[0],
providerType: "openai-images" as const,
baseUrl: "https://1.1.1.1/v1",
providerModelId: "base-model",
resolutionModelMap: { "2K": "mapped-2k" },
secretRef: encryptSecret("1234567890123456", store.channelEncryptionKey),
};
}
test("OpenAI image generation uses the mapped model and normalized v1 endpoint", async () => {
const store = createStore();
const requests: Array<{ url: string; body: unknown; headers: Headers }> = [];
const previousFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
requests.push({ url: String(input), body: init?.body, headers: new Headers(init?.headers) });
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json", "x-request-id": "request-123" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-1", taskType: "image", prompt: "一只猫", count: 1, params: { resolution: "2K" }, platformIdempotencyKey: "idem-1" });
assert.equal(result.status, "succeeded");
assert.equal(result.providerRequestId, "request-123");
assert.equal(result.outputs?.[0]?.mimeType, "image/png");
assert.equal(requests[0]?.url, "https://1.1.1.1/v1/images/generations");
assert.equal(requests[0]?.headers.get("authorization"), "Bearer 1234567890123456");
assert.equal(requests[0]?.headers.get("idempotency-key"), "idem-1");
assert.equal((JSON.parse(String(requests[0]?.body)) as Record<string, unknown>).model, "mapped-2k");
} finally {
globalThis.fetch = previousFetch;
}
});
test("display model IDs resolve to distinct upstream request model IDs", async () => {
const store = createStore();
const configured = { ...channel(store), modelMappings: [{ displayModelId: "平台模型", requestModelId: "供应商模型" }] };
const previousFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
assert.equal((JSON.parse(String(init?.body)) as Record<string, unknown>).model, "供应商模型");
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, configured, { taskId: "mapping-task", taskType: "image", publicModelId: "平台模型", prompt: "测试", params: { resolution: "2K" }, platformIdempotencyKey: "mapping-idem" });
assert.equal(result.status, "succeeded");
} finally {
globalThis.fetch = previousFetch;
}
});
test("OpenAI image edits preserve reference order with image[] fields", async () => {
const store = createStore();
for (const [id, color] of [["ref-a", 0x11], ["ref-b", 0x22]] as const) {
const bytes = Buffer.from(png);
bytes[bytes.length - 1] ^= color;
store.objects.set(id, { id, ownerId: "user-1", mimeType: "image/png", data: bytes.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
}
const previousFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
const form = init?.body as FormData;
const names = [...form.getAll("image[]")].map((value) => (value instanceof File ? value.name : ""));
assert.deepEqual(names, ["reference-0.png", "reference-1.png"]);
assert.equal(form.get("model"), "base-model");
assert.equal(new Headers(init?.headers).get("idempotency-key"), "idem-2");
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-2", taskType: "image", prompt: "@图片一和@图片二", count: 1, referenceImages: [{ objectId: "ref-b", seq: 2, name: "图片二" }, { objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-2" });
assert.equal(result.status, "succeeded");
} finally {
globalThis.fetch = previousFetch;
}
});
test("invalid image base64 and empty model probes fail closed", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
globalThis.fetch = async (input) => {
if (String(input).endsWith("/models")) return new Response(JSON.stringify({ data: [] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response(JSON.stringify({ data: [{ b64_json: "not-base64" }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-3", taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: "idem-3" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
await assert.rejects(() => probeProviderModels("https://1.1.1.1/v1", "1234567890123456"));
} finally {
globalThis.fetch = previousFetch;
}
});
test("duplicate references are uploaded once and URL outputs are validated", async () => {
const store = createStore();
store.objects.set("ref-a", { id: "ref-a", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const previousFetch = globalThis.fetch;
const urls: string[] = [];
globalThis.fetch = async (input, init) => {
urls.push(String(input));
if (String(input).endsWith("/images/edits")) {
const form = init?.body as FormData;
assert.equal(form.getAll("image[]").length, 1);
assert.equal(form.get("prompt"), "使用第一张图");
return new Response(JSON.stringify({ data: [{ url: "https://1.1.1.1/result.png" }] }), { status: 200, headers: { "content-type": "application/json" } });
}
return new Response(png, { status: 200, headers: { "content-type": "image/png" } });
};
try {
const result = await invokeProvider(store, channel(store), {
taskId: "task-4",
taskType: "image",
prompt: "使用@图片一",
count: 1,
referenceImages: [
{ objectId: "ref-a", seq: 1, name: "图片一" },
{ objectId: "ref-a", seq: 2, name: "图片二" },
],
platformIdempotencyKey: "idem-4",
});
assert.equal(result.status, "succeeded");
assert.equal(result.outputs?.[0]?.mimeType, "image/png");
assert.deepEqual(urls, ["https://1.1.1.1/v1/images/edits", "https://1.1.1.1/result.png"]);
} finally {
globalThis.fetch = previousFetch;
}
});
test("unavailable reference objects fail without retrying the provider", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async () => {
called = true;
return new Response(JSON.stringify({ data: [] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), {
taskId: "task-5",
taskType: "image",
prompt: "使用第一张图",
count: 1,
referenceImages: [{ objectId: "missing", seq: 1, name: "图片一" }],
platformIdempotencyKey: "idem-5",
});
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
assert.equal(called, false);
} finally {
globalThis.fetch = previousFetch;
}
});
test("expired, revoked, and missing staging references fail deterministically", async () => {
const store = createStore();
store.objects.set("expired", { id: "expired", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() - 1).toISOString() });
store.objects.set("revoked", { id: "revoked", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString(), revoked: true });
store.objects.set("staging-missing", { id: "staging-missing", ownerId: "user-1", mimeType: "image/png", stagingKey: "missing/reference.png", expiresAt: new Date(Date.now() + 60_000).toISOString() });
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async () => { called = true; return new Response(JSON.stringify({ data: [] }), { status: 200 }); };
try {
for (const [index, objectId] of ["expired", "revoked", "staging-missing"].entries()) {
const result = await invokeProvider(store, channel(store), { taskId: `reference-unavailable-${index}`, taskType: "image", prompt: "使用第一张图", count: 1, referenceImages: [{ objectId, seq: 1, name: "图片一" }], platformIdempotencyKey: `reference-unavailable-${index}` });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
assert.equal(result.retryable, false);
}
assert.equal(called, false);
} finally {
globalThis.fetch = previousFetch;
}
});
test("invalid image output URLs are non-retryable provider failures", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
const outputs = [
"not a url",
"https://127.0.0.1/result.png",
"https://1.1.1.1/result.png",
];
try {
for (const [index, outputUrl] of outputs.entries()) {
globalThis.fetch = async (input) => {
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: outputUrl }] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response(index === 2 ? png : "not an image", { status: index === 2 ? 404 : 200, headers: { "content-type": "image/png" } });
};
const result = await invokeProvider(store, channel(store), { taskId: `invalid-output-${index}`, taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: `invalid-output-${index}` });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
assert.equal(result.retryable, false);
}
} finally {
globalThis.fetch = previousFetch;
}
});
test("redirected image output URLs are rejected as invalid output", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
let downloadRedirected = false;
globalThis.fetch = async (input) => {
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: "https://1.1.1.1/result.png" }] }), { status: 200, headers: { "content-type": "application/json" } });
downloadRedirected = true;
return new Response(null, { status: 302, headers: { location: "https://example.com/private.png" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "redirect-output", taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: "redirect-output" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
assert.equal(result.retryable, false);
assert.equal(downloadRedirected, true);
} finally {
globalThis.fetch = previousFetch;
}
});
test("image output metadata prefers dimensions decoded from the returned bytes", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
globalThis.fetch = async (input) => {
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64"), size: "999x999" }] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response(null, { status: 500 });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "metadata-bytes", taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: "metadata-bytes" });
assert.equal(result.status, "succeeded");
assert.deepEqual(result.outputs?.[0]?.metadata, { width: 1, height: 1, format: "png", size: "1x1", revisedPrompt: undefined, usage: undefined, source: "base64" });
} finally {
globalThis.fetch = previousFetch;
}
});
test("image references validate mentions and add a default reference instruction", async () => {
const store = createStore();
store.objects.set("ref-a", { id: "ref-a", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const previousFetch = globalThis.fetch;
const prompts: string[] = [];
globalThis.fetch = async (_input, init) => {
const form = init?.body as FormData;
prompts.push(String(form.get("prompt")));
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-6", taskType: "image", prompt: "请保持主体", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-6" });
assert.equal(result.status, "succeeded");
assert.equal(prompts[0], "请保持主体\n基于以下参考图生成。");
const missing = await invokeProvider(store, channel(store), { taskId: "task-7", taskType: "image", prompt: "使用@图片二", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-7" });
assert.equal(missing.status, "failed");
assert.equal(missing.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
} finally {
globalThis.fetch = previousFetch;
}
});
test("invalid provider reference sequences fail closed", async () => {
const store = createStore();
const result = await invokeProvider(store, channel(store), { taskId: "task-8", taskType: "image", prompt: "测试", count: 1, referenceImages: [{ objectId: "ref-a", seq: 17, name: "图片十七" }], platformIdempotencyKey: "idem-8" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_INVALID");
});
test("duplicate reference sequences are rejected instead of producing ambiguous 图片 names", async () => {
const store = createStore();
for (const objectId of ["duplicate-seq-a", "duplicate-seq-b"]) store.objects.set(objectId, { id: objectId, ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const result = await invokeProvider(store, channel(store), { taskId: "duplicate-seq", taskType: "image", prompt: "测试", count: 1, referenceImages: [{ objectId: "duplicate-seq-a", seq: 1, name: "图片一" }, { objectId: "duplicate-seq-b", seq: 1, name: "图片一" }], platformIdempotencyKey: "duplicate-seq" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_INVALID");
});
test("provider image requests reject non-positive or fractional counts", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async () => { called = true; return new Response(JSON.stringify({ data: [] }), { status: 200 }); };
try {
for (const count of [0, -1, 1.5]) {
const result = await invokeProvider(store, channel(store), { taskId: `invalid-count-${count}`, taskType: "image", prompt: "测试", count, platformIdempotencyKey: `invalid-count-${String(count).replace(".", "-")}` });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_REQUEST");
}
assert.equal(called, false);
} finally {
globalThis.fetch = previousFetch;
}
});
test("OpenAI image edits accept the full sixteen-reference boundary and reject the seventeenth", async () => {
const store = createStore();
const references = Array.from({ length: 16 }, (_, index) => {
const objectId = `boundary-ref-${index + 1}`;
store.objects.set(objectId, { id: objectId, ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
return { objectId, seq: index + 1, name: `图片${index + 1}` };
});
const previousFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
const form = init?.body as FormData;
assert.equal(form.getAll("image[]").length, 16);
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const accepted = await invokeProvider(store, channel(store), { taskId: "sixteen-references", taskType: "image", prompt: "组合参考图", count: 1, referenceImages: references, platformIdempotencyKey: "sixteen-references" });
assert.equal(accepted.status, "succeeded");
const rejected = await invokeProvider(store, channel(store), { taskId: "seventeen-references", taskType: "image", prompt: "组合参考图", count: 1, referenceImages: [...references, { objectId: "boundary-ref-1", seq: 17, name: "图片十七" }], platformIdempotencyKey: "seventeen-references" });
assert.equal(rejected.status, "failed");
assert.equal(rejected.errorCode, "PROVIDER_REFERENCE_LIMIT");
} finally {
globalThis.fetch = previousFetch;
}
});
test("provider output metadata detects JPEG and WebP bytes instead of trusting a declared format", async () => {
const store = createStore();
const jpeg = Buffer.from("ffd8ffc00011080001000101011100021100031100ffd9", "hex");
const webp = Buffer.alloc(30);
webp.write("RIFF", 0, "ascii"); webp.writeUInt32LE(22, 4); webp.write("WEBP", 8, "ascii"); webp.write("VP8X", 12, "ascii"); webp.writeUInt32LE(10, 16);
const previousFetch = globalThis.fetch;
let requestedFormat = "jpeg";
globalThis.fetch = async (input) => {
const url = String(input);
if (url.endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: `https://1.1.1.1/result.${requestedFormat}` }] }), { status: 200, headers: { "content-type": "application/json" } });
const bytes = requestedFormat === "jpeg" ? jpeg : webp;
return new Response(bytes, { status: 200, headers: { "content-type": requestedFormat === "jpeg" ? "image/jpg" : "image/webp" } });
};
try {
for (const [index, suffix] of ["jpeg", "webp"].entries()) {
requestedFormat = suffix;
const result = await invokeProvider(store, channel(store), { taskId: `format-${suffix}`, taskType: "image", prompt: "格式测试", count: 1, params: { output_format: suffix }, platformIdempotencyKey: `format-${index}` });
assert.equal(result.status, "succeeded");
assert.equal(result.outputs?.[0]?.mimeType, `image/${suffix}`);
assert.deepEqual(result.outputs?.[0]?.metadata && { width: result.outputs[0].metadata.width, height: result.outputs[0].metadata.height, format: result.outputs[0].metadata.format }, { width: 1, height: 1, format: suffix });
}
} finally {
globalThis.fetch = previousFetch;
}
});
test("provider probes use GET and Bearer authentication, and transport failures become unknown", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
const probeRequests: Array<{ method: string; authorization: string | null }> = [];
globalThis.fetch = async (input, init) => {
if (String(input).endsWith("/models")) {
probeRequests.push({ method: init?.method || "GET", authorization: new Headers(init?.headers).get("authorization") });
return new Response(JSON.stringify({ data: [{ id: "gpt-image-2" }] }), { status: 200, headers: { "content-type": "application/json" } });
}
throw new Error("socket closed");
};
try {
const probe = await probeProviderModels("https://1.1.1.1/v1", "1234567890123456");
assert.deepEqual(probe, { healthy: true, models: ["gpt-image-2"] });
assert.deepEqual(probeRequests, [{ method: "GET", authorization: "Bearer 1234567890123456" }]);
const unknown = await invokeProvider(store, channel(store), { taskId: "transport-unknown", taskType: "image", prompt: "网络中断", count: 1, platformIdempotencyKey: "transport-unknown" });
assert.equal(unknown.status, "unknown");
assert.equal(unknown.errorCode, "PROVIDER_TRANSPORT_UNKNOWN");
const abortedFetch = globalThis.fetch;
globalThis.fetch = async () => { throw Object.assign(new Error("timeout"), { name: "AbortError" }); };
const timeout = await invokeProvider(store, channel(store), { taskId: "timeout-unknown", taskType: "image", prompt: "超时", count: 1, platformIdempotencyKey: "timeout-unknown" });
assert.equal(timeout.status, "unknown");
assert.equal(timeout.errorCode, "PROVIDER_TIMEOUT_UNKNOWN");
globalThis.fetch = async () => { throw new Error("probe transport failed"); };
await assert.rejects(() => probeProviderModels("https://1.1.1.1/v1", "1234567890123456"));
globalThis.fetch = abortedFetch;
} finally {
globalThis.fetch = previousFetch;
}
});
test("OpenAI image channels stay in manual reconciliation because no query endpoint is assumed", async () => {
const store = createStore();
const result = await queryProvider(store, channel(store), "provider-request-1");
assert.deepEqual(result, { status: "unknown", providerRequestId: "provider-request-1", errorCode: "PROVIDER_QUERY_UNAVAILABLE" });
});
+363
View File
@@ -0,0 +1,363 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import { createHttpServer } from "../src/app/http.ts";
import { loadConfig } from "../src/config.ts";
import { createTotpSecret, encryptSecret, issueAccessToken, verifyAccessToken } from "../src/shared/auth.ts";
import { createStore } from "../src/store.ts";
function config(role = "operator") {
return { host: "127.0.0.1", port: 0, accessTokenSecret: "test-secret-for-security-tests", accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: false, userMfaRequired: false, adminRole: role };
}
test("ordinary-user verification enhancements default to disabled", () => {
const config = loadConfig({ NODE_ENV: "development", MIRAGENFLOW_ACCESS_TOKEN_SECRET: "default-test-secret" });
assert.equal(config.adminMfaRequired, false);
assert.equal(config.userMfaRequired, false);
assert.equal(config.userVerificationRequired, false);
assert.equal(config.captchaRequired, false);
assert.equal(config.adminAccounts?.[0]?.mfaRequired, false);
});
test("cookie security can be explicitly disabled for an HTTP reverse proxy", () => {
const productionEnv = {
NODE_ENV: "production",
MIRAGENFLOW_ACCESS_TOKEN_SECRET: "a-production-secret-that-is-long-enough",
MIRAGENFLOW_ADMIN_PASSWORD: "a-production-admin-password",
MIRAGENFLOW_ADMIN_MFA_SECRET: "JBSWY3DPEHPK3PXP",
MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY: "a-production-channel-key",
MIRAGENFLOW_PERSISTENCE_ADAPTER: "postgres",
DATABASE_URL: "postgres://example",
MIRAGENFLOW_QUEUE_ADAPTER: "redis",
REDIS_URL: "redis://example",
};
const productionDefaults = loadConfig(productionEnv);
assert.equal(productionDefaults.cookieSecure, true);
assert.equal(loadConfig({ ...productionEnv, MIRAGENFLOW_COOKIE_SECURE: "false" }).cookieSecure, false);
assert.equal(loadConfig({ NODE_ENV: "development", MIRAGENFLOW_ACCESS_TOKEN_SECRET: "default-test-secret", MIRAGENFLOW_COOKIE_SECURE: "true" }).cookieSecure, true);
});
function totpCode(secret: string, now = Date.now()) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let buffer = 0; let bits = 0; const bytes: number[] = [];
for (const character of secret) { const value = alphabet.indexOf(character); if (value < 0) continue; buffer = (buffer << 5) | value; bits += 5; if (bits >= 8) { bytes.push((buffer >>> (bits - 8)) & 255); bits -= 8; } }
const key = Buffer.from(bytes); const message = Buffer.alloc(8); message.writeBigInt64BE(BigInt(Math.floor(now / 30_000))); const digest = createHmac("sha1", key).update(message).digest(); const offset = digest[digest.length - 1] & 15; const value = ((digest[offset] & 127) << 24) | (digest[offset + 1] << 16) | (digest[offset + 2] << 8) | digest[offset + 3]; return String(value % 1_000_000).padStart(6, "0");
}
test("CAPTCHA challenge is hashed, bound, retry-limited, and one-time", async () => {
const store = createStore();
const server = createHttpServer(store, { ...config(), captchaRequired: true });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address(); assert.ok(address && typeof address !== "string");
const base = `http://127.0.0.1:${address.port}`; const email = "captcha@example.com"; const deviceId = "captcha-device";
try {
const challengeResponse = await fetch(`${base}/api/v1/auth/challenge`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "register", target: email, deviceId }) });
assert.equal(challengeResponse.status, 200);
const challenge = (await challengeResponse.json() as { data: { challengeId: string; question: string } }).data;
const numbers = challenge.question.match(/(\d+) \+ (\d+)/); assert.ok(numbers);
const answer = String(Number(numbers[1]) + Number(numbers[2]));
const baseBody = { email, password: "password123", deviceId, captchaChallengeId: challenge.challengeId };
const wrong = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...baseBody, captchaAnswer: "0" }) });
assert.equal(wrong.status, 422);
const registered = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...baseBody, captchaAnswer: answer }) });
assert.equal(registered.status, 201);
const reused = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...baseBody, email: "captcha-second@example.com", captchaAnswer: answer }) });
assert.equal(reused.status, 422);
assert.equal(store.captchaChallenges.get(challenge.challengeId)?.consumedAt !== undefined, true);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("administrator MFA retry is exempt only after a valid CAPTCHA-bound challenge", async () => {
const secret = "JBSWY3DPEHPK3PXP";
const store = createStore();
const server = createHttpServer(store, { ...config(), captchaRequired: true, adminMfaRequired: true, adminMfaSecret: secret });
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address(); assert.ok(address && typeof address !== "string");
const base = `http://127.0.0.1:${address.port}`;
try {
const challengeResponse = await fetch(`${base}/api/v1/auth/challenge`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "admin-login", target: "admin@example.com", deviceId: "admin-browser" }) });
const challenge = (await challengeResponse.json() as { data: { challengeId: string; question: string } }).data; const numbers = challenge.question.match(/(\d+) \+ (\d+)/); assert.ok(numbers); const answer = String(Number(numbers[1]) + Number(numbers[2]));
const first = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now", deviceId: "admin-browser", captchaChallengeId: challenge.challengeId, captchaAnswer: answer }) });
const firstData = (await first.json() as { data: { mfaRequired?: boolean; challengeId?: string } }).data; assert.equal(first.status, 200); assert.equal(firstData.mfaRequired, true); assert.ok(firstData.challengeId);
const second = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now", deviceId: "admin-browser", challengeId: firstData.challengeId, mfaCode: totpCode(secret) }) });
const secondData = (await second.json() as { data: { accessToken?: string } }).data; assert.equal(second.status, 200); assert.ok(secondData.accessToken);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("refresh restores a missing legacy CSRF cookie without weakening business writes", async () => {
const { store, server, base } = await start("super_admin");
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
assert.equal(login.status, 200);
const setCookies = login.headers.getSetCookie();
const refresh = setCookies.find((value) => value.startsWith("refresh_admin="));
assert.ok(refresh);
const legacyCookie = refresh.split(";", 1)[0];
const refreshed = await fetch(`${base}/api/v1/admin/auth/refresh`, { method: "POST", headers: { cookie: `${legacyCookie}` } });
assert.equal(refreshed.status, 200);
const restoredCookies = refreshed.headers.getSetCookie();
const nextRefresh = restoredCookies.find((value) => value.startsWith("refresh_admin="));
assert.ok(nextRefresh);
assert.equal(restoredCookies.some((value) => value.startsWith("miragenflow_csrf_admin=") && !value.includes("HttpOnly")), true);
const refreshedPayload = await refreshed.json() as { data: { accessToken: string } };
const protectedWrite = await fetch(`${base}/api/v1/admin/settings`, { method: "PATCH", headers: { cookie: `${nextRefresh!.split(";", 1)[0]}`, authorization: `Bearer ${refreshedPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "legacy-csrf-write" }, body: JSON.stringify({}) });
assert.equal(protectedWrite.status, 403);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
async function start(role = "operator") {
const store = createStore();
const server = createHttpServer(store, config(role));
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
return { store, server, base: `http://127.0.0.1:${address.port}` };
}
test("admin RBAC rejects finance access and redacts support channel internals", async () => {
const { store, server, base } = await start("support");
try {
store.channels[0].providerName = "internal-provider";
store.channels[0].providerModelId = "internal-model";
store.channels[0].baseUrl = "https://provider.example.com";
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
assert.ok(loginPayload.data.accessToken);
const auth = { authorization: `Bearer ${loginPayload.data.accessToken}` };
const finance = await fetch(`${base}/api/v1/admin/billing/ledger`, { headers: auth });
assert.equal(finance.status, 403);
const channels = await fetch(`${base}/api/v1/admin/channels`, { headers: auth });
assert.equal(channels.status, 200);
const payload = await channels.json() as { data: { items: Array<Record<string, unknown>> } };
assert.equal(payload.data.items[0].providerModelId, undefined);
assert.equal(payload.data.items[0].baseUrl, undefined);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("payment provider credentials are encrypted and redacted", async () => {
const { store, server, base } = await start("super_admin");
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } }; const auth = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "payment-provider-create" };
const created = await fetch(`${base}/api/v1/admin/payment-providers`, { method: "POST", headers: auth, body: JSON.stringify({ id: "secure-adapter", name: "Secure adapter", mode: "live", secret: "payment-secret-value" }) });
assert.equal(created.status, 201); const createdPayload = await created.json() as { data: { item: Record<string, unknown> } }; assert.equal(createdPayload.data.item.secretRef, undefined); assert.equal(createdPayload.data.item.secretConfigured, true);
const stored = store.paymentProviders.get("secure-adapter"); assert.ok(typeof stored?.secretRef === "string"); assert.notEqual(stored?.secretRef, "payment-secret-value");
const listed = await fetch(`${base}/api/v1/admin/payment-providers`, { headers: { authorization: `Bearer ${loginPayload.data.accessToken}` } }); const listedPayload = await listed.json() as { data: { items: Array<Record<string, unknown>> } }; assert.equal(listedPayload.data.items[0].secretRef, undefined); assert.equal(listedPayload.data.items[0].secret, undefined);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("OpenAI image channel creation does not probe implicitly and explicit probing uses Bearer auth", async () => {
const { store, server, base } = await start("super_admin");
const previousFetch = globalThis.fetch;
const probeRequests: Array<{ method: string; authorization: string | null; url: string }> = [];
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
globalThis.fetch = async (input, init) => {
const url = String(input);
if (url.startsWith("https://1.1.1.1/")) {
probeRequests.push({ url, method: init?.method || "GET", authorization: new Headers(init?.headers).get("authorization") });
return new Response(JSON.stringify({ data: [{ id: "gpt-image-2" }, { id: "gpt-image-2-2k" }] }), { status: 200, headers: { "content-type": "application/json" } });
}
return previousFetch(input, init);
};
const auth = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json" };
const created = await fetch(`${base}/api/v1/admin/channels`, { method: "POST", headers: { ...auth, "idempotency-key": "openai-channel-create" }, body: JSON.stringify({ label: "OpenAI 图片渠道", providerType: "openai-images", providerModelId: "gpt-image-2", baseUrl: "https://1.1.1.1/v1", secretRef: "k", resolutionModelMap: { "1K": "gpt-image-2", "2K": "gpt-image-2-2k", invalid: "should-drop" } }) });
assert.equal(created.status, 201);
const createdPayload = await created.json() as { data: { item: Record<string, unknown> } };
assert.equal(createdPayload.data.item.secretRef, "configured");
assert.equal(createdPayload.data.item.baseUrl, "https://1.1.1.1/v1");
assert.deepEqual(createdPayload.data.item.resolutionModelMap, { "1K": "gpt-image-2", "2K": "gpt-image-2-2k" });
const channel = store.channels.find((item) => item.id === createdPayload.data.item.id);
assert.ok(channel);
assert.notEqual(channel.secretRef, "k");
assert.equal(probeRequests.length, 0);
const probe = await fetch(`${base}/api/v1/admin/channels/probe`, { method: "POST", headers: { ...auth, "idempotency-key": "openai-channel-probe" }, body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://1.1.1.1/v1", secretRef: "k" }) });
assert.equal(probe.status, 200);
assert.equal(probeRequests[0]?.url, "https://1.1.1.1/v1/models");
assert.equal(probeRequests[0]?.method, "GET");
assert.equal(probeRequests[0]?.authorization, "Bearer k");
const invalid = await fetch(`${base}/api/v1/admin/channels`, { method: "POST", headers: { ...auth, "idempotency-key": "openai-channel-http" }, body: JSON.stringify({ label: "不安全渠道", providerType: "openai-images", providerModelId: "gpt-image-2", baseUrl: "http://1.1.1.1/v1", secretRef: "1234567890123456" }) });
assert.equal(invalid.status, 201);
assert.equal(store.channels.some((item) => item.label === "不安全渠道"), true);
} finally {
globalThis.fetch = previousFetch;
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin channel probe reports whether the upstream request was attempted and why it failed", async () => {
const { server, base } = await start("super_admin");
const previousFetch = globalThis.fetch;
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
let upstreamRequest: { method?: string; authorization?: string | null } | undefined;
globalThis.fetch = async (input, init) => {
if (String(input) === "https://1.1.1.1/v1/models") {
upstreamRequest = { method: init?.method, authorization: new Headers(init?.headers).get("authorization") };
return new Response(JSON.stringify({ error: { message: "invalid api key" } }), { status: 401, headers: { "content-type": "application/json" } });
}
return previousFetch(input, init);
};
const response = await fetch(`${base}/api/v1/admin/channels/probe`, {
method: "POST",
headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "channel-probe-failure-details" },
body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://1.1.1.1/v1", secretRef: "short-key" }),
});
assert.equal(response.status, 200);
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; requestPath: string; error?: string } } };
assert.equal(payload.data.item.healthy, false);
assert.equal(payload.data.item.requestAttempted, true);
assert.equal(payload.data.item.requestPath, "/v1/models");
assert.match(payload.data.item.error || "", /API Key/);
assert.deepEqual(upstreamRequest, { method: "GET", authorization: "Bearer short-key" });
} finally {
globalThis.fetch = previousFetch;
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin channel probe classifies transport causes without exposing credentials", async () => {
const { server, base } = await start("super_admin");
const previousFetch = globalThis.fetch;
const secret = "transport-secret-value";
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const cases = [
{ code: "ECONNREFUSED", text: "拒绝建立连接" },
{ code: "ECONNRESET", text: "连接被中断" },
{ code: "CERT_HAS_EXPIRED", text: "TLS" },
{ code: "UND_ERR_CONNECT_TIMEOUT", text: "超时" },
];
for (const [index, item] of cases.entries()) {
globalThis.fetch = async (input, init) => {
if (String(input) === "https://1.1.1.1/v1/models") throw Object.assign(new TypeError("fetch failed"), { cause: { code: item.code } });
return previousFetch(input, init);
};
const response = await fetch(`${base}/api/v1/admin/channels/probe`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": `transport-cause-${index}` }, body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://1.1.1.1/v1", secretRef: secret }) });
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; error?: string } } };
assert.equal(response.status, 200);
assert.equal(payload.data.item.healthy, false);
assert.equal(payload.data.item.requestAttempted, true);
assert.match(payload.data.item.error || "", new RegExp(item.text));
assert.doesNotMatch(payload.data.item.error || "", /transport-secret-value|Bearer/);
}
} finally {
globalThis.fetch = previousFetch;
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("unsupported generic channel probes do not infer health from a configured key", async () => {
const { server, base } = await start("super_admin");
const previousFetch = globalThis.fetch;
let upstreamCalls = 0;
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
globalThis.fetch = async (input, init) => {
if (String(input).startsWith("https://generic.example.com/")) upstreamCalls += 1;
return previousFetch(input, init);
};
const response = await fetch(`${base}/api/v1/admin/channels/probe`, {
method: "POST",
headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "generic-channel-probe" },
body: JSON.stringify({ providerType: "generic", baseUrl: "https://generic.example.com/v1", secretRef: "configured-key" }),
});
assert.equal(response.status, 200);
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; error?: string } } };
assert.equal(payload.data.item.healthy, false);
assert.equal(payload.data.item.requestAttempted, false);
assert.match(payload.data.item.error || "", /不支持自动模型列表探活/);
assert.equal(upstreamCalls, 0);
} finally {
globalThis.fetch = previousFetch;
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("provider probe reports blocked addresses before an upstream request", async () => {
const { server, base } = await start("super_admin");
const previousFetch = globalThis.fetch;
const previousAllowLocal = process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS;
let upstreamCalls = 0;
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS = "false";
globalThis.fetch = async (input, init) => {
if (String(input).startsWith("https://127.0.0.1")) upstreamCalls += 1;
return previousFetch(input, init);
};
const response = await fetch(`${base}/api/v1/admin/channels/probe`, {
method: "POST",
headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "dns-channel-probe" },
body: JSON.stringify({ providerType: "openai-images", baseUrl: "https://127.0.0.1/v1", secretRef: "configured-key" }),
});
assert.equal(response.status, 200);
const payload = await response.json() as { data: { item: { healthy: boolean; requestAttempted: boolean; error?: string } } };
assert.equal(payload.data.item.healthy, false);
assert.equal(payload.data.item.requestAttempted, false);
assert.match(payload.data.item.error || "", /不允许指向本机/);
assert.match(payload.data.item.error || "", /未发起|尚未发起/);
assert.equal(upstreamCalls, 0);
} finally {
globalThis.fetch = previousFetch;
if (previousAllowLocal === undefined) delete process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS;
else process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS = previousAllowLocal;
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("WebDAV If-Match mismatch returns a conflict before any remote request", async () => {
const { store, server, base } = await start();
try {
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-conflict@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-conflict@example.com", password: "password123" }) });
const session = await login.json() as { data: { accessToken: string } };
const auth = { authorization: `Bearer ${session.data.accessToken}`, "content-type": "application/json" };
store.webdav.set(registered.data.userId, { userId: registered.data.userId, configured: true, directory: "miragenflow", state: "ready", retentionDays: 30, encryptedUrl: encryptSecret("https://dav.example.com", config().accessTokenSecret) });
store.webdavFiles.set(`${registered.data.userId}:canvas/manifest.json`, { userId: registered.data.userId, path: "canvas/manifest.json", mimeType: "application/json", data: "eA==", checksum: "a".repeat(64), etag: '"remote-v2"', version: 2, updatedAt: new Date().toISOString() });
const response = await fetch(`${base}/api/v1/me/webdav/file`, { method: "PUT", headers: auth, body: JSON.stringify({ path: "canvas/manifest.json", mimeType: "application/json", data: "eA==", ifMatch: '"stale"' }) });
assert.equal(response.status, 409);
const payload = await response.json() as { error?: { code?: string } };
assert.equal(payload.error?.code, "VERSION_CONFLICT");
assert.equal([...store.webdavJobs.values()].length, 0);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("WebDAV rejects traversal paths before creating a retry job", async () => {
const { store, server, base } = await start();
try {
const register = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-path@example.com", password: "password123" }) });
const registered = await register.json() as { data: { userId: string; devVerificationCode: string } };
await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "webdav-path@example.com", password: "password123" }) });
const session = await login.json() as { data: { accessToken: string } };
store.webdav.set(registered.data.userId, { userId: registered.data.userId, configured: true, directory: "miragenflow", state: "ready", retentionDays: 30, encryptedUrl: encryptSecret("https://dav.example.com", config().accessTokenSecret) });
const response = await fetch(`${base}/api/v1/me/webdav/file`, { method: "PUT", headers: { authorization: `Bearer ${session.data.accessToken}`, "content-type": "application/json" }, body: JSON.stringify({ path: "../escape", data: "eA==" }) });
assert.equal(response.status, 422);
assert.equal(store.webdavJobs.size, 0);
} finally { await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); }
});
test("access tokens reject a signed token with an unexpected JOSE header", () => {
const token = issueAccessToken("user-1", "user", "header-test-secret", 900);
const [, body] = token.split(".");
const alteredHeader = Buffer.from(JSON.stringify({ alg: "HS512", typ: "JWT" })).toString("base64url");
const alteredBody = `${alteredHeader}.${body}`;
const alteredSignature = createHmac("sha256", "header-test-secret").update(alteredBody).digest("base64url");
assert.throws(() => verifyAccessToken(`${alteredBody}.${alteredSignature}`, "header-test-secret", "user"), /登录状态已失效/);
assert.doesNotThrow(() => verifyAccessToken(token, "header-test-secret", "user"));
});
@@ -0,0 +1,665 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { GenerationTask } from "@miragenflow/contracts";
import { createHttpServer } from "../src/app/http.ts";
import { createTotpSecret, encryptSecret, hashChallenge } from "../src/shared/auth.ts";
import { createStore, reserveBalance } from "../src/store.ts";
const accessTokenSecret = "v1-regression-test-secret";
function config(overrides: Record<string, unknown> = {}) {
return {
host: "127.0.0.1",
port: 0,
accessTokenSecret,
accessTokenTtlSeconds: 900,
refreshCookieName: "refresh",
adminEmail: "admin@example.com",
adminPassword: "change-me-now",
adminMfaRequired: false,
userMfaRequired: false,
...overrides,
};
}
function totpCode(secret: string, now = Date.now()) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let buffer = 0;
let bits = 0;
const bytes: number[] = [];
for (const character of secret) {
const value = alphabet.indexOf(character);
buffer = (buffer << 5) | value;
bits += 5;
if (bits >= 8) {
bytes.push((buffer >>> (bits - 8)) & 255);
bits -= 8;
}
}
const message = Buffer.alloc(8);
message.writeBigInt64BE(BigInt(Math.floor(now / 30_000)));
const digest = createHmac("sha1", Buffer.from(bytes)).update(message).digest();
const offset = digest[digest.length - 1] & 15;
const value = ((digest[offset] & 127) << 24) | (digest[offset + 1] << 16) | (digest[offset + 2] << 8) | digest[offset + 3];
return String(value % 1_000_000).padStart(6, "0");
}
async function start(overrides: Record<string, unknown> = {}) {
const store = createStore();
const server = createHttpServer(store, config(overrides));
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
return { store, server, base: `http://127.0.0.1:${address.port}` };
}
async function registerAndLogin(base: string, email: string, password = "password123", deviceId?: string) {
const registration = await fetch(`${base}/api/v1/auth/register`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password, deviceId }) });
const registered = await registration.json() as { data: { userId: string; devVerificationCode: string } };
assert.equal(registration.status, 201);
const verification = await fetch(`${base}/api/v1/auth/verify-email`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId: registered.data.userId, code: registered.data.devVerificationCode }) });
assert.equal(verification.status, 200);
const login = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password, deviceId }) });
return { registered: registered.data, login, payload: await login.json() as { data: { accessToken?: string; mfaRequired?: boolean; challengeId?: string; challengeType?: "verify" | "setup"; mfaEnrollment?: { secret: string; recoveryCodes: string[] } } } };
}
async function loginAdmin(base: string, email: string, password: string, secret: string, recoveryCode?: string) {
const first = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password }) });
const firstPayload = await first.json() as { data: { accessToken?: string; challengeId?: string; mfaRequired?: boolean } };
assert.equal(firstPayload.data.mfaRequired, true);
const second = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password, challengeId: firstPayload.data.challengeId, ...(recoveryCode ? { recoveryCode } : { mfaCode: totpCode(secret) }) }) });
const secondPayload = await second.json() as { data: { accessToken: string } };
assert.equal(second.status, 200);
return secondPayload.data.accessToken;
}
test("user MFA recovery endpoint consumes a recovery code once", async () => {
const { server, base } = await start({ userMfaRequired: true });
try {
const first = await registerAndLogin(base, "recovery-endpoint@example.com");
assert.equal(first.payload.data.challengeType, "setup");
const recoveryCode = first.payload.data.mfaEnrollment?.recoveryCodes[0];
assert.ok(recoveryCode);
assert.equal(first.payload.data.mfaRequired, true);
const confirmed = await fetch(`${base}/api/v1/auth/mfa/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: first.payload.data.challengeId, code: totpCode(first.payload.data.mfaEnrollment!.secret) }) });
assert.equal(confirmed.status, 200);
const secondLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "recovery-endpoint@example.com", password: "password123" }) });
const second = { payload: await secondLogin.json() as { data: { accessToken?: string; mfaRequired?: boolean; challengeId?: string } } };
const recovered = await fetch(`${base}/api/v1/auth/mfa/recovery`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: second.payload.data.challengeId, recoveryCode }) });
assert.equal(recovered.status, 200);
assert.ok((await recovered.json() as { data: { accessToken?: string } }).data.accessToken);
const thirdLogin = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "recovery-endpoint@example.com", password: "password123" }) });
const third = { payload: await thirdLogin.json() as { data: { challengeId?: string } } };
const repeated = await fetch(`${base}/api/v1/auth/mfa/recovery`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: third.payload.data.challengeId, recoveryCode }) });
assert.equal(repeated.status, 401);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("sessions, plan entitlements, and remaining plan balance are user-scoped", async () => {
const { server, base } = await start();
try {
const first = await registerAndLogin(base, "sessions-plans@example.com", "password123", "browser-a");
const second = await fetch(`${base}/api/v1/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "sessions-plans@example.com", password: "password123", deviceId: "browser-b" }) });
const secondPayload = await second.json() as { data: { accessToken: string } };
const auth = (token: string) => ({ authorization: `Bearer ${token}`, "content-type": "application/json" });
const sessions = await fetch(`${base}/api/v1/me/sessions`, { headers: auth(first.payload.data.accessToken!) });
const sessionItems = (await sessions.json() as { data: { items: Array<{ id: string; deviceId?: string }> } }).data.items;
assert.equal(sessionItems.length, 2);
const browserA = sessionItems.find((item) => item.deviceId === "browser-a");
assert.ok(browserA);
const revoked = await fetch(`${base}/api/v1/me/sessions/${browserA.id}`, { method: "DELETE", headers: auth(first.payload.data.accessToken!) });
assert.equal(revoked.status, 200);
assert.equal((await fetch(`${base}/api/v1/me`, { headers: auth(first.payload.data.accessToken!) })).status, 401);
const remaining = await fetch(`${base}/api/v1/me/sessions`, { headers: auth(secondPayload.data.accessToken) });
assert.equal((await remaining.json() as { data: { items: unknown[] } }).data.items.length, 1);
const purchase = await fetch(`${base}/api/v1/plans/starter/purchase`, { method: "POST", headers: { ...auth(secondPayload.data.accessToken), "idempotency-key": "plan-regression" }, body: "{}" });
assert.equal(purchase.status, 202);
const plans = await fetch(`${base}/api/v1/me/plans`, { headers: auth(secondPayload.data.accessToken) });
const planPayload = await plans.json() as { data: { items: Array<{ plan?: { id: string }; remainingBalance: number }> } };
assert.equal(plans.status, 200);
assert.equal(planPayload.data.items[0].plan?.id, "starter");
assert.equal(planPayload.data.items[0].remainingBalance, 100);
const task = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: { ...auth(secondPayload.data.accessToken), "idempotency-key": "plan-bucket-task" }, body: JSON.stringify({ modelProductId: "basic-image-v1", prompt: "plan bucket" }) });
assert.equal(task.status, 202);
await new Promise((resolve) => setTimeout(resolve, 150));
const afterTask = await fetch(`${base}/api/v1/me/plans`, { headers: auth(secondPayload.data.accessToken) });
assert.equal((await afterTask.json() as { data: { items: Array<{ remainingBalance: number }> } }).data.items[0].remainingBalance, 90);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin image model products publish capability parameters and require a resolution matrix", async () => {
const { server, base } = await start();
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const headers = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json" };
const created = await fetch(`${base}/api/v1/admin/model-products`, { method: "POST", headers: { ...headers, "idempotency-key": "model-capability-create" }, body: JSON.stringify({ id: "openai-image-product", publicModelId: "gpt-image-2", name: "OpenAI 图片模型", capabilities: ["image"], channelGroupId: "image-default", basePrice: 20, resolutions: [{ id: "1K", label: "1K", width: 1024, height: 1024, priceMultiplier: 1, ratios: ["1:1", "16:9"] }] }) });
assert.equal(created.status, 201);
const createdPayload = await created.json() as { data: { item: { capabilitiesSchema?: Array<{ capability: string; parameters?: Record<string, { enum?: string[] }> }> } } };
const imageSchema = createdPayload.data.item.capabilitiesSchema?.find((item) => item.capability === "image");
assert.ok(imageSchema);
assert.deepEqual(imageSchema.parameters?.ratio.enum, ["auto", "1:1", "16:9", "9:16", "3:4", "4:5", "3:2", "2:3", "4:3", "1.91:1", "2.35:1", "21:9"]);
assert.deepEqual(imageSchema.parameters?.resolution.enum, ["1K", "2K", "4K"]);
assert.deepEqual(imageSchema.parameters?.input_fidelity.enum, ["low", "high"]);
const invalid = await fetch(`${base}/api/v1/admin/model-products`, { method: "POST", headers: { ...headers, "idempotency-key": "model-capability-invalid" }, body: JSON.stringify({ id: "openai-image-invalid", publicModelId: "gpt-image-invalid", name: "无分辨率图片模型", capabilities: ["image"], channelGroupId: "image-default", basePrice: 20, resolutions: [] }) });
assert.equal(invalid.status, 422);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin channel groups bind model products separately from request channels", async () => {
const { server, base, store } = await start();
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const headers = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json" };
const firstProduct = await fetch(`${base}/api/v1/admin/model-products`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-product-first" }, body: JSON.stringify({ id: "group-basic-image", publicModelId: "group-basic-image-v1", name: "分组基础图片", capabilities: ["image"], enabled: false, basePrice: 10, resolutions: [{ id: "1K", label: "1K", width: 1024, height: 1024, priceMultiplier: 1, ratios: ["1:1"] }] }) });
assert.equal(firstProduct.status, 201);
const secondProduct = await fetch(`${base}/api/v1/admin/model-products`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-product-second" }, body: JSON.stringify({ id: "group-advanced-image", publicModelId: "group-advanced-image-v1", name: "分组高级图片", capabilities: ["image"], enabled: false, basePrice: 25, resolutions: [{ id: "2K", label: "2K", width: 2048, height: 2048, priceMultiplier: 1.5, ratios: ["1:1"] }] }) });
assert.equal(secondProduct.status, 201);
const created = await fetch(`${base}/api/v1/admin/channel-groups`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-product-bind" }, body: JSON.stringify({ id: "image-product-group", name: "图片产品组", capabilities: ["image"], productIds: ["group-basic-image"], channelIds: ["channel-a"], retryBudget: 2 }) });
assert.equal(created.status, 201);
const createdPayload = await created.json() as { data: { item: { productIds?: string[] } } };
assert.deepEqual(createdPayload.data.item.productIds, ["group-basic-image"]);
assert.equal(store.products.find((product) => product.id === "group-basic-image")?.channelGroupId, "image-product-group");
const listed = await fetch(`${base}/api/v1/admin/channel-groups`, { headers });
assert.equal(listed.status, 200);
const listedPayload = await listed.json() as { data: { items: Array<{ id: string; productIds?: string[]; products?: Array<{ productId: string }> }> } };
const listedGroup = listedPayload.data.items.find((item) => item.id === "image-product-group");
assert.deepEqual(listedGroup?.productIds, ["group-basic-image"]);
assert.deepEqual(listedGroup?.products?.map((product) => product.productId), ["group-basic-image"]);
const edited = await fetch(`${base}/api/v1/admin/channel-groups/image-product-group`, { method: "PATCH", headers: { ...headers, "If-Match": "1", "idempotency-key": "channel-group-product-rebind" }, body: JSON.stringify({ productIds: ["group-advanced-image"], capabilities: ["image"], channelIds: ["channel-a"], retryBudget: 2, enabled: true }) });
assert.equal(edited.status, 200);
assert.equal(store.products.find((product) => product.id === "group-basic-image")?.channelGroupId, undefined);
assert.equal(store.products.find((product) => product.id === "group-basic-image")?.enabled, false);
assert.equal(store.products.find((product) => product.id === "group-advanced-image")?.channelGroupId, "image-product-group");
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin channel groups reject incompatible or silently migrated model products", async () => {
const { server, base, store } = await start();
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const headers = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json" };
const imageGroup = await fetch(`${base}/api/v1/admin/channel-groups`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-capability-image" }, body: JSON.stringify({ id: "capability-image-group", name: "图片能力组", capabilities: ["image"], productIds: [], channelIds: ["channel-a"], retryBudget: 1 }) });
assert.equal(imageGroup.status, 201);
const product = await fetch(`${base}/api/v1/admin/model-products`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-capability-product" }, body: JSON.stringify({ id: "capability-image-product", publicModelId: "capability-image-v1", name: "能力校验图片产品", capabilities: ["image"], channelGroupId: "capability-image-group", basePrice: 5, resolutions: [{ id: "1K", label: "1K", width: 1024, height: 1024, priceMultiplier: 1, ratios: ["1:1"] }] }) });
assert.equal(product.status, 201);
const textGroup = await fetch(`${base}/api/v1/admin/channel-groups`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-capability-text" }, body: JSON.stringify({ id: "capability-text-group", name: "文字能力组", capabilities: ["text"], productIds: [], channelIds: ["channel-a"], retryBudget: 1 }) });
assert.equal(textGroup.status, 201);
const incompatibleMode = await fetch(`${base}/api/v1/admin/channel-groups/capability-image-group`, { method: "PATCH", headers: { ...headers, "If-Match": "1", "idempotency-key": "channel-group-capability-change" }, body: JSON.stringify({ capabilities: ["text"] }) });
assert.equal(incompatibleMode.status, 422);
assert.deepEqual(store.channelGroups.find((group) => group.id === "capability-image-group")?.capabilities, ["image"]);
assert.equal(store.products.find((item) => item.id === "capability-image-product")?.channelGroupId, "capability-image-group");
const incompatibleProduct = await fetch(`${base}/api/v1/admin/model-products/capability-image-product`, { method: "PATCH", headers: { ...headers, "If-Match": "1", "idempotency-key": "channel-group-capability-product-change" }, body: JSON.stringify({ channelGroupId: "capability-text-group" }) });
assert.equal(incompatibleProduct.status, 422);
assert.equal(store.products.find((item) => item.id === "capability-image-product")?.channelGroupId, "capability-image-group");
const duplicateBinding = await fetch(`${base}/api/v1/admin/channel-groups`, { method: "POST", headers: { ...headers, "idempotency-key": "channel-group-ownership-conflict" }, body: JSON.stringify({ id: "capability-other-group", name: "重复绑定组", capabilities: ["image"], productIds: ["capability-image-product"], channelIds: ["channel-a"], retryBudget: 1 }) });
assert.equal(duplicateBinding.status, 409);
assert.equal(store.channelGroups.some((group) => group.id === "capability-other-group"), false);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("message provider and template writes survive a file-store restart", async () => {
const directory = await mkdtemp(join(tmpdir(), "miragenflow-message-persistence-"));
const file = join(directory, "store.json");
const store = createStore(file);
const server = createHttpServer(store, config());
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
const base = `http://127.0.0.1:${address.port}`;
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const headers = { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json" };
const provider = await fetch(`${base}/api/v1/admin/message-providers`, { method: "POST", headers: { ...headers, "idempotency-key": "persistent-message-provider-create" }, body: JSON.stringify({ id: "persistent-message-provider", name: "持久化消息适配器", channel: "email" }) });
assert.equal(provider.status, 201);
const template = await fetch(`${base}/api/v1/admin/message-templates`, { method: "POST", headers: { ...headers, "idempotency-key": "persistent-message-template-create" }, body: JSON.stringify({ purpose: "register", channel: "email", content: "验证码:{{code}}" }) });
assert.equal(template.status, 201);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
const restored = createStore(file);
try {
assert.equal(restored.messageProviders.has("persistent-message-provider"), true);
assert.equal([...restored.messageTemplates.values()].some((item) => item.content === "验证码:{{code}}"), true);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("admin unknown-task reconciliation stores real image metadata and clamps the charge", async () => {
const { store, server, base } = await start();
const taskId = "reconcile-image-task";
const ownerId = "reconcile-user";
const now = new Date().toISOString();
assert.equal(reserveBalance(store, ownerId, 10, taskId), false);
store.balances.set(ownerId, { available: 100, reserved: 0 });
assert.equal(reserveBalance(store, ownerId, 10, taskId), true);
store.tasks.set(taskId, {
id: taskId,
ownerId,
taskType: "image",
modelProductId: "basic-image",
status: "unknown",
estimatedCost: 10,
reservedCost: 10,
count: 2,
pricingSnapshot: { basePrice: 5, multiplier: 1, unitVersion: 1 },
createdAt: now,
updatedAt: now,
eventSequence: 0,
channelGroupId: "image-default",
routeSnapshotVersion: 1,
attempts: [{ id: "reconcile-attempt", channelId: "channel-a", sequence: 1, status: "unknown", startedAt: now, reconciliationStatus: "pending" }],
outputs: [],
inputSnapshot: { references: [], params: { resolution: "2K" } },
} satisfies GenerationTask);
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const invalidForm = new FormData(); invalidForm.set("outcome", "success"); invalidForm.append("file", new Blob([Buffer.from("not-an-image")], { type: "image/png" }), "invalid.png");
const invalid = await fetch(`${base}/api/v1/admin/tasks/${taskId}/reconcile`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "idempotency-key": "reconcile-image-invalid", "if-match": "0" }, body: invalidForm });
assert.equal(invalid.status, 415);
assert.equal(store.tasks.get(taskId)?.status, "unknown");
assert.equal(store.balances.get(ownerId)?.reserved, 10);
const form = new FormData(); form.set("outcome", "success"); form.set("chargedAmount", "99"); form.append("file", new Blob([Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAF/gL+XwAAAABJRU5ErkJggg==", "base64")], { type: "image/png" }), "result.png");
const response = await fetch(`${base}/api/v1/admin/tasks/${taskId}/reconcile`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "idempotency-key": "reconcile-image-success", "if-match": "0" }, body: form });
assert.equal(response.status, 200);
const payload = await response.json() as { data: { item: GenerationTask } };
const settledEntries = store.ledger.filter((entry) => entry.type === "settle" && entry.referenceId === taskId).length;
const repeatedForm = new FormData(); repeatedForm.set("outcome", "success"); repeatedForm.set("chargedAmount", "99"); repeatedForm.append("file", new Blob([Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAF/gL+XwAAAABJRU5ErkJggg==", "base64")], { type: "image/png" }), "result.png");
const repeated = await fetch(`${base}/api/v1/admin/tasks/${taskId}/reconcile`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "idempotency-key": "reconcile-image-success", "if-match": "0" }, body: repeatedForm });
assert.equal(repeated.status, 200);
assert.equal(store.ledger.filter((entry) => entry.type === "settle" && entry.referenceId === taskId).length, settledEntries);
const conflicting = await fetch(`${base}/api/v1/admin/tasks/${taskId}/reconcile`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "reconcile-image-success" }, body: JSON.stringify({ outcome: "failure" }) });
assert.equal(conflicting.status, 409);
const output = payload.data.item.outputs[0];
assert.equal(payload.data.item.status, "succeeded");
assert.equal(payload.data.item.reservedCost, 0);
assert.equal(output.metadata?.width, 1);
assert.equal(output.metadata?.height, 1);
assert.equal(output.metadata?.size, "1x1");
assert.equal(output.metadata?.format, "png");
assert.equal(output.metadata?.source, "base64");
assert.equal(output.metadata?.resolution, "2K");
assert.equal(output.unitPriceSnapshot, 5);
assert.equal(output.chargedAmount, 10);
assert.equal(store.balances.get(ownerId)?.reserved, 0);
assert.equal(store.ledger.some((entry) => entry.type === "settle" && entry.referenceId === taskId), true);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin unknown-task failure reconciliation releases the full reservation", async () => {
const { store, server, base } = await start();
const taskId = "reconcile-failure-task";
const ownerId = "reconcile-failure-user";
const now = new Date().toISOString();
store.balances.set(ownerId, { available: 20, reserved: 0 });
assert.equal(reserveBalance(store, ownerId, 10, taskId), true);
store.tasks.set(taskId, {
id: taskId,
ownerId,
taskType: "image",
modelProductId: "basic-image",
status: "unknown",
estimatedCost: 10,
reservedCost: 10,
createdAt: now,
updatedAt: now,
eventSequence: 0,
channelGroupId: "image-default",
routeSnapshotVersion: 1,
attempts: [{ id: "reconcile-failure-attempt", channelId: "channel-a", sequence: 1, status: "unknown", startedAt: now, reconciliationStatus: "pending" }],
outputs: [],
} satisfies GenerationTask);
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const response = await fetch(`${base}/api/v1/admin/tasks/${taskId}/reconcile`, { method: "POST", headers: { authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": "reconcile-failure", "if-match": "0" }, body: JSON.stringify({ outcome: "failure" }) });
assert.equal(response.status, 200);
assert.equal(store.tasks.get(taskId)?.status, "failed");
assert.equal(store.tasks.get(taskId)?.reservedCost, 0);
assert.deepEqual({ available: store.balances.get(ownerId)?.available, reserved: store.balances.get(ownerId)?.reserved }, { available: 20, reserved: 0 });
assert.equal(store.tasks.get(taskId)?.attempts[0]?.reconciliationStatus, "confirmed_failure");
assert.equal(store.ledger.some((entry) => entry.type === "release" && entry.referenceId === taskId), true);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("finance administrators can list balance targets and require separate MFA approval", async () => {
const firstSecret = createTotpSecret();
const secondSecret = createTotpSecret();
const securitySecret = createTotpSecret();
const recoveryCode = "ADMINRECOVERY";
const { store, server, base } = await start({
adminMfaRequired: true,
adminAccounts: [
{ id: "admin:first", email: "first-admin@example.com", password: "first-admin-password", role: "finance", mfaSecret: firstSecret, mfaRequired: true },
{ id: "admin:second", email: "second-admin@example.com", password: "second-admin-password", role: "finance", mfaSecret: secondSecret, mfaRequired: true, recoveryCodeHashes: [hashChallenge(recoveryCode, accessTokenSecret)] },
{ id: "admin:security", email: "security-admin@example.com", password: "security-admin-password", role: "support", mfaSecret: securitySecret, mfaRequired: true },
],
});
try {
const user = await registerAndLogin(base, "approval-user@example.com");
const requester = await loginAdmin(base, "first-admin@example.com", "first-admin-password", firstSecret);
const approver = await loginAdmin(base, "second-admin@example.com", "second-admin-password", secondSecret, recoveryCode);
const securityOnly = await loginAdmin(base, "security-admin@example.com", "security-admin-password", securitySecret);
const targets = await fetch(`${base}/api/v1/admin/billing/users`, { headers: { authorization: `Bearer ${requester}` } });
assert.equal(targets.status, 200);
const target = ((await targets.json()) as { data: { items: Array<Record<string, unknown>> } }).data.items.find((item) => item.id === user.registered.userId);
assert.ok(target);
const allowedTargetFields = ["balance", "displayName", "email", "id", "phone", "status", "username"];
assert.deepEqual(Object.keys(target).filter((key) => !allowedTargetFields.includes(key)), []);
assert.ok("balance" in target);
assert.equal((await fetch(`${base}/api/v1/admin/users`, { headers: { authorization: `Bearer ${requester}` } })).status, 403);
const forbidden = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}/balance-adjustments`, { method: "POST", headers: { authorization: `Bearer ${securityOnly}`, "content-type": "application/json", "idempotency-key": "security-balance-adjustment" }, body: JSON.stringify({ amount: 1, reason: "不应允许安全管理员调账" }) });
assert.equal(forbidden.status, 403);
const request = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}/balance-adjustments`, { method: "POST", headers: { authorization: `Bearer ${requester}`, "content-type": "application/json", "idempotency-key": "balance-adjustment-request" }, body: JSON.stringify({ amount: 25, reason: "人工账本回归测试" }) });
assert.equal(request.status, 202);
assert.equal((await fetch(`${base}/api/v1/balance`, { headers: { authorization: `Bearer ${user.payload.data.accessToken!}` } })).status, 200);
const before = await fetch(`${base}/api/v1/balance`, { headers: { authorization: `Bearer ${user.payload.data.accessToken!}` } });
assert.equal((await before.json() as { data: { available: number } }).data.available, 0);
const approval = ((await request.json()) as { data: { item: { id: string } } }).data.item;
const execute = await fetch(`${base}/api/v1/admin/approvals/${approval.id}/approve`, { method: "POST", headers: { authorization: `Bearer ${approver}`, "content-type": "application/json", "idempotency-key": "balance-adjustment-approve", "if-match": "1" }, body: JSON.stringify({ mfaCode: totpCode(secondSecret) }) });
assert.equal(execute.status, 200);
assert.ok(store.adminAccounts.get("admin:second")?.mfaLastTotpCounter !== undefined);
const after = await fetch(`${base}/api/v1/balance`, { headers: { authorization: `Bearer ${user.payload.data.accessToken!}` } });
assert.equal((await after.json() as { data: { available: number } }).data.available, 25);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("local super administrator can approve its own balance increase and deduction", async () => {
const { store, server, base } = await start({ adminSelfApprovalAllowed: true });
try {
const user = await registerAndLogin(base, "self-approval-balance@example.com");
store.balances.set(user.registered.userId, { available: 10, reserved: 0 });
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const adminHeaders = (idempotencyKey: string) => ({ authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": idempotencyKey });
const adjust = async (amount: number, key: string) => {
const request = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}/balance-adjustments`, { method: "POST", headers: adminHeaders(`${key}-request`), body: JSON.stringify({ amount, reason: amount > 0 ? "本地增加金币" : "本地扣减金币" }) });
assert.equal(request.status, 202);
const approval = ((await request.json()) as { data: { item: { id: string; version: number } } }).data.item;
const execute = await fetch(`${base}/api/v1/admin/approvals/${approval.id}/approve`, { method: "POST", headers: { ...adminHeaders(`${key}-approve`), "if-match": String(approval.version) }, body: "{}" });
assert.equal(execute.status, 200);
};
await adjust(40, "self-balance-add");
assert.equal((await (await fetch(`${base}/api/v1/balance`, { headers: { authorization: `Bearer ${user.payload.data.accessToken!}` } })).json() as { data: { available: number } }).data.available, 50);
assert.equal((store.buckets.get(user.registered.userId) || []).reduce((total, bucket) => total + bucket.remaining, 0), 50);
await adjust(10, "self-balance-add-second-bucket");
assert.equal((store.buckets.get(user.registered.userId) || []).reduce((total, bucket) => total + bucket.remaining, 0), 60);
await adjust(-55, "self-balance-deduct-across-buckets");
assert.equal((await (await fetch(`${base}/api/v1/balance`, { headers: { authorization: `Bearer ${user.payload.data.accessToken!}` } })).json() as { data: { available: number } }).data.available, 5);
assert.deepEqual((store.buckets.get(user.registered.userId) || []).map((bucket) => bucket.remaining), [0, 0, 5]);
assert.ok(store.ledger.filter((entry) => entry.userId === user.registered.userId && entry.type === "adjustment").every((entry) => Boolean(entry.bucketId)));
assert.equal([...store.adminIdempotency.values()].every((record) => record.state === "completed"), true);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("administrator initial plan grant creates matching balance bucket ledger and entitlement", async () => {
const { store, server, base } = await start();
try {
const plan = store.plans.find((item) => item.includedBalance > 0);
assert.ok(plan);
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const accessToken = (await login.json() as { data: { accessToken: string } }).data.accessToken;
const headers = { authorization: `Bearer ${accessToken}`, "content-type": "application/json", "idempotency-key": "initial-plan-user" };
const body = JSON.stringify({ username: "initial-plan-user", email: "initial-plan@example.com", password: "password123", planId: plan.id, welcomeMessage: "欢迎使用" });
const response = await fetch(`${base}/api/v1/admin/users`, { method: "POST", headers, body });
assert.equal(response.status, 201);
const firstPayload = await response.json() as { data: { item: { id: string }; initialPassword?: string } };
const userId = firstPayload.data.item.id;
assert.equal(firstPayload.data.initialPassword, "password123");
const entitlement = [...store.entitlements.values()].find((item) => item.userId === userId);
assert.ok(entitlement?.creditBucketId);
const bucket = (store.buckets.get(userId) || []).find((item) => item.id === entitlement.creditBucketId);
assert.equal(bucket?.remaining, plan.includedBalance);
assert.equal(store.balances.get(userId)?.available, plan.includedBalance);
assert.equal(store.ledger.find((entry) => entry.referenceId === entitlement.id)?.bucketId, bucket?.id);
assert.ok([...store.messageOutbox.values()].some((item) => item.idempotencyKey === "initial-plan-user:welcome"));
assert.ok(store.audit.some((item) => item.action === "user.create" && item.objectId === userId));
const counts = { users: store.users.size, buckets: store.buckets.get(userId)?.length, ledger: store.ledger.length, entitlements: store.entitlements.size, messages: store.messageOutbox.size, audit: store.audit.length };
const replay = await fetch(`${base}/api/v1/admin/users`, { method: "POST", headers, body });
assert.equal(replay.status, 201);
const replayPayload = await replay.json() as { data: { item: { id: string }; initialPassword?: string } };
assert.equal(replayPayload.data.item.id, userId);
assert.equal(replayPayload.data.initialPassword, undefined);
assert.deepEqual({ users: store.users.size, buckets: store.buckets.get(userId)?.length, ledger: store.ledger.length, entitlements: store.entitlements.size, messages: store.messageOutbox.size, audit: store.audit.length }, counts);
const idempotency = [...store.adminIdempotency.values()].find((item) => item.statusCode === 201);
assert.equal(idempotency?.state, "completed");
assert.equal(JSON.stringify(idempotency?.data).includes("password123"), false);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin user status mutations reject stale versions", async () => {
const { store, server, base } = await start();
try {
const user = await registerAndLogin(base, "user-version-conflict@example.com");
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const accessToken = (await login.json() as { data: { accessToken: string } }).data.accessToken;
const auth = { authorization: `Bearer ${accessToken}`, "content-type": "application/json" };
const detail = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}`, { headers: auth });
assert.equal(detail.status, 200);
const detailPayload = await detail.json() as { data: { item: { version: number; status: string } } };
assert.equal(detailPayload.data.item.version, 1);
const freeze = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}/freeze`, { method: "POST", headers: { ...auth, "if-match": String(detailPayload.data.item.version), "idempotency-key": "user-version-freeze" }, body: "{}" });
assert.equal(freeze.status, 200);
const freezePayload = await freeze.json() as { data: { version: number; status: string } };
assert.equal(freezePayload.data.status, "frozen");
assert.equal(freezePayload.data.version, 2);
const stale = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}`, { method: "PATCH", headers: { ...auth, "if-match": "1", "idempotency-key": "user-version-stale-update" }, body: JSON.stringify({ status: "active" }) });
assert.equal(stale.status, 409);
const stalePayload = await stale.json() as { error?: { code?: string } };
assert.equal(stalePayload.error?.code, "VERSION_CONFLICT");
assert.equal(store.users.get(user.registered.userId)?.status, "frozen");
assert.equal(store.users.get(user.registered.userId)?.version, 2);
const unfreeze = await fetch(`${base}/api/v1/admin/users/${user.registered.userId}/unfreeze`, { method: "POST", headers: { ...auth, "if-match": "2", "idempotency-key": "user-version-unfreeze" }, body: "{}" });
assert.equal(unfreeze.status, 200);
assert.equal((await unfreeze.json() as { data: { version: number; status: string } }).data.version, 3);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("assets enforce ownership and deduplicate repeated object saves", async () => {
const { store, server, base } = await start();
try {
const owner = await registerAndLogin(base, "asset-owner@example.com");
const other = await registerAndLogin(base, "asset-other@example.com");
const objectId = "object-owner-1";
store.objects.set(objectId, { id: objectId, ownerId: owner.registered.userId, mimeType: "image/png", data: Buffer.from("x").toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const auth = { authorization: `Bearer ${owner.payload.data.accessToken!}`, "content-type": "application/json" };
const first = await fetch(`${base}/api/v1/assets`, { method: "POST", headers: auth, body: JSON.stringify({ objectId, type: "image", title: "测试资产" }) });
const firstPayload = await first.json() as { data: { id: string } };
assert.equal(first.status, 201);
const second = await fetch(`${base}/api/v1/assets`, { method: "POST", headers: auth, body: JSON.stringify({ objectId, type: "image", title: "重复保存" }) });
const secondPayload = await second.json() as { data: { id: string } };
assert.equal(second.status, 200);
assert.equal(secondPayload.data.id, firstPayload.data.id);
const denied = await fetch(`${base}/api/v1/assets`, { method: "POST", headers: { authorization: `Bearer ${other.payload.data.accessToken!}`, "content-type": "application/json" }, body: JSON.stringify({ objectId, type: "image" }) });
assert.equal(denied.status, 403);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("WebDAV manifest retention is capped and valid writes keep the local record", async () => {
const { store, server, base } = await start();
try {
const user = await registerAndLogin(base, "webdav-retention@example.com");
store.webdav.set(user.registered.userId, { userId: user.registered.userId, configured: true, directory: "miragenflow", state: "ready", retentionDays: 30, encryptedUrl: encryptSecret("https://127.0.0.1:1", accessTokenSecret), manifestRetentionExpiresAt: new Date().toISOString(), manifestExtensionDays: 0 });
const auth = { authorization: `Bearer ${user.payload.data.accessToken!}`, "content-type": "application/json" };
const extension = await fetch(`${base}/api/v1/me/webdav/manifest/retention`, { method: "POST", headers: auth, body: JSON.stringify({ days: 20 }) });
assert.equal(extension.status, 200);
assert.equal((await extension.json() as { data: { manifestExtensionDays: number } }).data.manifestExtensionDays, 20);
const overLimit = await fetch(`${base}/api/v1/me/webdav/manifest/retention`, { method: "POST", headers: auth, body: JSON.stringify({ days: 11 }) });
assert.equal(overLimit.status, 422);
const write = await fetch(`${base}/api/v1/me/webdav/file`, { method: "PUT", headers: auth, body: JSON.stringify({ path: "canvas/manifest.json", mimeType: "application/json", data: "eA==" }) });
assert.equal(write.status, 202);
assert.ok(store.webdavFiles.has(`${user.registered.userId}:canvas/manifest.json`));
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("expired buckets append one adjustment ledger entry before reservation", () => {
const store = createStore();
const userId = "expired-bucket-user";
const bucketId = "expired-bucket";
store.balances.set(userId, { available: 10, reserved: 0 });
store.buckets.set(userId, [{ id: bucketId, userId, source: "plan", remaining: 10, priority: 0, expiresAt: new Date(Date.now() - 1_000).toISOString() }]);
assert.equal(reserveBalance(store, userId, 1, "expired-reservation"), false);
assert.equal(store.buckets.get(userId)?.[0].remaining, 0);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 0, reserved: 0 });
assert.equal(store.ledger.filter((entry) => entry.idempotencyKey === `expire:${userId}:${bucketId}`).length, 1);
});
test("expired bucket adjustment never drives aggregate balance below zero", () => {
const store = createStore();
const userId = "expired-bucket-clamp";
store.balances.set(userId, { available: 3, reserved: 0 });
store.buckets.set(userId, [{ id: "expired-large", userId, source: "plan", remaining: 10, priority: 0, expiresAt: new Date(Date.now() - 1_000).toISOString() }]);
assert.equal(reserveBalance(store, userId, 1, "clamp-reservation"), false);
assert.deepEqual({ available: store.balances.get(userId)?.available, reserved: store.balances.get(userId)?.reserved }, { available: 0, reserved: 0 });
assert.equal(store.ledger.find((entry) => entry.idempotencyKey === `expire:${userId}:expired-large`)?.amount, -3);
});
test("task route snapshots follow channel-group priority order", async () => {
const { store, server, base } = await start();
try {
const user = await registerAndLogin(base, "channel-order@example.com");
store.balances.set(user.registered.userId, { available: 100, reserved: 0 });
const group = store.channelGroups.find((item) => item.id === "image-default");
assert.ok(group);
group.channelIds = ["channel-c", "channel-a", "channel-d", "channel-b"];
group.channelPriorities = { "channel-c": 0, "channel-a": 1, "channel-d": 2, "channel-b": 3 };
const response = await fetch(`${base}/api/v1/tasks/image`, { method: "POST", headers: { authorization: `Bearer ${user.payload.data.accessToken!}`, "content-type": "application/json", "idempotency-key": "channel-order-task" }, body: JSON.stringify({ modelProductId: "basic-image-v1", prompt: "route snapshot" }) });
assert.equal(response.status, 202);
const task = [...store.tasks.values()].find((item) => item.ownerId === user.registered.userId);
assert.deepEqual(task?.routeSnapshot?.channelIds, ["channel-c", "channel-a", "channel-d", "channel-b"]);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("documented admin singleton and detail method aliases remain callable", async () => {
const { server, base } = await start();
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const headers = (key: string) => ({ authorization: `Bearer ${loginPayload.data.accessToken}`, "content-type": "application/json", "idempotency-key": key });
assert.equal((await fetch(`${base}/api/v1/admin/billing/units`, { headers: headers("alias-units-get") })).status, 200);
assert.equal((await fetch(`${base}/api/v1/admin/billing/units`, { method: "POST", headers: headers("alias-units-post"), body: JSON.stringify({ name: "金币" }) })).status, 201);
assert.equal((await fetch(`${base}/api/v1/admin/billing/conversion-rules`, { method: "POST", headers: headers("alias-conversion-post"), body: JSON.stringify({ conversionRate: 1, rounding: "round" }) })).status, 201);
assert.equal((await fetch(`${base}/api/v1/admin/pricing-rules`, { method: "POST", headers: headers("alias-pricing-post"), body: JSON.stringify({ modelProductId: "basic-image", basePrice: 10 }) })).status, 201);
assert.equal((await fetch(`${base}/api/v1/admin/storage-policies`, { method: "POST", headers: headers("alias-storage-post"), body: JSON.stringify({ retentionDays: 30 }) })).status, 201);
assert.equal((await fetch(`${base}/api/v1/admin/recharge-orders/unknown-order`, { headers: headers("alias-order-detail") })).status, 404);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("audit actors use the user name and direct administrator email", async () => {
const { store, server, base } = await start();
try {
const user = await registerAndLogin(base, "audit-actor@example.com");
const userRecord = store.users.get(user.registered.userId);
assert.ok(userRecord);
userRecord.username = "日志用户";
const now = new Date().toISOString();
store.audit.push(
{ id: "audit-user-actor", actorId: userRecord.id, action: "user.password.change", objectType: "user", objectId: userRecord.id, requestId: "audit-user-request", createdAt: now },
{ id: "audit-admin-actor", actorId: "admin", action: "admin.login.succeeded", objectType: "admin", objectId: "admin", requestId: "audit-admin-request", createdAt: now },
);
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const response = await fetch(`${base}/api/v1/admin/audit-logs`, { headers: { authorization: `Bearer ${loginPayload.data.accessToken}` } });
assert.equal(response.status, 200);
const items = (await response.json() as { data: { items: Array<{ id: string; actorName: string }> } }).data.items;
assert.equal(items.find((item) => item.id === "audit-user-actor")?.actorName, "日志用户");
assert.equal(items.find((item) => item.id === "audit-admin-actor")?.actorName, "admin@example.com");
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin system settings are independent, versioned, and apply auth switches immediately", async () => {
const { server, base } = await start();
try {
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const loginPayload = await login.json() as { data: { accessToken: string } };
const authHeaders = { authorization: `Bearer ${loginPayload.data.accessToken}` };
const initial = await fetch(`${base}/api/v1/admin/settings`, { headers: authHeaders });
assert.equal(initial.status, 200);
const initialPayload = await initial.json() as { data: { items: Array<{ version: number; userVerificationRequired: boolean }> } };
const current = initialPayload.data.items[0];
assert.ok(current);
assert.equal(current.userVerificationRequired, true);
const updated = await fetch(`${base}/api/v1/admin/settings`, { method: "PATCH", headers: { ...authHeaders, "content-type": "application/json", "idempotency-key": "settings-update-1", "if-match": String(current.version) }, body: JSON.stringify({ adminMfaRequired: false, adminCaptchaRequired: false, userMfaRequired: false, userVerificationRequired: false, captchaRequired: false, rateLimitWindowMs: 120000, rateLimitMax: 20, queueMaxConcurrent: 8, auditRetentionDays: 180, auditMaxEntries: 5000 }) });
assert.equal(updated.status, 200);
const updatedPayload = await updated.json() as { data: { item: { version: number; userVerificationRequired: boolean; rateLimitWindowMs: number } } };
assert.equal(updatedPayload.data.item.version, current.version + 1);
assert.equal(updatedPayload.data.item.userVerificationRequired, false);
assert.equal(updatedPayload.data.item.rateLimitWindowMs, 120000);
const policy = await fetch(`${base}/api/v1/auth/policy`);
assert.deepEqual((await policy.json() as { data: unknown }).data, { captchaRequired: false, userVerificationRequired: false, userMfaRequired: false });
const stale = await fetch(`${base}/api/v1/admin/settings`, { method: "PATCH", headers: { ...authHeaders, "content-type": "application/json", "idempotency-key": "settings-update-stale", "if-match": String(current.version) }, body: JSON.stringify({ rateLimitMax: 21 }) });
assert.equal(stale.status, 409);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
test("admin recharge order PATCH only uses guarded failure/refund transitions", async () => {
const { store, server, base } = await start();
try {
const user = await registerAndLogin(base, "admin-order-patch@example.com");
const login = await fetch(`${base}/api/v1/admin/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: "change-me-now" }) });
const admin = await login.json() as { data: { accessToken: string } };
const headers = { authorization: `Bearer ${admin.data.accessToken}`, "content-type": "application/json", "idempotency-key": "admin-order-patch" };
const orderId = "admin-order-fixture";
store.rechargeOrders.set(orderId, { id: orderId, userId: user.registered.userId, provider: "mock", status: "pending", fiatAmount: 10, balanceAmount: 10, unitVersion: 1, idempotencyKey: "fixture-order", createdAt: new Date().toISOString() });
const failed = await fetch(`${base}/api/v1/admin/recharge-orders/${orderId}`, { method: "PATCH", headers: { ...headers, "if-match": "1" }, body: JSON.stringify({ status: "failed" }) });
assert.equal(failed.status, 200);
const refund = await fetch(`${base}/api/v1/admin/recharge-orders/${orderId}`, { method: "PATCH", headers: { ...headers, "idempotency-key": "admin-order-refund", "if-match": "2" }, body: JSON.stringify({ status: "refunded" }) });
assert.equal(refund.status, 409);
assert.equal(store.balances.get(user.registered.userId)?.available, 0);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
+65
View File
@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { WebSocket } from "ws";
import { createHttpServer } from "../src/app/http.ts";
import { attachTaskWebSocket } from "../src/app/ws.ts";
import { createStore } from "../src/store.ts";
import { hashRefreshToken, issueAccessToken } from "../src/shared/auth.ts";
test("task websocket authenticates and replays a subscribed task cursor", async () => {
const secret = "test";
const store = createStore();
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: secret, accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
attachTaskWebSocket(server, store, secret, "refresh");
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
const taskId = "ws-task";
store.users.set("ws-user", { id: "ws-user", version: 1, password: "", verified: true, status: "active", mfaRequired: false, mfaEnabled: false, roles: ["user"], failedLoginCount: 0, createdAt: new Date().toISOString() });
store.tasks.set(taskId, { id: taskId, ownerId: "ws-user", taskType: "image", modelProductId: "basic-image", status: "queued", estimatedCost: 1, reservedCost: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), eventSequence: 0, channelGroupId: "image-default", routeSnapshotVersion: 1, attempts: [], outputs: [] });
const token = issueAccessToken("ws-user", "user", secret, 900);
const socket = new WebSocket(`ws://127.0.0.1:${address.port}/api/v1/ws/tasks?access_token=${encodeURIComponent(token)}`);
const messages: Array<{ type: string; event?: { taskId: string } }> = [];
socket.on("message", (raw) => messages.push(JSON.parse(raw.toString())));
await new Promise<void>((resolve) => socket.once("open", () => resolve()));
socket.send(JSON.stringify({ type: "subscribe", taskId, cursor: 0 }));
await new Promise((resolve) => setTimeout(resolve, 25));
assert.equal(messages[0]?.type, "ready");
store.events.set(taskId, []);
const { appendEvent } = await import("../src/store.ts");
appendEvent(store, taskId, { taskId, type: "task.progress", payload: { progress: 20 } });
await new Promise((resolve) => setTimeout(resolve, 25));
assert.ok(messages.some((message) => message.type === "event" && message.event?.taskId === taskId));
socket.close();
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
});
test("task websocket rejects a revoked refresh session family", async () => {
const secret = "test";
const store = createStore();
const server = createHttpServer(store, { host: "127.0.0.1", port: 0, accessTokenSecret: secret, accessTokenTtlSeconds: 900, refreshCookieName: "refresh", adminEmail: "admin@example.com", adminPassword: "change-me-now", adminMfaRequired: true });
attachTaskWebSocket(server, store, secret, "refresh");
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address(); assert.ok(address && typeof address !== "string");
const userId = "ws-cookie-user"; const familyId = randomUUID(); const refresh = randomUUID();
store.users.set(userId, { id: userId, version: 1, password: "", verified: true, status: "active", mfaRequired: false, mfaEnabled: false, roles: ["user"], failedLoginCount: 0, createdAt: new Date().toISOString() });
store.sessionFamilies.set(familyId, { userId, scope: "user" });
store.sessions.set(hashRefreshToken(refresh), { userId, scope: "user", expiresAt: Date.now() + 60_000, absoluteExpiresAt: Date.now() + 60_000, createdAt: Date.now(), rotationId: randomUUID(), familyId });
const first = new WebSocket(`ws://127.0.0.1:${address.port}/api/v1/ws/tasks`, { headers: { Cookie: `refresh=${refresh}` } });
await new Promise<void>((resolve, reject) => { first.once("open", () => resolve()); first.once("error", reject); });
store.sessionFamilies.get(familyId)!.revokedAt = Date.now();
first.send(JSON.stringify({ type: "ping" }));
await Promise.race([
new Promise<void>((resolve) => first.once("close", () => resolve())),
new Promise<void>((resolve) => setTimeout(resolve, 1_000)),
]);
assert.equal(first.readyState, WebSocket.CLOSED);
const second = new WebSocket(`ws://127.0.0.1:${address.port}/api/v1/ws/tasks`, { headers: { Cookie: `refresh=${refresh}` } });
await Promise.race([
new Promise<void>((resolve) => { second.once("close", () => resolve()); second.once("error", () => resolve()); }),
new Promise<void>((resolve) => setTimeout(resolve, 1_000)),
]);
second.terminate();
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
});