forked from EduCraft/curriculum-project-hub
fix: clear dependency security advisories
This commit is contained in:
@@ -21,6 +21,11 @@ run the same command again. No systemd unit is installed until configuration,
|
||||
paths, Node, bubblewrap, `socat`, `cph --version`, PostgreSQL connectivity and directory
|
||||
ownership all pass preflight.
|
||||
|
||||
The deployment path also runs `npm run audit:production` from the locked clean
|
||||
install before building or restarting the service. A current high or critical
|
||||
production advisory therefore blocks deployment instead of becoming a warning
|
||||
buried in CI output.
|
||||
|
||||
An existing service account is accepted only when its primary group, home and
|
||||
non-login shell match the requested configuration and it has no supplementary
|
||||
groups. After provisioning, the installer executes path-access, built Hub,
|
||||
|
||||
@@ -36,8 +36,8 @@ rsync -az --delete \
|
||||
-e "ssh ${SSH_OPTS[*]}" \
|
||||
"$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/"
|
||||
|
||||
# 2. Install deps + build on the host.
|
||||
ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "cd '$HUB_DIR' && npm ci && npm run build"
|
||||
# 2. Install deps, prove the production dependency tree, then build on the host.
|
||||
ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "cd '$HUB_DIR' && npm ci && npm run audit:production && npm run build"
|
||||
|
||||
# 3. Ensure the service is installed (idempotent), then restart.
|
||||
ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "
|
||||
|
||||
Generated
+43
-16
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@larksuiteoapi/node-sdk": "^1.66.1",
|
||||
"@larksuiteoapi/node-sdk": "^1.70.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"ai": "^7.0.16",
|
||||
"dotenv": "^17.4.2",
|
||||
@@ -874,9 +874,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@larksuiteoapi/node-sdk": {
|
||||
"version": "1.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.68.0.tgz",
|
||||
"integrity": "sha512-ip14+pWAv2La3bss4oDDtee2P5xBLDXuvmzhTvkxQMcPA5jCffFsler1TvNt4MyW6pexscj72AEWektyZwU9Yw==",
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.70.0.tgz",
|
||||
"integrity": "sha512-CzVfMbh2MO+cVWQgQGeYN9KkFdX6mofYYTsIjImU0hQqlxaXKzHcUEwG6mrpsmU2LWqYYtIGwCSIZjYQRjD6kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "~1.13.3",
|
||||
@@ -1621,6 +1621,18 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "7.0.16",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-7.0.16.tgz",
|
||||
@@ -1717,14 +1729,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
@@ -2004,7 +2017,6 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -2823,6 +2835,19 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
@@ -3353,8 +3378,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
@@ -3735,10 +3759,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
|
||||
+7
-1
@@ -9,7 +9,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@larksuiteoapi/node-sdk": "^1.66.1",
|
||||
"@larksuiteoapi/node-sdk": "^1.70.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"ai": "^7.0.16",
|
||||
"dotenv": "^17.4.2",
|
||||
@@ -22,12 +22,18 @@
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"overrides": {
|
||||
"@larksuiteoapi/node-sdk": {
|
||||
"axios": "1.18.1"
|
||||
}
|
||||
},
|
||||
"description": "Curriculum Project Hub — Feishu-group collaboration + Claude Code SDK agent runtime. Aligns to spec/System (ADR-0001..0004, 0017).",
|
||||
"scripts": {
|
||||
"dev": "npm run prisma:migrate && tsx watch src/server.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "npm run prisma:migrate && node dist/server.js",
|
||||
"check": "tsc -p tsconfig.json --noEmit",
|
||||
"audit:production": "npm audit --omit=dev --audit-level=high",
|
||||
"prisma:generate": "prisma generate --schema prisma/schema.prisma",
|
||||
"prisma:validate": "DATABASE_URL=${DATABASE_URL:-postgresql://stub:stub@127.0.0.1:5432/stub} prisma validate --schema prisma/schema.prisma",
|
||||
"prisma:migrate": "DATABASE_URL=${DATABASE_URL:-postgresql://paradigm:paradigm@127.0.0.1:5432/paradigm} prisma migrate deploy --schema prisma/schema.prisma",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import * as lark from "@larksuiteoapi/node-sdk";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const openServers: ReturnType<typeof createServer>[] = [];
|
||||
|
||||
describe("real Lark SDK HTTP transport", () => {
|
||||
afterEach(async () => {
|
||||
await Promise.all(openServers.splice(0).map(async (server) => {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => error === undefined ? resolve() : reject(error));
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it("uses overridden Axios for token, authenticated success, and HTTP error paths", async () => {
|
||||
const requests: Array<{
|
||||
readonly method: string | undefined;
|
||||
readonly url: string | undefined;
|
||||
readonly authorization: string | undefined;
|
||||
readonly body: string;
|
||||
}> = [];
|
||||
const server = createServer(async (request, response) => {
|
||||
const body = await readBody(request);
|
||||
requests.push({
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
authorization: request.headers.authorization,
|
||||
body,
|
||||
});
|
||||
routeSmokeRequest(request, response, body);
|
||||
});
|
||||
openServers.push(server);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string") throw new Error("local HTTP server has no TCP address");
|
||||
const domain = `http://127.0.0.1:${address.port}`;
|
||||
const client = new lark.Client({
|
||||
appId: "cli_transport_smoke",
|
||||
appSecret: "transport-smoke-secret",
|
||||
domain,
|
||||
loggerLevel: lark.LoggerLevel.error,
|
||||
});
|
||||
|
||||
await expect(client.request<{ readonly code: number; readonly data: { readonly ok: boolean } }>({
|
||||
method: "GET",
|
||||
url: "/open-apis/smoke/success",
|
||||
params: { probe: "axios-override" },
|
||||
})).resolves.toEqual({ code: 0, data: { ok: true } });
|
||||
|
||||
await expect(client.request({
|
||||
method: "GET",
|
||||
url: "/open-apis/smoke/failure",
|
||||
})).rejects.toMatchObject({
|
||||
response: {
|
||||
status: 429,
|
||||
data: { code: 999_429, msg: "forced smoke failure" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(requests).toHaveLength(3);
|
||||
expect(requests[0]).toMatchObject({
|
||||
method: "POST",
|
||||
url: "/open-apis/auth/v3/tenant_access_token/internal",
|
||||
authorization: undefined,
|
||||
});
|
||||
expect(JSON.parse(requests[0]!.body)).toEqual({
|
||||
app_id: "cli_transport_smoke",
|
||||
app_secret: "transport-smoke-secret",
|
||||
});
|
||||
expect(requests[1]).toMatchObject({
|
||||
method: "GET",
|
||||
url: "/open-apis/smoke/success?probe=axios-override",
|
||||
authorization: "Bearer tenant-transport-smoke",
|
||||
});
|
||||
expect(requests[2]).toMatchObject({
|
||||
method: "GET",
|
||||
url: "/open-apis/smoke/failure",
|
||||
authorization: "Bearer tenant-transport-smoke",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function readBody(request: IncomingMessage): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of request) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
function routeSmokeRequest(request: IncomingMessage, response: ServerResponse, body: string): void {
|
||||
if (request.method === "POST" && request.url === "/open-apis/auth/v3/tenant_access_token/internal") {
|
||||
const credential = JSON.parse(body) as { app_id?: unknown; app_secret?: unknown };
|
||||
if (credential.app_id !== "cli_transport_smoke" || credential.app_secret !== "transport-smoke-secret") {
|
||||
sendJson(response, 401, { code: 401, msg: "bad smoke credential" });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, {
|
||||
code: 0,
|
||||
msg: "ok",
|
||||
tenant_access_token: "tenant-transport-smoke",
|
||||
expire: 3_600,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && request.url === "/open-apis/smoke/success?probe=axios-override") {
|
||||
sendJson(response, 200, { code: 0, data: { ok: true } });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && request.url === "/open-apis/smoke/failure") {
|
||||
sendJson(response, 429, { code: 999_429, msg: "forced smoke failure" });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 404, { code: 404, msg: "unknown smoke route" });
|
||||
}
|
||||
|
||||
function sendJson(response: ServerResponse, statusCode: number, body: unknown): void {
|
||||
response.writeHead(statusCode, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify(body));
|
||||
}
|
||||
Reference in New Issue
Block a user