Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nodejs/.npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
omit-lockfile-registry-resolved=true
save-exact=true
13 changes: 10 additions & 3 deletions nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"clean": "rimraf --glob dist *.tgz",
"build": "tsx esbuild-copilotsdk-nodejs.ts",
"acquire:runtime-packages": "tsx scripts/runtime-package-acquisition.ts",
"pack:release": "tsx scripts/package-sdk.ts",
"pack:release": "npm run verify:dependency-policy && tsx scripts/package-sdk.ts",
"verify:dependency-policy": "tsx scripts/verify-dependency-policy.ts",
"verify:release-packages": "tsx scripts/verify-release-packages.ts",
"release:manifest": "tsx scripts/release-manifest.ts",
"prepare:runtime": "tsx scripts/prepare-runtime.ts",
Expand All @@ -64,13 +65,14 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"koffi": "^3.2.1",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
"koffi": "3.2.1",
"vscode-jsonrpc": "8.2.1",
"zod": "4.3.6"
},
"devDependencies": {
"@platformatic/vfs": "^0.3.0",
"@types/node": "^25.2.0",
"@types/semver": "7.8.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
Expand Down
7 changes: 4 additions & 3 deletions nodejs/samples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

182 changes: 182 additions & 0 deletions nodejs/scripts/dependency-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as semver from "semver";

const NPM_REGISTRY_URL = "https://registry.npmjs.org";

export const MINIMUM_DEPENDENCY_AGE_MS = 7 * 24 * 60 * 60 * 1000;

export interface ProductionDependencyManifest {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}

export interface ProductionDependency {
name: string;
version: string;
}

export interface RegistryResponse {
json(): Promise<unknown>;
ok: boolean;
status: number;
statusText: string;
}

export type RegistryRequest = (url: string) => Promise<RegistryResponse>;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function dependencyKey(dependency: ProductionDependency): string {
return `${dependency.name}@${dependency.version}`;
}

function readDependencySection(
manifest: Record<string, unknown>,
section: "dependencies" | "optionalDependencies"
): Record<string, string> | undefined {
const value = manifest[section];
if (value === undefined) {
return undefined;
}
assert(isRecord(value), `${section} must be a JSON object`);
const dependencies: Record<string, string> = {};
for (const [name, version] of Object.entries(value)) {
assert(typeof version === "string", `${section}.${name} must be a string`);
dependencies[name] = version;
}
return dependencies;
}

export function readProductionDependencyManifest(path: string): ProductionDependencyManifest {
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
assert(isRecord(parsed), "package.json must contain a JSON object");
return {
dependencies: readDependencySection(parsed, "dependencies"),
optionalDependencies: readDependencySection(parsed, "optionalDependencies"),
};
}

export function assertExactProductionDependencies(
manifest: ProductionDependencyManifest
): ProductionDependency[] {
const dependencies: ProductionDependency[] = [];
for (const section of ["dependencies", "optionalDependencies"] as const) {
for (const [name, version] of Object.entries(manifest[section] ?? {})) {
assert.equal(
semver.valid(version),
version,
`${section}.${name} must use an exact SemVer version; found '${version}'`
);
dependencies.push({ name, version });
}
}
return dependencies;
}

function packageNameFromLockfilePath(path: string): string | undefined {
return /(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)$/.exec(path)?.[1];
}

export function parseResolvedProductionDependencies(lockfile: unknown): ProductionDependency[] {
assert(isRecord(lockfile), "package-lock.json must contain a JSON object");
assert.equal(lockfile.lockfileVersion, 3, "package-lock.json must use lockfileVersion 3");
assert(isRecord(lockfile.packages), "package-lock.json must contain a packages object");

const dependencies = new Map<string, ProductionDependency>();
for (const [path, value] of Object.entries(lockfile.packages)) {
if (path === "") {
continue;
}
assert(isRecord(value), `Package-lock entry '${path}' must be a JSON object`);
// npm marks dev=true only when a package is strictly dev-only. Optional and
// devOptional packages reachable from production dependencies remain included.
if (value.dev === true || value.link === true || value.inBundle === true) {
continue;
}
const name = packageNameFromLockfilePath(path);
assert(name, `Unsupported production package-lock path '${path}'`);
const version = value.version;
assert(
typeof version === "string" && semver.valid(version) === version,
`Resolved production dependency ${name} must use an exact SemVer version`
);
dependencies.set(`${name}@${version}`, { name, version });
}
return [...dependencies.values()];
}

export function readResolvedProductionDependencies(path: string): ProductionDependency[] {
return parseResolvedProductionDependencies(JSON.parse(readFileSync(path, "utf8")) as unknown);
}

export function requiresPublicationCooldown(dependencyName: string): boolean {
return !dependencyName.startsWith("@github/");
}

export async function loadNpmPublicationTimes(
dependencies: readonly ProductionDependency[],
request: RegistryRequest = (url) => fetch(url)
): Promise<Map<string, string>> {
const externalDependencies = dependencies.filter((dependency) =>
requiresPublicationCooldown(dependency.name)
);
const packageNames = [...new Set(externalDependencies.map((dependency) => dependency.name))];
const metadata = new Map<string, Record<string, unknown>>();

await Promise.all(
packageNames.map(async (packageName) => {
const response = await request(
`${NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}`
);
assert(
response.ok,
`npm metadata request for ${packageName} failed: ${response.status} ${response.statusText}`
);
const body = await response.json();
assert(isRecord(body), `npm metadata for ${packageName} must be a JSON object`);
const times = body.time;
assert(isRecord(times), `npm metadata for ${packageName} is missing publication times`);
metadata.set(packageName, times);
})
);

return new Map(
externalDependencies.map((dependency) => {
const publishedAt = metadata.get(dependency.name)?.[dependency.version];
assert(
typeof publishedAt === "string",
`npm metadata is missing a publication time for ${dependencyKey(dependency)}`
);
return [dependencyKey(dependency), publishedAt];
})
);
}

export function assertMinimumPublicationAge(
dependencies: readonly ProductionDependency[],
publicationTimes: ReadonlyMap<string, string>,
now = Date.now()
): void {
assert(Number.isFinite(now), "Dependency policy verification time must be finite");

for (const dependency of dependencies) {
if (!requiresPublicationCooldown(dependency.name)) {
continue;
}
const key = dependencyKey(dependency);
const publishedAt = publicationTimes.get(key);
assert(publishedAt, `Missing npm publication time for ${key}`);
const publishedAtMs = Date.parse(publishedAt);
assert(Number.isFinite(publishedAtMs), `Invalid npm publication time for ${key}`);
const eligibleAtMs = publishedAtMs + MINIMUM_DEPENDENCY_AGE_MS;
assert(
now >= eligibleAtMs,
`${key} was published at ${publishedAt} and is not eligible until ${new Date(
eligibleAtMs
).toISOString()}`
);
}
}
32 changes: 32 additions & 0 deletions nodejs/scripts/verify-dependency-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
assertExactProductionDependencies,
assertMinimumPublicationAge,
loadNpmPublicationTimes,
readProductionDependencyManifest,
readResolvedProductionDependencies,
requiresPublicationCooldown,
} from "./dependency-policy.js";

async function main(): Promise<void> {
const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const manifest = readProductionDependencyManifest(join(nodeRoot, "package.json"));
const directDependencies = assertExactProductionDependencies(manifest);
const resolvedDependencies = readResolvedProductionDependencies(
join(nodeRoot, "package-lock.json")
);
const publicationTimes = await loadNpmPublicationTimes(resolvedDependencies);
assertMinimumPublicationAge(resolvedDependencies, publicationTimes);
const externalCount = resolvedDependencies.filter((dependency) =>
requiresPublicationCooldown(dependency.name)
).length;
console.log(
`Verified ${directDependencies.length} exact direct production dependencies; ${externalCount} resolved production package versions meet the seven-day npm publication age requirement.`
);
}

main().catch((error) => {
console.error(`::error::${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
});
11 changes: 10 additions & 1 deletion nodejs/scripts/verify-release-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { fileURLToPath } from "node:url";
import { globSync } from "glob";
import { t as listTar, x as extractTar } from "tar";
import { getRuntimePackageName, RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js";
import { assertExactProductionDependencies } from "./dependency-policy.js";

interface PackedPackage {
manifest: {
dependencies?: Record<string, string>;
name: string;
optionalDependencies?: Record<string, string>;
version: string;
repository?: string | { type?: string; url?: string };
optionalDependencies?: Record<string, string>;
};
entries: Set<string>;
}
Expand All @@ -27,6 +29,7 @@ assert(
: sourceManifest.repository?.url?.trim(),
"Main package is missing repository metadata"
);
assertExactProductionDependencies(sourceManifest);
const expectedRuntimePackages = Object.fromEntries(
RUNTIME_PLATFORMS.map((platform) => [getRuntimePackageName(platform), sourceManifest.version])
);
Expand Down Expand Up @@ -77,11 +80,17 @@ assert.deepEqual(

const mainPackage = packages.get(sourceManifest.name);
assert(mainPackage, `Missing ${sourceManifest.name} tarball`);
assert.deepEqual(
mainPackage.manifest.dependencies,
sourceManifest.dependencies,
"Main package dependencies do not match the source manifest"
);
assert.deepEqual(
mainPackage.manifest.optionalDependencies,
expectedRuntimePackages,
"Main package optional dependencies do not match the platform packages"
);
assertExactProductionDependencies(mainPackage.manifest);
assert(mainPackage.entries.has("package/dist/index.js"), "Main package is missing dist/index.js");
assert(
mainPackage.entries.has("package/dist/cjs/index.js"),
Expand Down
Loading
Loading