@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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One query shape, used by the page, the API, MCP, the CLI and the federated
|
|
3
|
+
* search across instances.
|
|
4
|
+
*
|
|
5
|
+
* Federation is the reason this is a declared type rather than whatever
|
|
6
|
+
* querystring each surface felt like parsing: a search fanned out to twelve
|
|
7
|
+
* instances has to mean the same thing on all of them, and an instance running
|
|
8
|
+
* an older release has to be able to ignore a field it does not know without
|
|
9
|
+
* silently returning the wrong set.
|
|
10
|
+
*/
|
|
11
|
+
import { isAgentPolicy, isEmploymentType, isSeniority, isWorkplace, } from "./job.js";
|
|
12
|
+
export const SORTS = ['recent', 'relevant', 'salary'];
|
|
13
|
+
export const MAX_LIMIT = 100;
|
|
14
|
+
export const EMPTY_QUERY = {
|
|
15
|
+
q: null,
|
|
16
|
+
employmentType: null,
|
|
17
|
+
workplace: null,
|
|
18
|
+
seniority: null,
|
|
19
|
+
agentPolicy: null,
|
|
20
|
+
tags: [],
|
|
21
|
+
salaryMin: null,
|
|
22
|
+
org: null,
|
|
23
|
+
sort: 'recent',
|
|
24
|
+
limit: 25,
|
|
25
|
+
offset: 0,
|
|
26
|
+
};
|
|
27
|
+
function one(params, key) {
|
|
28
|
+
const value = params.get(key);
|
|
29
|
+
if (value === null)
|
|
30
|
+
return null;
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed === '' ? null : trimmed;
|
|
33
|
+
}
|
|
34
|
+
function integer(value, fallback, min, max) {
|
|
35
|
+
if (value === null)
|
|
36
|
+
return fallback;
|
|
37
|
+
const parsed = Number.parseInt(value, 10);
|
|
38
|
+
if (!Number.isFinite(parsed))
|
|
39
|
+
return fallback;
|
|
40
|
+
return Math.min(max, Math.max(min, parsed));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse a querystring into a query.
|
|
44
|
+
*
|
|
45
|
+
* Unknown values are dropped rather than rejected. A filter this instance does
|
|
46
|
+
* not understand must not turn a federated search into an error on one node
|
|
47
|
+
* and a result set on the rest — the user would see a shorter list and no
|
|
48
|
+
* explanation.
|
|
49
|
+
*/
|
|
50
|
+
export function parseQuery(params) {
|
|
51
|
+
const employmentType = one(params, 'employmentType');
|
|
52
|
+
const workplace = one(params, 'workplace');
|
|
53
|
+
const seniority = one(params, 'seniority');
|
|
54
|
+
const agentPolicy = one(params, 'agentPolicy');
|
|
55
|
+
const sort = one(params, 'sort');
|
|
56
|
+
const salaryMin = one(params, 'salaryMin');
|
|
57
|
+
return {
|
|
58
|
+
q: one(params, 'q'),
|
|
59
|
+
employmentType: isEmploymentType(employmentType) ? employmentType : null,
|
|
60
|
+
workplace: isWorkplace(workplace) ? workplace : null,
|
|
61
|
+
seniority: isSeniority(seniority) ? seniority : null,
|
|
62
|
+
agentPolicy: isAgentPolicy(agentPolicy) ? agentPolicy : null,
|
|
63
|
+
tags: params
|
|
64
|
+
.getAll('tag')
|
|
65
|
+
.flatMap((value) => value.split(','))
|
|
66
|
+
.map((value) => value.trim().toLowerCase())
|
|
67
|
+
.filter((value) => value !== '')
|
|
68
|
+
.slice(0, 10),
|
|
69
|
+
salaryMin: salaryMin === null ? null : integer(salaryMin, 0, 0, 100_000_000) || null,
|
|
70
|
+
org: one(params, 'org'),
|
|
71
|
+
sort: sort !== null && SORTS.includes(sort) ? sort : 'recent',
|
|
72
|
+
limit: integer(one(params, 'limit'), 25, 1, MAX_LIMIT),
|
|
73
|
+
offset: integer(one(params, 'offset'), 0, 0, 100_000),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The inverse, so a client can rebuild a link to the search it just ran. */
|
|
77
|
+
export function queryToParams(query) {
|
|
78
|
+
const params = new URLSearchParams();
|
|
79
|
+
if (query.q)
|
|
80
|
+
params.set('q', query.q);
|
|
81
|
+
if (query.employmentType)
|
|
82
|
+
params.set('employmentType', query.employmentType);
|
|
83
|
+
if (query.workplace)
|
|
84
|
+
params.set('workplace', query.workplace);
|
|
85
|
+
if (query.seniority)
|
|
86
|
+
params.set('seniority', query.seniority);
|
|
87
|
+
if (query.agentPolicy)
|
|
88
|
+
params.set('agentPolicy', query.agentPolicy);
|
|
89
|
+
for (const tag of query.tags)
|
|
90
|
+
params.append('tag', tag);
|
|
91
|
+
if (query.salaryMin !== null)
|
|
92
|
+
params.set('salaryMin', String(query.salaryMin));
|
|
93
|
+
if (query.org)
|
|
94
|
+
params.set('org', query.org);
|
|
95
|
+
if (query.sort !== 'recent')
|
|
96
|
+
params.set('sort', query.sort);
|
|
97
|
+
if (query.limit !== 25)
|
|
98
|
+
params.set('limit', String(query.limit));
|
|
99
|
+
if (query.offset !== 0)
|
|
100
|
+
params.set('offset', String(query.offset));
|
|
101
|
+
return params;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/schema/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small text helpers shared by the database layer, the views and the clients.
|
|
3
|
+
* They live here because a slug computed one way on the server and another way
|
|
4
|
+
* in the CLI produces two URLs for one job.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Trim, flatten control characters, cap.
|
|
8
|
+
*
|
|
9
|
+
* Written as a scan rather than a regex character class because this text is
|
|
10
|
+
* printed into a terminal by the TUI and the CLI as well as escaped into HTML
|
|
11
|
+
* by the views, and the escape that starts an ANSI sequence is exactly the
|
|
12
|
+
* kind of character a source-level class is easy to get subtly wrong.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clean(value: unknown, max: number): string;
|
|
15
|
+
export declare function slugify(value: string): string;
|
|
16
|
+
/** A short, unambiguous suffix for slug collisions. No l/1/0/O. */
|
|
17
|
+
export declare function suffix(length?: number): string;
|
|
18
|
+
export declare function parseList(value: unknown, max: number, each?: number): string[];
|
|
19
|
+
/** "$120k - $160k a year", or null when the employer did not say. */
|
|
20
|
+
export declare function formatSalary(salary: {
|
|
21
|
+
min: number | null;
|
|
22
|
+
max: number | null;
|
|
23
|
+
currency: string;
|
|
24
|
+
period: string;
|
|
25
|
+
}): string | null;
|
|
26
|
+
/** "3 days ago". Used identically in HTML and in the TUI. */
|
|
27
|
+
export declare function ago(iso: string | null, now?: number): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small text helpers shared by the database layer, the views and the clients.
|
|
3
|
+
* They live here because a slug computed one way on the server and another way
|
|
4
|
+
* in the CLI produces two URLs for one job.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Trim, flatten control characters, cap.
|
|
8
|
+
*
|
|
9
|
+
* Written as a scan rather than a regex character class because this text is
|
|
10
|
+
* printed into a terminal by the TUI and the CLI as well as escaped into HTML
|
|
11
|
+
* by the views, and the escape that starts an ANSI sequence is exactly the
|
|
12
|
+
* kind of character a source-level class is easy to get subtly wrong.
|
|
13
|
+
*/
|
|
14
|
+
export function clean(value, max) {
|
|
15
|
+
if (typeof value !== 'string')
|
|
16
|
+
return '';
|
|
17
|
+
let out = '';
|
|
18
|
+
for (const char of value) {
|
|
19
|
+
const code = char.codePointAt(0) ?? 0;
|
|
20
|
+
out += code < 0x20 || code === 0x7f ? ' ' : char;
|
|
21
|
+
}
|
|
22
|
+
return out.trim().slice(0, max);
|
|
23
|
+
}
|
|
24
|
+
export function slugify(value) {
|
|
25
|
+
const base = value
|
|
26
|
+
.normalize('NFKD')
|
|
27
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
30
|
+
.replace(/^-+|-+$/g, '')
|
|
31
|
+
.slice(0, 70)
|
|
32
|
+
.replace(/-+$/g, '');
|
|
33
|
+
// A title made entirely of punctuation or of a non-Latin script leaves
|
|
34
|
+
// nothing behind, and an empty slug collides with every other empty slug.
|
|
35
|
+
return base === '' ? 'job' : base;
|
|
36
|
+
}
|
|
37
|
+
/** A short, unambiguous suffix for slug collisions. No l/1/0/O. */
|
|
38
|
+
export function suffix(length = 5) {
|
|
39
|
+
const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
40
|
+
const bytes = new Uint8Array(length);
|
|
41
|
+
crypto.getRandomValues(bytes);
|
|
42
|
+
let out = '';
|
|
43
|
+
for (const byte of bytes)
|
|
44
|
+
out += alphabet[byte % alphabet.length];
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function parseList(value, max, each = 40) {
|
|
48
|
+
const raw = Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const item of raw) {
|
|
52
|
+
const cleaned = clean(item, each).toLowerCase();
|
|
53
|
+
if (cleaned === '' || seen.has(cleaned))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(cleaned);
|
|
56
|
+
out.push(cleaned);
|
|
57
|
+
if (out.length >= max)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
const SYMBOLS = {
|
|
63
|
+
USD: '$',
|
|
64
|
+
EUR: '\u20AC',
|
|
65
|
+
GBP: '\u00A3',
|
|
66
|
+
CAD: 'CA$',
|
|
67
|
+
AUD: 'A$',
|
|
68
|
+
JPY: '\u00A5',
|
|
69
|
+
};
|
|
70
|
+
/** "$120k - $160k a year", or null when the employer did not say. */
|
|
71
|
+
export function formatSalary(salary) {
|
|
72
|
+
if (salary.min === null && salary.max === null)
|
|
73
|
+
return null;
|
|
74
|
+
const money = (amount) => {
|
|
75
|
+
const symbol = SYMBOLS[salary.currency.toUpperCase()] ?? `${salary.currency.toUpperCase()} `;
|
|
76
|
+
if (amount >= 1000 && amount % 1000 === 0)
|
|
77
|
+
return `${symbol}${amount / 1000}k`;
|
|
78
|
+
return `${symbol}${amount.toLocaleString('en-US')}`;
|
|
79
|
+
};
|
|
80
|
+
const min = salary.min;
|
|
81
|
+
const max = salary.max;
|
|
82
|
+
const range = min !== null && max !== null && min !== max
|
|
83
|
+
? `${money(min)} - ${money(max)}`
|
|
84
|
+
: money((min ?? max));
|
|
85
|
+
return `${range} a ${salary.period}`;
|
|
86
|
+
}
|
|
87
|
+
/** Each entry: below this many seconds, report in the unit named. */
|
|
88
|
+
const UNITS = [
|
|
89
|
+
[3600, 60, 'minute'],
|
|
90
|
+
[86_400, 3600, 'hour'],
|
|
91
|
+
[604_800, 86_400, 'day'],
|
|
92
|
+
[2_629_800, 604_800, 'week'],
|
|
93
|
+
[31_557_600, 2_629_800, 'month'],
|
|
94
|
+
];
|
|
95
|
+
/** "3 days ago". Used identically in HTML and in the TUI. */
|
|
96
|
+
export function ago(iso, now = Date.now()) {
|
|
97
|
+
if (iso === null)
|
|
98
|
+
return 'not published';
|
|
99
|
+
const then = Date.parse(iso);
|
|
100
|
+
if (!Number.isFinite(then))
|
|
101
|
+
return 'unknown';
|
|
102
|
+
const seconds = Math.max(0, Math.round((now - then) / 1000));
|
|
103
|
+
if (seconds < 45)
|
|
104
|
+
return 'just now';
|
|
105
|
+
let divisor = 31_557_600;
|
|
106
|
+
let unit = 'year';
|
|
107
|
+
for (const entry of UNITS) {
|
|
108
|
+
if (seconds < entry[0]) {
|
|
109
|
+
divisor = entry[1];
|
|
110
|
+
unit = entry[2];
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const value = Math.max(1, Math.floor(seconds / divisor));
|
|
115
|
+
return `${value} ${unit}${value === 1 ? '' : 's'} ago`;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/schema/text.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The application, assembled.
|
|
3
|
+
*
|
|
4
|
+
* One Hono app carries every surface a browser or a machine can reach. The MCP
|
|
5
|
+
* endpoint dispatches through this app's own fetch rather than reaching into
|
|
6
|
+
* the database, so a tool call takes the same code path and the same
|
|
7
|
+
* permission checks a browser request does.
|
|
8
|
+
*/
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
import type pg from 'pg';
|
|
11
|
+
import type { Config } from '../config.ts';
|
|
12
|
+
import type { AppEnv } from './deps.ts';
|
|
13
|
+
export declare function createApp(pool: pg.Pool, config: Config): Hono<AppEnv>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The application, assembled.
|
|
4
|
+
*
|
|
5
|
+
* One Hono app carries every surface a browser or a machine can reach. The MCP
|
|
6
|
+
* endpoint dispatches through this app's own fetch rather than reaching into
|
|
7
|
+
* the database, so a tool call takes the same code path and the same
|
|
8
|
+
* permission checks a browser request does.
|
|
9
|
+
*/
|
|
10
|
+
import { Hono } from 'hono';
|
|
11
|
+
import { apiRoutes } from "./routes/api.js";
|
|
12
|
+
import { pageRoutes } from "./routes/pages.js";
|
|
13
|
+
import { discoveryRoutes } from "./routes/discovery.js";
|
|
14
|
+
import { mcpRoutes } from "./routes/mcp.js";
|
|
15
|
+
import { passkeyRoutes } from "./routes/passkey.js";
|
|
16
|
+
import { apiCors, securityHeaders, withViewer } from "./middleware.js";
|
|
17
|
+
import { Layout } from "../views/layout.js";
|
|
18
|
+
export function createApp(pool, config) {
|
|
19
|
+
const deps = { pool, config };
|
|
20
|
+
const app = new Hono();
|
|
21
|
+
app.use('*', withViewer(deps));
|
|
22
|
+
app.use('*', securityHeaders());
|
|
23
|
+
app.use('/api/*', apiCors());
|
|
24
|
+
app.route('/api/v1', apiRoutes());
|
|
25
|
+
// The MCP tools call this same app, so the getter is resolved lazily: the
|
|
26
|
+
// app does not exist yet at the point the routes are mounted on it.
|
|
27
|
+
app.route('/api/mcp', mcpRoutes(() => app));
|
|
28
|
+
app.route('/auth/passkey', passkeyRoutes());
|
|
29
|
+
app.route('/', discoveryRoutes());
|
|
30
|
+
app.route('/', pageRoutes());
|
|
31
|
+
app.notFound((c) => {
|
|
32
|
+
// A machine asking for JSON gets JSON. Handing an HTML 404 to a client
|
|
33
|
+
// that asked for JSON is how a parse error ends up masking a typo in a URL.
|
|
34
|
+
if (wantsJson(c.req.header('accept'), c.req.path)) {
|
|
35
|
+
return c.json({ error: { message: `Nothing at ${c.req.path}.`, code: 'not_found' } }, 404);
|
|
36
|
+
}
|
|
37
|
+
return c.html(_jsx(Layout, { viewer: c.get('viewer'), boardName: config.boardName, publicUrl: config.publicUrl, path: c.req.path, isDirectory: config.isDirectory, title: "Not found", noindex: true, children: _jsxs("div", { class: "empty", children: [_jsx("h1", { children: "Not found" }), _jsxs("p", { children: ["Nothing lives at ", _jsx("code", { children: c.req.path }), "."] }), _jsx("p", { children: _jsx("a", { class: "btn", href: "/", children: "Back to the jobs" }) })] }) }), 404);
|
|
38
|
+
});
|
|
39
|
+
app.onError((error, c) => {
|
|
40
|
+
// The message is logged, never returned: it can carry a query, a column
|
|
41
|
+
// name or a connection string.
|
|
42
|
+
console.error(`error on ${c.req.method} ${c.req.path}:`, error);
|
|
43
|
+
if (wantsJson(c.req.header('accept'), c.req.path)) {
|
|
44
|
+
return c.json({ error: { message: 'Something went wrong on this board.', code: 'internal' } }, 500);
|
|
45
|
+
}
|
|
46
|
+
return c.html(_jsx(Layout, { viewer: null, boardName: config.boardName, publicUrl: config.publicUrl, path: c.req.path, title: "Something went wrong", noindex: true, children: _jsxs("div", { class: "empty", children: [_jsx("h1", { children: "Something went wrong" }), _jsx("p", { children: "That is this board's fault, not yours. It has been logged." })] }) }), 500);
|
|
47
|
+
});
|
|
48
|
+
return app;
|
|
49
|
+
}
|
|
50
|
+
function wantsJson(accept, path) {
|
|
51
|
+
if (path.startsWith('/api/'))
|
|
52
|
+
return true;
|
|
53
|
+
if (accept === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
return accept.includes('application/json') && !accept.includes('text/html');
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type pg from 'pg';
|
|
2
|
+
import type { Config } from '../config.ts';
|
|
3
|
+
import type { Viewer } from '../core/auth.ts';
|
|
4
|
+
export interface Deps {
|
|
5
|
+
pool: pg.Pool;
|
|
6
|
+
config: Config;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Hono's per-request bag.
|
|
10
|
+
*
|
|
11
|
+
* `viewer` is resolved once by middleware and read everywhere else. Resolving
|
|
12
|
+
* it per handler is how a page ends up doing four session lookups, and how one
|
|
13
|
+
* handler forgets to and renders a signed-in person as a stranger.
|
|
14
|
+
*/
|
|
15
|
+
export interface AppEnv {
|
|
16
|
+
Variables: {
|
|
17
|
+
viewer: Viewer | null;
|
|
18
|
+
deps: Deps;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/server/deps.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-cutting request handling: who is asking, and what the browser is
|
|
3
|
+
* allowed to do with the answer.
|
|
4
|
+
*/
|
|
5
|
+
import type { MiddlewareHandler } from 'hono';
|
|
6
|
+
import type { AppEnv, Deps } from './deps.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the caller.
|
|
9
|
+
*
|
|
10
|
+
* Two sources, and the order matters. A bearer token wins over a cookie so
|
|
11
|
+
* that a person signed in to the board in one tab can still drive the API as
|
|
12
|
+
* a different identity from a script in another.
|
|
13
|
+
*
|
|
14
|
+
* A bad token is not an error here: it is simply nobody. Handlers that need
|
|
15
|
+
* an identity say so themselves, and the ones that do not - the job list, a
|
|
16
|
+
* job page - must keep working for a stranger with a stale token in their
|
|
17
|
+
* config file.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withViewer(deps: Deps): MiddlewareHandler<AppEnv>;
|
|
20
|
+
/**
|
|
21
|
+
* Security headers.
|
|
22
|
+
*
|
|
23
|
+
* The board ships no inline script and no third-party script, so the policy
|
|
24
|
+
* can be the strict one: `script-src 'self'` covers the single progressive
|
|
25
|
+
* enhancement file and nothing else. Styles need 'unsafe-inline' only because
|
|
26
|
+
* a few components carry an inline `style` attribute for a computed width -
|
|
27
|
+
* note that in CSP a style ATTRIBUTE is covered by style-src, not by
|
|
28
|
+
* style-src-attr alone, which is a distinction that has cost time before.
|
|
29
|
+
*/
|
|
30
|
+
export declare function securityHeaders(): MiddlewareHandler<AppEnv>;
|
|
31
|
+
/**
|
|
32
|
+
* CORS for the API only.
|
|
33
|
+
*
|
|
34
|
+
* Wide open for reads, because a federated search from another instance's
|
|
35
|
+
* browser is the point. Credentials are never reflected: a cross-origin caller
|
|
36
|
+
* authenticates with a bearer token it holds deliberately, never with a cookie
|
|
37
|
+
* the browser attached on its behalf.
|
|
38
|
+
*/
|
|
39
|
+
export declare function apiCors(): MiddlewareHandler<AppEnv>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-cutting request handling: who is asking, and what the browser is
|
|
3
|
+
* allowed to do with the answer.
|
|
4
|
+
*/
|
|
5
|
+
import { getCookie } from 'hono/cookie';
|
|
6
|
+
import { SESSION_COOKIE, viewerFromToken } from "../core/auth.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the caller.
|
|
9
|
+
*
|
|
10
|
+
* Two sources, and the order matters. A bearer token wins over a cookie so
|
|
11
|
+
* that a person signed in to the board in one tab can still drive the API as
|
|
12
|
+
* a different identity from a script in another.
|
|
13
|
+
*
|
|
14
|
+
* A bad token is not an error here: it is simply nobody. Handlers that need
|
|
15
|
+
* an identity say so themselves, and the ones that do not - the job list, a
|
|
16
|
+
* job page - must keep working for a stranger with a stale token in their
|
|
17
|
+
* config file.
|
|
18
|
+
*/
|
|
19
|
+
export function withViewer(deps) {
|
|
20
|
+
return async (c, next) => {
|
|
21
|
+
c.set('deps', deps);
|
|
22
|
+
c.set('viewer', null);
|
|
23
|
+
const header = c.req.header('authorization') ?? '';
|
|
24
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(header)?.[1]?.trim();
|
|
25
|
+
if (bearer !== undefined && bearer !== '') {
|
|
26
|
+
c.set('viewer', await viewerFromToken(deps.pool, bearer));
|
|
27
|
+
await next();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const cookie = getCookie(c, SESSION_COOKIE);
|
|
31
|
+
if (cookie !== undefined && cookie !== '') {
|
|
32
|
+
c.set('viewer', await viewerFromToken(deps.pool, cookie));
|
|
33
|
+
}
|
|
34
|
+
await next();
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Security headers.
|
|
39
|
+
*
|
|
40
|
+
* The board ships no inline script and no third-party script, so the policy
|
|
41
|
+
* can be the strict one: `script-src 'self'` covers the single progressive
|
|
42
|
+
* enhancement file and nothing else. Styles need 'unsafe-inline' only because
|
|
43
|
+
* a few components carry an inline `style` attribute for a computed width -
|
|
44
|
+
* note that in CSP a style ATTRIBUTE is covered by style-src, not by
|
|
45
|
+
* style-src-attr alone, which is a distinction that has cost time before.
|
|
46
|
+
*/
|
|
47
|
+
export function securityHeaders() {
|
|
48
|
+
const policy = [
|
|
49
|
+
`default-src 'self'`,
|
|
50
|
+
`base-uri 'self'`,
|
|
51
|
+
`form-action 'self'`,
|
|
52
|
+
`frame-ancestors 'none'`,
|
|
53
|
+
`object-src 'none'`,
|
|
54
|
+
`script-src 'self'`,
|
|
55
|
+
`style-src 'self' 'unsafe-inline'`,
|
|
56
|
+
// Employer logos and avatars come from wherever the employer hosts them.
|
|
57
|
+
// 'self' has to be listed explicitly: omitting it works in production and
|
|
58
|
+
// breaks every local asset, which is a fun afternoon.
|
|
59
|
+
`img-src 'self' https: data:`,
|
|
60
|
+
`font-src 'self'`,
|
|
61
|
+
// The board itself is same-origin, but a federated search in the browser
|
|
62
|
+
// talks to other instances over https.
|
|
63
|
+
`connect-src 'self' https:`,
|
|
64
|
+
].join('; ');
|
|
65
|
+
return async (c, next) => {
|
|
66
|
+
await next();
|
|
67
|
+
const headers = c.res.headers;
|
|
68
|
+
if (!headers.has('content-security-policy')) {
|
|
69
|
+
headers.set('content-security-policy', policy);
|
|
70
|
+
}
|
|
71
|
+
headers.set('x-content-type-options', 'nosniff');
|
|
72
|
+
headers.set('referrer-policy', 'strict-origin-when-cross-origin');
|
|
73
|
+
headers.set('x-frame-options', 'DENY');
|
|
74
|
+
headers.set('permissions-policy', 'geolocation=(), microphone=(), camera=()');
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* CORS for the API only.
|
|
79
|
+
*
|
|
80
|
+
* Wide open for reads, because a federated search from another instance's
|
|
81
|
+
* browser is the point. Credentials are never reflected: a cross-origin caller
|
|
82
|
+
* authenticates with a bearer token it holds deliberately, never with a cookie
|
|
83
|
+
* the browser attached on its behalf.
|
|
84
|
+
*/
|
|
85
|
+
export function apiCors() {
|
|
86
|
+
return async (c, next) => {
|
|
87
|
+
if (c.req.method === 'OPTIONS') {
|
|
88
|
+
return c.body(null, 204, {
|
|
89
|
+
'access-control-allow-origin': '*',
|
|
90
|
+
'access-control-allow-methods': 'GET, POST, PATCH, DELETE, OPTIONS',
|
|
91
|
+
'access-control-allow-headers': 'authorization, content-type',
|
|
92
|
+
'access-control-max-age': '86400',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
await next();
|
|
96
|
+
c.res.headers.set('access-control-allow-origin', '*');
|
|
97
|
+
c.res.headers.set('vary', 'origin');
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,IAAU;IACnC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACpB,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAEtB,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QAC5D,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAC1C,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC1D,MAAM,IAAI,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;QAC5C,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAC1C,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG;QACb,oBAAoB;QACpB,iBAAiB;QACjB,oBAAoB;QACpB,wBAAwB;QACxB,mBAAmB;QACnB,mBAAmB;QACnB,kCAAkC;QAClC,yEAAyE;QACzE,0EAA0E;QAC1E,sDAAsD;QACtD,6BAA6B;QAC7B,iBAAiB;QACjB,yEAAyE;QACzE,uCAAuC;QACvC,2BAA2B;KAC5B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,EAAE,CAAC;YAC5C,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,iCAAiC,CAAC,CAAC;QAClE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,0CAA0C,CAAC,CAAC;IAChF,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO;IACrB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE;gBACvB,6BAA6B,EAAE,GAAG;gBAClC,8BAA8B,EAAE,mCAAmC;gBACnE,8BAA8B,EAAE,6BAA6B;gBAC7D,wBAAwB,EAAE,OAAO;aAClC,CAAC,CAAC;QACL,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;QACb,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The REST API.
|
|
3
|
+
*
|
|
4
|
+
* Every other surface is a client of this: the CLI, the TUI, the desktop app,
|
|
5
|
+
* the MCP tools and one instance searching another. The HTML pages are the one
|
|
6
|
+
* exception, and only because they render server-side from the same core
|
|
7
|
+
* functions rather than from a second set of queries.
|
|
8
|
+
*
|
|
9
|
+
* Two rules hold throughout:
|
|
10
|
+
*
|
|
11
|
+
* 1. An error is an object with a message a person could act on, not a status
|
|
12
|
+
* code and an empty body. Half the callers here are models.
|
|
13
|
+
* 2. Reads are public unless the data belongs to someone. A job board whose
|
|
14
|
+
* listings need a key is a job board nobody's agent will ever read.
|
|
15
|
+
*/
|
|
16
|
+
import { Hono } from 'hono';
|
|
17
|
+
import { requireAdmin } from '../../core/auth.ts';
|
|
18
|
+
import type { AppEnv } from '../deps.ts';
|
|
19
|
+
export declare function apiRoutes(): Hono<AppEnv>;
|
|
20
|
+
export { requireAdmin };
|