@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.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/bin/agenticjobs-mcp.mjs +2 -0
  4. package/bin/agenticjobs.mjs +2 -0
  5. package/dist/cli/args.d.ts +17 -0
  6. package/dist/cli/args.js +103 -0
  7. package/dist/cli/args.js.map +1 -0
  8. package/dist/cli/format.d.ts +15 -0
  9. package/dist/cli/format.js +24 -0
  10. package/dist/cli/format.js.map +1 -0
  11. package/dist/cli/index.d.ts +13 -0
  12. package/dist/cli/index.js +620 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/cli/jobfile.d.ts +15 -0
  15. package/dist/cli/jobfile.js +94 -0
  16. package/dist/cli/jobfile.js.map +1 -0
  17. package/dist/client/client.d.ts +105 -0
  18. package/dist/client/client.js +182 -0
  19. package/dist/client/client.js.map +1 -0
  20. package/dist/client/config.d.ts +33 -0
  21. package/dist/client/config.js +92 -0
  22. package/dist/client/config.js.map +1 -0
  23. package/dist/client/fanout.d.ts +35 -0
  24. package/dist/client/fanout.js +77 -0
  25. package/dist/client/fanout.js.map +1 -0
  26. package/dist/client/index.d.ts +4 -0
  27. package/dist/client/index.js +5 -0
  28. package/dist/client/index.js.map +1 -0
  29. package/dist/client/login.d.ts +23 -0
  30. package/dist/client/login.js +45 -0
  31. package/dist/client/login.js.map +1 -0
  32. package/dist/config.d.ts +40 -0
  33. package/dist/config.js +64 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/core/applications.d.ts +67 -0
  36. package/dist/core/applications.js +187 -0
  37. package/dist/core/applications.js.map +1 -0
  38. package/dist/core/auth.d.ts +120 -0
  39. package/dist/core/auth.js +246 -0
  40. package/dist/core/auth.js.map +1 -0
  41. package/dist/core/import.d.ts +52 -0
  42. package/dist/core/import.js +311 -0
  43. package/dist/core/import.js.map +1 -0
  44. package/dist/core/jobs.d.ts +91 -0
  45. package/dist/core/jobs.js +381 -0
  46. package/dist/core/jobs.js.map +1 -0
  47. package/dist/core/orgs.d.ts +21 -0
  48. package/dist/core/orgs.js +111 -0
  49. package/dist/core/orgs.js.map +1 -0
  50. package/dist/core/resumes.d.ts +51 -0
  51. package/dist/core/resumes.js +144 -0
  52. package/dist/core/resumes.js.map +1 -0
  53. package/dist/core/zip.d.ts +17 -0
  54. package/dist/core/zip.js +77 -0
  55. package/dist/core/zip.js.map +1 -0
  56. package/dist/db/index.d.ts +3 -0
  57. package/dist/db/index.js +3 -0
  58. package/dist/db/index.js.map +1 -0
  59. package/dist/db/migrate.d.ts +15 -0
  60. package/dist/db/migrate.js +67 -0
  61. package/dist/db/migrate.js.map +1 -0
  62. package/dist/db/pool.d.ts +13 -0
  63. package/dist/db/pool.js +68 -0
  64. package/dist/db/pool.js.map +1 -0
  65. package/dist/db/seed.d.ts +9 -0
  66. package/dist/db/seed.js +120 -0
  67. package/dist/db/seed.js.map +1 -0
  68. package/dist/directory/announce.d.ts +32 -0
  69. package/dist/directory/announce.js +59 -0
  70. package/dist/directory/announce.js.map +1 -0
  71. package/dist/directory/federate.d.ts +59 -0
  72. package/dist/directory/federate.js +126 -0
  73. package/dist/directory/federate.js.map +1 -0
  74. package/dist/directory/fetch.d.ts +35 -0
  75. package/dist/directory/fetch.js +138 -0
  76. package/dist/directory/fetch.js.map +1 -0
  77. package/dist/directory/index.d.ts +4 -0
  78. package/dist/directory/index.js +5 -0
  79. package/dist/directory/index.js.map +1 -0
  80. package/dist/directory/registry.d.ts +55 -0
  81. package/dist/directory/registry.js +155 -0
  82. package/dist/directory/registry.js.map +1 -0
  83. package/dist/index.d.ts +14 -0
  84. package/dist/index.js +14 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/markup/escape.d.ts +20 -0
  87. package/dist/markup/escape.js +56 -0
  88. package/dist/markup/escape.js.map +1 -0
  89. package/dist/markup/markdown.d.ts +35 -0
  90. package/dist/markup/markdown.js +279 -0
  91. package/dist/markup/markdown.js.map +1 -0
  92. package/dist/markup/resume.d.ts +72 -0
  93. package/dist/markup/resume.js +260 -0
  94. package/dist/markup/resume.js.map +1 -0
  95. package/dist/mcp/protocol.d.ts +56 -0
  96. package/dist/mcp/protocol.js +48 -0
  97. package/dist/mcp/protocol.js.map +1 -0
  98. package/dist/mcp/server.d.ts +21 -0
  99. package/dist/mcp/server.js +73 -0
  100. package/dist/mcp/server.js.map +1 -0
  101. package/dist/mcp/stdio.d.ts +13 -0
  102. package/dist/mcp/stdio.js +85 -0
  103. package/dist/mcp/stdio.js.map +1 -0
  104. package/dist/mcp/tools.d.ts +26 -0
  105. package/dist/mcp/tools.js +351 -0
  106. package/dist/mcp/tools.js.map +1 -0
  107. package/dist/schema/index.d.ts +5 -0
  108. package/dist/schema/index.js +6 -0
  109. package/dist/schema/index.js.map +1 -0
  110. package/dist/schema/instance.d.ts +85 -0
  111. package/dist/schema/instance.js +147 -0
  112. package/dist/schema/instance.js.map +1 -0
  113. package/dist/schema/job.d.ts +151 -0
  114. package/dist/schema/job.js +73 -0
  115. package/dist/schema/job.js.map +1 -0
  116. package/dist/schema/jsonld.d.ts +18 -0
  117. package/dist/schema/jsonld.js +120 -0
  118. package/dist/schema/jsonld.js.map +1 -0
  119. package/dist/schema/query.d.ts +47 -0
  120. package/dist/schema/query.js +103 -0
  121. package/dist/schema/query.js.map +1 -0
  122. package/dist/schema/text.d.ts +27 -0
  123. package/dist/schema/text.js +117 -0
  124. package/dist/schema/text.js.map +1 -0
  125. package/dist/server/app.d.ts +13 -0
  126. package/dist/server/app.js +57 -0
  127. package/dist/server/app.js.map +1 -0
  128. package/dist/server/deps.d.ts +20 -0
  129. package/dist/server/deps.js +2 -0
  130. package/dist/server/deps.js.map +1 -0
  131. package/dist/server/middleware.d.ts +39 -0
  132. package/dist/server/middleware.js +100 -0
  133. package/dist/server/middleware.js.map +1 -0
  134. package/dist/server/routes/api.d.ts +20 -0
  135. package/dist/server/routes/api.js +584 -0
  136. package/dist/server/routes/api.js.map +1 -0
  137. package/dist/server/routes/descriptor.d.ts +11 -0
  138. package/dist/server/routes/descriptor.js +31 -0
  139. package/dist/server/routes/descriptor.js.map +1 -0
  140. package/dist/server/routes/discovery.d.ts +12 -0
  141. package/dist/server/routes/discovery.js +305 -0
  142. package/dist/server/routes/discovery.js.map +1 -0
  143. package/dist/server/routes/mcp.d.ts +19 -0
  144. package/dist/server/routes/mcp.js +85 -0
  145. package/dist/server/routes/mcp.js.map +1 -0
  146. package/dist/server/routes/openapi.d.ts +11 -0
  147. package/dist/server/routes/openapi.js +297 -0
  148. package/dist/server/routes/openapi.js.map +1 -0
  149. package/dist/server/routes/pages.d.ts +11 -0
  150. package/dist/server/routes/pages.js +445 -0
  151. package/dist/server/routes/pages.js.map +1 -0
  152. package/dist/server/routes/passkey.d.ts +20 -0
  153. package/dist/server/routes/passkey.js +198 -0
  154. package/dist/server/routes/passkey.js.map +1 -0
  155. package/dist/server/routes/specs.d.ts +10 -0
  156. package/dist/server/routes/specs.js +42 -0
  157. package/dist/server/routes/specs.js.map +1 -0
  158. package/dist/server/serve.d.ts +11 -0
  159. package/dist/server/serve.js +79 -0
  160. package/dist/server/serve.js.map +1 -0
  161. package/dist/tui/index.d.ts +13 -0
  162. package/dist/tui/index.js +266 -0
  163. package/dist/tui/index.js.map +1 -0
  164. package/dist/tui/state.d.ts +63 -0
  165. package/dist/tui/state.js +136 -0
  166. package/dist/tui/state.js.map +1 -0
  167. package/dist/tui/types.d.ts +2 -0
  168. package/dist/tui/types.js +2 -0
  169. package/dist/tui/types.js.map +1 -0
  170. package/dist/tui/views.d.ts +11 -0
  171. package/dist/tui/views.js +197 -0
  172. package/dist/tui/views.js.map +1 -0
  173. package/dist/views/auth.d.ts +21 -0
  174. package/dist/views/auth.js +5 -0
  175. package/dist/views/auth.js.map +1 -0
  176. package/dist/views/docs.d.ts +17 -0
  177. package/dist/views/docs.js +40 -0
  178. package/dist/views/docs.js.map +1 -0
  179. package/dist/views/jobs.d.ts +49 -0
  180. package/dist/views/jobs.js +44 -0
  181. package/dist/views/jobs.js.map +1 -0
  182. package/dist/views/layout.d.ts +58 -0
  183. package/dist/views/layout.js +36 -0
  184. package/dist/views/layout.js.map +1 -0
  185. package/dist/views/me.d.ts +27 -0
  186. package/dist/views/me.js +6 -0
  187. package/dist/views/me.js.map +1 -0
  188. package/dist/views/network.d.ts +21 -0
  189. package/dist/views/network.js +17 -0
  190. package/dist/views/network.js.map +1 -0
  191. package/dist/views/post.d.ts +22 -0
  192. package/dist/views/post.js +20 -0
  193. package/dist/views/post.js.map +1 -0
  194. package/docs/openjob.md +200 -0
  195. package/docs/openresume.md +143 -0
  196. package/migrations/0001_init.sql +109 -0
  197. package/migrations/0002_auth.sql +82 -0
  198. package/migrations/0003_directory.sql +27 -0
  199. package/migrations/0004_resumes.sql +63 -0
  200. package/migrations/0005_application_drafts.sql +20 -0
  201. package/package.json +92 -0
  202. package/web/public/app.css +731 -0
  203. package/web/public/app.js +173 -0
  204. package/web/public/icon.svg +5 -0
  205. package/web/public/sw.js +64 -0
  206. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=deps.js.map
@@ -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 };