@mertushka/webrtc-node 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +143 -0
- package/LICENSE +373 -0
- package/README.md +166 -0
- package/docs/README.md +14 -0
- package/docs/architecture.md +38 -0
- package/docs/conformance.md +53 -0
- package/docs/development.md +161 -0
- package/docs/divergences.md +230 -0
- package/examples/datachannel.js +57 -0
- package/index.d.ts +340 -0
- package/lib/index.js +4139 -0
- package/lib/load-native.js +42 -0
- package/package.json +94 -0
- package/scripts/check-api-surface.js +149 -0
- package/scripts/check-ci-evidence.js +124 -0
- package/scripts/check-native-integration.js +124 -0
- package/scripts/check-package-artifact.js +91 -0
- package/scripts/check-prebuilds.js +31 -0
- package/scripts/check-wpt-results.js +117 -0
- package/scripts/check-wpt-selection.js +72 -0
- package/scripts/ensure-wpt.js +118 -0
- package/scripts/install-native.js +116 -0
- package/scripts/package-prebuild.js +69 -0
- package/scripts/print-wpt-manifest.js +7 -0
- package/scripts/run-docker-linux-ci.ps1 +73 -0
- package/scripts/run-docker-linux-ci.sh +97 -0
- package/scripts/run-wpt-smoke.js +32 -0
- package/scripts/run-wpt-subset.js +1193 -0
- package/scripts/write-ci-evidence.js +118 -0
- package/scripts/write-wpt-report.js +143 -0
- package/src/native/addon.cc +1202 -0
- package/wpt-manifest.json +129 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(__dirname, "..");
|
|
7
|
+
const moduleName = "webrtc_node.node";
|
|
8
|
+
|
|
9
|
+
function existing(file) {
|
|
10
|
+
return fs.existsSync(file) ? file : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function localBuildCandidates() {
|
|
14
|
+
return [
|
|
15
|
+
path.join(root, "build", "Release", moduleName),
|
|
16
|
+
path.join(root, "build", "Debug", moduleName),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadNative() {
|
|
21
|
+
const override = process.env.WEBRTC_NODE_NATIVE_PATH;
|
|
22
|
+
const candidates = [...(override ? [path.resolve(override)] : []), ...localBuildCandidates()];
|
|
23
|
+
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (!existing(candidate)) continue;
|
|
26
|
+
try {
|
|
27
|
+
return require(candidate);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
error.message = `Failed to load native addon at ${candidate}: ${error.message}`;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const searched = candidates.map((candidate) => ` - ${candidate}`).join("\n");
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Native addon not found for ${process.platform}-${process.arch}.\n` +
|
|
37
|
+
`Searched:\n${searched}\n` +
|
|
38
|
+
'Install a package with a matching prebuild or run "npm run build".',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = loadNative();
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mertushka/webrtc-node",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "W3C-style RTCPeerConnection and RTCDataChannel for Node.js, backed by libdatachannel.",
|
|
5
|
+
"author": "mertushka",
|
|
6
|
+
"homepage": "https://github.com/mertushka/webrtc-node#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/mertushka/webrtc-node/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mertushka/webrtc-node.git"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"webrtc",
|
|
19
|
+
"rtcpeerconnection",
|
|
20
|
+
"rtcdatachannel",
|
|
21
|
+
"node",
|
|
22
|
+
"node-api",
|
|
23
|
+
"n-api",
|
|
24
|
+
"libdatachannel",
|
|
25
|
+
"web-platform-tests"
|
|
26
|
+
],
|
|
27
|
+
"main": "lib/index.js",
|
|
28
|
+
"types": "index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./index.d.ts",
|
|
32
|
+
"require": "./lib/index.js",
|
|
33
|
+
"import": "./lib/index.js",
|
|
34
|
+
"default": "./lib/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"docs/README.md",
|
|
40
|
+
"docs/architecture.md",
|
|
41
|
+
"docs/conformance.md",
|
|
42
|
+
"docs/development.md",
|
|
43
|
+
"docs/divergences.md",
|
|
44
|
+
"examples",
|
|
45
|
+
"lib",
|
|
46
|
+
"scripts",
|
|
47
|
+
"src",
|
|
48
|
+
"CMakeLists.txt",
|
|
49
|
+
"index.d.ts",
|
|
50
|
+
"wpt-manifest.json"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "cmake-js compile",
|
|
54
|
+
"check": "biome check .",
|
|
55
|
+
"clean": "cmake-js clean",
|
|
56
|
+
"example:datachannel": "node examples/datachannel.js",
|
|
57
|
+
"format": "biome format --write .",
|
|
58
|
+
"format:check": "biome format .",
|
|
59
|
+
"install": "node scripts/install-native.js",
|
|
60
|
+
"lint": "biome lint .",
|
|
61
|
+
"native:check": "node scripts/check-native-integration.js",
|
|
62
|
+
"pack:check": "node scripts/check-package-artifact.js",
|
|
63
|
+
"prebuild:package": "node scripts/package-prebuild.js",
|
|
64
|
+
"prebuild:check": "node scripts/check-prebuilds.js",
|
|
65
|
+
"api:check": "node scripts/check-api-surface.js",
|
|
66
|
+
"test": "node --test --test-force-exit --test-timeout=30000 test/basic.test.js test/ci-evidence.test.js",
|
|
67
|
+
"types:check": "tsc -p tsconfig.types.json",
|
|
68
|
+
"wpt:ensure": "node scripts/ensure-wpt.js",
|
|
69
|
+
"wpt:smoke": "node scripts/run-wpt-smoke.js",
|
|
70
|
+
"wpt:smoke:check": "node scripts/check-wpt-results.js --fail-on-retries --expected-total 4",
|
|
71
|
+
"wpt:test": "node scripts/run-wpt-subset.js",
|
|
72
|
+
"wpt:check": "node scripts/check-wpt-results.js",
|
|
73
|
+
"wpt:check:strict": "node scripts/check-wpt-results.js --fail-on-retries",
|
|
74
|
+
"wpt:selection:check": "node scripts/check-wpt-selection.js",
|
|
75
|
+
"wpt:report": "node scripts/write-wpt-report.js",
|
|
76
|
+
"wpt:manifest": "node scripts/print-wpt-manifest.js",
|
|
77
|
+
"ci:evidence": "node scripts/write-ci-evidence.js",
|
|
78
|
+
"ci:evidence:check": "node scripts/check-ci-evidence.js"
|
|
79
|
+
},
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"cmake-js": "^8.0.0",
|
|
82
|
+
"detect-libc": "^2.1.2",
|
|
83
|
+
"node-addon-api": "^8.3.1",
|
|
84
|
+
"tar": "^7.5.15"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@biomejs/biome": "2.4.16",
|
|
88
|
+
"typescript": "^6.0.3"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">=20"
|
|
92
|
+
},
|
|
93
|
+
"license": "MPL-2.0"
|
|
94
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const api = require("..");
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(__dirname, "..");
|
|
8
|
+
const declarations = fs.readFileSync(path.join(root, "index.d.ts"), "utf8");
|
|
9
|
+
|
|
10
|
+
function fail(message) {
|
|
11
|
+
console.error(`API surface check failed: ${message}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseDeclaredClasses(source) {
|
|
16
|
+
const classes = new Map();
|
|
17
|
+
const pattern = /^export class (\w+)(?:[^{]*)\{\n([\s\S]*?)^}/gm;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = pattern.exec(source))) {
|
|
20
|
+
const [, name, body] = match;
|
|
21
|
+
const staticMembers = new Set();
|
|
22
|
+
const instanceMembers = new Set();
|
|
23
|
+
let skippingSignature = false;
|
|
24
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
25
|
+
const line = rawLine.trim();
|
|
26
|
+
if (skippingSignature) {
|
|
27
|
+
if (line.endsWith(";")) skippingSignature = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!line) continue;
|
|
31
|
+
if (line.startsWith("constructor(")) {
|
|
32
|
+
if (!line.endsWith(";")) skippingSignature = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (line.endsWith(",")) continue;
|
|
36
|
+
let memberMatch = /^static\s+(\w+)\s*\(/.exec(line);
|
|
37
|
+
if (memberMatch) {
|
|
38
|
+
staticMembers.add(memberMatch[1]);
|
|
39
|
+
if (!line.endsWith(";")) skippingSignature = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
memberMatch = /^(?:readonly\s+)?(\w+)\s*(?:\(|:)/.exec(line);
|
|
43
|
+
if (memberMatch) {
|
|
44
|
+
instanceMembers.add(memberMatch[1]);
|
|
45
|
+
if (line.includes("(") && !line.endsWith(";")) skippingSignature = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
classes.set(name, { staticMembers, instanceMembers });
|
|
49
|
+
}
|
|
50
|
+
return classes;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseNonstandardNamespace(source) {
|
|
54
|
+
const namespaceMatch = /^export namespace nonstandard \{\n([\s\S]*?)^}/m.exec(source);
|
|
55
|
+
if (!namespaceMatch) return new Set();
|
|
56
|
+
const members = new Set();
|
|
57
|
+
for (const rawLine of namespaceMatch[1].split(/\r?\n/)) {
|
|
58
|
+
const line = rawLine.trim();
|
|
59
|
+
const match = /^const\s+(\w+)\s*:/.exec(line);
|
|
60
|
+
if (match) members.add(match[1]);
|
|
61
|
+
}
|
|
62
|
+
return members;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createInstances() {
|
|
66
|
+
const peerConnection = new api.RTCPeerConnection();
|
|
67
|
+
const channel = peerConnection.createDataChannel("surface-check");
|
|
68
|
+
const candidate = new api.RTCIceCandidate({ sdpMid: "0" });
|
|
69
|
+
const candidatePair = Object.assign(Object.create(api.RTCIceCandidatePair.prototype), {
|
|
70
|
+
local: candidate,
|
|
71
|
+
remote: candidate,
|
|
72
|
+
});
|
|
73
|
+
const iceTransport = Object.assign(Object.create(api.RTCIceTransport.prototype), {
|
|
74
|
+
onstatechange: null,
|
|
75
|
+
ongatheringstatechange: null,
|
|
76
|
+
onselectedcandidatepairchange: null,
|
|
77
|
+
});
|
|
78
|
+
const dtlsTransport = Object.assign(Object.create(api.RTCDtlsTransport.prototype), {
|
|
79
|
+
iceTransport,
|
|
80
|
+
onstatechange: null,
|
|
81
|
+
onerror: null,
|
|
82
|
+
});
|
|
83
|
+
const sctpTransport = Object.assign(Object.create(api.RTCSctpTransport.prototype), {
|
|
84
|
+
onstatechange: null,
|
|
85
|
+
});
|
|
86
|
+
const instances = {
|
|
87
|
+
Event: new api.Event("surface-check"),
|
|
88
|
+
MessageEvent: new api.MessageEvent("message", { data: "surface-check" }),
|
|
89
|
+
EventTarget: new api.EventTarget(),
|
|
90
|
+
RTCSessionDescription: new api.RTCSessionDescription({ type: "offer", sdp: "v=0\r\n" }),
|
|
91
|
+
RTCIceCandidate: candidate,
|
|
92
|
+
RTCIceCandidatePair: candidatePair,
|
|
93
|
+
RTCCertificate: new api.RTCCertificate(),
|
|
94
|
+
RTCDataChannel: channel,
|
|
95
|
+
RTCDataChannelEvent: new api.RTCDataChannelEvent("datachannel", { channel }),
|
|
96
|
+
RTCPeerConnectionIceEvent: new api.RTCPeerConnectionIceEvent("icecandidate"),
|
|
97
|
+
RTCPeerConnectionIceErrorEvent: new api.RTCPeerConnectionIceErrorEvent("icecandidateerror"),
|
|
98
|
+
RTCError: new api.RTCError({ errorDetail: "data-channel-failure" }),
|
|
99
|
+
RTCErrorEvent: new api.RTCErrorEvent("error"),
|
|
100
|
+
RTCDtlsTransport: dtlsTransport,
|
|
101
|
+
RTCIceTransport: iceTransport,
|
|
102
|
+
RTCSctpTransport: sctpTransport,
|
|
103
|
+
RTCPeerConnection: peerConnection,
|
|
104
|
+
};
|
|
105
|
+
return { instances, cleanup: () => peerConnection.close() };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const declaredClasses = parseDeclaredClasses(declarations);
|
|
109
|
+
const declaredNonstandardMembers = parseNonstandardNamespace(declarations);
|
|
110
|
+
const declaredExports = new Set([...declaredClasses.keys(), "nonstandard"]);
|
|
111
|
+
const runtimeExports = new Set(Object.keys(api));
|
|
112
|
+
|
|
113
|
+
for (const name of declaredExports) {
|
|
114
|
+
if (!runtimeExports.has(name)) fail(`declared export ${name} is missing at runtime`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const name of runtimeExports) {
|
|
118
|
+
if (!declaredExports.has(name)) fail(`runtime export ${name} is missing from index.d.ts`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { instances, cleanup } = createInstances();
|
|
122
|
+
try {
|
|
123
|
+
for (const [className, members] of declaredClasses) {
|
|
124
|
+
const ctor = api[className];
|
|
125
|
+
if (typeof ctor !== "function") fail(`${className} is not a constructor at runtime`);
|
|
126
|
+
for (const member of members.staticMembers) {
|
|
127
|
+
if (!(member in ctor)) fail(`${className}.${member} is declared but missing at runtime`);
|
|
128
|
+
}
|
|
129
|
+
const instance = instances[className];
|
|
130
|
+
if (!instance) fail(`no runtime sample for ${className}`);
|
|
131
|
+
for (const member of members.instanceMembers) {
|
|
132
|
+
if (!(member in instance)) fail(`${className}#${member} is declared but missing at runtime`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof api.nonstandard !== "object" || api.nonstandard === null) {
|
|
137
|
+
fail("nonstandard namespace is missing at runtime");
|
|
138
|
+
}
|
|
139
|
+
for (const member of declaredNonstandardMembers) {
|
|
140
|
+
if (!(member in api.nonstandard))
|
|
141
|
+
fail(`nonstandard.${member} is declared but missing at runtime`);
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
cleanup();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(
|
|
148
|
+
`API surface verified: ${declaredClasses.size} classes, ${declaredNonstandardMembers.size} nonstandard members`,
|
|
149
|
+
);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(__dirname, "..");
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const artifactsIndex = args.indexOf("--artifacts");
|
|
9
|
+
const artifactsRoot =
|
|
10
|
+
artifactsIndex === -1
|
|
11
|
+
? path.join(root, "ci-artifacts")
|
|
12
|
+
: path.resolve(root, args[artifactsIndex + 1] || "");
|
|
13
|
+
const manifestPath = path.join(root, "wpt-manifest.json");
|
|
14
|
+
const requiredOs = ["Linux", "macOS", "Windows"];
|
|
15
|
+
const requiredNodeMajors = [20, 22, 24];
|
|
16
|
+
|
|
17
|
+
function fail(message) {
|
|
18
|
+
console.error(`CI evidence check failed: ${message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readJson(file) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
fail(`could not read ${file}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function walk(dir, matches = []) {
|
|
31
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
const fullPath = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) walk(fullPath, matches);
|
|
34
|
+
else if (entry.isFile() && entry.name === "ci-evidence.json") matches.push(fullPath);
|
|
35
|
+
}
|
|
36
|
+
return matches;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nodeMajor(version) {
|
|
40
|
+
const match = /^v?(\d+)\./.exec(String(version || ""));
|
|
41
|
+
return match ? Number(match[1]) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (artifactsIndex !== -1 && !args[artifactsIndex + 1]) fail("--artifacts requires a directory");
|
|
45
|
+
if (!fs.existsSync(artifactsRoot)) {
|
|
46
|
+
fail(`${artifactsRoot} does not exist; download CI artifacts there or pass --artifacts <dir>`);
|
|
47
|
+
}
|
|
48
|
+
if (!fs.statSync(artifactsRoot).isDirectory()) fail(`${artifactsRoot} is not a directory`);
|
|
49
|
+
if (!fs.existsSync(manifestPath)) fail(`${manifestPath} does not exist`);
|
|
50
|
+
|
|
51
|
+
const manifest = readJson(manifestPath);
|
|
52
|
+
const evidenceFiles = walk(artifactsRoot);
|
|
53
|
+
if (!evidenceFiles.length) fail(`no ci-evidence.json files found under ${artifactsRoot}`);
|
|
54
|
+
|
|
55
|
+
const byMatrix = new Map();
|
|
56
|
+
|
|
57
|
+
for (const evidencePath of evidenceFiles) {
|
|
58
|
+
const evidence = readJson(evidencePath);
|
|
59
|
+
const dir = path.dirname(evidencePath);
|
|
60
|
+
const os = evidence.runner?.os;
|
|
61
|
+
const major = nodeMajor(evidence.node?.version);
|
|
62
|
+
const key = `${os}|${major}`;
|
|
63
|
+
|
|
64
|
+
if (!requiredOs.includes(os) || !requiredNodeMajors.includes(major)) continue;
|
|
65
|
+
if (byMatrix.has(key)) fail(`duplicate evidence for ${os} Node ${major}`);
|
|
66
|
+
|
|
67
|
+
const resultsPath = path.join(dir, "wpt-results.json");
|
|
68
|
+
const reportPath = path.join(dir, "wpt-report.md");
|
|
69
|
+
const artifactManifestPath = path.join(dir, "wpt-manifest.json");
|
|
70
|
+
const manifestTextPath = path.join(dir, "wpt-manifest.txt");
|
|
71
|
+
|
|
72
|
+
for (const requiredPath of [resultsPath, reportPath, artifactManifestPath, manifestTextPath]) {
|
|
73
|
+
if (!fs.existsSync(requiredPath)) fail(`${path.relative(root, requiredPath)} is missing`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const artifactManifest = readJson(artifactManifestPath);
|
|
77
|
+
const results = readJson(resultsPath);
|
|
78
|
+
const retries = Array.isArray(results.results)
|
|
79
|
+
? results.results.filter((result) => Number(result.retries) > 0).length
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
if (artifactManifest.libdatachannelCommit !== manifest.libdatachannelCommit) {
|
|
83
|
+
fail(`${key} libdatachannel pin mismatch`);
|
|
84
|
+
}
|
|
85
|
+
if (artifactManifest.wptCommit !== manifest.wptCommit) fail(`${key} WPT pin mismatch`);
|
|
86
|
+
if (artifactManifest.expectedSelectedSubtests !== manifest.expectedSelectedSubtests) {
|
|
87
|
+
fail(`${key} selected subtest count mismatch`);
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(results.results)) fail(`${key} WPT result artifact is invalid`);
|
|
90
|
+
if (results.results.length !== results.total) fail(`${key} result length mismatch`);
|
|
91
|
+
if (results.total !== manifest.expectedSelectedSubtests) fail(`${key} WPT total mismatch`);
|
|
92
|
+
if (results.pass !== results.total || results.fail !== 0 || retries !== 0) {
|
|
93
|
+
fail(
|
|
94
|
+
`${key} WPT is not strict-green: pass=${results.pass} total=${results.total} fail=${results.fail} retries=${retries}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (evidence.pins?.libdatachannel !== manifest.libdatachannelCommit) {
|
|
98
|
+
fail(`${key} evidence libdatachannel pin mismatch`);
|
|
99
|
+
}
|
|
100
|
+
if (evidence.pins?.wpt !== manifest.wptCommit) fail(`${key} evidence WPT pin mismatch`);
|
|
101
|
+
if (
|
|
102
|
+
evidence.wpt?.total !== manifest.expectedSelectedSubtests ||
|
|
103
|
+
evidence.wpt?.pass !== manifest.expectedSelectedSubtests ||
|
|
104
|
+
evidence.wpt?.fail !== 0 ||
|
|
105
|
+
evidence.wpt?.retries !== 0
|
|
106
|
+
) {
|
|
107
|
+
fail(`${key} evidence WPT summary is not strict-green`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
byMatrix.set(key, { os, major, evidencePath });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const missing = [];
|
|
114
|
+
for (const os of requiredOs) {
|
|
115
|
+
for (const major of requiredNodeMajors) {
|
|
116
|
+
if (!byMatrix.has(`${os}|${major}`)) missing.push(`${os} Node ${major}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (missing.length) fail(`missing matrix evidence: ${missing.join(", ")}`);
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`CI evidence verified: ${byMatrix.size}/${requiredOs.length * requiredNodeMajors.length} matrix jobs strict-green`,
|
|
124
|
+
);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(__dirname, "..");
|
|
8
|
+
const addonPath = path.join(root, "src", "native", "addon.cc");
|
|
9
|
+
const cmakePath = path.join(root, "CMakeLists.txt");
|
|
10
|
+
const manifestPath = path.join(root, "wpt-manifest.json");
|
|
11
|
+
const packagePath = path.join(root, "package.json");
|
|
12
|
+
|
|
13
|
+
const addon = fs.readFileSync(addonPath, "utf8");
|
|
14
|
+
const cmake = fs.readFileSync(cmakePath, "utf8");
|
|
15
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
17
|
+
|
|
18
|
+
function fail(message) {
|
|
19
|
+
console.error(`Native integration check failed: ${message}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireMatch(name, value, pattern) {
|
|
24
|
+
if (!pattern.test(value)) fail(`${name} is missing`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function forbidMatch(name, value, pattern) {
|
|
28
|
+
const match = pattern.exec(value);
|
|
29
|
+
if (match) fail(`${name} is forbidden: ${match[0]}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
requireMatch(
|
|
33
|
+
"node-addon-api dependency",
|
|
34
|
+
JSON.stringify(pkg.dependencies || {}),
|
|
35
|
+
/"node-addon-api"\s*:/,
|
|
36
|
+
);
|
|
37
|
+
requireMatch(
|
|
38
|
+
"detect-libc runtime dependency",
|
|
39
|
+
JSON.stringify(pkg.dependencies || {}),
|
|
40
|
+
/"detect-libc"\s*:/,
|
|
41
|
+
);
|
|
42
|
+
requireMatch(
|
|
43
|
+
"cmake-js source-build dependency",
|
|
44
|
+
JSON.stringify(pkg.dependencies || {}),
|
|
45
|
+
/"cmake-js"\s*:/,
|
|
46
|
+
);
|
|
47
|
+
requireMatch("native install script", JSON.stringify(pkg.scripts || {}), /install-native\.js/);
|
|
48
|
+
requireMatch("prebuild package script", JSON.stringify(pkg.scripts || {}), /package-prebuild\.js/);
|
|
49
|
+
requireMatch("prebuild check script", JSON.stringify(pkg.scripts || {}), /check-prebuilds\.js/);
|
|
50
|
+
requireMatch("native <napi.h> include", addon, /#include\s+<napi\.h>/);
|
|
51
|
+
requireMatch("Node-API module initializer", addon, /\bNODE_API_MODULE\s*\(/);
|
|
52
|
+
requireMatch("ThreadSafeFunction dispatcher", addon, /Napi::ThreadSafeFunction::New/);
|
|
53
|
+
requireMatch("nonblocking callback dispatch", addon, /\.NonBlockingCall\s*\(/);
|
|
54
|
+
requireMatch("dispatcher release on close", addon, /\.Release\s*\(/);
|
|
55
|
+
requireMatch("weak callback captures", addon, /\[weak\]/);
|
|
56
|
+
requireMatch("peer callback reset", addon, /peerConnection->resetCallbacks\s*\(\)/);
|
|
57
|
+
requireMatch("channel callback reset", addon, /->resetCallbacks\s*\(\)/);
|
|
58
|
+
|
|
59
|
+
forbidMatch("direct V8 namespace usage", addon, /\bv8::/);
|
|
60
|
+
forbidMatch("direct V8 include", addon, /#include\s+[<"]v8(?:-[^>"]+)?\.h[>"]/);
|
|
61
|
+
forbidMatch("direct Node addon include", addon, /#include\s+[<"]node(?:_object_wrap)?\.h[>"]/);
|
|
62
|
+
forbidMatch("NAN include", addon, /#include\s+[<"]nan\.h[>"]/);
|
|
63
|
+
forbidMatch("NAN namespace usage", addon, /\bNan::/);
|
|
64
|
+
forbidMatch("non-Node-API module initializer", addon, /\bNODE_MODULE\s*\(/);
|
|
65
|
+
|
|
66
|
+
const callbackCallMatches = [...addon.matchAll(/\bcallback\.Call\s*\(/g)];
|
|
67
|
+
if (callbackCallMatches.length !== 1) {
|
|
68
|
+
fail(
|
|
69
|
+
`expected exactly one callback.Call site inside EventDispatcher::Dispatch, found ${callbackCallMatches.length}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cmakePinMatch = /set\s*\(\s*LIBDATACHANNEL_PINNED_COMMIT\s+"([0-9a-f]{40})"/i.exec(cmake);
|
|
74
|
+
if (!cmakePinMatch) fail("CMake libdatachannel pin is missing");
|
|
75
|
+
const cmakePin = cmakePinMatch[1];
|
|
76
|
+
if (cmakePin !== manifest.libdatachannelCommit) {
|
|
77
|
+
fail(
|
|
78
|
+
`CMake libdatachannel pin ${cmakePin} does not match manifest ${manifest.libdatachannelCommit}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
requireMatch(
|
|
83
|
+
"FetchContent libdatachannel fallback",
|
|
84
|
+
cmake,
|
|
85
|
+
/FetchContent_Declare\s*\(\s*libdatachannel_pinned[\s\S]*GIT_TAG\s+"\$\{LIBDATACHANNEL_PINNED_COMMIT\}"/,
|
|
86
|
+
);
|
|
87
|
+
requireMatch(
|
|
88
|
+
"local checkout pin verification",
|
|
89
|
+
cmake,
|
|
90
|
+
/verify_libdatachannel_pin\s*\(\s*"\$\{LIBDATACHANNEL_RESOLVED_SOURCE_DIR\}"\s*\)/,
|
|
91
|
+
);
|
|
92
|
+
requireMatch(
|
|
93
|
+
"NO_MEDIA scoped build",
|
|
94
|
+
cmake,
|
|
95
|
+
/set\s*\(\s*NO_MEDIA\s+ON\s+CACHE\s+BOOL\s+""\s+FORCE\s*\)/,
|
|
96
|
+
);
|
|
97
|
+
requireMatch(
|
|
98
|
+
"NO_WEBSOCKET scoped build",
|
|
99
|
+
cmake,
|
|
100
|
+
/set\s*\(\s*NO_WEBSOCKET\s+ON\s+CACHE\s+BOOL\s+""\s+FORCE\s*\)/,
|
|
101
|
+
);
|
|
102
|
+
requireMatch("Node-API version definition", cmake, /NAPI_VERSION=\$\{WEBRTC_NODE_NAPI_VERSION\}/);
|
|
103
|
+
requireMatch("prebuild napi build version", cmake, /napi_build_version/);
|
|
104
|
+
requireMatch("release static OpenSSL option", cmake, /WEBRTC_NODE_STATIC_OPENSSL/);
|
|
105
|
+
requireMatch("node-addon-api include discovery", cmake, /require\('node-addon-api'\)\.include_dir/);
|
|
106
|
+
requireMatch("static libdatachannel target", cmake, /LibDataChannel::LibDataChannelStatic/);
|
|
107
|
+
|
|
108
|
+
const localLibDataChannel = path.join(root, "libdatachannel");
|
|
109
|
+
if (fs.existsSync(path.join(localLibDataChannel, ".git"))) {
|
|
110
|
+
const git = spawnSync("git", ["-C", localLibDataChannel, "rev-parse", "HEAD"], {
|
|
111
|
+
cwd: root,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
});
|
|
114
|
+
if (git.status !== 0)
|
|
115
|
+
fail(`could not read local libdatachannel commit: ${git.stderr || git.error?.message}`);
|
|
116
|
+
const actual = git.stdout.trim();
|
|
117
|
+
if (actual !== cmakePin) {
|
|
118
|
+
fail(`local libdatachannel checkout is ${actual}, expected ${cmakePin}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`Native integration verified: Node-API addon, TSFN dispatch, libdatachannel ${cmakePin}`,
|
|
124
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
|
|
5
|
+
function fail(message) {
|
|
6
|
+
console.error(`Package artifact check failed: ${message}`);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const npmCommand = process.env.npm_execpath ? process.execPath : "npm";
|
|
11
|
+
const npmArgs = process.env.npm_execpath
|
|
12
|
+
? [process.env.npm_execpath, "pack", "--dry-run", "--json"]
|
|
13
|
+
: ["pack", "--dry-run", "--json"];
|
|
14
|
+
const result = spawnSync(npmCommand, npmArgs, {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (result.error) fail(result.error.message);
|
|
20
|
+
if (result.status !== 0 || result.signal) {
|
|
21
|
+
fail(result.stderr.trim() || result.stdout.trim() || `npm pack exited with ${result.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let payload;
|
|
25
|
+
try {
|
|
26
|
+
payload = JSON.parse(result.stdout);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
fail(`could not parse npm pack JSON: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const artifact = payload?.[0];
|
|
32
|
+
if (!artifact || !Array.isArray(artifact.files)) fail("npm pack did not return file metadata");
|
|
33
|
+
|
|
34
|
+
const files = new Set(artifact.files.map((file) => file.path.replace(/\\/g, "/")));
|
|
35
|
+
const requiredFiles = [
|
|
36
|
+
"package.json",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CMakeLists.txt",
|
|
40
|
+
"index.d.ts",
|
|
41
|
+
"lib/index.js",
|
|
42
|
+
"lib/load-native.js",
|
|
43
|
+
"src/native/addon.cc",
|
|
44
|
+
"scripts/check-prebuilds.js",
|
|
45
|
+
"scripts/install-native.js",
|
|
46
|
+
"scripts/package-prebuild.js",
|
|
47
|
+
"scripts/run-wpt-smoke.js",
|
|
48
|
+
"examples/datachannel.js",
|
|
49
|
+
"docs/README.md",
|
|
50
|
+
"docs/architecture.md",
|
|
51
|
+
"docs/conformance.md",
|
|
52
|
+
"docs/development.md",
|
|
53
|
+
"docs/divergences.md",
|
|
54
|
+
"wpt-manifest.json",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const file of requiredFiles) {
|
|
58
|
+
if (!files.has(file)) fail(`missing required file ${file}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const forbiddenPrefixes = [
|
|
62
|
+
".github/",
|
|
63
|
+
"build/",
|
|
64
|
+
"ci-artifacts/",
|
|
65
|
+
"coverage/",
|
|
66
|
+
"libdatachannel/",
|
|
67
|
+
"node_modules/",
|
|
68
|
+
"prebuild-artifacts/",
|
|
69
|
+
"prebuilds/",
|
|
70
|
+
"test/",
|
|
71
|
+
"wpt/",
|
|
72
|
+
];
|
|
73
|
+
const forbiddenFiles = new Set([
|
|
74
|
+
"AGENTS.md",
|
|
75
|
+
"CONTRIBUTING.md",
|
|
76
|
+
"SECURITY.md",
|
|
77
|
+
"ci-evidence.json",
|
|
78
|
+
"wpt-results.json",
|
|
79
|
+
"wpt-report.md",
|
|
80
|
+
"wpt-manifest.txt",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
if (forbiddenFiles.has(file)) fail(`forbidden file included: ${file}`);
|
|
85
|
+
const forbiddenPrefix = forbiddenPrefixes.find((prefix) => file.startsWith(prefix));
|
|
86
|
+
if (forbiddenPrefix) fail(`forbidden path included: ${file}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
`Package artifact verified: ${artifact.files.length} files, ${artifact.unpackedSize} bytes unpacked`,
|
|
91
|
+
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(__dirname, "..");
|
|
7
|
+
const packageJson = require("../package.json");
|
|
8
|
+
const prebuildsDir = path.join(root, "prebuild-artifacts");
|
|
9
|
+
const requiredTargets = [
|
|
10
|
+
"linux-x64-glibc",
|
|
11
|
+
"linux-x64-musl",
|
|
12
|
+
"darwin-x64",
|
|
13
|
+
"darwin-arm64",
|
|
14
|
+
"win32-x64",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function fail(message) {
|
|
18
|
+
console.error(`Prebuild check failed: ${message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const target of requiredTargets) {
|
|
23
|
+
const file = path.join(
|
|
24
|
+
prebuildsDir,
|
|
25
|
+
`webrtc-node-v${packageJson.version}-napi-v8-${target}.tar.gz`,
|
|
26
|
+
);
|
|
27
|
+
if (!fs.existsSync(file)) fail(`missing ${path.relative(root, file).replace(/\\/g, "/")}`);
|
|
28
|
+
if (fs.statSync(file).size === 0) fail(`empty ${path.relative(root, file).replace(/\\/g, "/")}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`Prebuild release assets verified: ${requiredTargets.join(", ")}`);
|