lastgen 1.2.0 → 1.2.1
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 +673 -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/src/core/verify.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Certificate verification logic. Platform-agnostic: no I/O, no display.
|
|
3
|
-
* Accepts parsed data and a HashFn, returns structured results.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Certificate, HashFn, VerifyResult } from './types.ts';
|
|
7
|
-
import { CUTOFF_DATE, THIRTY_DAYS_MS } from './types.ts';
|
|
8
|
-
import { fetchCommit } from './github.ts';
|
|
9
|
-
import { generateCertificateHash, resolveProofDate } from './proof.ts';
|
|
10
|
-
|
|
11
|
-
export function isValidCertificate(data: unknown): data is Certificate {
|
|
12
|
-
if (typeof data !== 'object' || data === null) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
const cert = data as Record<string, unknown>;
|
|
16
|
-
return (
|
|
17
|
-
cert.type === 'LASTGEN_CERTIFICATE' &&
|
|
18
|
-
typeof cert.version === 'string' &&
|
|
19
|
-
typeof cert.identity === 'object' &&
|
|
20
|
-
typeof cert.proof === 'object' &&
|
|
21
|
-
typeof cert.verification === 'object' &&
|
|
22
|
-
typeof cert.certificateNumber === 'string'
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function matchesNoreplyEmail(email: string, username: string): boolean {
|
|
27
|
-
const lower = email.toLowerCase();
|
|
28
|
-
const user = username.toLowerCase();
|
|
29
|
-
const pattern = new RegExp(`^(\\d+\\+)?${escapeRegex(user)}@users\\.noreply\\.github\\.com$`);
|
|
30
|
-
return pattern.test(lower);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function escapeRegex(str: string): string {
|
|
34
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface VerifyCertificateResult {
|
|
38
|
-
valid: boolean;
|
|
39
|
-
results: VerifyResult[];
|
|
40
|
-
certificateNumber: string;
|
|
41
|
-
username: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function verifyCertificateData(
|
|
45
|
-
cert: Certificate,
|
|
46
|
-
hashFn: HashFn,
|
|
47
|
-
token?: string,
|
|
48
|
-
): Promise<VerifyCertificateResult> {
|
|
49
|
-
const results: VerifyResult[] = [];
|
|
50
|
-
|
|
51
|
-
const expectedHash = await generateCertificateHash(
|
|
52
|
-
hashFn,
|
|
53
|
-
cert.identity.username,
|
|
54
|
-
cert.identity.githubId,
|
|
55
|
-
cert.proof.proofDate,
|
|
56
|
-
cert.era,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const actualHash = cert.verification.hash.replace('sha256:', '');
|
|
60
|
-
results.push({
|
|
61
|
-
check: 'Hash integrity',
|
|
62
|
-
passed: expectedHash === actualHash,
|
|
63
|
-
detail:
|
|
64
|
-
expectedHash === actualHash
|
|
65
|
-
? 'Certificate hash is valid'
|
|
66
|
-
: 'Certificate hash does not match - data may have been tampered with',
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const proofDate = new Date(cert.proof.proofDate);
|
|
70
|
-
const cutoff = new Date(CUTOFF_DATE);
|
|
71
|
-
const isBeforeCutoff = proofDate.getTime() < cutoff.getTime();
|
|
72
|
-
const expectedEra = isBeforeCutoff ? 'LAST_GEN' : 'AI_NATIVE';
|
|
73
|
-
results.push({
|
|
74
|
-
check: 'Era classification',
|
|
75
|
-
passed: cert.era === expectedEra,
|
|
76
|
-
detail:
|
|
77
|
-
cert.era === expectedEra
|
|
78
|
-
? `Era ${cert.era} is correct for proof date ${cert.proof.proofDate}`
|
|
79
|
-
: `Era should be ${expectedEra} but certificate claims ${cert.era}`,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const reconstructedUser = {
|
|
83
|
-
login: cert.identity.username,
|
|
84
|
-
id: cert.identity.githubId,
|
|
85
|
-
name: cert.identity.name,
|
|
86
|
-
createdAt: cert.proof.accountCreated,
|
|
87
|
-
};
|
|
88
|
-
const expectedProofDate = resolveProofDate(
|
|
89
|
-
reconstructedUser,
|
|
90
|
-
cert.proof.firstCommit.sha ? cert.proof.firstCommit : null,
|
|
91
|
-
);
|
|
92
|
-
const proofDateMatch =
|
|
93
|
-
Math.abs(new Date(expectedProofDate).getTime() - proofDate.getTime()) < 60000;
|
|
94
|
-
results.push({
|
|
95
|
-
check: 'Proof date',
|
|
96
|
-
passed: proofDateMatch,
|
|
97
|
-
detail: proofDateMatch
|
|
98
|
-
? `Proof date ${cert.proof.proofDate} is consistent with commit and account data`
|
|
99
|
-
: `Proof date should be ${expectedProofDate} but certificate claims ${cert.proof.proofDate}`,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (cert.proof.firstCommit.sha) {
|
|
103
|
-
try {
|
|
104
|
-
const commitDetail = await fetchCommit(
|
|
105
|
-
cert.proof.firstCommit.repo,
|
|
106
|
-
cert.proof.firstCommit.sha,
|
|
107
|
-
token,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const username = cert.identity.username.toLowerCase();
|
|
111
|
-
const authorMatch = (commitDetail.authorLogin ?? '').toLowerCase() === username;
|
|
112
|
-
const committerMatch = (commitDetail.committerLogin ?? '').toLowerCase() === username;
|
|
113
|
-
const emailMatch = commitDetail.authorEmail
|
|
114
|
-
? matchesNoreplyEmail(commitDetail.authorEmail, cert.identity.username)
|
|
115
|
-
: false;
|
|
116
|
-
|
|
117
|
-
const identityMatch = authorMatch || committerMatch || emailMatch;
|
|
118
|
-
const matchMethods: string[] = [];
|
|
119
|
-
if (authorMatch) matchMethods.push('author login');
|
|
120
|
-
if (committerMatch) matchMethods.push('committer login');
|
|
121
|
-
if (emailMatch) matchMethods.push('noreply email');
|
|
122
|
-
|
|
123
|
-
results.push({
|
|
124
|
-
check: 'Identity',
|
|
125
|
-
passed: identityMatch,
|
|
126
|
-
detail: identityMatch
|
|
127
|
-
? `Matched via: ${matchMethods.join(', ')}`
|
|
128
|
-
: `Commit author (${commitDetail.authorLogin}) does not match ${cert.identity.username}`,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const repoOwner = cert.proof.firstCommit.repo.split('/')[0] ?? '';
|
|
132
|
-
const isSelfOwned = repoOwner.toLowerCase() === cert.identity.username.toLowerCase();
|
|
133
|
-
results.push({
|
|
134
|
-
check: 'Repo ownership',
|
|
135
|
-
passed: true,
|
|
136
|
-
detail: isSelfOwned
|
|
137
|
-
? `Commit is in a repo owned by ${cert.identity.username}`
|
|
138
|
-
: `Commit is in a third-party repo (${cert.proof.firstCommit.repo})`,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (commitDetail.authorId !== null) {
|
|
142
|
-
const idMatch = commitDetail.authorId === cert.identity.githubId;
|
|
143
|
-
results.push({
|
|
144
|
-
check: 'GitHub ID',
|
|
145
|
-
passed: idMatch,
|
|
146
|
-
detail: idMatch
|
|
147
|
-
? `GitHub ID ${commitDetail.authorId} matches certificate`
|
|
148
|
-
: `Commit author ID ${commitDetail.authorId} does not match certificate ID ${cert.identity.githubId}`,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const commitDate = new Date(commitDetail.authorDate ?? '');
|
|
153
|
-
const certDate = new Date(cert.proof.firstCommit.date);
|
|
154
|
-
const datesClose = Math.abs(commitDate.getTime() - certDate.getTime()) < 60000;
|
|
155
|
-
results.push({
|
|
156
|
-
check: 'Commit date',
|
|
157
|
-
passed: datesClose,
|
|
158
|
-
detail: datesClose
|
|
159
|
-
? `Commit date matches certificate (${commitDetail.authorDate})`
|
|
160
|
-
: `Commit date ${commitDetail.authorDate} differs from certificate ${cert.proof.firstCommit.date}`,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
if (commitDetail.authorDate && commitDetail.committerDate) {
|
|
164
|
-
const authorTime = new Date(commitDetail.authorDate).getTime();
|
|
165
|
-
const committerTime = new Date(commitDetail.committerDate).getTime();
|
|
166
|
-
const driftMs = Math.abs(committerTime - authorTime);
|
|
167
|
-
const driftDays = Math.round(driftMs / (24 * 60 * 60 * 1000));
|
|
168
|
-
const consistent = driftMs <= THIRTY_DAYS_MS;
|
|
169
|
-
results.push({
|
|
170
|
-
check: 'Date consistency',
|
|
171
|
-
passed: consistent,
|
|
172
|
-
detail: consistent
|
|
173
|
-
? `Author/committer date drift: ${driftDays}d (within 30d threshold)`
|
|
174
|
-
: `Author/committer date drift: ${driftDays}d exceeds 30d - author date may be forged`,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (commitDetail.isRootCommit) {
|
|
179
|
-
results.push({
|
|
180
|
-
check: 'Root commit',
|
|
181
|
-
passed: true,
|
|
182
|
-
detail: 'Commit has no parents (first commit in repo - higher trust)',
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (commitDetail.verified) {
|
|
187
|
-
const reason = commitDetail.verificationReason;
|
|
188
|
-
const reasonDetail = reason && reason !== 'valid' ? ` (${reason})` : '';
|
|
189
|
-
results.push({
|
|
190
|
-
check: 'GPG signature',
|
|
191
|
-
passed: true,
|
|
192
|
-
detail: `Commit is GPG-signed${reasonDetail}`,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
} catch (fetchError) {
|
|
196
|
-
const message = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
197
|
-
results.push({
|
|
198
|
-
check: 'Commit verification',
|
|
199
|
-
passed: false,
|
|
200
|
-
detail: `Could not fetch commit from GitHub: ${message}`,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const allPassed = results.every((r) => r.passed);
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
valid: allPassed,
|
|
209
|
-
results,
|
|
210
|
-
certificateNumber: cert.certificateNumber,
|
|
211
|
-
username: cert.identity.username,
|
|
212
|
-
};
|
|
213
|
-
}
|
package/src/display.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Terminal output formatting with ASCII box-drawing for certificates and verification.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { styleText } from 'node:util';
|
|
6
|
-
|
|
7
|
-
import type { Certificate } from './core/types.ts';
|
|
8
|
-
import { ERAS } from './core/types.ts';
|
|
9
|
-
|
|
10
|
-
function shouldUseColor(): boolean {
|
|
11
|
-
if (process.env['NO_COLOR'] !== undefined) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
if (process.env['TERM'] === 'dumb') {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
if (process.argv.includes('--no-color')) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
return process.stdout.isTTY === true;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function style(format: string | string[], text: string): string {
|
|
24
|
-
if (!shouldUseColor()) {
|
|
25
|
-
return text;
|
|
26
|
-
}
|
|
27
|
-
return styleText(format as Parameters<typeof styleText>[0], text);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const BOX_WIDTH = 50;
|
|
31
|
-
|
|
32
|
-
export function boxRule(): string {
|
|
33
|
-
return style('dim', '+' + '-'.repeat(BOX_WIDTH + 2) + '+');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function boxLine(content: string, rawLength: number): string {
|
|
37
|
-
const pad = BOX_WIDTH - rawLength;
|
|
38
|
-
return style('dim', '|') + ' ' + content + ' '.repeat(Math.max(pad, 0)) + ' ' + style('dim', '|');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function boxEmpty(): string {
|
|
42
|
-
return boxLine('', 0);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const LABEL_WIDTH = 13;
|
|
46
|
-
|
|
47
|
-
function labelLine(label: string, value: string, styledValue: string, lines: string[]): void {
|
|
48
|
-
const max = BOX_WIDTH - LABEL_WIDTH;
|
|
49
|
-
if (value.length <= max) {
|
|
50
|
-
lines.push(boxLine(style('dim', label) + styledValue, LABEL_WIDTH + value.length));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const indent = ' '.repeat(LABEL_WIDTH);
|
|
54
|
-
let remaining = value;
|
|
55
|
-
let isFirst = true;
|
|
56
|
-
while (remaining.length > 0) {
|
|
57
|
-
const chunk = remaining.slice(0, max);
|
|
58
|
-
remaining = remaining.slice(max);
|
|
59
|
-
const prefix = isFirst ? style('dim', label) : indent;
|
|
60
|
-
lines.push(boxLine(prefix + chunk, LABEL_WIDTH + chunk.length));
|
|
61
|
-
isFirst = false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function displayCertificate(cert: Certificate): void {
|
|
66
|
-
const out = process.stdout;
|
|
67
|
-
const isLastGen = cert.era === 'LAST_GEN';
|
|
68
|
-
const eraInfo = ERAS[cert.era];
|
|
69
|
-
const lines: string[] = [];
|
|
70
|
-
|
|
71
|
-
lines.push(boxRule());
|
|
72
|
-
|
|
73
|
-
const title = style('bold', 'LASTGEN CERTIFICATE');
|
|
74
|
-
const titleRaw = 'LASTGEN CERTIFICATE';
|
|
75
|
-
const titlePadL = Math.floor((BOX_WIDTH - titleRaw.length) / 2);
|
|
76
|
-
const titlePadR = BOX_WIDTH - titleRaw.length - titlePadL;
|
|
77
|
-
lines.push(boxLine(' '.repeat(titlePadL) + title + ' '.repeat(titlePadR), BOX_WIDTH));
|
|
78
|
-
|
|
79
|
-
lines.push(boxRule());
|
|
80
|
-
|
|
81
|
-
labelLine('Certificate ', cert.certificateNumber, cert.certificateNumber, lines);
|
|
82
|
-
|
|
83
|
-
const issuedDate = new Date(cert.issuedAt).toISOString().slice(0, 10);
|
|
84
|
-
labelLine('Issued ', issuedDate, issuedDate, lines);
|
|
85
|
-
|
|
86
|
-
lines.push(boxEmpty());
|
|
87
|
-
|
|
88
|
-
const devValue = cert.identity.name
|
|
89
|
-
? `${cert.identity.username} (${cert.identity.name})`
|
|
90
|
-
: cert.identity.username;
|
|
91
|
-
labelLine('Developer ', devValue, devValue, lines);
|
|
92
|
-
|
|
93
|
-
const eraColor = isLastGen ? 'green' : 'cyan';
|
|
94
|
-
labelLine('Era ', eraInfo.title, style(eraColor, eraInfo.title), lines);
|
|
95
|
-
labelLine(' ', eraInfo.description, style('dim', eraInfo.description), lines);
|
|
96
|
-
|
|
97
|
-
if (cert.proof.firstCommit.sha) {
|
|
98
|
-
lines.push(boxEmpty());
|
|
99
|
-
|
|
100
|
-
const commitDate = new Date(cert.proof.firstCommit.date).toISOString().slice(0, 10);
|
|
101
|
-
const repo = cert.proof.firstCommit.repo;
|
|
102
|
-
labelLine('Proof Commit ', repo, repo, lines);
|
|
103
|
-
const shaShort = cert.proof.firstCommit.sha.slice(0, 7);
|
|
104
|
-
const commitMsg = `${shaShort} ${cert.proof.firstCommit.message.replace(/\n/g, ' ')}`;
|
|
105
|
-
labelLine(' ', commitMsg, style('dim', commitMsg), lines);
|
|
106
|
-
labelLine('Commit Date ', commitDate, commitDate, lines);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
lines.push(boxEmpty());
|
|
110
|
-
|
|
111
|
-
const hash = cert.verification.hash;
|
|
112
|
-
labelLine('Hash ', hash, style('dim', hash), lines);
|
|
113
|
-
|
|
114
|
-
lines.push(boxRule());
|
|
115
|
-
|
|
116
|
-
out.write('\n' + lines.join('\n') + '\n\n');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function displayBadgeMarkdown(cert: Certificate): void {
|
|
120
|
-
const out = process.stdout;
|
|
121
|
-
const eraLabel = cert.era === 'LAST_GEN' ? 'Last%20Gen' : 'AI%20Native';
|
|
122
|
-
const color = cert.era === 'LAST_GEN' ? 'blue' : 'brightgreen';
|
|
123
|
-
const badgeUrl = `https://img.shields.io/badge/lastgen-${eraLabel}-${color}?style=for-the-badge`;
|
|
124
|
-
|
|
125
|
-
out.write('\n');
|
|
126
|
-
out.write(style('bold', ' Add to your GitHub README:') + '\n');
|
|
127
|
-
out.write('\n');
|
|
128
|
-
out.write(` [](https://github.com/pgagnidze/lastgen)\n`);
|
|
129
|
-
out.write('\n');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function displayJson(cert: Certificate): void {
|
|
133
|
-
process.stdout.write(JSON.stringify(cert, null, 2) + '\n');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function info(message: string): void {
|
|
137
|
-
process.stderr.write(style('dim', ` ${message}`) + '\n');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function error(message: string): void {
|
|
141
|
-
process.stderr.write(style('red', ` ${message}`) + '\n');
|
|
142
|
-
}
|
package/src/hash.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Node.js SHA-256 hash implementation using node:crypto.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
6
|
-
|
|
7
|
-
import type { HashFn } from './core/types.ts';
|
|
8
|
-
|
|
9
|
-
export const nodeHash: HashFn = async (data: string): Promise<string> => {
|
|
10
|
-
return createHash('sha256').update(data).digest('hex');
|
|
11
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { run } from './cli.ts';
|
|
4
|
-
import { error } from './display.ts';
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
await run(process.argv.slice(2));
|
|
8
|
-
} catch (err) {
|
|
9
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
10
|
-
error(message);
|
|
11
|
-
process.exitCode = 1;
|
|
12
|
-
}
|
package/src/serve.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Static HTTP server for the pre-built web frontend.
|
|
3
|
-
* Zero dependencies — uses node:http and node:fs.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createServer } from 'node:http';
|
|
7
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
-
import { join, extname } from 'node:path';
|
|
9
|
-
import { dirname } from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
|
|
12
|
-
const MIME_TYPES: Record<string, string> = {
|
|
13
|
-
'.html': 'text/html; charset=utf-8',
|
|
14
|
-
'.css': 'text/css; charset=utf-8',
|
|
15
|
-
'.js': 'application/javascript; charset=utf-8',
|
|
16
|
-
'.json': 'application/json; charset=utf-8',
|
|
17
|
-
'.svg': 'image/svg+xml',
|
|
18
|
-
'.png': 'image/png',
|
|
19
|
-
'.ico': 'image/x-icon',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function serve(port: number = 3000): void {
|
|
23
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
-
const __dirname = dirname(__filename);
|
|
25
|
-
const distDir = join(__dirname, '..', 'web', 'dist');
|
|
26
|
-
|
|
27
|
-
if (!existsSync(distDir)) {
|
|
28
|
-
process.stderr.write(
|
|
29
|
-
` Web assets not found at ${distDir}\n` +
|
|
30
|
-
' Run "cd web && npm install && npm run build" first.\n',
|
|
31
|
-
);
|
|
32
|
-
process.exitCode = 1;
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const server = createServer((req, res) => {
|
|
37
|
-
let urlPath = req.url ?? '/';
|
|
38
|
-
|
|
39
|
-
const qIndex = urlPath.indexOf('?');
|
|
40
|
-
if (qIndex !== -1) urlPath = urlPath.slice(0, qIndex);
|
|
41
|
-
|
|
42
|
-
if (urlPath.startsWith('/lastgen/')) {
|
|
43
|
-
urlPath = urlPath.slice('/lastgen'.length);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (urlPath === '/' || urlPath === '') urlPath = '/index.html';
|
|
47
|
-
|
|
48
|
-
const filePath = join(distDir, urlPath);
|
|
49
|
-
|
|
50
|
-
if (!filePath.startsWith(distDir)) {
|
|
51
|
-
res.writeHead(403);
|
|
52
|
-
res.end('Forbidden');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const content = readFileSync(filePath);
|
|
58
|
-
const ext = extname(filePath);
|
|
59
|
-
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
60
|
-
res.writeHead(200, { 'Content-Type': contentType });
|
|
61
|
-
res.end(content);
|
|
62
|
-
} catch {
|
|
63
|
-
try {
|
|
64
|
-
const indexContent = readFileSync(join(distDir, 'index.html'));
|
|
65
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
66
|
-
res.end(indexContent);
|
|
67
|
-
} catch {
|
|
68
|
-
res.writeHead(404);
|
|
69
|
-
res.end('Not found');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
server.listen(port, '127.0.0.1', () => {
|
|
75
|
-
process.stderr.write(`\n lastgen web UI running at http://localhost:${port}/\n\n`);
|
|
76
|
-
});
|
|
77
|
-
}
|
package/src/verify-cli.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview CLI wrapper for certificate verification. Handles file I/O and display.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
|
|
7
|
-
import type { HashFn } from './core/types.ts';
|
|
8
|
-
import { isValidCertificate, verifyCertificateData } from './core/verify.ts';
|
|
9
|
-
import { BOX_WIDTH, boxLine, boxRule, error, info, style } from './display.ts';
|
|
10
|
-
|
|
11
|
-
export async function verifyCertificate(
|
|
12
|
-
filePath: string,
|
|
13
|
-
hashFn: HashFn,
|
|
14
|
-
token?: string,
|
|
15
|
-
): Promise<boolean> {
|
|
16
|
-
let raw: string;
|
|
17
|
-
try {
|
|
18
|
-
raw = readFileSync(filePath, 'utf-8');
|
|
19
|
-
} catch {
|
|
20
|
-
error(`Cannot read file: ${filePath}`);
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let cert: unknown;
|
|
25
|
-
try {
|
|
26
|
-
cert = JSON.parse(raw);
|
|
27
|
-
} catch {
|
|
28
|
-
error('Invalid JSON in certificate file.');
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!isValidCertificate(cert)) {
|
|
33
|
-
error('File is not a valid lastgen certificate.');
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
info(`Verifying certificate ${cert.certificateNumber}...`);
|
|
38
|
-
info(`Developer: ${cert.identity.username}`);
|
|
39
|
-
info('');
|
|
40
|
-
|
|
41
|
-
if (cert.proof.firstCommit.sha) {
|
|
42
|
-
info(`Fetching commit ${cert.proof.firstCommit.sha.slice(0, 7)} from GitHub...`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const { valid, results } = await verifyCertificateData(cert, hashFn, token);
|
|
46
|
-
|
|
47
|
-
const out = process.stdout;
|
|
48
|
-
const lines: string[] = [];
|
|
49
|
-
|
|
50
|
-
lines.push(boxRule());
|
|
51
|
-
|
|
52
|
-
const title = style('bold', 'VERIFICATION');
|
|
53
|
-
const titleRaw = 'VERIFICATION';
|
|
54
|
-
const titlePadL = Math.floor((BOX_WIDTH - titleRaw.length) / 2);
|
|
55
|
-
const titlePadR = BOX_WIDTH - titleRaw.length - titlePadL;
|
|
56
|
-
lines.push(boxLine(' '.repeat(titlePadL) + title + ' '.repeat(titlePadR), BOX_WIDTH));
|
|
57
|
-
|
|
58
|
-
lines.push(boxRule());
|
|
59
|
-
|
|
60
|
-
for (const result of results) {
|
|
61
|
-
const icon = result.passed ? style('green', 'PASS') : style('red', 'FAIL');
|
|
62
|
-
const checkLine = `${icon} ${style('bold', result.check)}`;
|
|
63
|
-
lines.push(boxLine(checkLine, 4 + 2 + result.check.length));
|
|
64
|
-
const indent = 6;
|
|
65
|
-
const maxLen = BOX_WIDTH - indent;
|
|
66
|
-
let remaining = result.detail;
|
|
67
|
-
while (remaining.length > 0) {
|
|
68
|
-
const chunk = remaining.slice(0, maxLen);
|
|
69
|
-
remaining = remaining.slice(maxLen);
|
|
70
|
-
const line = ' '.repeat(indent) + style('dim', chunk);
|
|
71
|
-
lines.push(boxLine(line, indent + chunk.length));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
lines.push(boxRule());
|
|
76
|
-
|
|
77
|
-
if (valid) {
|
|
78
|
-
const msg = style('green', 'Certificate is valid.');
|
|
79
|
-
lines.push(boxLine(msg, 21));
|
|
80
|
-
} else {
|
|
81
|
-
const msg = style('red', 'Certificate verification failed.');
|
|
82
|
-
lines.push(boxLine(msg, 32));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
lines.push(boxRule());
|
|
86
|
-
|
|
87
|
-
out.write('\n' + lines.join('\n') + '\n\n');
|
|
88
|
-
return valid;
|
|
89
|
-
}
|