feat(ql3): add bounded panel bootstrap

This commit is contained in:
whyour
2026-09-02 11:30:42 +08:00
parent 83966a1287
commit 882ce07d7e
20 changed files with 1095 additions and 134 deletions
@@ -38,6 +38,7 @@ import type {
LocalApiSecretPutRoute,
} from '../secret/secretRoutes';
import type { PanelCronListRoute } from '../panel-compatibility/panelCronListRoute';
import type { PanelBootstrapRoute } from '../panel-compatibility/panelBootstrapRoute';
import type { LocalApiResponse } from '../transport/contract';
export type LocalApiAdmissionOperation =
@@ -133,6 +134,10 @@ export type LocalApiAdmissionOperation =
page: number;
size: number;
maximumRows: number;
}>
| Readonly<{
operationId: 'panel.user.get' | 'panel.system.config.get';
projectId: 'default';
}>;
export interface LocalApiAdmissionRequest {
@@ -177,6 +182,7 @@ export interface LocalApiAdmissionOptions {
readonly secretListRoute: LocalApiSecretListRoute;
readonly secretPutRoute: LocalApiSecretPutRoute;
readonly panelCronListRoute: PanelCronListRoute;
readonly panelBootstrapRoute: PanelBootstrapRoute;
readonly now?: () => number;
readonly randomUuid?: () => string;
}
@@ -263,6 +269,7 @@ export function createLocalApiAdmission(
typeof options.secretListRoute?.handle !== 'function' ||
typeof options.secretPutRoute?.handle !== 'function' ||
typeof options.panelCronListRoute?.handle !== 'function' ||
typeof options.panelBootstrapRoute?.handle !== 'function' ||
(options.now !== undefined && typeof options.now !== 'function') ||
(options.randomUuid !== undefined &&
typeof options.randomUuid !== 'function')
@@ -404,7 +411,9 @@ export function createLocalApiAdmission(
request.operation.operationId === 'task.get' ||
request.operation.operationId === 'trigger.list' ||
request.operation.operationId === 'trigger.get' ||
request.operation.operationId === 'panel.cron.list'
request.operation.operationId === 'panel.cron.list' ||
request.operation.operationId === 'panel.user.get' ||
request.operation.operationId === 'panel.system.config.get'
? 'task.read'
: request.operation.operationId === 'secret.list'
? 'secret.manage'
@@ -572,6 +581,13 @@ export function createLocalApiAdmission(
size: request.operation.size,
maximumRows: request.operation.maximumRows,
});
case 'panel.user.get':
case 'panel.system.config.get':
if (body !== null) return response(400, 'invalid_request_body');
return options.panelBootstrapRoute.handle({
operationId: request.operation.operationId,
principal: authenticated.principal,
});
case 'task.put':
case 'task.authoring':
case 'trigger.put':
@@ -32,6 +32,7 @@ import {
createLocalApiSecretPutRoute,
} from '../secret/secretRoutes';
import { createPanelCronListRoute } from '../panel-compatibility/panelCronListRoute';
import { createPanelBootstrapRoute } from '../panel-compatibility/panelBootstrapRoute';
import { startLocalApiHttpSurface } from '../transport/httpSurface';
export interface LocalApiProductSurfaceEvent {
@@ -226,6 +227,7 @@ export function createLocalApiProductSurface(
tasks: authority.taskDefinitions,
triggers: authority.triggers,
});
const panelBootstrapRoute = createPanelBootstrapRoute(authority.profile);
const admission = createLocalApiAdmission({
authenticator,
policy,
@@ -247,6 +249,7 @@ export function createLocalApiProductSurface(
secretListRoute,
secretPutRoute,
panelCronListRoute,
panelBootstrapRoute,
...(options.now === undefined ? {} : { now: options.now }),
...(options.randomUuid === undefined
? {}
@@ -0,0 +1,169 @@
import type { LocalApplicationProfile } from '@qinglong/local-application';
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
import type { LocalApiResponse } from '../transport/contract';
const PRODUCT_VERSION = '3.0.0-alpha.2';
export type PanelBootstrapOperation =
| 'panel.user.get'
| 'panel.system.config.get';
export interface PanelBootstrapRequest {
readonly operationId: PanelBootstrapOperation;
readonly principal: Readonly<SecurityPrincipal>;
}
export interface PanelBootstrapRoute {
handle(request: Readonly<PanelBootstrapRequest>): Promise<LocalApiResponse>;
}
function response(
statusCode: number,
body: Readonly<Record<string, unknown>>,
): Readonly<LocalApiResponse> {
return Object.freeze({ statusCode, body: Object.freeze(body) });
}
function maximumRows(profile: LocalApplicationProfile): number {
return profile === 'edge' ? 64 : 256;
}
export function panelCapabilities(
profile: LocalApplicationProfile,
): Readonly<Record<string, unknown>> {
if (profile !== 'edge' && profile !== 'standalone') {
throw new TypeError('Panel capability profile is invalid');
}
const logChunkBytes = profile === 'edge' ? 16 * 1_024 : 32 * 1_024;
return Object.freeze({
schemaVersion: 1,
product: 'qinglong3',
version: PRODUCT_VERSION,
deployment: Object.freeze({ mode: 'local', profile }),
authentication: Object.freeze({
kind: 'api_credential',
transport: 'bearer',
persistence: 'memory_only',
loginEndpoint: null,
}),
project: Object.freeze({ selection: 'explicit', defaultId: 'default' }),
panel: Object.freeze({
bootstrap: true,
cronList: 'bounded_read_only',
taskRead: true,
triggerRead: true,
runRead: true,
runLogRead: true,
legacyMutations: false,
legacyLogin: false,
subscriptions: false,
scripts: false,
environmentVariables: false,
webSocket: false,
}),
limits: Object.freeze({
cronRows: maximumRows(profile),
cronPageSize: 64,
logChunkBytes,
}),
});
}
export function panelPublicResponse(
operation: 'capabilities' | 'health' | 'system',
profile: LocalApplicationProfile,
): Readonly<LocalApiResponse> {
const capabilities = panelCapabilities(profile);
if (operation === 'capabilities') {
return response(200, { capabilities });
}
if (operation === 'health') {
return response(200, {
code: 200,
data: Object.freeze({
status: 'ok',
ql3: Object.freeze({
schemaVersion: 1,
apiVersion: 'v3',
capabilitiesPath: '/api/v3/capabilities',
}),
}),
});
}
return response(200, {
code: 200,
data: Object.freeze({
branch: 'develop',
isInitialized: true,
publishTime: 0,
version: PRODUCT_VERSION,
changeLog: '',
changeLogLink: '',
ql3: Object.freeze({
schemaVersion: 1,
mode: 'local',
profile,
capabilitiesPath: '/api/v3/capabilities',
}),
}),
});
}
export function createPanelBootstrapRoute(
profile: LocalApplicationProfile,
): Readonly<PanelBootstrapRoute> {
if (profile !== 'edge' && profile !== 'standalone') {
throw new TypeError('Panel bootstrap profile is invalid');
}
return Object.freeze({
async handle(request: Readonly<PanelBootstrapRequest>) {
const principal = request?.principal;
if (
!principal ||
typeof principal !== 'object' ||
Array.isArray(principal) ||
principal.subject?.type !== 'user' ||
typeof principal.subject?.id !== 'string' ||
principal.subject.id.length < 1
) {
return response(503, {
code: 503,
message: 'QL3 面板身份暂不可用',
});
}
if (request.operationId === 'panel.user.get') {
return response(200, {
code: 200,
data: Object.freeze({
username: principal.subject.id,
ql3: Object.freeze({
schemaVersion: 1,
subjectType: principal.subject.type,
assurance: principal.assurance,
expiresAtMs: principal.expiresAtMs,
credentialPersistence: 'memory_only',
panelHome: '/crontab',
}),
}),
});
}
if (request.operationId !== 'panel.system.config.get') {
return response(503, {
code: 503,
message: 'QL3 面板启动入口暂不可用',
});
}
return response(200, {
code: 200,
data: Object.freeze({
info: Object.freeze({
panelTitle: 'QingLong 3.0',
lang: 'zh-cn',
}),
ql3: panelCapabilities(profile),
}),
});
},
});
}
@@ -49,7 +49,7 @@ function validRequest(request: Readonly<PanelCronListRequest>): boolean {
Number.isSafeInteger(request.maximumRows) &&
request.maximumRows >= 1 &&
request.maximumRows <= 256 &&
request.page * request.size <= request.maximumRows
(request.page - 1) * request.size < request.maximumRows
);
}
@@ -72,7 +72,10 @@ export function createPanelCronListRoute(
return response(400, { code: 400, message: '参数错误' });
}
try {
const scanLimit = request.page * request.size;
const scanLimit = Math.min(
request.page * request.size,
request.maximumRows,
);
const page = await sources.triggers.listTriggers({
projectId: request.projectId,
limit: scanLimit,
@@ -149,7 +152,10 @@ export function createPanelCronListRoute(
code: 200,
data: Object.freeze({
data: Object.freeze(data),
total: page.triggers.length + (page.truncated ? 1 : 0),
total: Math.min(
request.maximumRows,
page.triggers.length + (page.truncated ? 1 : 0),
),
}),
});
} catch {
@@ -18,6 +18,7 @@ import {
loadLocalConsoleAssets,
type LocalConsoleAsset,
} from '../console/localConsoleAssets';
import { panelPublicResponse } from '../panel-compatibility/panelBootstrapRoute';
import type { LocalApiResponse } from './contract';
const MAX_HEADER_BYTES = 8 * 1_024;
@@ -65,9 +66,12 @@ type LocalApiRouteResolution =
| 'invalid_task_list_query'
| 'invalid_trigger_list_query'
| 'invalid_secret_list_query'
| 'invalid_panel_cron_list_query';
| 'invalid_panel_cron_list_query'
| 'invalid_panel_bootstrap_query';
}>;
type PanelPublicOperation = 'capabilities' | 'health' | 'system';
export interface LocalApiHttpSurfaceOptions {
readonly profile: LocalApplicationProfile;
readonly host: '127.0.0.1' | '::1';
@@ -589,7 +593,7 @@ function parsePanelCronListRoute(
size < 1 ||
size > 64 ||
String(size) !== rawSize ||
page * size > maximumRows ||
(page - 1) * size >= maximumRows ||
(timestamp !== null && !/^\d{1,20}$/u.test(timestamp))
) {
throw new TypeError();
@@ -606,6 +610,53 @@ function parsePanelCronListRoute(
}
}
function panelExactGetPath(
rawUrl: string,
expectedPath: string,
): 'invalid' | 'match' | 'unrelated' {
const separator = rawUrl.indexOf('?');
const path = separator < 0 ? rawUrl : rawUrl.slice(0, separator);
if (path !== expectedPath) return 'unrelated';
if (separator !== rawUrl.lastIndexOf('?')) return 'invalid';
if (separator < 0) return 'match';
try {
const query = new URLSearchParams(rawUrl.slice(separator + 1));
if (
[...query.keys()].some(
(key) => key !== 't' || query.getAll(key).length !== 1,
)
) {
return 'invalid';
}
const timestamp = query.get('t');
return timestamp !== null && /^\d{1,20}$/u.test(timestamp)
? 'match'
: 'invalid';
} catch {
return 'invalid';
}
}
function panelPublicOperation(
request: IncomingMessage,
): PanelPublicOperation | 'invalid' | null {
if (request.method !== 'GET' || typeof request.url !== 'string') return null;
const paths = Object.freeze([
Object.freeze({ path: '/api/health', operation: 'health' as const }),
Object.freeze({ path: '/api/system', operation: 'system' as const }),
Object.freeze({
path: '/api/v3/capabilities',
operation: 'capabilities' as const,
}),
]);
for (const candidate of paths) {
const result = panelExactGetPath(request.url, candidate.path);
if (result === 'invalid') return 'invalid';
if (result === 'match') return candidate.operation;
}
return null;
}
function route(
request: IncomingMessage,
profile: LocalApplicationProfile,
@@ -622,6 +673,24 @@ function route(
if (request.method === 'GET') {
const panelCronList = parsePanelCronListRoute(rawUrl, profile);
if (panelCronList) return panelCronList;
const panelUser = panelExactGetPath(rawUrl, '/api/user');
if (panelUser !== 'unrelated') {
return panelUser === 'match'
? Object.freeze({
operationId: 'panel.user.get',
projectId: 'default',
})
: Object.freeze({ errorCode: 'invalid_panel_bootstrap_query' });
}
const panelSystemConfig = panelExactGetPath(rawUrl, '/api/system/config');
if (panelSystemConfig !== 'unrelated') {
return panelSystemConfig === 'match'
? Object.freeze({
operationId: 'panel.system.config.get',
projectId: 'default',
})
: Object.freeze({ errorCode: 'invalid_panel_bootstrap_query' });
}
}
if (rawUrl.includes('%')) return null;
const separator = rawUrl.indexOf('?');
@@ -940,6 +1009,22 @@ export async function startLocalApiHttpSurface(
sendConsoleFavicon(response, requestId);
return;
}
const publicOperation = panelPublicOperation(request);
if (publicOperation) {
if (hasRequestBody(request)) {
send(response, requestId, errorResponse(400, 'invalid_request_body'));
request.resume();
return;
}
send(
response,
requestId,
publicOperation === 'invalid'
? errorResponse(400, 'invalid_panel_bootstrap_query')
: panelPublicResponse(publicOperation, options.profile),
);
return;
}
const resolvedRoute = route(request, options.profile);
if (!resolvedRoute) {
send(response, requestId, errorResponse(404, 'route_not_found'));
@@ -314,6 +314,17 @@ function fixture(overrides = {}) {
};
},
},
panelBootstrapRoute: {
async handle(value) {
events.push(
`panel-bootstrap:${value.operationId}:${value.principal.subject.id}`,
);
return {
statusCode: 200,
body: { code: 200, data: { username: value.principal.subject.id } },
};
},
},
now: () => 10_000,
randomUuid: () => '019f70c0-0000-4000-8000-000000000002',
...overrides,
@@ -548,6 +559,27 @@ test('uses task.read and the exact panel Cron audit identity for the compatibili
]);
});
test('uses the same authenticated task.read chain for the panel bootstrap identity', async () => {
for (const operationId of ['panel.user.get', 'panel.system.config.get']) {
const { admission, events } = fixture();
const result = await execute(
admission,
request({
operation: Object.freeze({ operationId, projectId: 'default' }),
}),
);
assert.equal(result.statusCode, 200);
assert.equal(result.body.data.username, 'usr_local');
assert.deepEqual(events, [
'authenticate',
'authorize:task.read:default',
`audit:allowed:${operationId}`,
'confirm',
`panel-bootstrap:${operationId}:usr_local`,
]);
}
});
test('authorizes and audits run.stop before exposing the cancellation body handler', async () => {
const { admission, events } = fixture();
const prepared = await admission.prepare(
@@ -277,6 +277,14 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
});
assert.deepEqual(observed[6].operation, panelCrons.body.input);
const finalEdgePage = await request(
port,
'/api/crons?searchValue=&page=4&size=20&filters=%7B%7D',
);
assert.equal(finalEdgePage.statusCode, 200);
assert.equal(finalEdgePage.body.input.maximumRows, 64);
assert.equal(finalEdgePage.body.input.page, 4);
const unsupportedPanelQuery = await request(
port,
'/api/crons?searchValue=private&page=1&size=20&filters=%7B%7D',
@@ -285,7 +293,7 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
code: 'invalid_panel_cron_list_query',
});
assert.equal(unsupportedPanelQuery.statusCode, 400);
assert.equal(observed.length, 7);
assert.equal(observed.length, 8);
const cancellationBody = JSON.stringify({
schema: 'qinglong/run-cancellation@v1',
@@ -306,7 +314,7 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
);
assert.equal(cancellation.statusCode, 202);
assert.deepEqual(cancellation.body.accepted, JSON.parse(cancellationBody));
assert.deepEqual(observed[7].operation, {
assert.deepEqual(observed[8].operation, {
operationId: 'run.cancel',
projectId: 'prj_default',
runId: 'run_123',
@@ -333,7 +341,7 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
);
assert.equal(taskStart.statusCode, 202);
assert.deepEqual(taskStart.body.accepted, JSON.parse(taskStartBody));
assert.deepEqual(observed[8].operation, {
assert.deepEqual(observed[9].operation, {
operationId: 'task.start',
projectId: 'prj_default',
taskId: 'task_1',
@@ -357,20 +365,20 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
);
assert.equal(taskPut.statusCode, 202);
assert.deepEqual(taskPut.body.accepted, JSON.parse(taskPutBody));
assert.deepEqual(observed[9].operation, {
assert.deepEqual(observed[10].operation, {
operationId: 'task.put',
projectId: 'prj_default',
taskId: 'task_1',
});
assert.equal(observed[9].localPresence, 'ql3p_request_bound_proof');
assert.equal(observed[9].taskAuthoringLease, 'ql3a_exact_snapshot_lease');
assert.equal(observed[10].localPresence, 'ql3p_request_bound_proof');
assert.equal(observed[10].taskAuthoringLease, 'ql3a_exact_snapshot_lease');
const log = await request(
port,
'/api/v3/projects/prj_default/runs/run_123/attempts/attempt_1/log?offset=4&length=32',
);
assert.deepEqual(log.body, { range: { offset: 4, length: 32 } });
assert.deepEqual(observed[10].operation, {
assert.deepEqual(observed[11].operation, {
operationId: 'run.log.read',
projectId: 'prj_default',
runId: 'run_123',
@@ -399,19 +407,19 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
},
);
assert.equal(authoring.statusCode, 200);
assert.deepEqual(observed[12].operation, {
assert.deepEqual(observed[13].operation, {
operationId: 'task.authoring',
projectId: 'prj_default',
taskId: 'task_1',
});
assert.equal(observed[12].localPresence, 'ql3p_authoring_read_proof');
assert.equal(observed[13].localPresence, 'ql3p_authoring_read_proof');
const secrets = await request(
port,
'/api/v3/projects/prj_default/secrets?limit=8&after=YWxwaGE',
);
assert.deepEqual(secrets.body, { secrets: [], truncated: false });
assert.deepEqual(observed[13].operation, {
assert.deepEqual(observed[14].operation, {
operationId: 'secret.list',
projectId: 'prj_default',
limit: 8,
@@ -435,11 +443,11 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
);
assert.equal(secretPut.statusCode, 202);
assert.deepEqual(secretPut.body.accepted, JSON.parse(secretPutBody));
assert.deepEqual(observed[14].operation, {
assert.deepEqual(observed[15].operation, {
operationId: 'secret.put',
projectId: 'prj_default',
});
assert.equal(observed[14].localPresence, 'ql3p_secret_bound_proof');
assert.equal(observed[15].localPresence, 'ql3p_secret_bound_proof');
for (const invalidPath of [
'/api/v3/projects/prj_default/runs/run_123?expanded=true',
@@ -521,13 +529,86 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
assert.equal(invalid.statusCode, 400);
assert.deepEqual(invalid.body, { code: 'invalid_run_step_list_query' });
}
assert.equal(observed.length, 15);
assert.equal(observed.length, 16);
assert.deepEqual(
await Promise.all([surface.stopAndDrain(), surface.stopAndDrain()]),
['stopped', 'stopped'],
);
});
test('serves the public capability shell and authenticates private panel bootstrap reads', async (t) => {
const port = await reservePort();
const observed = [];
const surface = await startLocalApiHttpSurface({
profile: 'edge',
host: '127.0.0.1',
port,
admission: preparedAdmission(async (value) => {
observed.push(value);
return {
statusCode: 200,
body: {
code: 200,
data: { operationId: value.operation.operationId },
},
};
}),
randomUuid: () => '019f70c0-0000-4000-8000-000000000013',
});
t.after(() => surface.stopAndDrain());
const health = await request(port, '/api/health?t=100', { headers: {} });
assert.equal(health.statusCode, 200);
assert.equal(health.body.data.status, 'ok');
assert.equal(health.body.data.ql3.capabilitiesPath, '/api/v3/capabilities');
const system = await request(port, '/api/system', { headers: {} });
assert.equal(system.statusCode, 200);
assert.equal(system.body.data.isInitialized, true);
assert.equal(system.body.data.ql3.profile, 'edge');
const capabilities = await request(port, '/api/v3/capabilities?t=101', {
headers: {},
});
assert.equal(capabilities.statusCode, 200);
assert.equal(
capabilities.body.capabilities.authentication.loginEndpoint,
null,
);
assert.equal(capabilities.body.capabilities.panel.legacyLogin, false);
assert.equal(capabilities.body.capabilities.limits.cronRows, 64);
assert.equal(observed.length, 0);
const user = await request(port, '/api/user?t=102');
assert.deepEqual(user.body, {
code: 200,
data: { operationId: 'panel.user.get' },
});
assert.deepEqual(observed[0].operation, {
operationId: 'panel.user.get',
projectId: 'default',
});
const config = await request(port, '/api/system/config?t=103');
assert.deepEqual(config.body, {
code: 200,
data: { operationId: 'panel.system.config.get' },
});
assert.deepEqual(observed[1].operation, {
operationId: 'panel.system.config.get',
projectId: 'default',
});
const invalid = await request(port, '/api/system?search=wide', {
headers: {},
});
assert.equal(invalid.statusCode, 400);
assert.deepEqual(invalid.body, { code: 'invalid_panel_bootstrap_query' });
const encoded = await request(port, '/api/%73ystem', { headers: {} });
assert.equal(encoded.statusCode, 404);
assert.equal(observed.length, 2);
});
test('rejects GET bodies without invoking the prepared route handler', async (t) => {
const port = await reservePort();
let handlers = 0;
@@ -0,0 +1,150 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const {
createPanelBootstrapRoute,
panelCapabilities,
panelPublicResponse,
} = require('../dist/panel-compatibility/panelBootstrapRoute.js');
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner' }),
authenticationId: 'auth:owner',
authenticatedAtMs: 1_787_200_000_000,
expiresAtMs: 1_787_200_060_000,
assurance: 'local_console',
});
test('publishes an exact profile-aware capability contract', () => {
const edge = panelCapabilities('edge');
const standalone = panelCapabilities('standalone');
assert.deepEqual(edge, {
schemaVersion: 1,
product: 'qinglong3',
version: '3.0.0-alpha.2',
deployment: { mode: 'local', profile: 'edge' },
authentication: {
kind: 'api_credential',
transport: 'bearer',
persistence: 'memory_only',
loginEndpoint: null,
},
project: { selection: 'explicit', defaultId: 'default' },
panel: {
bootstrap: true,
cronList: 'bounded_read_only',
taskRead: true,
triggerRead: true,
runRead: true,
runLogRead: true,
legacyMutations: false,
legacyLogin: false,
subscriptions: false,
scripts: false,
environmentVariables: false,
webSocket: false,
},
limits: {
cronRows: 64,
cronPageSize: 64,
logChunkBytes: 16 * 1_024,
},
});
assert.deepEqual(standalone.limits, {
cronRows: 256,
cronPageSize: 64,
logChunkBytes: 32 * 1_024,
});
assert.throws(() => panelCapabilities('cluster'));
});
test('serves public health, system and native capability envelopes', () => {
assert.deepEqual(panelPublicResponse('health', 'edge'), {
statusCode: 200,
body: {
code: 200,
data: {
status: 'ok',
ql3: {
schemaVersion: 1,
apiVersion: 'v3',
capabilitiesPath: '/api/v3/capabilities',
},
},
},
});
const system = panelPublicResponse('system', 'standalone');
assert.equal(system.body.code, 200);
assert.equal(system.body.data.isInitialized, true);
assert.equal(system.body.data.version, '3.0.0-alpha.2');
assert.equal(system.body.data.ql3.profile, 'standalone');
const capabilities = panelPublicResponse('capabilities', 'edge');
assert.equal(capabilities.body.capabilities.limits.cronRows, 64);
});
test('projects the authenticated principal and bounded panel configuration', async () => {
const route = createPanelBootstrapRoute('edge');
const user = await route.handle({
operationId: 'panel.user.get',
principal: PRINCIPAL,
});
assert.deepEqual(user, {
statusCode: 200,
body: {
code: 200,
data: {
username: 'owner',
ql3: {
schemaVersion: 1,
subjectType: 'user',
assurance: 'local_console',
expiresAtMs: 1_787_200_060_000,
credentialPersistence: 'memory_only',
panelHome: '/crontab',
},
},
},
});
const config = await route.handle({
operationId: 'panel.system.config.get',
principal: PRINCIPAL,
});
assert.equal(config.statusCode, 200);
assert.deepEqual(config.body.data.info, {
panelTitle: 'QingLong 3.0',
lang: 'zh-cn',
});
assert.equal(config.body.data.ql3.limits.cronRows, 64);
});
test('fails closed for invalid profile, principal and operation', async () => {
assert.throws(() => createPanelBootstrapRoute('cluster'));
const route = createPanelBootstrapRoute('standalone');
assert.equal(
(await route.handle({ operationId: 'panel.user.get', principal: null }))
.statusCode,
503,
);
assert.equal(
(
await route.handle({
operationId: 'panel.user.get',
principal: {
...PRINCIPAL,
subject: { type: 'api_app', id: 'service' },
assurance: 'service',
},
})
).statusCode,
503,
);
assert.equal(
(
await route.handle({
operationId: 'panel.unknown',
principal: PRINCIPAL,
})
).statusCode,
503,
);
});
@@ -230,6 +230,18 @@ test('fails closed for detached pins, unsupported triggers, invalid budgets and
},
},
});
assert.deepEqual(
await route.handle({
projectId: 'default',
page: 4,
size: 20,
maximumRows: 64,
}),
{
statusCode: 200,
body: { code: 200, data: { data: [], total: 0 } },
},
);
assert.equal(
(
await route.handle({
@@ -462,6 +462,34 @@ test('serves an authenticated Run through one real SQLite authority and durable
});
t.after(() => active.stopAndDrain());
const health = await request(
port,
'Bearer ignored-public-credential',
'/api/health?t=100',
);
assert.equal(health.statusCode, 200);
assert.equal(health.body.data.status, 'ok');
const capabilities = await request(
port,
'Bearer ignored-public-credential',
'/api/v3/capabilities',
);
assert.equal(capabilities.statusCode, 200);
assert.equal(capabilities.body.capabilities.deployment.profile, 'edge');
assert.equal(capabilities.body.capabilities.panel.legacyMutations, false);
const panelUser = await request(port, `Bearer ${TOKEN}`, '/api/user?t=101');
assert.equal(panelUser.statusCode, 200);
assert.equal(panelUser.body.data.username, 'local-api-user');
assert.equal(panelUser.body.data.ql3.credentialPersistence, 'memory_only');
const panelConfig = await request(
port,
`Bearer ${TOKEN}`,
'/api/system/config?t=102',
);
assert.equal(panelConfig.statusCode, 200);
assert.equal(panelConfig.body.data.info.panelTitle, 'QingLong 3.0');
assert.equal(panelConfig.body.data.ql3.limits.cronRows, 64);
const accepted = await request(port, `Bearer ${TOKEN}`);
assert.equal(accepted.statusCode, 200);
assert.equal(accepted.body.run.id, RUN_ID);
@@ -1132,6 +1160,7 @@ test('serves an authenticated Run through one real SQLite authority and durable
'run.get', 'run.list', 'run.events.list', 'run.steps.list',
'run.cancel', 'task.authoring.read', 'task.create', 'task.get',
'task.list', 'task.start', 'task.update', 'run.log.read',
'panel.system.config.get', 'panel.user.get',
'trigger.create', 'trigger.get', 'trigger.list', 'trigger.update',
'secret.create', 'secret.list'
)
@@ -1140,6 +1169,8 @@ test('serves an authenticated Run through one real SQLite authority and durable
.all()
.map(({ operation_id, outcome }) => `${operation_id}:${outcome}`),
[
'panel.system.config.get:allowed',
'panel.user.get:allowed',
'run.cancel:allowed',
'run.cancel:allowed',
'run.events.list:allowed',