@profullstack/agenticjobs 0.1.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/LICENSE +21 -0
- package/README.md +219 -0
- package/bin/agenticjobs-mcp.mjs +2 -0
- package/bin/agenticjobs.mjs +2 -0
- package/dist/cli/args.d.ts +17 -0
- package/dist/cli/args.js +103 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/format.d.ts +15 -0
- package/dist/cli/format.js +24 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.js +620 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/jobfile.d.ts +15 -0
- package/dist/cli/jobfile.js +94 -0
- package/dist/cli/jobfile.js.map +1 -0
- package/dist/client/client.d.ts +105 -0
- package/dist/client/client.js +182 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/config.d.ts +33 -0
- package/dist/client/config.js +92 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/fanout.d.ts +35 -0
- package/dist/client/fanout.js +77 -0
- package/dist/client/fanout.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/login.d.ts +23 -0
- package/dist/client/login.js +45 -0
- package/dist/client/login.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/core/applications.d.ts +67 -0
- package/dist/core/applications.js +187 -0
- package/dist/core/applications.js.map +1 -0
- package/dist/core/auth.d.ts +120 -0
- package/dist/core/auth.js +246 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/import.d.ts +52 -0
- package/dist/core/import.js +311 -0
- package/dist/core/import.js.map +1 -0
- package/dist/core/jobs.d.ts +91 -0
- package/dist/core/jobs.js +381 -0
- package/dist/core/jobs.js.map +1 -0
- package/dist/core/orgs.d.ts +21 -0
- package/dist/core/orgs.js +111 -0
- package/dist/core/orgs.js.map +1 -0
- package/dist/core/resumes.d.ts +51 -0
- package/dist/core/resumes.js +144 -0
- package/dist/core/resumes.js.map +1 -0
- package/dist/core/zip.d.ts +17 -0
- package/dist/core/zip.js +77 -0
- package/dist/core/zip.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +15 -0
- package/dist/db/migrate.js +67 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/pool.d.ts +13 -0
- package/dist/db/pool.js +68 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/seed.d.ts +9 -0
- package/dist/db/seed.js +120 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/directory/announce.d.ts +32 -0
- package/dist/directory/announce.js +59 -0
- package/dist/directory/announce.js.map +1 -0
- package/dist/directory/federate.d.ts +59 -0
- package/dist/directory/federate.js +126 -0
- package/dist/directory/federate.js.map +1 -0
- package/dist/directory/fetch.d.ts +35 -0
- package/dist/directory/fetch.js +138 -0
- package/dist/directory/fetch.js.map +1 -0
- package/dist/directory/index.d.ts +4 -0
- package/dist/directory/index.js +5 -0
- package/dist/directory/index.js.map +1 -0
- package/dist/directory/registry.d.ts +55 -0
- package/dist/directory/registry.js +155 -0
- package/dist/directory/registry.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/markup/escape.d.ts +20 -0
- package/dist/markup/escape.js +56 -0
- package/dist/markup/escape.js.map +1 -0
- package/dist/markup/markdown.d.ts +35 -0
- package/dist/markup/markdown.js +279 -0
- package/dist/markup/markdown.js.map +1 -0
- package/dist/markup/resume.d.ts +72 -0
- package/dist/markup/resume.js +260 -0
- package/dist/markup/resume.js.map +1 -0
- package/dist/mcp/protocol.d.ts +56 -0
- package/dist/mcp/protocol.js +48 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.js +73 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +13 -0
- package/dist/mcp/stdio.js +85 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.js +351 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +6 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/instance.d.ts +85 -0
- package/dist/schema/instance.js +147 -0
- package/dist/schema/instance.js.map +1 -0
- package/dist/schema/job.d.ts +151 -0
- package/dist/schema/job.js +73 -0
- package/dist/schema/job.js.map +1 -0
- package/dist/schema/jsonld.d.ts +18 -0
- package/dist/schema/jsonld.js +120 -0
- package/dist/schema/jsonld.js.map +1 -0
- package/dist/schema/query.d.ts +47 -0
- package/dist/schema/query.js +103 -0
- package/dist/schema/query.js.map +1 -0
- package/dist/schema/text.d.ts +27 -0
- package/dist/schema/text.js +117 -0
- package/dist/schema/text.js.map +1 -0
- package/dist/server/app.d.ts +13 -0
- package/dist/server/app.js +57 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/deps.d.ts +20 -0
- package/dist/server/deps.js +2 -0
- package/dist/server/deps.js.map +1 -0
- package/dist/server/middleware.d.ts +39 -0
- package/dist/server/middleware.js +100 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/routes/api.d.ts +20 -0
- package/dist/server/routes/api.js +584 -0
- package/dist/server/routes/api.js.map +1 -0
- package/dist/server/routes/descriptor.d.ts +11 -0
- package/dist/server/routes/descriptor.js +31 -0
- package/dist/server/routes/descriptor.js.map +1 -0
- package/dist/server/routes/discovery.d.ts +12 -0
- package/dist/server/routes/discovery.js +305 -0
- package/dist/server/routes/discovery.js.map +1 -0
- package/dist/server/routes/mcp.d.ts +19 -0
- package/dist/server/routes/mcp.js +85 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/openapi.d.ts +11 -0
- package/dist/server/routes/openapi.js +297 -0
- package/dist/server/routes/openapi.js.map +1 -0
- package/dist/server/routes/pages.d.ts +11 -0
- package/dist/server/routes/pages.js +445 -0
- package/dist/server/routes/pages.js.map +1 -0
- package/dist/server/routes/passkey.d.ts +20 -0
- package/dist/server/routes/passkey.js +198 -0
- package/dist/server/routes/passkey.js.map +1 -0
- package/dist/server/routes/specs.d.ts +10 -0
- package/dist/server/routes/specs.js +42 -0
- package/dist/server/routes/specs.js.map +1 -0
- package/dist/server/serve.d.ts +11 -0
- package/dist/server/serve.js +79 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/tui/index.d.ts +13 -0
- package/dist/tui/index.js +266 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/state.d.ts +63 -0
- package/dist/tui/state.js +136 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/tui/types.d.ts +2 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/tui/views.d.ts +11 -0
- package/dist/tui/views.js +197 -0
- package/dist/tui/views.js.map +1 -0
- package/dist/views/auth.d.ts +21 -0
- package/dist/views/auth.js +5 -0
- package/dist/views/auth.js.map +1 -0
- package/dist/views/docs.d.ts +17 -0
- package/dist/views/docs.js +40 -0
- package/dist/views/docs.js.map +1 -0
- package/dist/views/jobs.d.ts +49 -0
- package/dist/views/jobs.js +44 -0
- package/dist/views/jobs.js.map +1 -0
- package/dist/views/layout.d.ts +58 -0
- package/dist/views/layout.js +36 -0
- package/dist/views/layout.js.map +1 -0
- package/dist/views/me.d.ts +27 -0
- package/dist/views/me.js +6 -0
- package/dist/views/me.js.map +1 -0
- package/dist/views/network.d.ts +21 -0
- package/dist/views/network.js +17 -0
- package/dist/views/network.js.map +1 -0
- package/dist/views/post.d.ts +22 -0
- package/dist/views/post.js +20 -0
- package/dist/views/post.js.map +1 -0
- package/docs/openjob.md +200 -0
- package/docs/openresume.md +143 -0
- package/migrations/0001_init.sql +109 -0
- package/migrations/0002_auth.sql +82 -0
- package/migrations/0003_directory.sql +27 -0
- package/migrations/0004_resumes.sql +63 -0
- package/migrations/0005_application_drafts.sql +20 -0
- package/package.json +92 -0
- package/web/public/app.css +731 -0
- package/web/public/app.js +173 -0
- package/web/public/icon.svg +5 -0
- package/web/public/sw.js +64 -0
- package/web/public/tokens.css +153 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who someone is, established four ways: a magic link, a passkey, an existing
|
|
3
|
+
* session cookie, or the device flow for anything without a browser.
|
|
4
|
+
*
|
|
5
|
+
* No passwords anywhere. That is the house rule and it is also the reason the
|
|
6
|
+
* device flow exists: a terminal cannot follow a magic link, so it asks the
|
|
7
|
+
* board for a short code instead and a browser approves it.
|
|
8
|
+
*/
|
|
9
|
+
import { hashToken, mintToken } from "../config.js";
|
|
10
|
+
import { clean } from "../schema/text.js";
|
|
11
|
+
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
|
+
const MAGIC_TTL_MS = 15 * 60 * 1000;
|
|
13
|
+
const DEVICE_TTL_MS = 10 * 60 * 1000;
|
|
14
|
+
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
15
|
+
export const SESSION_COOKIE = 'aj_session';
|
|
16
|
+
export function normaliseEmail(value) {
|
|
17
|
+
const email = clean(value, 200).toLowerCase();
|
|
18
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
|
19
|
+
return null;
|
|
20
|
+
return email;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Find or create the account for an address.
|
|
24
|
+
*
|
|
25
|
+
* The first account on a board becomes its administrator. Any other rule
|
|
26
|
+
* requires a setup step someone has to be told about, and an installer that
|
|
27
|
+
* leaves the operator locked out of their own board is not an installer.
|
|
28
|
+
*/
|
|
29
|
+
export async function ensureUser(pool, email, name) {
|
|
30
|
+
const existing = await pool.query(`select id, email, name, is_admin from users where lower(email) = lower($1)`, [email]);
|
|
31
|
+
const found = existing.rows[0];
|
|
32
|
+
if (found !== undefined)
|
|
33
|
+
return found;
|
|
34
|
+
const created = await pool.query(`insert into users (email, name, is_admin)
|
|
35
|
+
values ($1, $2, (select count(*) = 0 from users))
|
|
36
|
+
returning id, email, name, is_admin`, [email, name === undefined ? null : clean(name, 120) || null]);
|
|
37
|
+
const row = created.rows[0];
|
|
38
|
+
if (row === undefined)
|
|
39
|
+
throw new Error('user insert returned no row');
|
|
40
|
+
return row;
|
|
41
|
+
}
|
|
42
|
+
export async function createSession(pool, userId, options = {}) {
|
|
43
|
+
const token = mintToken();
|
|
44
|
+
await pool.query(`insert into sessions (user_id, token_hash, label, via_token, expires_at)
|
|
45
|
+
values ($1, $2, $3, $4, now() + ($5 || ' milliseconds')::interval)`, [
|
|
46
|
+
userId,
|
|
47
|
+
hashToken(token),
|
|
48
|
+
clean(options.label, 80) || 'session',
|
|
49
|
+
options.viaToken === true,
|
|
50
|
+
String(options.ttlMs ?? SESSION_TTL_MS),
|
|
51
|
+
]);
|
|
52
|
+
return token;
|
|
53
|
+
}
|
|
54
|
+
export async function viewerFromToken(pool, token) {
|
|
55
|
+
if (token.trim() === '')
|
|
56
|
+
return null;
|
|
57
|
+
const result = await pool.query(`select u.id, u.email, u.name, u.is_admin, s.via_token, s.id as session_id
|
|
58
|
+
from sessions s
|
|
59
|
+
join users u on u.id = s.user_id
|
|
60
|
+
where s.token_hash = $1 and s.expires_at > now()
|
|
61
|
+
limit 1`, [hashToken(token)]);
|
|
62
|
+
const row = result.rows[0];
|
|
63
|
+
if (row === undefined)
|
|
64
|
+
return null;
|
|
65
|
+
// Best effort: a failed touch must not cost the caller their request.
|
|
66
|
+
void pool
|
|
67
|
+
.query(`update sessions set last_used_at = now() where id = $1`, [row.session_id])
|
|
68
|
+
.catch(() => undefined);
|
|
69
|
+
return {
|
|
70
|
+
id: row.id,
|
|
71
|
+
email: row.email,
|
|
72
|
+
name: row.name,
|
|
73
|
+
// A device-flow token is never an administrator, and that is enforced HERE
|
|
74
|
+
// rather than only at the routes: the token was typed into a terminal and
|
|
75
|
+
// lives in a file on disk, while a browser session was established in
|
|
76
|
+
// front of the person it belongs to. Reporting `isAdmin: true` for one and
|
|
77
|
+
// relying on every call site to also check `viaToken` is how the third
|
|
78
|
+
// call site gets it wrong.
|
|
79
|
+
isAdmin: row.is_admin && !row.via_token,
|
|
80
|
+
viaToken: row.via_token,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function revokeSession(pool, token) {
|
|
84
|
+
await pool.query(`delete from sessions where token_hash = $1`, [hashToken(token)]);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A device-flow token is never an administrator.
|
|
88
|
+
*
|
|
89
|
+
* The token was typed into a terminal and lives in a file on disk; a browser
|
|
90
|
+
* session was established in front of the person it belongs to. Treating them
|
|
91
|
+
* as equivalent would make every CLI token a full admin credential.
|
|
92
|
+
*/
|
|
93
|
+
export function requireAdmin(viewer) {
|
|
94
|
+
return viewer !== null && viewer.isAdmin && !viewer.viaToken;
|
|
95
|
+
}
|
|
96
|
+
/** Only same-origin paths. An open redirect on a login link is a phish. */
|
|
97
|
+
export function safeRedirect(value) {
|
|
98
|
+
const raw = clean(value, 500);
|
|
99
|
+
if (raw === '' || !raw.startsWith('/') || raw.startsWith('//'))
|
|
100
|
+
return null;
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
export async function startMagicLink(pool, email, redirect) {
|
|
104
|
+
const token = mintToken();
|
|
105
|
+
const result = await pool.query(`insert into magic_links (token_hash, email, redirect, expires_at)
|
|
106
|
+
values ($1, $2, $3, now() + ($4 || ' milliseconds')::interval)
|
|
107
|
+
returning expires_at`, [hashToken(token), email, redirect, String(MAGIC_TTL_MS)]);
|
|
108
|
+
return { token, expiresAt: result.rows[0]?.expires_at ?? new Date().toISOString() };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Spend a magic link.
|
|
112
|
+
*
|
|
113
|
+
* The update is what claims it, not a read followed by a write: two clicks
|
|
114
|
+
* arriving together (a mail client prefetching the link, then the person
|
|
115
|
+
* clicking it) would both pass a read-then-write check.
|
|
116
|
+
*/
|
|
117
|
+
export async function consumeMagicLink(pool, token) {
|
|
118
|
+
const claimed = await pool.query(`update magic_links
|
|
119
|
+
set used_at = now()
|
|
120
|
+
where token_hash = $1 and used_at is null and expires_at > now()
|
|
121
|
+
returning email, redirect`, [hashToken(token)]);
|
|
122
|
+
const row = claimed.rows[0];
|
|
123
|
+
if (row === undefined)
|
|
124
|
+
return null;
|
|
125
|
+
const user = await ensureUser(pool, row.email);
|
|
126
|
+
return { user, redirect: row.redirect };
|
|
127
|
+
}
|
|
128
|
+
/** No characters that look like each other when read off a screen. */
|
|
129
|
+
const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
130
|
+
function userCode() {
|
|
131
|
+
const bytes = new Uint8Array(8);
|
|
132
|
+
crypto.getRandomValues(bytes);
|
|
133
|
+
let out = '';
|
|
134
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
135
|
+
if (index === 4)
|
|
136
|
+
out += '-';
|
|
137
|
+
out += CODE_ALPHABET[(bytes[index] ?? 0) % CODE_ALPHABET.length];
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
export async function startDeviceAuth(pool, label, publicUrl) {
|
|
142
|
+
const deviceCode = mintToken();
|
|
143
|
+
const code = userCode();
|
|
144
|
+
const result = await pool.query(`insert into device_codes (device_code_hash, user_code, label, expires_at)
|
|
145
|
+
values ($1, $2, $3, now() + ($4 || ' milliseconds')::interval)
|
|
146
|
+
returning expires_at`, [hashToken(deviceCode), code, clean(label, 80) || 'terminal', String(DEVICE_TTL_MS)]);
|
|
147
|
+
return {
|
|
148
|
+
deviceCode,
|
|
149
|
+
userCode: code,
|
|
150
|
+
verifyUrl: `${publicUrl}/device`,
|
|
151
|
+
interval: 2,
|
|
152
|
+
expiresAt: Date.parse(result.rows[0]?.expires_at ?? '') || Date.now() + DEVICE_TTL_MS,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Collect the token for an approved code.
|
|
157
|
+
*
|
|
158
|
+
* The token is cleared in the same statement that returns it, so it is handed
|
|
159
|
+
* over exactly once: a second poll with the same device code gets nothing,
|
|
160
|
+
* which is what makes a leaked poll response useless.
|
|
161
|
+
*
|
|
162
|
+
* An unknown or expired code reports `expired` rather than `pending`. A client
|
|
163
|
+
* told `pending` for a code the server has forgotten polls until its own
|
|
164
|
+
* timeout with no way to tell that it is waiting for nothing.
|
|
165
|
+
*/
|
|
166
|
+
export async function pollDeviceAuth(pool, deviceCode) {
|
|
167
|
+
const hash = hashToken(deviceCode);
|
|
168
|
+
// The token has to be read as it was BEFORE the update, and Postgres
|
|
169
|
+
// RETURNING gives post-update values - so a plain
|
|
170
|
+
// `update ... set token = null ... returning token` hands back the null it
|
|
171
|
+
// just wrote and the terminal waits forever. The CTE holds the old row (and
|
|
172
|
+
// locks it, so two polls cannot both claim), and RETURNING reads from that.
|
|
173
|
+
const claimed = await pool.query(`with claimed as (
|
|
174
|
+
select device_code_hash, token
|
|
175
|
+
from device_codes
|
|
176
|
+
where device_code_hash = $1 and status = 'approved' and token is not null
|
|
177
|
+
for update
|
|
178
|
+
)
|
|
179
|
+
update device_codes d
|
|
180
|
+
set token = null, status = 'collected'
|
|
181
|
+
from claimed
|
|
182
|
+
where d.device_code_hash = claimed.device_code_hash
|
|
183
|
+
returning claimed.token`, [hash]);
|
|
184
|
+
const token = claimed.rows[0]?.token;
|
|
185
|
+
if (typeof token === 'string')
|
|
186
|
+
return { status: 'approved', token };
|
|
187
|
+
const pending = await pool.query(`select status from device_codes
|
|
188
|
+
where device_code_hash = $1 and expires_at > now() and status = 'pending'`, [hash]);
|
|
189
|
+
return pending.rows.length > 0 ? { status: 'pending' } : { status: 'expired' };
|
|
190
|
+
}
|
|
191
|
+
export async function findDeviceCode(pool, code) {
|
|
192
|
+
const result = await pool.query(`select user_code, label from device_codes
|
|
193
|
+
where user_code = $1 and status = 'pending' and expires_at > now()`, [code.trim().toUpperCase()]);
|
|
194
|
+
const row = result.rows[0];
|
|
195
|
+
return row === undefined ? null : { userCode: row.user_code, label: row.label };
|
|
196
|
+
}
|
|
197
|
+
export async function approveDeviceCode(pool, code, userId) {
|
|
198
|
+
const label = await findDeviceCode(pool, code);
|
|
199
|
+
if (label === null)
|
|
200
|
+
return false;
|
|
201
|
+
// The terminal's token is marked via_token, which is what keeps it out of
|
|
202
|
+
// the admin routes for the rest of its life.
|
|
203
|
+
const token = await createSession(pool, userId, {
|
|
204
|
+
label: `terminal: ${label.label}`,
|
|
205
|
+
viaToken: true,
|
|
206
|
+
});
|
|
207
|
+
const updated = await pool.query(`update device_codes
|
|
208
|
+
set status = 'approved', user_id = $2, token = $3
|
|
209
|
+
where user_code = $1 and status = 'pending' and expires_at > now()`, [code.trim().toUpperCase(), userId, token]);
|
|
210
|
+
return (updated.rowCount ?? 0) > 0;
|
|
211
|
+
}
|
|
212
|
+
// --- webauthn challenges ------------------------------------------------
|
|
213
|
+
export async function storeChallenge(pool, challenge, purpose, who) {
|
|
214
|
+
await pool.query(`insert into webauthn_challenges (challenge, email, user_id, purpose, expires_at)
|
|
215
|
+
values ($1, $2, $3, $4, now() + ($5 || ' milliseconds')::interval)
|
|
216
|
+
on conflict (challenge) do nothing`, [
|
|
217
|
+
challenge,
|
|
218
|
+
who.email ?? null,
|
|
219
|
+
who.userId ?? null,
|
|
220
|
+
purpose,
|
|
221
|
+
String(CHALLENGE_TTL_MS),
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
/** Claimed on read: a challenge is single use by definition. */
|
|
225
|
+
export async function takeChallenge(pool, challenge) {
|
|
226
|
+
const result = await pool.query(`delete from webauthn_challenges
|
|
227
|
+
where challenge = $1 and expires_at > now()
|
|
228
|
+
returning challenge, email, user_id, purpose`, [challenge]);
|
|
229
|
+
const row = result.rows[0];
|
|
230
|
+
if (row === undefined)
|
|
231
|
+
return null;
|
|
232
|
+
return {
|
|
233
|
+
challenge: row.challenge,
|
|
234
|
+
email: row.email,
|
|
235
|
+
userId: row.user_id,
|
|
236
|
+
purpose: row.purpose,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Swept opportunistically rather than on a timer, so there is no worker. */
|
|
240
|
+
export async function sweepExpired(pool) {
|
|
241
|
+
await pool.query(`delete from sessions where expires_at < now()`);
|
|
242
|
+
await pool.query(`delete from magic_links where expires_at < now()`);
|
|
243
|
+
await pool.query(`delete from device_codes where expires_at < now() - interval '1 hour'`);
|
|
244
|
+
await pool.query(`delete from webauthn_challenges where expires_at < now()`);
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning whatever a candidate has into Markdown.
|
|
3
|
+
*
|
|
4
|
+
* The rule the rest of the board depends on: after this module runs there is
|
|
5
|
+
* exactly one representation of a resume, and it is Markdown the candidate can
|
|
6
|
+
* read and edit. An upload is an import step, not a storage format.
|
|
7
|
+
*
|
|
8
|
+
* Four inputs, in descending order of how well they survive the trip:
|
|
9
|
+
*
|
|
10
|
+
* .md used as-is; nothing is inferred and nothing is lost
|
|
11
|
+
* .txt structure guessed from blank lines and capitalisation, then edited
|
|
12
|
+
* .docx read directly here, because a .docx is a ZIP with one XML in it
|
|
13
|
+
* .pdf handed to pdftotext, because laying text back out of a PDF is a
|
|
14
|
+
* genuinely hard problem and poppler has already solved it
|
|
15
|
+
*
|
|
16
|
+
* External tools are used when they are installed and their absence is
|
|
17
|
+
* reported as a sentence a person can act on, never as a stack trace. The
|
|
18
|
+
* container image installs both, so this only degrades on a bare laptop.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_UPLOAD_BYTES: number;
|
|
21
|
+
export interface ImportResult {
|
|
22
|
+
markdown: string;
|
|
23
|
+
/** How it got here, for the "converted from your PDF" line on the editor. */
|
|
24
|
+
via: 'markdown' | 'text' | 'docx' | 'pdf' | 'pandoc';
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare class ImportProblem extends Error {
|
|
28
|
+
}
|
|
29
|
+
export declare function extensionOf(filename: string): string;
|
|
30
|
+
export declare function importDocument(filename: string, bytes: Buffer, mime?: string): Promise<ImportResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Plain text to Markdown.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately conservative. Only two things are inferred - a line that is
|
|
35
|
+
* short, standalone and title-cased or upper-case becomes a heading, and a
|
|
36
|
+
* line already starting with a bullet character becomes a list item - because
|
|
37
|
+
* the candidate is about to see the result in an editor and a wrong guess
|
|
38
|
+
* costs them more than a missing one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function textToMarkdown(source: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* .docx to Markdown, without a library.
|
|
43
|
+
*
|
|
44
|
+
* Word's XML is verbose but the parts that matter are few: a paragraph carries
|
|
45
|
+
* a style name that says whether it is a heading or a list item, and its text
|
|
46
|
+
* is the concatenation of the runs inside it. Bold and italic runs are mapped;
|
|
47
|
+
* everything else - fonts, colours, tracked changes - is dropped on purpose,
|
|
48
|
+
* because none of it survives into a Markdown resume anyway.
|
|
49
|
+
*/
|
|
50
|
+
export declare function docxToMarkdown(bytes: Buffer): Promise<ImportResult>;
|
|
51
|
+
/** Laying text back out of a PDF is poppler's job, not ours. */
|
|
52
|
+
export declare function pdfToMarkdown(bytes: Buffer): Promise<ImportResult>;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning whatever a candidate has into Markdown.
|
|
3
|
+
*
|
|
4
|
+
* The rule the rest of the board depends on: after this module runs there is
|
|
5
|
+
* exactly one representation of a resume, and it is Markdown the candidate can
|
|
6
|
+
* read and edit. An upload is an import step, not a storage format.
|
|
7
|
+
*
|
|
8
|
+
* Four inputs, in descending order of how well they survive the trip:
|
|
9
|
+
*
|
|
10
|
+
* .md used as-is; nothing is inferred and nothing is lost
|
|
11
|
+
* .txt structure guessed from blank lines and capitalisation, then edited
|
|
12
|
+
* .docx read directly here, because a .docx is a ZIP with one XML in it
|
|
13
|
+
* .pdf handed to pdftotext, because laying text back out of a PDF is a
|
|
14
|
+
* genuinely hard problem and poppler has already solved it
|
|
15
|
+
*
|
|
16
|
+
* External tools are used when they are installed and their absence is
|
|
17
|
+
* reported as a sentence a person can act on, never as a stack trace. The
|
|
18
|
+
* container image installs both, so this only degrades on a bare laptop.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { promisify } from 'node:util';
|
|
25
|
+
import { readZipEntry, ZipProblem } from "./zip.js";
|
|
26
|
+
const run = promisify(execFile);
|
|
27
|
+
export const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
28
|
+
export class ImportProblem extends Error {
|
|
29
|
+
}
|
|
30
|
+
export function extensionOf(filename) {
|
|
31
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(filename.trim());
|
|
32
|
+
return (match?.[1] ?? '').toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
export async function importDocument(filename, bytes, mime = '') {
|
|
35
|
+
if (bytes.length === 0)
|
|
36
|
+
throw new ImportProblem('That file is empty.');
|
|
37
|
+
if (bytes.length > MAX_UPLOAD_BYTES) {
|
|
38
|
+
throw new ImportProblem(`That file is ${Math.round(bytes.length / 1024 / 1024)}MB. The limit is ${MAX_UPLOAD_BYTES / 1024 / 1024}MB.`);
|
|
39
|
+
}
|
|
40
|
+
const extension = extensionOf(filename);
|
|
41
|
+
const kind = classify(extension, mime, bytes);
|
|
42
|
+
switch (kind) {
|
|
43
|
+
case 'markdown':
|
|
44
|
+
return { markdown: decodeText(bytes).trim(), via: 'markdown', warnings: [] };
|
|
45
|
+
case 'text':
|
|
46
|
+
return { markdown: textToMarkdown(decodeText(bytes)), via: 'text', warnings: [] };
|
|
47
|
+
case 'docx':
|
|
48
|
+
return docxToMarkdown(bytes);
|
|
49
|
+
case 'pdf':
|
|
50
|
+
return pdfToMarkdown(bytes);
|
|
51
|
+
default:
|
|
52
|
+
return legacyToMarkdown(bytes, extension);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function classify(extension, mime, bytes) {
|
|
56
|
+
// The magic bytes win over the extension. A resume renamed from .pdf to
|
|
57
|
+
// .docx is a mistake someone makes once, and the file itself is not lying.
|
|
58
|
+
if (bytes.subarray(0, 5).toString('latin1') === '%PDF-')
|
|
59
|
+
return 'pdf';
|
|
60
|
+
if (bytes.subarray(0, 2).toString('latin1') === 'PK' && (extension === 'docx' || mime.includes('wordprocessingml'))) {
|
|
61
|
+
return 'docx';
|
|
62
|
+
}
|
|
63
|
+
if (['md', 'markdown', 'mdown', 'mkd'].includes(extension))
|
|
64
|
+
return 'markdown';
|
|
65
|
+
if (extension === 'txt' || mime === 'text/plain')
|
|
66
|
+
return 'text';
|
|
67
|
+
if (extension === 'docx')
|
|
68
|
+
return 'docx';
|
|
69
|
+
if (extension === 'pdf')
|
|
70
|
+
return 'pdf';
|
|
71
|
+
if (['doc', 'odt', 'rtf', 'html', 'htm'].includes(extension))
|
|
72
|
+
return 'legacy';
|
|
73
|
+
// Anything that decodes cleanly as UTF-8 is treated as text rather than
|
|
74
|
+
// refused: a resume with no extension is still a resume.
|
|
75
|
+
return looksTextual(bytes) ? 'text' : 'legacy';
|
|
76
|
+
}
|
|
77
|
+
function looksTextual(bytes) {
|
|
78
|
+
const sample = bytes.subarray(0, 4096);
|
|
79
|
+
let control = 0;
|
|
80
|
+
for (const byte of sample) {
|
|
81
|
+
if (byte === 0)
|
|
82
|
+
return false;
|
|
83
|
+
if (byte < 9 || (byte > 13 && byte < 32))
|
|
84
|
+
control += 1;
|
|
85
|
+
}
|
|
86
|
+
return control / Math.max(1, sample.length) < 0.02;
|
|
87
|
+
}
|
|
88
|
+
function decodeText(bytes) {
|
|
89
|
+
// A BOM left in place shows up as a stray character in the first heading.
|
|
90
|
+
const text = bytes.toString('utf8');
|
|
91
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Plain text to Markdown.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately conservative. Only two things are inferred - a line that is
|
|
97
|
+
* short, standalone and title-cased or upper-case becomes a heading, and a
|
|
98
|
+
* line already starting with a bullet character becomes a list item - because
|
|
99
|
+
* the candidate is about to see the result in an editor and a wrong guess
|
|
100
|
+
* costs them more than a missing one.
|
|
101
|
+
*/
|
|
102
|
+
export function textToMarkdown(source) {
|
|
103
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
104
|
+
const out = [];
|
|
105
|
+
let first = true;
|
|
106
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
107
|
+
const line = (lines[index] ?? '').trimEnd();
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (trimmed === '') {
|
|
110
|
+
out.push('');
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const bullet = /^[•·▪●*+-]\s+(.*)$/.exec(trimmed);
|
|
114
|
+
if (bullet !== null) {
|
|
115
|
+
out.push(`- ${bullet[1] ?? ''}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (first) {
|
|
119
|
+
out.push(`# ${trimmed}`);
|
|
120
|
+
first = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const previousBlank = (lines[index - 1] ?? '').trim() === '';
|
|
124
|
+
const nextBlank = (lines[index + 1] ?? '').trim() === '';
|
|
125
|
+
const heading = previousBlank &&
|
|
126
|
+
nextBlank &&
|
|
127
|
+
trimmed.length <= 60 &&
|
|
128
|
+
!trimmed.endsWith('.') &&
|
|
129
|
+
(trimmed === trimmed.toUpperCase() || /^[A-Z][a-z]+(\s+[A-Za-z&/]+){0,4}$/.test(trimmed));
|
|
130
|
+
out.push(heading ? `## ${titleCase(trimmed)}` : trimmed);
|
|
131
|
+
}
|
|
132
|
+
return collapseBlankRuns(out.join('\n')).trim();
|
|
133
|
+
}
|
|
134
|
+
function titleCase(value) {
|
|
135
|
+
if (value !== value.toUpperCase())
|
|
136
|
+
return value;
|
|
137
|
+
return value
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/\b([a-z])/g, (character) => character.toUpperCase());
|
|
140
|
+
}
|
|
141
|
+
function collapseBlankRuns(value) {
|
|
142
|
+
return value.replace(/\n{3,}/g, '\n\n');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* .docx to Markdown, without a library.
|
|
146
|
+
*
|
|
147
|
+
* Word's XML is verbose but the parts that matter are few: a paragraph carries
|
|
148
|
+
* a style name that says whether it is a heading or a list item, and its text
|
|
149
|
+
* is the concatenation of the runs inside it. Bold and italic runs are mapped;
|
|
150
|
+
* everything else - fonts, colours, tracked changes - is dropped on purpose,
|
|
151
|
+
* because none of it survives into a Markdown resume anyway.
|
|
152
|
+
*/
|
|
153
|
+
export async function docxToMarkdown(bytes) {
|
|
154
|
+
const warnings = [];
|
|
155
|
+
let xml;
|
|
156
|
+
try {
|
|
157
|
+
xml = readZipEntry(bytes, 'word/document.xml');
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error instanceof ZipProblem)
|
|
161
|
+
throw new ImportProblem(error.message);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
if (xml === null) {
|
|
165
|
+
throw new ImportProblem('That .docx has no word/document.xml inside it.');
|
|
166
|
+
}
|
|
167
|
+
const document = xml.toString('utf8');
|
|
168
|
+
const out = [];
|
|
169
|
+
const paragraphs = document.match(/<w:p[ >][\s\S]*?<\/w:p>|<w:p\/>/g) ?? [];
|
|
170
|
+
for (const paragraph of paragraphs) {
|
|
171
|
+
const style = /<w:pStyle[^>]*w:val="([^"]+)"/.exec(paragraph)?.[1] ?? '';
|
|
172
|
+
const isList = /<w:numPr[ >]/.test(paragraph);
|
|
173
|
+
const text = runsToText(paragraph).trim();
|
|
174
|
+
if (text === '') {
|
|
175
|
+
out.push('');
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const headingLevel = /^Heading(\d)$/i.exec(style)?.[1];
|
|
179
|
+
if (headingLevel !== undefined) {
|
|
180
|
+
out.push('', `${'#'.repeat(Math.min(6, Number(headingLevel)))} ${stripMarks(text)}`, '');
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (/^Title$/i.test(style)) {
|
|
184
|
+
out.push('', `# ${stripMarks(text)}`, '');
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (isList) {
|
|
188
|
+
out.push(`- ${text}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
out.push(text);
|
|
192
|
+
}
|
|
193
|
+
const tables = (document.match(/<w:tbl[ >]/g) ?? []).length;
|
|
194
|
+
if (tables > 0) {
|
|
195
|
+
// Word tables are commonly used purely for two-column layout, so turning
|
|
196
|
+
// them into Markdown tables produces something worse than flat text.
|
|
197
|
+
warnings.push(`${tables} table${tables === 1 ? '' : 's'} in the document were flattened to plain lines. Check the layout.`);
|
|
198
|
+
}
|
|
199
|
+
const markdown = collapseBlankRuns(out.join('\n')).trim();
|
|
200
|
+
if (markdown === '')
|
|
201
|
+
throw new ImportProblem('No text could be read out of that .docx.');
|
|
202
|
+
return { markdown, via: 'docx', warnings };
|
|
203
|
+
}
|
|
204
|
+
function runsToText(paragraph) {
|
|
205
|
+
let out = '';
|
|
206
|
+
const runs = paragraph.match(/<w:r[ >][\s\S]*?<\/w:r>/g) ?? [];
|
|
207
|
+
for (const run of runs) {
|
|
208
|
+
const pieces = run.match(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g) ?? [];
|
|
209
|
+
let text = pieces
|
|
210
|
+
.map((piece) => decodeXml(piece.replace(/<[^>]+>/g, '')))
|
|
211
|
+
.join('');
|
|
212
|
+
if (text === '') {
|
|
213
|
+
// A tab or an explicit break carries meaning even with no text.
|
|
214
|
+
if (/<w:tab\b/.test(run))
|
|
215
|
+
out += ' ';
|
|
216
|
+
if (/<w:br\b/.test(run))
|
|
217
|
+
out += '\n';
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (/<w:b\/>|<w:b\s[^>]*w:val="(?:1|true|on)"/.test(run))
|
|
221
|
+
text = `**${text}**`;
|
|
222
|
+
if (/<w:i\/>|<w:i\s[^>]*w:val="(?:1|true|on)"/.test(run))
|
|
223
|
+
text = `*${text}*`;
|
|
224
|
+
out += text;
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
function stripMarks(value) {
|
|
229
|
+
return value.replace(/\*+/g, '').trim();
|
|
230
|
+
}
|
|
231
|
+
function decodeXml(value) {
|
|
232
|
+
return value
|
|
233
|
+
.replace(/</g, '<')
|
|
234
|
+
.replace(/>/g, '>')
|
|
235
|
+
.replace(/"/g, '"')
|
|
236
|
+
.replace(/'/g, "'")
|
|
237
|
+
.replace(/&#(\d+);/g, (_whole, code) => String.fromCodePoint(Number(code)))
|
|
238
|
+
.replace(/&/g, '&');
|
|
239
|
+
}
|
|
240
|
+
/** Laying text back out of a PDF is poppler's job, not ours. */
|
|
241
|
+
export async function pdfToMarkdown(bytes) {
|
|
242
|
+
const text = await withTempFile(bytes, 'pdf', async (path) => {
|
|
243
|
+
try {
|
|
244
|
+
// -layout keeps columns apart, which is the difference between a
|
|
245
|
+
// readable resume and every line of a two-column CV interleaved.
|
|
246
|
+
const { stdout } = await run('pdftotext', ['-layout', '-nopgbrk', '-enc', 'UTF-8', path, '-'], {
|
|
247
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
248
|
+
});
|
|
249
|
+
return stdout;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (isMissingBinary(error)) {
|
|
253
|
+
throw new ImportProblem('This board cannot read PDFs because pdftotext is not installed. Install poppler-utils, or upload a .md, .txt or .docx instead.');
|
|
254
|
+
}
|
|
255
|
+
throw new ImportProblem(`pdftotext could not read that PDF: ${error instanceof Error ? error.message : String(error)}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const markdown = textToMarkdown(text);
|
|
259
|
+
if (markdown.replace(/[#\s-]/g, '') === '') {
|
|
260
|
+
throw new ImportProblem('That PDF has no extractable text. It is probably a scan; upload a .md, .txt or .docx instead.');
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
markdown,
|
|
264
|
+
via: 'pdf',
|
|
265
|
+
warnings: [
|
|
266
|
+
'Converted from a PDF, so the structure is a guess. Check the headings before you save.',
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** .doc, .odt, .rtf and HTML, if pandoc happens to be installed. */
|
|
271
|
+
async function legacyToMarkdown(bytes, extension) {
|
|
272
|
+
return withTempFile(bytes, extension || 'bin', async (path) => {
|
|
273
|
+
try {
|
|
274
|
+
const { stdout } = await run('pandoc', ['--from', pandocFormat(extension), '--to', 'gfm', '--wrap=none', path], { maxBuffer: 16 * 1024 * 1024 });
|
|
275
|
+
return { markdown: collapseBlankRuns(stdout).trim(), via: 'pandoc', warnings: [] };
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (isMissingBinary(error)) {
|
|
279
|
+
throw new ImportProblem(`This board cannot read .${extension} files because pandoc is not installed. Upload a .md, .txt, .docx or .pdf instead.`);
|
|
280
|
+
}
|
|
281
|
+
throw new ImportProblem(`That .${extension} could not be converted: ${error instanceof Error ? error.message : String(error)}`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function pandocFormat(extension) {
|
|
286
|
+
if (extension === 'odt')
|
|
287
|
+
return 'odt';
|
|
288
|
+
if (extension === 'rtf')
|
|
289
|
+
return 'rtf';
|
|
290
|
+
if (extension === 'html' || extension === 'htm')
|
|
291
|
+
return 'html';
|
|
292
|
+
return 'doc';
|
|
293
|
+
}
|
|
294
|
+
function isMissingBinary(error) {
|
|
295
|
+
return (typeof error === 'object' &&
|
|
296
|
+
error !== null &&
|
|
297
|
+
'code' in error &&
|
|
298
|
+
error.code === 'ENOENT');
|
|
299
|
+
}
|
|
300
|
+
async function withTempFile(bytes, extension, work) {
|
|
301
|
+
const directory = await mkdtemp(join(tmpdir(), 'agenticjobs-'));
|
|
302
|
+
const path = join(directory, `upload.${extension}`);
|
|
303
|
+
try {
|
|
304
|
+
await writeFile(path, bytes);
|
|
305
|
+
return await work(path);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.js","sourceRoot":"","sources":["../../src/core/import.ts"],"names":[],"mappings":"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"}
|