mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): fence console capabilities by session
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
(function () {
|
||||
const schema = 'qinglong/cluster-copilot-console-read-request@v1';
|
||||
const capabilityRequestSchema =
|
||||
'qinglong/cluster-copilot-console-capabilities-request@v1';
|
||||
const capabilitySchema = 'qinglong/cluster-copilot-console-capabilities@v1';
|
||||
const routes = Object.freeze({
|
||||
inspect: '/api/v1/copilot/inspect',
|
||||
output: '/api/v1/copilot/output',
|
||||
@@ -55,11 +58,13 @@
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const message = document.getElementById('message');
|
||||
const statusChip = document.getElementById('status-chip');
|
||||
const authoritySummary = document.getElementById('authority-summary');
|
||||
const ledgerMeta = document.getElementById('ledger-meta');
|
||||
const exportButton = document.getElementById('export-evidence');
|
||||
const clearButton = document.getElementById('clear-evidence');
|
||||
const evidenceRecords = [];
|
||||
let sessionToken = '';
|
||||
let allowedOperations = new Set();
|
||||
let busy = false;
|
||||
let exporting = false;
|
||||
let evidenceBytes = 0;
|
||||
@@ -95,7 +100,7 @@
|
||||
const setBusy = function (next) {
|
||||
busy = next;
|
||||
document.querySelectorAll('[data-read]').forEach(function (button) {
|
||||
button.disabled = next;
|
||||
button.disabled = next || !allowedOperations.has(button.dataset.read);
|
||||
});
|
||||
if (next) {
|
||||
statusChip.textContent = '读取中';
|
||||
@@ -107,6 +112,72 @@
|
||||
updateLedgerState();
|
||||
};
|
||||
|
||||
const applyCapabilities = function (capabilities) {
|
||||
allowedOperations = new Set(capabilities.operations);
|
||||
document.querySelectorAll('[data-read]').forEach(function (button) {
|
||||
const available = allowedOperations.has(button.dataset.read);
|
||||
button.hidden = !available;
|
||||
button.disabled = !available;
|
||||
});
|
||||
document.querySelectorAll('.mode-tab').forEach(function (tab) {
|
||||
const panel = document.getElementById(tab.dataset.panel);
|
||||
const available = Array.from(panel.querySelectorAll('[data-read]')).some(
|
||||
function (button) {
|
||||
return allowedOperations.has(button.dataset.read);
|
||||
},
|
||||
);
|
||||
tab.hidden = !available;
|
||||
tab.classList.toggle('active', tab.dataset.panel === 'runtime-panel');
|
||||
tab.setAttribute(
|
||||
'aria-pressed',
|
||||
String(tab.dataset.panel === 'runtime-panel'),
|
||||
);
|
||||
panel.hidden = tab.dataset.panel !== 'runtime-panel';
|
||||
});
|
||||
const enabled = ['Run', 'Task', 'Workflow', 'Copilot'];
|
||||
if (allowedOperations.has('run_cancellation_status')) {
|
||||
enabled.push('取消诊断');
|
||||
}
|
||||
if (allowedOperations.has('worker_list')) enabled.push('Worker');
|
||||
if (allowedOperations.has('package_list')) enabled.push('Package');
|
||||
authoritySummary.textContent = enabled.join(' · ');
|
||||
};
|
||||
|
||||
const discoverCapabilities = async function () {
|
||||
const response = await fetch('/api/v1/session/capabilities', {
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
credentials: 'omit',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: 'QL3-Console ' + sessionToken,
|
||||
},
|
||||
body: JSON.stringify({ schema: capabilityRequestSchema }),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
typeof body.code === 'string' ? body.code : 'capability_read_failed',
|
||||
);
|
||||
}
|
||||
if (
|
||||
body.schema !== capabilitySchema ||
|
||||
!Array.isArray(body.operations) ||
|
||||
body.operations.length !== new Set(body.operations).size ||
|
||||
body.operations.some(function (operation) {
|
||||
return (
|
||||
typeof operation !== 'string' || !Object.hasOwn(routes, operation)
|
||||
);
|
||||
})
|
||||
) {
|
||||
throw new Error('capability_response_invalid');
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
const base = function (operation) {
|
||||
const projectId = value('project-id');
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(projectId)) {
|
||||
@@ -363,7 +434,7 @@
|
||||
};
|
||||
|
||||
const execute = async function (operation, prepared) {
|
||||
if (busy) return;
|
||||
if (busy || !allowedOperations.has(operation)) return;
|
||||
setBusy(true);
|
||||
setMessage('正在读取 ' + labels[operation] + '…');
|
||||
try {
|
||||
@@ -477,21 +548,42 @@
|
||||
}
|
||||
};
|
||||
|
||||
sessionForm.addEventListener('submit', function (event) {
|
||||
sessionForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
const candidate = sessionInput.value.trim();
|
||||
if (!/^[A-Za-z0-9_-]{43}$/.test(candidate)) {
|
||||
setMessage('浏览器访问密钥格式无效。', 'error');
|
||||
return;
|
||||
}
|
||||
const submitButton = sessionForm.querySelector('button[type="submit"]');
|
||||
submitButton.disabled = true;
|
||||
sessionToken = candidate;
|
||||
sessionInput.value = '';
|
||||
sessionForm.hidden = true;
|
||||
controls.hidden = false;
|
||||
statusChip.textContent = '只读就绪';
|
||||
statusChip.dataset.tone = 'success';
|
||||
setMessage('本页已解锁;Cluster credential 仍只存在于服务端。', 'success');
|
||||
document.getElementById('project-id').focus();
|
||||
setMessage('正在核验会话并读取本机能力边界…');
|
||||
try {
|
||||
const capabilities = await discoverCapabilities();
|
||||
applyCapabilities(capabilities);
|
||||
sessionInput.value = '';
|
||||
sessionForm.hidden = true;
|
||||
controls.hidden = false;
|
||||
statusChip.textContent = '只读就绪';
|
||||
statusChip.dataset.tone = 'success';
|
||||
setMessage(
|
||||
'本页已解锁并仅显示服务端启用的 ' +
|
||||
String(capabilities.operations.length) +
|
||||
' 个只读操作;Cluster credential 仍只存在于服务端。',
|
||||
'success',
|
||||
);
|
||||
document.getElementById('project-id').focus();
|
||||
} catch (error) {
|
||||
sessionToken = '';
|
||||
setMessage(
|
||||
'无法解锁:' +
|
||||
(error instanceof Error ? error.message : 'capability_read_failed'),
|
||||
'error',
|
||||
);
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.mode-tab').forEach(function (tab) {
|
||||
@@ -521,6 +613,7 @@
|
||||
|
||||
window.addEventListener('pagehide', function () {
|
||||
sessionToken = '';
|
||||
allowedOperations.clear();
|
||||
evidenceRecords.length = 0;
|
||||
evidenceBytes = 0;
|
||||
ledger.textContent = '';
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="boundary" aria-label="当前权限边界">
|
||||
<span class="boundary-dot" aria-hidden="true"></span>
|
||||
<span>本机只读 BFF</span>
|
||||
<strong>Run · Task · Workflow · Worker · Package · Copilot</strong>
|
||||
<strong id="authority-summary">等待会话能力</strong>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
|
||||
<footer>
|
||||
<span>Loopback only · explicit reads · zero polling</span>
|
||||
<span>QingLong 3.0 incubation / D-376</span>
|
||||
<span>QingLong 3.0 incubation / D-377</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -24,7 +24,7 @@ const ASSETS = Object.freeze([
|
||||
name: 'index.html',
|
||||
field: 'html',
|
||||
maximumBytes: 32 * 1024,
|
||||
digest: '429d7b3dd2da4989865be6ac07180cc9c3ebdcbbac5028054cddaac870ad520c',
|
||||
digest: '7e024103aaf03553995b09bfb9d6da5ba058ce77f7fff65c56b564017c79f28a',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'app.css',
|
||||
@@ -42,7 +42,7 @@ const ASSETS = Object.freeze([
|
||||
name: 'app.js',
|
||||
field: 'javascript',
|
||||
maximumBytes: 32 * 1024,
|
||||
digest: '365ccd43ae2aa4b11a0ab3d142cd04e89ec0b96711bef253f63584e8182589ee',
|
||||
digest: '224895776a1df1b4571a8cdb9ce8126861d0284e1fef801e6de3ef31ca96c931',
|
||||
}),
|
||||
] as const);
|
||||
|
||||
|
||||
@@ -577,6 +577,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const server = await startClusterCopilotConsoleServer({
|
||||
allowedOperations: operations,
|
||||
assets,
|
||||
executor: Object.freeze({
|
||||
execute(request: Readonly<ClusterCopilotConsoleReadRequest>) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../management-support/pluginPackageManagementClient';
|
||||
import { type ClusterCopilotConsoleAssets } from './assets';
|
||||
import {
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA,
|
||||
InvalidClusterCopilotConsoleReadRequestError,
|
||||
normalizeClusterCopilotConsoleReadRequest,
|
||||
@@ -43,6 +44,7 @@ export interface ClusterCopilotConsoleExecutor {
|
||||
}
|
||||
|
||||
export interface ClusterCopilotConsoleServerOptions {
|
||||
readonly allowedOperations: readonly ClusterCopilotConsoleReadOperation[];
|
||||
readonly assets: Readonly<ClusterCopilotConsoleAssets>;
|
||||
readonly executor: ClusterCopilotConsoleExecutor;
|
||||
readonly networkBoundary?: ClusterCopilotConsoleNetworkBoundary;
|
||||
@@ -69,6 +71,31 @@ export class ClusterCopilotConsoleConfigurationError extends TypeError {
|
||||
}
|
||||
|
||||
const SESSION_TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const CAPABILITIES_PATH = '/api/v1/session/capabilities';
|
||||
const CAPABILITIES_REQUEST_SCHEMA =
|
||||
'qinglong/cluster-copilot-console-capabilities-request@v1';
|
||||
const CAPABILITIES_SCHEMA = 'qinglong/cluster-copilot-console-capabilities@v1';
|
||||
const RUN_MANAGEMENT_OPERATIONS = Object.freeze([
|
||||
'run_cancellation_status',
|
||||
'run_cancellation_blocked_list',
|
||||
'run_cancellation_inspect',
|
||||
] as const satisfies readonly ClusterCopilotConsoleReadOperation[]);
|
||||
const WORKER_MANAGEMENT_OPERATIONS = Object.freeze([
|
||||
'worker_list',
|
||||
'worker_inspect',
|
||||
] as const satisfies readonly ClusterCopilotConsoleReadOperation[]);
|
||||
const PACKAGE_MANAGEMENT_OPERATIONS = Object.freeze([
|
||||
'package_list',
|
||||
'package_inspect',
|
||||
] as const satisfies readonly ClusterCopilotConsoleReadOperation[]);
|
||||
const OPTIONAL_OPERATION_GROUPS = Object.freeze([
|
||||
RUN_MANAGEMENT_OPERATIONS,
|
||||
WORKER_MANAGEMENT_OPERATIONS,
|
||||
PACKAGE_MANAGEMENT_OPERATIONS,
|
||||
]);
|
||||
const OPTIONAL_OPERATIONS = new Set<ClusterCopilotConsoleReadOperation>(
|
||||
OPTIONAL_OPERATION_GROUPS.flat(),
|
||||
);
|
||||
const SESSION_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong-cluster-copilot-console-session-v1\0',
|
||||
'utf8',
|
||||
@@ -219,6 +246,57 @@ function targetPath(
|
||||
return request.url === undefined ? null : READ_ROUTES[request.url] ?? null;
|
||||
}
|
||||
|
||||
function validatedAllowedOperations(
|
||||
value: unknown,
|
||||
): readonly ClusterCopilotConsoleReadOperation[] {
|
||||
if (!Array.isArray(value)) return invalid();
|
||||
const canonical = new Set<ClusterCopilotConsoleReadOperation>(
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
);
|
||||
const operations = value as ClusterCopilotConsoleReadOperation[];
|
||||
const selected = new Set(operations);
|
||||
if (
|
||||
selected.size !== operations.length ||
|
||||
operations.some((operation) => !canonical.has(operation)) ||
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.some(
|
||||
(operation) =>
|
||||
!OPTIONAL_OPERATIONS.has(operation) && !selected.has(operation),
|
||||
) ||
|
||||
OPTIONAL_OPERATION_GROUPS.some((group) => {
|
||||
const selectedCount = group.filter((operation) =>
|
||||
selected.has(operation),
|
||||
).length;
|
||||
return selectedCount !== 0 && selectedCount !== group.length;
|
||||
})
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return Object.freeze(
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter((operation) =>
|
||||
selected.has(operation),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function operationGroupEnabled(
|
||||
allowedOperations: ReadonlySet<ClusterCopilotConsoleReadOperation>,
|
||||
operations: readonly ClusterCopilotConsoleReadOperation[],
|
||||
): boolean {
|
||||
return operations.every((operation) => allowedOperations.has(operation));
|
||||
}
|
||||
|
||||
function validateCapabilitiesRequest(value: unknown): void {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).length !== 1 ||
|
||||
(value as Record<string, unknown>).schema !== CAPABILITIES_REQUEST_SCHEMA
|
||||
) {
|
||||
throw new InvalidClusterCopilotConsoleReadRequestError();
|
||||
}
|
||||
}
|
||||
|
||||
function authorize(
|
||||
request: IncomingMessage,
|
||||
expectedOrigin: string,
|
||||
@@ -331,7 +409,13 @@ function remoteFailure(
|
||||
export async function startClusterCopilotConsoleServer(
|
||||
options: ClusterCopilotConsoleServerOptions,
|
||||
): Promise<Readonly<ClusterCopilotConsoleServer>> {
|
||||
const optionKeys = ['assets', 'executor', 'port', 'sessionDigest'];
|
||||
const optionKeys = [
|
||||
'allowedOperations',
|
||||
'assets',
|
||||
'executor',
|
||||
'port',
|
||||
'sessionDigest',
|
||||
];
|
||||
if (Object.hasOwn(options, 'networkBoundary')) {
|
||||
optionKeys.push('networkBoundary');
|
||||
}
|
||||
@@ -342,6 +426,8 @@ export async function startClusterCopilotConsoleServer(
|
||||
'html',
|
||||
'javascript',
|
||||
]);
|
||||
const operations = validatedAllowedOperations(record.allowedOperations);
|
||||
const allowedOperations = new Set(operations);
|
||||
const networkBoundary =
|
||||
record.networkBoundary === undefined
|
||||
? 'host-loopback'
|
||||
@@ -412,16 +498,23 @@ export async function startClusterCopilotConsoleServer(
|
||||
}
|
||||
}
|
||||
|
||||
const capabilitiesRequest =
|
||||
request.method === 'POST' && request.url === CAPABILITIES_PATH;
|
||||
const operation = targetPath(request);
|
||||
if (
|
||||
!hostMatches ||
|
||||
operation === null ||
|
||||
(!capabilitiesRequest && operation === null) ||
|
||||
!authorize(request, expectedOrigin, sessionDigest)
|
||||
) {
|
||||
sendJson(response, 404, Object.freeze({ code: 'not_found' }));
|
||||
request.resume();
|
||||
return;
|
||||
}
|
||||
if (!capabilitiesRequest && !allowedOperations.has(operation!)) {
|
||||
sendJson(response, 404, Object.freeze({ code: 'not_found' }));
|
||||
request.resume();
|
||||
return;
|
||||
}
|
||||
if (inFlight >= CLUSTER_COPILOT_CONSOLE_LIMITS.maximumConcurrentRequests) {
|
||||
sendJson(
|
||||
response,
|
||||
@@ -439,6 +532,41 @@ export async function startClusterCopilotConsoleServer(
|
||||
inFlight += 1;
|
||||
try {
|
||||
const body = await readJsonBody(request);
|
||||
if (capabilitiesRequest) {
|
||||
validateCapabilitiesRequest(body);
|
||||
sendJson(
|
||||
response,
|
||||
200,
|
||||
Object.freeze({
|
||||
schema: CAPABILITIES_SCHEMA,
|
||||
operations,
|
||||
authorities: Object.freeze({
|
||||
projectObservation: 'server_only',
|
||||
runManagement: operationGroupEnabled(
|
||||
allowedOperations,
|
||||
RUN_MANAGEMENT_OPERATIONS,
|
||||
)
|
||||
? 'server_only'
|
||||
: 'disabled',
|
||||
workerManagement: operationGroupEnabled(
|
||||
allowedOperations,
|
||||
WORKER_MANAGEMENT_OPERATIONS,
|
||||
)
|
||||
? 'server_only'
|
||||
: 'disabled',
|
||||
packageManagement: operationGroupEnabled(
|
||||
allowedOperations,
|
||||
PACKAGE_MANAGEMENT_OPERATIONS,
|
||||
)
|
||||
? 'server_only'
|
||||
: 'disabled',
|
||||
}),
|
||||
mutation: false,
|
||||
upstreamReads: 0,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeClusterCopilotConsoleReadRequest(body);
|
||||
if (normalized.operation !== operation) {
|
||||
throw new InvalidClusterCopilotConsoleReadRequestError();
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
loadClusterCopilotConsoleAssets,
|
||||
} = require('../dist/copilot-console/assets.js');
|
||||
const {
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
|
||||
clusterCopilotConsoleClientCommand,
|
||||
clusterCopilotConsoleProjectReadPath,
|
||||
@@ -26,6 +27,18 @@ const {
|
||||
} = require('../dist/copilot-console/server.js');
|
||||
|
||||
const moduleDirectory = resolve(__dirname, '../dist/copilot-console');
|
||||
const optionalOperations = new Set([
|
||||
'run_cancellation_status',
|
||||
'run_cancellation_blocked_list',
|
||||
'run_cancellation_inspect',
|
||||
'worker_list',
|
||||
'worker_inspect',
|
||||
'package_list',
|
||||
'package_inspect',
|
||||
]);
|
||||
const baseOperations = CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter(
|
||||
(operation) => !optionalOperations.has(operation),
|
||||
);
|
||||
|
||||
function target(operation = 'inspect') {
|
||||
return {
|
||||
@@ -151,9 +164,13 @@ function request(origin, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
async function fixture(execute = async () => inspection()) {
|
||||
async function fixture(
|
||||
execute = async () => inspection(),
|
||||
allowedOperations = CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
) {
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
const server = await startClusterCopilotConsoleServer({
|
||||
allowedOperations,
|
||||
assets: loadClusterCopilotConsoleAssets(moduleDirectory),
|
||||
executor: { execute },
|
||||
port: 0,
|
||||
@@ -501,6 +518,7 @@ test('allows only an explicit fixed-port container listener behind host loopback
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
await assert.rejects(
|
||||
startClusterCopilotConsoleServer({
|
||||
allowedOperations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
assets: loadClusterCopilotConsoleAssets(moduleDirectory),
|
||||
executor: { execute: async () => inspection() },
|
||||
networkBoundary: 'container-published-loopback',
|
||||
@@ -510,6 +528,7 @@ test('allows only an explicit fixed-port container listener behind host loopback
|
||||
ClusterCopilotConsoleConfigurationError,
|
||||
);
|
||||
const server = await startClusterCopilotConsoleServer({
|
||||
allowedOperations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
assets: loadClusterCopilotConsoleAssets(moduleDirectory),
|
||||
executor: { execute: async () => inspection() },
|
||||
networkBoundary: 'container-published-loopback',
|
||||
@@ -521,6 +540,89 @@ test('allows only an explicit fixed-port container listener behind host loopback
|
||||
assert.equal((await request(server.origin)).statusCode, 200);
|
||||
});
|
||||
|
||||
test('discovers session capabilities locally and fences disabled operations before execution', async (t) => {
|
||||
const reads = [];
|
||||
const { server, headers } = await fixture(async (read) => {
|
||||
reads.push(read);
|
||||
return inspection();
|
||||
}, baseOperations);
|
||||
t.after(() => server.close());
|
||||
|
||||
const unauthenticated = await request(server.origin, {
|
||||
method: 'POST',
|
||||
path: '/api/v1/session/capabilities',
|
||||
});
|
||||
assert.equal(unauthenticated.statusCode, 404);
|
||||
|
||||
const invalidCapabilities = await request(server.origin, {
|
||||
method: 'POST',
|
||||
path: '/api/v1/session/capabilities',
|
||||
headers,
|
||||
body: {},
|
||||
});
|
||||
assert.equal(invalidCapabilities.statusCode, 400);
|
||||
|
||||
const capabilities = await request(server.origin, {
|
||||
method: 'POST',
|
||||
path: '/api/v1/session/capabilities',
|
||||
headers,
|
||||
body: {
|
||||
schema: 'qinglong/cluster-copilot-console-capabilities-request@v1',
|
||||
},
|
||||
});
|
||||
assert.equal(capabilities.statusCode, 200);
|
||||
assert.deepEqual(capabilities.body, {
|
||||
schema: 'qinglong/cluster-copilot-console-capabilities@v1',
|
||||
operations: baseOperations,
|
||||
authorities: {
|
||||
projectObservation: 'server_only',
|
||||
runManagement: 'disabled',
|
||||
workerManagement: 'disabled',
|
||||
packageManagement: 'disabled',
|
||||
},
|
||||
mutation: false,
|
||||
upstreamReads: 0,
|
||||
});
|
||||
|
||||
const disabled = await request(server.origin, {
|
||||
method: 'POST',
|
||||
path: '/api/v1/package-management/installations',
|
||||
headers,
|
||||
body: {
|
||||
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
|
||||
operation: 'package_list',
|
||||
projectId: 'project-main',
|
||||
requestId: 'console-package-list-1',
|
||||
afterPackageName: null,
|
||||
},
|
||||
});
|
||||
assert.equal(disabled.statusCode, 404);
|
||||
assert.deepEqual(reads, []);
|
||||
});
|
||||
|
||||
test('rejects duplicate, incomplete base and partial optional operation sets', async () => {
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
const common = {
|
||||
assets: loadClusterCopilotConsoleAssets(moduleDirectory),
|
||||
executor: { execute: async () => inspection() },
|
||||
port: 0,
|
||||
sessionDigest: clusterCopilotConsoleSessionDigest(token),
|
||||
};
|
||||
for (const allowedOperations of [
|
||||
[...CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS, 'inspect'],
|
||||
baseOperations.filter((operation) => operation !== 'inspect'),
|
||||
[...baseOperations, 'run_cancellation_status'],
|
||||
]) {
|
||||
await assert.rejects(
|
||||
startClusterCopilotConsoleServer({
|
||||
allowedOperations,
|
||||
...common,
|
||||
}),
|
||||
ClusterCopilotConsoleConfigurationError,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps the Cluster credential server-side and forwards one exact inspect', async (t) => {
|
||||
const commands = [];
|
||||
const { server, headers } = await fixture(async (command) => {
|
||||
|
||||
Reference in New Issue
Block a user