lastgen 1.2.0 → 1.3.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/bin/lastgen +17 -0
- package/dist/cli.mjs +672 -0
- package/package.json +7 -4
- package/src/cli.ts +0 -179
- package/src/core/github.ts +0 -177
- package/src/core/proof.ts +0 -111
- package/src/core/types.ts +0 -89
- package/src/core/verify.ts +0 -213
- package/src/display.ts +0 -142
- package/src/hash.ts +0 -11
- package/src/index.ts +0 -12
- package/src/serve.ts +0 -77
- package/src/verify-cli.ts +0 -89
package/bin/lastgen
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const { run } = await import(join(__dirname, '..', 'dist', 'cli.mjs'));
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await run(process.argv.slice(2));
|
|
13
|
+
} catch (err) {
|
|
14
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parseArgs, styleText } from "node:util";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { dirname, extname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region src/core/types.ts
|
|
9
|
+
/**
|
|
10
|
+
* @fileoverview Shared interfaces, constants, and type definitions for lastgen.
|
|
11
|
+
*/
|
|
12
|
+
const CUTOFF_DATE = "2025-02-21T00:00:00Z";
|
|
13
|
+
const ERAS = {
|
|
14
|
+
LAST_GEN: {
|
|
15
|
+
title: "Last Generation Coder",
|
|
16
|
+
description: "Wrote code before AI agents shipped"
|
|
17
|
+
},
|
|
18
|
+
AI_NATIVE: {
|
|
19
|
+
title: "AI Native Coder",
|
|
20
|
+
description: "First verifiable commit after AI agents shipped"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const CERTIFICATE_SALT = "lastgen_v1";
|
|
24
|
+
const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/core/github.ts
|
|
27
|
+
const GITHUB_API = "https://api.github.com";
|
|
28
|
+
const USER_AGENT = "lastgen";
|
|
29
|
+
function buildHeaders(token) {
|
|
30
|
+
const headers = {
|
|
31
|
+
Accept: "application/vnd.github.v3+json",
|
|
32
|
+
"User-Agent": USER_AGENT
|
|
33
|
+
};
|
|
34
|
+
if (token) headers["Authorization"] = `token ${token}`;
|
|
35
|
+
return headers;
|
|
36
|
+
}
|
|
37
|
+
async function githubFetch(url, token, extraHeaders) {
|
|
38
|
+
const response = await fetch(url, { headers: {
|
|
39
|
+
...buildHeaders(token),
|
|
40
|
+
...extraHeaders
|
|
41
|
+
} });
|
|
42
|
+
if (response.status === 403) {
|
|
43
|
+
if (response.headers.get("x-ratelimit-remaining") === "0") {
|
|
44
|
+
const resetTimestamp = response.headers.get("x-ratelimit-reset");
|
|
45
|
+
const resetDate = resetTimestamp ? (/* @__PURE__ */ new Date(Number(resetTimestamp) * 1e3)).toLocaleTimeString() : "soon";
|
|
46
|
+
throw new Error(`GitHub API rate limit exceeded. Resets at ${resetDate}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (response.status === 404) {
|
|
50
|
+
const userMatch = url.match(/\/users\/([^/?]+)/);
|
|
51
|
+
if (userMatch?.[1]) throw new Error(`GitHub user '${decodeURIComponent(userMatch[1])}' not found. Check the spelling?`);
|
|
52
|
+
throw new Error(`Not found: ${url}`);
|
|
53
|
+
}
|
|
54
|
+
if (!response.ok) throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
55
|
+
return response;
|
|
56
|
+
}
|
|
57
|
+
async function fetchUser(username, token) {
|
|
58
|
+
const data = await (await githubFetch(`${GITHUB_API}/users/${encodeURIComponent(username)}`, token)).json();
|
|
59
|
+
return {
|
|
60
|
+
login: data.login,
|
|
61
|
+
id: data.id,
|
|
62
|
+
name: data.name ?? null,
|
|
63
|
+
createdAt: data.created_at
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function fetchFirstCommit(username, token) {
|
|
67
|
+
const commit = await searchFirstCommit(username, token);
|
|
68
|
+
if (commit?.repo) commit.repoCreatedAt = await fetchRepoCreatedAt(commit.repo, token);
|
|
69
|
+
return commit;
|
|
70
|
+
}
|
|
71
|
+
async function fetchRepoCreatedAt(repoFullName, token) {
|
|
72
|
+
try {
|
|
73
|
+
return (await (await githubFetch(`${GITHUB_API}/repos/${repoFullName}`, token)).json()).created_at ?? void 0;
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function searchFirstCommitByQuery(query, token, order = "asc") {
|
|
79
|
+
try {
|
|
80
|
+
const item = (await (await githubFetch(`${GITHUB_API}/search/commits?q=${encodeURIComponent(query)}&sort=committer-date&order=${order}&per_page=1`, token, { Accept: "application/vnd.github.cloak-preview+json" })).json()).items?.[0];
|
|
81
|
+
if (!item) return null;
|
|
82
|
+
const commit = item.commit;
|
|
83
|
+
const author = commit.author;
|
|
84
|
+
const commitCommitter = commit.committer;
|
|
85
|
+
const repo = item.repository;
|
|
86
|
+
return {
|
|
87
|
+
date: author.date,
|
|
88
|
+
repo: repo.full_name ?? "",
|
|
89
|
+
sha: item.sha,
|
|
90
|
+
message: (commit.message ?? "").split("\n")[0] ?? "",
|
|
91
|
+
committerDate: commitCommitter?.date ?? void 0
|
|
92
|
+
};
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof Error && err.message.includes("rate limit")) throw err;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function searchFirstCommit(username, token) {
|
|
99
|
+
const cutoffDate = CUTOFF_DATE.slice(0, 10);
|
|
100
|
+
return await searchFirstCommitByQuery(`author:${username} user:${username} committer-date:<${cutoffDate}`, token, "desc") ?? await searchFirstCommitByQuery(`author:${username} committer-date:<${cutoffDate}`, token, "desc") ?? await searchFirstCommitByQuery(`author:${username}`, token);
|
|
101
|
+
}
|
|
102
|
+
async function fetchCommit(repoFullName, sha, token) {
|
|
103
|
+
const data = await (await githubFetch(`${GITHUB_API}/repos/${repoFullName}/commits/${sha}`, token)).json();
|
|
104
|
+
const commit = data.commit;
|
|
105
|
+
const commitAuthor = commit.author;
|
|
106
|
+
const commitCommitter = commit.committer;
|
|
107
|
+
const verification = commit.verification;
|
|
108
|
+
const author = data.author;
|
|
109
|
+
const committer = data.committer;
|
|
110
|
+
const parents = data.parents;
|
|
111
|
+
return {
|
|
112
|
+
sha: data.sha,
|
|
113
|
+
authorLogin: author?.login ?? null,
|
|
114
|
+
committerLogin: committer?.login ?? null,
|
|
115
|
+
authorEmail: commitAuthor.email ?? null,
|
|
116
|
+
authorDate: commitAuthor.date ?? null,
|
|
117
|
+
committerDate: commitCommitter?.date ?? null,
|
|
118
|
+
authorId: author?.id ?? null,
|
|
119
|
+
verificationReason: verification?.reason ?? null,
|
|
120
|
+
isRootCommit: Array.isArray(parents) && parents.length === 0,
|
|
121
|
+
message: (commit.message ?? "").split("\n")[0] ?? "",
|
|
122
|
+
verified: Boolean(verification?.verified)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/core/proof.ts
|
|
127
|
+
function classifyEra(proofDate) {
|
|
128
|
+
const cutoff = new Date(CUTOFF_DATE).getTime();
|
|
129
|
+
return new Date(proofDate).getTime() < cutoff ? "LAST_GEN" : "AI_NATIVE";
|
|
130
|
+
}
|
|
131
|
+
function resolveProofDate(user, firstCommit) {
|
|
132
|
+
if (!firstCommit) return (/* @__PURE__ */ new Date()).toISOString();
|
|
133
|
+
const effectiveDate = getEffectiveCommitDate(firstCommit);
|
|
134
|
+
const commitTime = new Date(effectiveDate).getTime();
|
|
135
|
+
const accountTime = new Date(user.createdAt).getTime();
|
|
136
|
+
if (commitTime < (firstCommit.repoCreatedAt ? new Date(firstCommit.repoCreatedAt).getTime() : Infinity)) return firstCommit.repoCreatedAt ?? user.createdAt;
|
|
137
|
+
return commitTime < accountTime ? effectiveDate : user.createdAt;
|
|
138
|
+
}
|
|
139
|
+
function getEffectiveCommitDate(commit) {
|
|
140
|
+
if (!commit.committerDate) return commit.date;
|
|
141
|
+
const authorTime = new Date(commit.date).getTime();
|
|
142
|
+
if (new Date(commit.committerDate).getTime() - authorTime > 2592e6) return commit.committerDate;
|
|
143
|
+
return commit.date;
|
|
144
|
+
}
|
|
145
|
+
async function generateCertificateHash(hashFn, username, githubId, proofDate, era) {
|
|
146
|
+
return hashFn(JSON.stringify({
|
|
147
|
+
username,
|
|
148
|
+
githubId,
|
|
149
|
+
proofDate,
|
|
150
|
+
era,
|
|
151
|
+
salt: CERTIFICATE_SALT
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
function generateCertificateNumber(hash) {
|
|
155
|
+
const prefix = hash.slice(0, 4).toUpperCase();
|
|
156
|
+
const numericPart = parseInt(hash.slice(4, 12), 16) % 1e6;
|
|
157
|
+
return `LGC-${prefix}-${String(numericPart).padStart(6, "0")}`;
|
|
158
|
+
}
|
|
159
|
+
async function createCertificate(hashFn, user, firstCommit) {
|
|
160
|
+
const proofDate = resolveProofDate(user, firstCommit);
|
|
161
|
+
const era = classifyEra(proofDate);
|
|
162
|
+
const hash = await generateCertificateHash(hashFn, user.login, user.id, proofDate, era);
|
|
163
|
+
const certificateNumber = generateCertificateNumber(hash);
|
|
164
|
+
return {
|
|
165
|
+
version: "1.0",
|
|
166
|
+
type: "LASTGEN_CERTIFICATE",
|
|
167
|
+
identity: {
|
|
168
|
+
username: user.login,
|
|
169
|
+
githubId: user.id,
|
|
170
|
+
name: user.name
|
|
171
|
+
},
|
|
172
|
+
proof: {
|
|
173
|
+
accountCreated: user.createdAt,
|
|
174
|
+
firstCommit: firstCommit ?? {
|
|
175
|
+
date: user.createdAt,
|
|
176
|
+
repo: "",
|
|
177
|
+
sha: "",
|
|
178
|
+
message: "(no public commits found - using account creation date)"
|
|
179
|
+
},
|
|
180
|
+
proofDate
|
|
181
|
+
},
|
|
182
|
+
era,
|
|
183
|
+
verification: {
|
|
184
|
+
hash: `sha256:${hash}`,
|
|
185
|
+
salt: CERTIFICATE_SALT
|
|
186
|
+
},
|
|
187
|
+
certificateNumber,
|
|
188
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/hash.ts
|
|
193
|
+
/**
|
|
194
|
+
* @fileoverview Node.js SHA-256 hash implementation using node:crypto.
|
|
195
|
+
*/
|
|
196
|
+
const nodeHash = async (data) => {
|
|
197
|
+
return createHash("sha256").update(data).digest("hex");
|
|
198
|
+
};
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/display.ts
|
|
201
|
+
/**
|
|
202
|
+
* @fileoverview Terminal output formatting with ASCII box-drawing for certificates and verification.
|
|
203
|
+
*/
|
|
204
|
+
function shouldUseColor() {
|
|
205
|
+
if (process.env["NO_COLOR"] !== void 0) return false;
|
|
206
|
+
if (process.env["TERM"] === "dumb") return false;
|
|
207
|
+
if (process.argv.includes("--no-color")) return false;
|
|
208
|
+
return process.stdout.isTTY === true;
|
|
209
|
+
}
|
|
210
|
+
function style(format, text) {
|
|
211
|
+
if (!shouldUseColor()) return text;
|
|
212
|
+
return styleText(format, text);
|
|
213
|
+
}
|
|
214
|
+
function boxRule() {
|
|
215
|
+
return style("dim", "+" + "-".repeat(52) + "+");
|
|
216
|
+
}
|
|
217
|
+
function boxLine(content, rawLength) {
|
|
218
|
+
const pad = 50 - rawLength;
|
|
219
|
+
return style("dim", "|") + " " + content + " ".repeat(Math.max(pad, 0)) + " " + style("dim", "|");
|
|
220
|
+
}
|
|
221
|
+
function boxEmpty() {
|
|
222
|
+
return boxLine("", 0);
|
|
223
|
+
}
|
|
224
|
+
const LABEL_WIDTH = 13;
|
|
225
|
+
function labelLine(label, value, styledValue, lines) {
|
|
226
|
+
const max = 50 - LABEL_WIDTH;
|
|
227
|
+
if (value.length <= max) {
|
|
228
|
+
lines.push(boxLine(style("dim", label) + styledValue, LABEL_WIDTH + value.length));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const indent = " ".repeat(LABEL_WIDTH);
|
|
232
|
+
let remaining = value;
|
|
233
|
+
let isFirst = true;
|
|
234
|
+
while (remaining.length > 0) {
|
|
235
|
+
const chunk = remaining.slice(0, max);
|
|
236
|
+
remaining = remaining.slice(max);
|
|
237
|
+
const prefix = isFirst ? style("dim", label) : indent;
|
|
238
|
+
lines.push(boxLine(prefix + chunk, LABEL_WIDTH + chunk.length));
|
|
239
|
+
isFirst = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function displayCertificate(cert) {
|
|
243
|
+
const out = process.stdout;
|
|
244
|
+
const isLastGen = cert.era === "LAST_GEN";
|
|
245
|
+
const eraInfo = ERAS[cert.era];
|
|
246
|
+
const lines = [];
|
|
247
|
+
lines.push(boxRule());
|
|
248
|
+
const title = style("bold", "LASTGEN CERTIFICATE");
|
|
249
|
+
const titlePadL = Math.floor(31 / 2);
|
|
250
|
+
const titlePadR = 31 - titlePadL;
|
|
251
|
+
lines.push(boxLine(" ".repeat(titlePadL) + title + " ".repeat(titlePadR), 50));
|
|
252
|
+
lines.push(boxRule());
|
|
253
|
+
labelLine("Certificate ", cert.certificateNumber, cert.certificateNumber, lines);
|
|
254
|
+
const issuedDate = new Date(cert.issuedAt).toISOString().slice(0, 10);
|
|
255
|
+
labelLine("Issued ", issuedDate, issuedDate, lines);
|
|
256
|
+
lines.push(boxEmpty());
|
|
257
|
+
const devValue = cert.identity.name ? `${cert.identity.username} (${cert.identity.name})` : cert.identity.username;
|
|
258
|
+
labelLine("Developer ", devValue, devValue, lines);
|
|
259
|
+
const eraColor = isLastGen ? "green" : "cyan";
|
|
260
|
+
labelLine("Era ", eraInfo.title, style(eraColor, eraInfo.title), lines);
|
|
261
|
+
labelLine(" ", eraInfo.description, style("dim", eraInfo.description), lines);
|
|
262
|
+
if (cert.proof.firstCommit.sha) {
|
|
263
|
+
lines.push(boxEmpty());
|
|
264
|
+
const commitDate = new Date(cert.proof.firstCommit.date).toISOString().slice(0, 10);
|
|
265
|
+
const repo = cert.proof.firstCommit.repo;
|
|
266
|
+
labelLine("Proof Commit ", repo, repo, lines);
|
|
267
|
+
const commitMsg = `${cert.proof.firstCommit.sha.slice(0, 7)} ${cert.proof.firstCommit.message.replace(/\n/g, " ")}`;
|
|
268
|
+
labelLine(" ", commitMsg, style("dim", commitMsg), lines);
|
|
269
|
+
labelLine("Commit Date ", commitDate, commitDate, lines);
|
|
270
|
+
}
|
|
271
|
+
lines.push(boxEmpty());
|
|
272
|
+
const hash = cert.verification.hash;
|
|
273
|
+
labelLine("Hash ", hash, style("dim", hash), lines);
|
|
274
|
+
lines.push(boxRule());
|
|
275
|
+
out.write("\n" + lines.join("\n") + "\n\n");
|
|
276
|
+
}
|
|
277
|
+
function displayBadgeMarkdown(cert) {
|
|
278
|
+
const out = process.stdout;
|
|
279
|
+
const badgeUrl = `https://img.shields.io/badge/lastgen-${cert.era === "LAST_GEN" ? "Last%20Gen" : "AI%20Native"}-${cert.era === "LAST_GEN" ? "blue" : "brightgreen"}?style=for-the-badge`;
|
|
280
|
+
out.write("\n");
|
|
281
|
+
out.write(style("bold", " Add to your GitHub README:") + "\n");
|
|
282
|
+
out.write("\n");
|
|
283
|
+
out.write(` [](https://github.com/pgagnidze/lastgen)\n`);
|
|
284
|
+
out.write("\n");
|
|
285
|
+
}
|
|
286
|
+
function displayJson(cert) {
|
|
287
|
+
process.stdout.write(JSON.stringify(cert, null, 2) + "\n");
|
|
288
|
+
}
|
|
289
|
+
function info(message) {
|
|
290
|
+
process.stderr.write(style("dim", ` ${message}`) + "\n");
|
|
291
|
+
}
|
|
292
|
+
function error(message) {
|
|
293
|
+
process.stderr.write(style("red", ` ${message}`) + "\n");
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/core/verify.ts
|
|
297
|
+
function isValidCertificate(data) {
|
|
298
|
+
if (typeof data !== "object" || data === null) return false;
|
|
299
|
+
const cert = data;
|
|
300
|
+
return cert.type === "LASTGEN_CERTIFICATE" && typeof cert.version === "string" && typeof cert.identity === "object" && typeof cert.proof === "object" && typeof cert.verification === "object" && typeof cert.certificateNumber === "string";
|
|
301
|
+
}
|
|
302
|
+
function matchesNoreplyEmail(email, username) {
|
|
303
|
+
const lower = email.toLowerCase();
|
|
304
|
+
const user = username.toLowerCase();
|
|
305
|
+
return new RegExp(`^(\\d+\\+)?${escapeRegex(user)}@users\\.noreply\\.github\\.com$`).test(lower);
|
|
306
|
+
}
|
|
307
|
+
function escapeRegex(str) {
|
|
308
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
309
|
+
}
|
|
310
|
+
async function verifyCertificateData(cert, hashFn, token) {
|
|
311
|
+
const results = [];
|
|
312
|
+
const expectedHash = await generateCertificateHash(hashFn, cert.identity.username, cert.identity.githubId, cert.proof.proofDate, cert.era);
|
|
313
|
+
const actualHash = cert.verification.hash.replace("sha256:", "");
|
|
314
|
+
results.push({
|
|
315
|
+
check: "Hash integrity",
|
|
316
|
+
passed: expectedHash === actualHash,
|
|
317
|
+
detail: expectedHash === actualHash ? "Certificate hash is valid" : "Certificate hash does not match - data may have been tampered with"
|
|
318
|
+
});
|
|
319
|
+
const proofDate = new Date(cert.proof.proofDate);
|
|
320
|
+
const cutoff = new Date(CUTOFF_DATE);
|
|
321
|
+
const expectedEra = proofDate.getTime() < cutoff.getTime() ? "LAST_GEN" : "AI_NATIVE";
|
|
322
|
+
results.push({
|
|
323
|
+
check: "Era classification",
|
|
324
|
+
passed: cert.era === expectedEra,
|
|
325
|
+
detail: cert.era === expectedEra ? `Era ${cert.era} is correct for proof date ${cert.proof.proofDate}` : `Era should be ${expectedEra} but certificate claims ${cert.era}`
|
|
326
|
+
});
|
|
327
|
+
const expectedProofDate = resolveProofDate({
|
|
328
|
+
login: cert.identity.username,
|
|
329
|
+
id: cert.identity.githubId,
|
|
330
|
+
name: cert.identity.name,
|
|
331
|
+
createdAt: cert.proof.accountCreated
|
|
332
|
+
}, cert.proof.firstCommit.sha ? cert.proof.firstCommit : null);
|
|
333
|
+
const proofDateMatch = Math.abs(new Date(expectedProofDate).getTime() - proofDate.getTime()) < 6e4;
|
|
334
|
+
results.push({
|
|
335
|
+
check: "Proof date",
|
|
336
|
+
passed: proofDateMatch,
|
|
337
|
+
detail: proofDateMatch ? `Proof date ${cert.proof.proofDate} is consistent with commit and account data` : `Proof date should be ${expectedProofDate} but certificate claims ${cert.proof.proofDate}`
|
|
338
|
+
});
|
|
339
|
+
if (cert.proof.firstCommit.sha) try {
|
|
340
|
+
const commitDetail = await fetchCommit(cert.proof.firstCommit.repo, cert.proof.firstCommit.sha, token);
|
|
341
|
+
const username = cert.identity.username.toLowerCase();
|
|
342
|
+
const authorMatch = (commitDetail.authorLogin ?? "").toLowerCase() === username;
|
|
343
|
+
const committerMatch = (commitDetail.committerLogin ?? "").toLowerCase() === username;
|
|
344
|
+
const emailMatch = commitDetail.authorEmail ? matchesNoreplyEmail(commitDetail.authorEmail, cert.identity.username) : false;
|
|
345
|
+
const identityMatch = authorMatch || committerMatch || emailMatch;
|
|
346
|
+
const matchMethods = [];
|
|
347
|
+
if (authorMatch) matchMethods.push("author login");
|
|
348
|
+
if (committerMatch) matchMethods.push("committer login");
|
|
349
|
+
if (emailMatch) matchMethods.push("noreply email");
|
|
350
|
+
results.push({
|
|
351
|
+
check: "Identity",
|
|
352
|
+
passed: identityMatch,
|
|
353
|
+
detail: identityMatch ? `Matched via: ${matchMethods.join(", ")}` : `Commit author (${commitDetail.authorLogin}) does not match ${cert.identity.username}`
|
|
354
|
+
});
|
|
355
|
+
const isSelfOwned = (cert.proof.firstCommit.repo.split("/")[0] ?? "").toLowerCase() === cert.identity.username.toLowerCase();
|
|
356
|
+
results.push({
|
|
357
|
+
check: "Repo ownership",
|
|
358
|
+
passed: true,
|
|
359
|
+
detail: isSelfOwned ? `Commit is in a repo owned by ${cert.identity.username}` : `Commit is in a third-party repo (${cert.proof.firstCommit.repo})`
|
|
360
|
+
});
|
|
361
|
+
if (commitDetail.authorId !== null) {
|
|
362
|
+
const idMatch = commitDetail.authorId === cert.identity.githubId;
|
|
363
|
+
results.push({
|
|
364
|
+
check: "GitHub ID",
|
|
365
|
+
passed: idMatch,
|
|
366
|
+
detail: idMatch ? `GitHub ID ${commitDetail.authorId} matches certificate` : `Commit author ID ${commitDetail.authorId} does not match certificate ID ${cert.identity.githubId}`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const commitDate = new Date(commitDetail.authorDate ?? "");
|
|
370
|
+
const certDate = new Date(cert.proof.firstCommit.date);
|
|
371
|
+
const datesClose = Math.abs(commitDate.getTime() - certDate.getTime()) < 6e4;
|
|
372
|
+
results.push({
|
|
373
|
+
check: "Commit date",
|
|
374
|
+
passed: datesClose,
|
|
375
|
+
detail: datesClose ? `Commit date matches certificate (${commitDetail.authorDate})` : `Commit date ${commitDetail.authorDate} differs from certificate ${cert.proof.firstCommit.date}`
|
|
376
|
+
});
|
|
377
|
+
if (commitDetail.authorDate && commitDetail.committerDate) {
|
|
378
|
+
const authorTime = new Date(commitDetail.authorDate).getTime();
|
|
379
|
+
const committerTime = new Date(commitDetail.committerDate).getTime();
|
|
380
|
+
const driftMs = Math.abs(committerTime - authorTime);
|
|
381
|
+
const driftDays = Math.round(driftMs / (1440 * 60 * 1e3));
|
|
382
|
+
const consistent = driftMs <= THIRTY_DAYS_MS;
|
|
383
|
+
results.push({
|
|
384
|
+
check: "Date consistency",
|
|
385
|
+
passed: consistent,
|
|
386
|
+
detail: consistent ? `Author/committer date drift: ${driftDays}d (within 30d threshold)` : `Author/committer date drift: ${driftDays}d exceeds 30d - author date may be forged`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (commitDetail.isRootCommit) results.push({
|
|
390
|
+
check: "Root commit",
|
|
391
|
+
passed: true,
|
|
392
|
+
detail: "Commit has no parents (first commit in repo - higher trust)"
|
|
393
|
+
});
|
|
394
|
+
if (commitDetail.verified) {
|
|
395
|
+
const reason = commitDetail.verificationReason;
|
|
396
|
+
const reasonDetail = reason && reason !== "valid" ? ` (${reason})` : "";
|
|
397
|
+
results.push({
|
|
398
|
+
check: "GPG signature",
|
|
399
|
+
passed: true,
|
|
400
|
+
detail: `Commit is GPG-signed${reasonDetail}`
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} catch (fetchError) {
|
|
404
|
+
const message = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
405
|
+
results.push({
|
|
406
|
+
check: "Commit verification",
|
|
407
|
+
passed: false,
|
|
408
|
+
detail: `Could not fetch commit from GitHub: ${message}`
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
valid: results.every((r) => r.passed),
|
|
413
|
+
results,
|
|
414
|
+
certificateNumber: cert.certificateNumber,
|
|
415
|
+
username: cert.identity.username
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/verify-cli.ts
|
|
420
|
+
/**
|
|
421
|
+
* @fileoverview CLI wrapper for certificate verification. Handles file I/O and display.
|
|
422
|
+
*/
|
|
423
|
+
async function verifyCertificate(filePath, hashFn, token) {
|
|
424
|
+
let raw;
|
|
425
|
+
try {
|
|
426
|
+
raw = readFileSync(filePath, "utf-8");
|
|
427
|
+
} catch {
|
|
428
|
+
error(`Cannot read file: ${filePath}`);
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
let cert;
|
|
432
|
+
try {
|
|
433
|
+
cert = JSON.parse(raw);
|
|
434
|
+
} catch {
|
|
435
|
+
error("Invalid JSON in certificate file.");
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (!isValidCertificate(cert)) {
|
|
439
|
+
error("File is not a valid lastgen certificate.");
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
info(`Verifying certificate ${cert.certificateNumber}...`);
|
|
443
|
+
info(`Developer: ${cert.identity.username}`);
|
|
444
|
+
info("");
|
|
445
|
+
if (cert.proof.firstCommit.sha) info(`Fetching commit ${cert.proof.firstCommit.sha.slice(0, 7)} from GitHub...`);
|
|
446
|
+
const { valid, results } = await verifyCertificateData(cert, hashFn, token);
|
|
447
|
+
const out = process.stdout;
|
|
448
|
+
const lines = [];
|
|
449
|
+
lines.push(boxRule());
|
|
450
|
+
const title = style("bold", "VERIFICATION");
|
|
451
|
+
const titlePadL = Math.floor(38 / 2);
|
|
452
|
+
const titlePadR = 38 - titlePadL;
|
|
453
|
+
lines.push(boxLine(" ".repeat(titlePadL) + title + " ".repeat(titlePadR), 50));
|
|
454
|
+
lines.push(boxRule());
|
|
455
|
+
for (const result of results) {
|
|
456
|
+
const checkLine = `${result.passed ? style("green", "PASS") : style("red", "FAIL")} ${style("bold", result.check)}`;
|
|
457
|
+
lines.push(boxLine(checkLine, 6 + result.check.length));
|
|
458
|
+
const indent = 6;
|
|
459
|
+
const maxLen = 50 - indent;
|
|
460
|
+
let remaining = result.detail;
|
|
461
|
+
while (remaining.length > 0) {
|
|
462
|
+
const chunk = remaining.slice(0, maxLen);
|
|
463
|
+
remaining = remaining.slice(maxLen);
|
|
464
|
+
const line = " ".repeat(indent) + style("dim", chunk);
|
|
465
|
+
lines.push(boxLine(line, indent + chunk.length));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
lines.push(boxRule());
|
|
469
|
+
if (valid) {
|
|
470
|
+
const msg = style("green", "Certificate is valid.");
|
|
471
|
+
lines.push(boxLine(msg, 21));
|
|
472
|
+
} else {
|
|
473
|
+
const msg = style("red", "Certificate verification failed.");
|
|
474
|
+
lines.push(boxLine(msg, 32));
|
|
475
|
+
}
|
|
476
|
+
lines.push(boxRule());
|
|
477
|
+
out.write("\n" + lines.join("\n") + "\n\n");
|
|
478
|
+
return valid;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/serve.ts
|
|
482
|
+
/**
|
|
483
|
+
* @fileoverview Static HTTP server for the pre-built web frontend.
|
|
484
|
+
* Zero dependencies — uses node:http and node:fs.
|
|
485
|
+
*/
|
|
486
|
+
const MIME_TYPES = {
|
|
487
|
+
".html": "text/html; charset=utf-8",
|
|
488
|
+
".css": "text/css; charset=utf-8",
|
|
489
|
+
".js": "application/javascript; charset=utf-8",
|
|
490
|
+
".json": "application/json; charset=utf-8",
|
|
491
|
+
".svg": "image/svg+xml",
|
|
492
|
+
".png": "image/png",
|
|
493
|
+
".ico": "image/x-icon"
|
|
494
|
+
};
|
|
495
|
+
function serve(port = 3e3) {
|
|
496
|
+
const distDir = join(dirname(fileURLToPath(import.meta.url)), "..", "web", "dist");
|
|
497
|
+
if (!existsSync(distDir)) {
|
|
498
|
+
process.stderr.write(` Web assets not found at ${distDir}\n Run "cd web && npm install && npm run build" first.
|
|
499
|
+
`);
|
|
500
|
+
process.exitCode = 1;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
createServer((req, res) => {
|
|
504
|
+
let urlPath = req.url ?? "/";
|
|
505
|
+
const qIndex = urlPath.indexOf("?");
|
|
506
|
+
if (qIndex !== -1) urlPath = urlPath.slice(0, qIndex);
|
|
507
|
+
if (urlPath.startsWith("/lastgen/")) urlPath = urlPath.slice(8);
|
|
508
|
+
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
509
|
+
const filePath = join(distDir, urlPath);
|
|
510
|
+
if (!filePath.startsWith(distDir)) {
|
|
511
|
+
res.writeHead(403);
|
|
512
|
+
res.end("Forbidden");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const content = readFileSync(filePath);
|
|
517
|
+
const contentType = MIME_TYPES[extname(filePath)] ?? "application/octet-stream";
|
|
518
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
519
|
+
res.end(content);
|
|
520
|
+
} catch {
|
|
521
|
+
try {
|
|
522
|
+
const indexContent = readFileSync(join(distDir, "index.html"));
|
|
523
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
524
|
+
res.end(indexContent);
|
|
525
|
+
} catch {
|
|
526
|
+
res.writeHead(404);
|
|
527
|
+
res.end("Not found");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}).listen(port, "127.0.0.1", () => {
|
|
531
|
+
process.stderr.write(`\n lastgen web UI running at http://localhost:${port}/\n\n`);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/cli.ts
|
|
536
|
+
/**
|
|
537
|
+
* @fileoverview CLI argument parsing and command routing using built-in parseArgs.
|
|
538
|
+
*/
|
|
539
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
540
|
+
const buildInfo = [];
|
|
541
|
+
buildInfo.push(`commit: ${"389e4cd7746c6add8cd1a5fae255a7875a3a036d".substring(0, 7)}`);
|
|
542
|
+
buildInfo.push(`built: 2026-03-10T23:44:21+04:00`);
|
|
543
|
+
const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
|
|
544
|
+
const VERSION = `lastgen ${pkg.version}${buildString}`;
|
|
545
|
+
const HELP_BRIEF = `
|
|
546
|
+
_ _
|
|
547
|
+
| | __ _ ___ | |_ __ _ ___ _ __
|
|
548
|
+
| |/ _\` / __|| __/ _\` |/ _ \\ '_ \\
|
|
549
|
+
| | (_| \\__ \\| || (_| | __/ | | |
|
|
550
|
+
|_|\\__,_|___/ \\__\\__, |\\___|_| |_|
|
|
551
|
+
|___/
|
|
552
|
+
Check if you started coding before or after AI agents.
|
|
553
|
+
|
|
554
|
+
Usage:
|
|
555
|
+
lastgen <username> Classify a GitHub user
|
|
556
|
+
lastgen verify <file.json> Verify a saved certificate
|
|
557
|
+
lastgen serve [--port <port>] Launch web UI
|
|
558
|
+
|
|
559
|
+
Options:
|
|
560
|
+
--token <token> GitHub personal access token
|
|
561
|
+
--json Output as JSON
|
|
562
|
+
--badge Output as README badge markdown
|
|
563
|
+
--port <port> Port for web UI (default: 3000)
|
|
564
|
+
--no-color Disable colors
|
|
565
|
+
-h, --help Show this help
|
|
566
|
+
-v, --version Show version
|
|
567
|
+
|
|
568
|
+
Environment:
|
|
569
|
+
GITHUB_TOKEN GitHub token (alternative to --token)
|
|
570
|
+
NO_COLOR Disable colors (any value)
|
|
571
|
+
|
|
572
|
+
Examples:
|
|
573
|
+
npx lastgen torvalds
|
|
574
|
+
npx lastgen --json torvalds > proof.json
|
|
575
|
+
npx lastgen verify proof.json
|
|
576
|
+
npx lastgen --badge torvalds
|
|
577
|
+
npx lastgen serve
|
|
578
|
+
`;
|
|
579
|
+
function parseCli(argv) {
|
|
580
|
+
const { values, positionals } = parseArgs({
|
|
581
|
+
args: argv,
|
|
582
|
+
options: {
|
|
583
|
+
token: { type: "string" },
|
|
584
|
+
json: {
|
|
585
|
+
type: "boolean",
|
|
586
|
+
default: false
|
|
587
|
+
},
|
|
588
|
+
badge: {
|
|
589
|
+
type: "boolean",
|
|
590
|
+
default: false
|
|
591
|
+
},
|
|
592
|
+
port: { type: "string" },
|
|
593
|
+
help: {
|
|
594
|
+
type: "boolean",
|
|
595
|
+
short: "h",
|
|
596
|
+
default: false
|
|
597
|
+
},
|
|
598
|
+
version: {
|
|
599
|
+
type: "boolean",
|
|
600
|
+
short: "v",
|
|
601
|
+
default: false
|
|
602
|
+
},
|
|
603
|
+
"no-color": {
|
|
604
|
+
type: "boolean",
|
|
605
|
+
default: false
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
allowPositionals: true,
|
|
609
|
+
strict: false
|
|
610
|
+
});
|
|
611
|
+
const first = positionals[0] ?? "";
|
|
612
|
+
const isVerify = first === "verify";
|
|
613
|
+
return {
|
|
614
|
+
command: isVerify ? "verify" : first === "serve" ? "serve" : first ? "lookup" : "",
|
|
615
|
+
target: isVerify ? positionals[1] ?? "" : first,
|
|
616
|
+
token: values.token ?? process.env["GITHUB_TOKEN"],
|
|
617
|
+
port: Number(values.port) || 3e3,
|
|
618
|
+
json: Boolean(values.json),
|
|
619
|
+
badge: Boolean(values.badge),
|
|
620
|
+
help: Boolean(values.help),
|
|
621
|
+
version: Boolean(values.version)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async function run(argv) {
|
|
625
|
+
const opts = parseCli(argv);
|
|
626
|
+
if (opts.version) {
|
|
627
|
+
process.stdout.write(VERSION + "\n");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (opts.help || !opts.command) {
|
|
631
|
+
process.stdout.write(HELP_BRIEF);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
switch (opts.command) {
|
|
635
|
+
case "lookup":
|
|
636
|
+
await handleLookup(opts);
|
|
637
|
+
break;
|
|
638
|
+
case "verify":
|
|
639
|
+
await handleVerify(opts);
|
|
640
|
+
break;
|
|
641
|
+
case "serve":
|
|
642
|
+
serve(opts.port);
|
|
643
|
+
break;
|
|
644
|
+
default:
|
|
645
|
+
error(`Unknown command: ${opts.command}`);
|
|
646
|
+
process.stdout.write(HELP_BRIEF);
|
|
647
|
+
process.exitCode = 2;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function handleLookup(opts) {
|
|
651
|
+
if (!opts.target) {
|
|
652
|
+
error("Username required. Usage: lastgen <username>");
|
|
653
|
+
process.exitCode = 2;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (!opts.json && !opts.badge) info(`Looking up ${opts.target} on GitHub...`);
|
|
657
|
+
const [user, firstCommit] = await Promise.all([fetchUser(opts.target, opts.token), fetchFirstCommit(opts.target, opts.token)]);
|
|
658
|
+
const cert = await createCertificate(nodeHash, user, firstCommit);
|
|
659
|
+
if (opts.badge) displayBadgeMarkdown(cert);
|
|
660
|
+
else if (opts.json) displayJson(cert);
|
|
661
|
+
else displayCertificate(cert);
|
|
662
|
+
}
|
|
663
|
+
async function handleVerify(opts) {
|
|
664
|
+
if (!opts.target) {
|
|
665
|
+
error("Certificate file required. Usage: lastgen verify <file.json>");
|
|
666
|
+
process.exitCode = 2;
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (!await verifyCertificate(opts.target, nodeHash, opts.token)) process.exitCode = 1;
|
|
670
|
+
}
|
|
671
|
+
//#endregion
|
|
672
|
+
export { parseCli, run };
|