Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.

147 changes: 147 additions & 0 deletions nodejs/scripts/dependency-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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;
section: "dependencies" | "optionalDependencies";
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, section, version });
}
}
return dependencies;
}

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()}`
);
}
}
28 changes: 28 additions & 0 deletions nodejs/scripts/verify-dependency-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
assertExactProductionDependencies,
assertMinimumPublicationAge,
loadNpmPublicationTimes,
readProductionDependencyManifest,
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 dependencies = assertExactProductionDependencies(manifest);
const publicationTimes = await loadNpmPublicationTimes(dependencies);
assertMinimumPublicationAge(dependencies, publicationTimes);
const externalCount = dependencies.filter((dependency) =>
requiresPublicationCooldown(dependency.name)
).length;
console.log(
`Verified ${dependencies.length} exact production dependencies; ${externalCount} 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