th3w1zard1 commited on
Commit
b5762ec
·
verified ·
1 Parent(s): 89b048c

Deploy from community-bots@55469478c3a131b0ee2704d23b4ab5d4ce087f77

Browse files
Files changed (3) hide show
  1. README.md +6 -10
  2. package.json +1 -1
  3. server.mjs +62 -438
README.md CHANGED
@@ -1,11 +1,7 @@
1
- ---
2
- title: OpenKotOR Trask API
3
- emoji: 📚
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- ---
10
 
11
- Public Trask API fallback for Holocron `qa-webui`.
 
 
 
 
 
1
+ # holocron-trask-api (deprecated)
 
 
 
 
 
 
 
 
2
 
3
+ This Hugging Face Space previously served **bundled** technical-reference answers. That path was removed.
4
+
5
+ Use **`OpenKotOR/holocron-trask-http`** (`infra/trask-http-public/`) for full GPTR + `trask-http-server` instead.
6
+
7
+ This directory remains only as a **503 stub** if the old Space is still deployed by CI. Do not point production Holocron at it.
package.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "hf-trask-api",
3
  "private": true,
4
  "type": "module",
5
  "engines": {
 
1
  {
2
+ "name": "@openkotor/holocron-trask-api",
3
  "private": true,
4
  "type": "module",
5
  "engines": {
server.mjs CHANGED
@@ -1,458 +1,82 @@
1
- import http from 'node:http'
2
- import { randomUUID } from 'node:crypto'
 
3
 
4
- const PORT = Number.parseInt(process.env.PORT ?? process.env.TRASK_HTTP_PORT ?? '7860', 10)
5
 
6
- const STOPWORDS = new Set([
7
- 'what',
8
- 'where',
9
- 'when',
10
- 'which',
11
- 'who',
12
- 'how',
13
- 'used',
14
- 'use',
15
- 'usedfor',
16
- 'does',
17
- 'the',
18
- 'and',
19
- 'for',
20
- 'with',
21
- 'from',
22
- 'into',
23
- 'about',
24
- 'that',
25
- 'this',
26
- 'game',
27
- 'games',
28
- 'star',
29
- 'wars',
30
- 'knights',
31
- 'old',
32
- 'republic',
33
- 'kotor',
34
- 'pc',
35
- ])
36
-
37
- const REFERENCES = [
38
- {
39
- slug: 'tslpatcher',
40
- title: 'TSLPatcher - KOTOR mod installer',
41
- summary:
42
- 'TSLPatcher is the standard KotOR and TSL mod installer. Mod authors use it to patch 2DA, GFF, TLK, NSS, and related game data in place so a mod can merge changes into an existing installation instead of overwriting whole files.',
43
- tags: ['tooling', 'modding', 'tslpatcher', 'installer', '2da', 'gff', 'tlk', 'nss'],
44
- aliases: ['tslpatcher'],
45
- },
46
- {
47
- slug: 'mdlops',
48
- title: 'MDLOps - KOTOR model conversion tool',
49
- summary:
50
- 'MDLOps is a KotOR model conversion utility used to inspect, decompile, and rebuild MDL and MDX models. Modders use it in the asset pipeline when converting Odyssey engine models between editable formats and game-ready binaries.',
51
- tags: ['tooling', 'mdlops', 'models', 'conversion', 'mdx', 'mdl', 'odyssey'],
52
- aliases: ['mdlops'],
53
- },
54
- {
55
- slug: 'widescreen',
56
- title: 'KOTOR widescreen troubleshooting on PC',
57
- summary:
58
- 'KOTOR widescreen troubleshooting usually involves matching the game resolution, HUD and menu fixes, and graphics settings. Common checks are the target resolution in the game configuration, widescreen UI patches, and verifying that movies and the HUD are using assets that match the chosen aspect ratio.',
59
- tags: ['technical', 'widescreen', 'resolution', 'hud', 'graphics', 'pc', 'troubleshooting'],
60
- aliases: ['widescreen', 'resolution', 'hud', 'aspect ratio'],
61
- },
62
- {
63
- slug: 'save-files-windows',
64
- title: 'KOTOR save files on Windows',
65
- summary:
66
- 'On Windows, Knights of the Old Republic save files are typically stored under the game installation directory in the saves folder, or under the user game data area depending on the distribution. Troubleshooting usually starts by checking the install path used by Steam, GOG, or the retail release and then opening the saves directory inside that install.',
67
- tags: ['technical', 'save', 'windows', 'paths', 'troubleshooting', 'pc'],
68
- aliases: ['save files', 'save folder', 'windows saves'],
69
- },
70
- {
71
- slug: 'reone',
72
- title: 'reone - Odyssey engine reimplementation',
73
- summary:
74
- 'reone is an open-source reimplementation of the Odyssey engine used by KotOR. It provides engine-level code and runtime work for loading game assets, reproducing Odyssey behavior, and experimenting with modern tooling around the original game formats.',
75
- tags: ['tooling', 'engine', 'reone', 'odyssey', 'runtime', 'open-source'],
76
- aliases: ['reone'],
77
- },
78
- ]
79
-
80
- const SOURCE_DESCRIPTOR = {
81
- id: 'trask-technical-reference',
82
- name: 'Trask Technical Reference',
83
- kind: 'website',
84
- description: 'Built-in technical reference notes used by the public Holocron fallback API.',
85
- freshnessPolicy: 'Bundled with the deployed fallback service.',
86
- }
87
-
88
- /** @type {Map<string, any>} */
89
- const queryStore = new Map()
90
- /** @type {Map<string, string[]>} */
91
- const threadStore = new Map()
92
- /** @type {Map<string, string[]>} */
93
- const userHistoryStore = new Map()
94
-
95
- function normalizeOrigin(origin) {
96
- return typeof origin === 'string' && origin.trim() ? origin.trim() : '*'
97
- }
98
-
99
- function externalOrigin(req) {
100
- const forwardedProto = typeof req.headers['x-forwarded-proto'] === 'string'
101
- ? req.headers['x-forwarded-proto'].split(',')[0].trim()
102
- : ''
103
- const forwardedHost = typeof req.headers['x-forwarded-host'] === 'string'
104
- ? req.headers['x-forwarded-host'].split(',')[0].trim()
105
- : ''
106
- const host = forwardedHost || req.headers.host || `127.0.0.1:${PORT}`
107
- const proto = forwardedProto || (host.includes('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https')
108
- return `${proto}://${host}`
109
- }
110
-
111
- function writeJson(res, status, body, origin, extraHeaders = {}) {
112
- const headers = {
113
- 'Content-Type': 'application/json; charset=utf-8',
114
- 'Access-Control-Allow-Origin': normalizeOrigin(origin),
115
- 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
116
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
117
- Vary: 'Origin',
118
- ...extraHeaders,
119
- }
120
- res.writeHead(status, headers)
121
- res.end(JSON.stringify(body))
122
- }
123
-
124
- function writeText(res, status, body, origin, contentType = 'text/plain; charset=utf-8') {
125
- res.writeHead(status, {
126
- 'Content-Type': contentType,
127
- 'Access-Control-Allow-Origin': normalizeOrigin(origin),
128
- 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
129
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
130
- Vary: 'Origin',
131
- })
132
- res.end(body)
133
- }
134
-
135
- function tokenize(value) {
136
- return value
137
- .toLowerCase()
138
- .replace(/[^a-z0-9]+/g, ' ')
139
- .trim()
140
- .split(/\s+/)
141
- .filter(Boolean)
142
- .filter((token) => !STOPWORDS.has(token))
143
  }
144
 
145
- function scoreReference(query, reference) {
146
- const lowered = query.toLowerCase()
147
- for (const alias of reference.aliases) {
148
- if (lowered.includes(alias)) return 10_000
149
- }
150
-
151
- const queryTokens = tokenize(query)
152
- if (queryTokens.length === 0) return 0
153
-
154
- const titleTokens = tokenize(reference.title)
155
- const summaryTokens = tokenize(reference.summary)
156
- const tagTokens = reference.tags.flatMap((tag) => tokenize(tag))
157
-
158
- let score = 0
159
- for (const token of queryTokens) {
160
- score += titleTokens.filter((entry) => entry === token).length * 5
161
- score += tagTokens.filter((entry) => entry === token).length * 3
162
- score += summaryTokens.filter((entry) => entry === token).length
163
- }
164
- return score
165
- }
166
-
167
- function chooseReference(query) {
168
- return REFERENCES
169
- .map((reference) => ({ reference, score: scoreReference(query, reference) }))
170
- .sort((left, right) => right.score - left.score)[0] ?? null
171
- }
172
-
173
- function sourceUrl(origin, slug) {
174
- return new URL(`/reference/${slug}`, origin).toString()
175
- }
176
-
177
- function sourceForReference(origin, reference) {
178
  return {
179
- id: `${SOURCE_DESCRIPTOR.id}:${reference.slug}`,
180
- name: `${SOURCE_DESCRIPTOR.name}: ${reference.title}`,
181
- url: sourceUrl(origin, reference.slug),
182
- }
183
- }
184
-
185
- function buildFallbackAnswer(query, origin, match) {
186
- if (match && match.score > 0) {
187
- const source = sourceForReference(origin, match.reference)
188
- return {
189
- answer: [
190
- `Based on indexed KOTOR archive material, here is a concise answer about ${query}:`,
191
- '',
192
- `- ${match.reference.title}: ${match.reference.summary} [1]`,
193
- '',
194
- 'Sources',
195
- `1. ${source.name} - ${source.url}`,
196
- ].join('\n'),
197
- sources: [source],
198
- retrievedSources: [source],
199
- }
200
- }
201
-
202
- const supported = REFERENCES.map((reference, index) => `${index + 1}. ${reference.title}`).join('\n')
203
- return {
204
- answer: [
205
- `I do not have enough built-in evidence to answer "${query}" confidently from this public fallback API.`,
206
- '',
207
- 'The public fallback currently has bundled references for these technical topics:',
208
- supported,
209
- '',
210
- 'Sources',
211
- `1. ${SOURCE_DESCRIPTOR.name} - ${new URL('/reference', origin).toString()}`,
212
- ].join('\n'),
213
- sources: [
214
- {
215
- id: SOURCE_DESCRIPTOR.id,
216
- name: SOURCE_DESCRIPTOR.name,
217
- url: new URL('/reference', origin).toString(),
218
- },
219
- ],
220
- retrievedSources: [],
221
- }
222
- }
223
-
224
- function rememberRecord(record) {
225
- queryStore.set(record.queryId, record)
226
-
227
- const threadIds = threadStore.get(record.threadId) ?? []
228
- threadIds.unshift(record.queryId)
229
- threadStore.set(record.threadId, [...new Set(threadIds)].slice(0, 50))
230
-
231
- const historyIds = userHistoryStore.get(record.userId) ?? []
232
- historyIds.unshift(record.queryId)
233
- userHistoryStore.set(record.userId, [...new Set(historyIds)].slice(0, 100))
234
- }
235
-
236
- function recordsForIds(ids) {
237
- return ids
238
- .map((id) => queryStore.get(id))
239
- .filter(Boolean)
240
- .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
241
- }
242
-
243
- function parseBody(req) {
244
- return new Promise((resolve, reject) => {
245
- let data = ''
246
- req.on('data', (chunk) => {
247
- data += chunk.toString('utf8')
248
- if (data.length > 1024 * 1024) {
249
- reject(new Error('request body too large'))
250
- req.destroy()
251
- }
252
- })
253
- req.on('end', () => {
254
- if (!data.trim()) {
255
- resolve({})
256
- return
257
- }
258
- try {
259
- resolve(JSON.parse(data))
260
- } catch (error) {
261
- reject(error)
262
- }
263
- })
264
- req.on('error', reject)
265
- })
266
- }
267
-
268
- function isUuid(value) {
269
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
270
- }
271
-
272
- function referencePage(reference) {
273
- return `<!doctype html>
274
- <html lang="en">
275
- <head>
276
- <meta charset="utf-8" />
277
- <meta name="viewport" content="width=device-width, initial-scale=1" />
278
- <title>${reference.title}</title>
279
- <style>
280
- body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.6; background: #111827; color: #f9fafb; }
281
- h1 { color: #fca5a5; }
282
- code { background: rgba(255,255,255,0.08); padding: 0.1rem 0.3rem; border-radius: 4px; }
283
- a { color: #fca5a5; }
284
- </style>
285
- </head>
286
- <body>
287
- <h1>${reference.title}</h1>
288
- <p>${reference.summary}</p>
289
- <p><strong>Tags:</strong> ${reference.tags.join(', ')}</p>
290
- <p>This reference is bundled with the public Holocron fallback API.</p>
291
- </body>
292
- </html>`
293
- }
294
-
295
- const server = http.createServer(async (req, res) => {
296
- const origin = req.headers.origin ?? '*'
297
- const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host ?? `127.0.0.1:${PORT}`}`)
298
- const publicOrigin = externalOrigin(req)
299
-
300
- if (req.method === 'OPTIONS') {
301
- res.writeHead(204, {
302
- 'Access-Control-Allow-Origin': normalizeOrigin(origin),
303
- 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
304
  'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
305
  Vary: 'Origin',
306
- })
307
- res.end()
308
- return
309
- }
310
-
311
- if (requestUrl.pathname === '/' && req.method === 'GET') {
312
- writeText(res, 200, 'OpenKotOR Trask API is running.\n', origin)
313
- return
314
- }
315
-
316
- if (requestUrl.pathname === '/healthz' && req.method === 'GET') {
317
- writeJson(res, 200, { ok: true, mode: 'fallback-public-api' }, origin)
318
- return
319
- }
320
-
321
- if (requestUrl.pathname === '/reference' && req.method === 'GET') {
322
- const body = [
323
- '<!doctype html><html lang="en"><head><meta charset="utf-8" /><title>Trask Technical References</title></head><body>',
324
- '<h1>Trask Technical References</h1>',
325
- '<ul>',
326
- ...REFERENCES.map((reference) => `<li><a href="${new URL(`/reference/${reference.slug}`, publicOrigin).toString()}">${reference.title}</a></li>`),
327
- '</ul>',
328
- '</body></html>',
329
- ].join('')
330
- writeText(res, 200, body, origin, 'text/html; charset=utf-8')
331
- return
332
- }
333
-
334
- if (requestUrl.pathname.startsWith('/reference/') && req.method === 'GET') {
335
- const slug = decodeURIComponent(requestUrl.pathname.slice('/reference/'.length))
336
- const reference = REFERENCES.find((entry) => entry.slug === slug)
337
- if (!reference) {
338
- writeText(res, 404, 'Not found', origin)
339
- return
340
- }
341
- writeText(res, 200, referencePage(reference), origin, 'text/html; charset=utf-8')
342
- return
343
- }
344
 
345
- if (requestUrl.pathname === '/api/trask/session' && req.method === 'GET') {
346
- writeJson(res, 200, { loggedIn: false, oauthAvailable: false }, origin)
347
- return
348
- }
349
 
350
- if (requestUrl.pathname === '/api/trask/auth/logout' && req.method === 'POST') {
351
- writeJson(res, 204, {}, origin)
352
- return
353
- }
354
 
355
- if (requestUrl.pathname === '/api/trask/models' && req.method === 'GET') {
356
- writeJson(
357
- res,
358
- 200,
359
- { models: [{ id: 'auto', label: 'Auto', provider: 'Public fallback', recommended: true }] },
360
- origin,
361
- )
362
- return
363
- }
364
-
365
- if (requestUrl.pathname === '/api/trask/sources' && req.method === 'GET') {
366
- const sources = REFERENCES.map((reference) => ({
367
- ...SOURCE_DESCRIPTOR,
368
- id: `${SOURCE_DESCRIPTOR.id}:${reference.slug}`,
369
- name: `${SOURCE_DESCRIPTOR.name}: ${reference.title}`,
370
- homeUrl: sourceUrl(publicOrigin, reference.slug),
371
- }))
372
- writeJson(res, 200, { sources }, origin)
373
- return
374
  }
375
 
376
- if (requestUrl.pathname.startsWith('/api/trask/thread/') && req.method === 'GET') {
377
- const threadId = decodeURIComponent(requestUrl.pathname.slice('/api/trask/thread/'.length))
378
- if (!isUuid(threadId)) {
379
- writeJson(res, 400, { error: 'Invalid thread id.' }, origin)
380
- return
381
- }
382
- const ids = threadStore.get(threadId) ?? []
383
- writeJson(res, 200, { history: recordsForIds(ids) }, origin)
384
- return
385
  }
386
 
387
- if (requestUrl.pathname === '/api/trask/history' && req.method === 'GET') {
388
- const threadId = requestUrl.searchParams.get('thread')
389
- const limit = Math.max(1, Math.min(100, Number.parseInt(requestUrl.searchParams.get('limit') ?? '20', 10) || 20))
390
- const ids = threadId ? (threadStore.get(threadId) ?? []) : (userHistoryStore.get('qa-webui') ?? [])
391
- writeJson(res, 200, { history: recordsForIds(ids).slice(0, limit) }, origin)
392
- return
393
  }
394
 
395
- if (requestUrl.pathname.startsWith('/api/trask/query/') && requestUrl.pathname.endsWith('/cancel') && req.method === 'POST') {
396
- const queryId = decodeURIComponent(requestUrl.pathname.slice('/api/trask/query/'.length, -'/cancel'.length))
397
- const record = queryStore.get(queryId)
398
- if (!record) {
399
- writeJson(res, 404, { error: 'Query not found.' }, origin)
400
- return
401
- }
402
- writeJson(res, 200, { query: record }, origin)
403
- return
404
  }
405
 
406
- if (requestUrl.pathname === '/api/trask/ask' && req.method === 'POST') {
407
- try {
408
- const body = await parseBody(req)
409
- const query = typeof body.query === 'string' ? body.query.trim() : ''
410
- if (!query) {
411
- writeJson(res, 400, { error: 'Query is required.' }, origin)
412
- return
413
- }
414
-
415
- const providedThreadId = typeof body.threadId === 'string' ? body.threadId.trim() : ''
416
- if (providedThreadId && !isUuid(providedThreadId)) {
417
- writeJson(res, 422, { error: 'threadId must be a valid UUID.' }, origin)
418
- return
419
- }
420
-
421
- const threadId = providedThreadId || randomUUID()
422
- const queryId = randomUUID()
423
- const now = new Date().toISOString()
424
- const match = chooseReference(query)
425
- const { answer, sources, retrievedSources } = buildFallbackAnswer(query, publicOrigin, match)
426
- const record = {
427
- queryId,
428
- threadId,
429
- userId: 'qa-webui',
430
- query,
431
- status: 'complete',
432
- answer,
433
- sources,
434
- retrievedSources,
435
- visitedUrls: [],
436
- error: null,
437
- createdAt: now,
438
- completedAt: now,
439
- liveTrace: [
440
- { at: now, phase: 'queued', detail: 'Fallback public Trask query accepted.' },
441
- { at: now, phase: 'compose', detail: 'Rendered bundled technical reference answer.' },
442
- ],
443
- }
444
- rememberRecord(record)
445
- writeJson(res, 201, { query: record }, origin)
446
- return
447
- } catch (error) {
448
- writeJson(res, 500, { error: error instanceof Error ? error.message : 'Unknown error.' }, origin)
449
- return
450
- }
451
- }
452
 
453
- writeJson(res, 404, { error: 'Not found' }, origin)
454
- })
455
 
456
- server.listen(PORT, '0.0.0.0', () => {
457
- console.log(`hf-trask-api listening on ${PORT}`)
458
- })
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Deprecated HF mirror — bundled reference Q&A removed. Deploy holocron-trask-http for live GPTR.
3
+ */
4
 
5
+ import { createServer } from 'node:http';
6
 
7
+ function normalizeCorsOrigin(origin) {
8
+ return origin?.trim() ? origin.trim() : '*';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
+ function jsonResponse(status, body, origin) {
12
+ const payload = JSON.stringify(body);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  return {
14
+ status,
15
+ headers: {
16
+ 'Content-Type': 'application/json; charset=utf-8',
17
+ 'Content-Length': String(Buffer.byteLength(payload)),
18
+ 'Access-Control-Allow-Origin': normalizeCorsOrigin(origin),
19
+ 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
21
  Vary: 'Origin',
22
+ },
23
+ body: payload,
24
+ };
25
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ const LIVE_RESEARCH_REQUIRED = {
28
+ error:
29
+ 'Bundled reference answers are disabled on this Space. Use OpenKotOR/holocron-trask-http (full trask-http-server + GPTR) as TRASK_RESEARCHWIZARD_BASE_URL.',
30
+ };
31
 
32
+ async function handleRequest(request) {
33
+ const origin = request.headers.get('origin');
34
+ const url = new URL(request.url);
 
35
 
36
+ if (request.method === 'OPTIONS') {
37
+ return {
38
+ status: 204,
39
+ headers: {
40
+ 'Access-Control-Allow-Origin': normalizeCorsOrigin(origin),
41
+ 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
42
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
43
+ Vary: 'Origin',
44
+ },
45
+ body: '',
46
+ };
 
 
 
 
 
 
 
 
47
  }
48
 
49
+ if (url.pathname === '/' && request.method === 'GET') {
50
+ return {
51
+ status: 200,
52
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
53
+ body: 'Holocron Trask API — live GPTR only (bundled references removed).\n',
54
+ };
 
 
 
55
  }
56
 
57
+ if (url.pathname === '/healthz' && request.method === 'GET') {
58
+ return jsonResponse(200, { ok: true, mode: 'live-gptr-required', bundledReferenceApi: false }, origin);
 
 
 
 
59
  }
60
 
61
+ if (url.pathname.startsWith('/reference') || url.pathname.startsWith('/api/trask')) {
62
+ return jsonResponse(503, LIVE_RESEARCH_REQUIRED, origin);
 
 
 
 
 
 
 
63
  }
64
 
65
+ return jsonResponse(404, { error: 'Not found' }, origin);
66
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ const port = Number(process.env.PORT || 7860);
 
69
 
70
+ createServer(async (req, res) => {
71
+ const request = new Request(`http://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {
72
+ method: req.method,
73
+ headers: req.headers,
74
+ body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req,
75
+ duplex: 'half',
76
+ });
77
+ const response = await handleRequest(request);
78
+ res.writeHead(response.status, response.headers);
79
+ res.end(response.body);
80
+ }).listen(port, () => {
81
+ console.log(`holocron-trask-api stub listening on ${port}`);
82
+ });