Compare commits
4
Commits
9e426e91ab
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c099a5daa | ||
|
|
625f28b00b | ||
|
|
f6fbeb39cf | ||
|
|
ce30321532 |
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"slug": "barricelli-phone-note",
|
||||||
|
"name": "Phone note",
|
||||||
|
"title": "Phone note",
|
||||||
|
"subtitle": "",
|
||||||
|
"brief": { "body": "", "concepts": [] },
|
||||||
|
"documents": [],
|
||||||
|
"folders": [],
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"title": "Note",
|
||||||
|
"content": "PHONE FOR GLITCH HUNTER\n\nCall: 5550100",
|
||||||
|
"presentation": "luggage",
|
||||||
|
"x": 420,
|
||||||
|
"y": 260,
|
||||||
|
"width": 230,
|
||||||
|
"height": 180
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import { readFile } from 'node:fs/promises'
|
import { readFile, stat } from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import type { CaseDocument, CaseState, ClaimExhibit, DocumentCaptureKind, PartyKind, SourceFileType } from '../src/types.js'
|
import type { CaseDocument, CaseState, ClaimExhibit, DocumentCaptureKind, NoteExhibit, NotePresentation, PartyKind, SourceFileType } from '../src/types.js'
|
||||||
|
|
||||||
type MysteryDocument = {
|
type MysteryDocument = {
|
||||||
key: string
|
key: string
|
||||||
@@ -37,22 +37,28 @@ type MysterySemanticRule = {
|
|||||||
goalKey:string; name:string; targetSubject:string; relatedSubject?:string; assertion:string; successFlagKey:string
|
goalKey:string; name:string; targetSubject:string; relatedSubject?:string; assertion:string; successFlagKey:string
|
||||||
relatedFlagKey?:string; minimumConfidence?:number; evaluatorVersion?:string; enabled?:boolean
|
relatedFlagKey?:string; minimumConfidence?:number; evaluatorVersion?:string; enabled?:boolean
|
||||||
}
|
}
|
||||||
type MysteryManifest = {
|
// A single playable board (level template). Assets are filenames resolved against
|
||||||
|
// the mystery folder (e.g. "assets/photo.png"), matching the on-disk layout.
|
||||||
|
type MysteryLevel = {
|
||||||
slug: string
|
slug: string
|
||||||
name: string
|
name: string
|
||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle?: string
|
||||||
timelineRange?: { start: string; end: string }
|
timelineRange?: { start: string; end: string }
|
||||||
brief: { body: string; concepts: { label: string; context: string; expectedPartyKind: PartyKind }[] }
|
brief?: { body: string; concepts: { label: string; context: string; expectedPartyKind: PartyKind }[] }
|
||||||
documents: MysteryDocument[]
|
documents?: MysteryDocument[]
|
||||||
folders: { key: string; title: string; content: string; x: number; y: number; width: number; members: string[] }[]
|
folders?: { key: string; title: string; content: string; x: number; y: number; width: number; members: string[] }[]
|
||||||
claims?: { key:string;statement:string;x:number;y:number;width?:number;height?:number }[]
|
claims?: { key:string;statement:string;x:number;y:number;width?:number;height?:number }[]
|
||||||
|
notes?: { title?:string;content:string;presentation?:NotePresentation;x:number;y:number;width?:number;height?:number }[]
|
||||||
report?: { title?:string;requiredForCompletion?:boolean }
|
report?: { title?:string;requiredForCompletion?:boolean }
|
||||||
goals?: MysteryGoal[]
|
goals?: MysteryGoal[]
|
||||||
evidenceMatchRules?: MysteryEvidenceMatchRule[]
|
evidenceMatchRules?: MysteryEvidenceMatchRule[]
|
||||||
evidenceSemanticRules?: MysterySemanticRule[]
|
evidenceSemanticRules?: MysterySemanticRule[]
|
||||||
narrative?: MysteryNarrative
|
|
||||||
}
|
}
|
||||||
|
// Legacy single-manifest: one level plus an optional narrative in the same file.
|
||||||
|
type MysteryManifest = MysteryLevel & { narrative?: MysteryNarrative }
|
||||||
|
// New self-contained format: a mystery with every level it uses embedded.
|
||||||
|
type MysteryFile = { slug: string; title: string; levels: MysteryLevel[]; narrative?: MysteryNarrative }
|
||||||
|
|
||||||
function requireOk(response: Response, action: string) {
|
function requireOk(response: Response, action: string) {
|
||||||
if (response.ok) return response
|
if (response.ok) return response
|
||||||
@@ -75,24 +81,30 @@ async function uploadAsset(baseUrl: string, levelId: string, manifestDir: string
|
|||||||
return await response.json() as CaseDocument
|
return await response.json() as CaseDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importMysteryTemplate(manifestPath: string, baseUrl = 'http://localhost:8787', adminJwt = process.env.OSINT_ADMIN_JWT) {
|
type ImportHeaders = Record<string, string>
|
||||||
const absoluteManifest = path.resolve(manifestPath)
|
function authHeaders(adminJwt?: string): { authorization?: string; headers: ImportHeaders } {
|
||||||
const manifest = JSON.parse(await readFile(absoluteManifest, 'utf8')) as MysteryManifest
|
|
||||||
const manifestDir = path.dirname(absoluteManifest)
|
|
||||||
const authoringId = `${manifest.slug}-authoring-${Date.now()}`
|
|
||||||
const authorization = adminJwt ? `Bearer ${adminJwt}` : undefined
|
const authorization = adminJwt ? `Bearer ${adminJwt}` : undefined
|
||||||
const headers = { 'content-type': 'application/json', ...(authorization ? { authorization } : {}) }
|
return { authorization, headers: { 'content-type': 'application/json', ...(authorization ? { authorization } : {}) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
type LevelResult = { template: { slug: string; currentVersion: number }; authoringLevelId: string; playableLevel: CaseState }
|
||||||
|
|
||||||
|
// Import one board: create a mutable authoring level, upload assets, save exhibits,
|
||||||
|
// goals and evidence rules, freeze an immutable template, and instantiate a playable copy.
|
||||||
|
async function importLevel(baseUrl: string, folderDir: string, level: MysteryLevel, headers: ImportHeaders, authorization?: string): Promise<LevelResult> {
|
||||||
|
const authoringId = `${level.slug}-authoring-${Date.now()}`
|
||||||
const createdResponse = await requireOk(await fetch(`${baseUrl}/api/levels`, {
|
const createdResponse = await requireOk(await fetch(`${baseUrl}/api/levels`, {
|
||||||
method: 'POST', headers,
|
method: 'POST', headers,
|
||||||
body: JSON.stringify({ id: authoringId, title: manifest.title, subtitle: manifest.subtitle }),
|
body: JSON.stringify({ id: authoringId, title: level.title, subtitle: level.subtitle || '' }),
|
||||||
}), 'Create authoring level')
|
}), 'Create authoring level')
|
||||||
const state = await createdResponse.json() as CaseState
|
const state = await createdResponse.json() as CaseState
|
||||||
|
|
||||||
const documentPositions = new Map<string, { x: number; y: number }>()
|
const documentPositions = new Map<string, { x: number; y: number }>()
|
||||||
manifest.folders.forEach((folder, folderIndex) => folder.members.forEach((key, memberIndex) => documentPositions.set(key, { x: folder.x + 70 + memberIndex * 205, y: folder.y + 230 + folderIndex * 35 })))
|
const levelFolders = level.folders || []
|
||||||
|
levelFolders.forEach((folder, folderIndex) => folder.members.forEach((key, memberIndex) => documentPositions.set(key, { x: folder.x + 70 + memberIndex * 205, y: folder.y + 230 + folderIndex * 35 })))
|
||||||
const documents = new Map<string, CaseDocument>()
|
const documents = new Map<string, CaseDocument>()
|
||||||
for (const source of manifest.documents) {
|
for (const source of level.documents || []) {
|
||||||
const uploaded = await uploadAsset(baseUrl, state.id, manifestDir, source, authorization)
|
const uploaded = await uploadAsset(baseUrl, state.id, folderDir, source, authorization)
|
||||||
documents.set(source.key, {
|
documents.set(source.key, {
|
||||||
id: uploaded?.id || randomUUID(), type: 'document', title: source.title, publishedAt: source.publishedAt,
|
id: uploaded?.id || randomUUID(), type: 'document', title: source.title, publishedAt: source.publishedAt,
|
||||||
x: documentPositions.get(source.key)?.x || 100, y: documentPositions.get(source.key)?.y || 100,
|
x: documentPositions.get(source.key)?.x || 100, y: documentPositions.get(source.key)?.y || 100,
|
||||||
@@ -103,17 +115,19 @@ export async function importMysteryTemplate(manifestPath: string, baseUrl = 'htt
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderIds = new Map(manifest.folders.map(folder => [folder.key, randomUUID()]))
|
const folderIds = new Map(levelFolders.map(folder => [folder.key, randomUUID()]))
|
||||||
state.brief = { body: manifest.brief.body, concepts: manifest.brief.concepts.map(concept => ({ id: randomUUID(), ...concept })) }
|
if (level.brief) state.brief = { body: level.brief.body, concepts: level.brief.concepts.map(concept => ({ id: randomUUID(), ...concept })) }
|
||||||
state.views = state.views.map(view => view.type === 'timeline' ? { ...view, rangeMode: manifest.timelineRange ? 'fixed' : 'auto', range: manifest.timelineRange } : view)
|
state.views = state.views.map(view => view.type === 'timeline' ? { ...view, rangeMode: level.timelineRange ? 'fixed' : 'auto', range: level.timelineRange } : view)
|
||||||
const folders = manifest.folders.map(folder => ({
|
const folders = levelFolders.map(folder => ({
|
||||||
id: folderIds.get(folder.key)!, type: 'folder', title: folder.title, content: folder.content,
|
id: folderIds.get(folder.key)!, type: 'folder', title: folder.title, content: folder.content,
|
||||||
x: folder.x, y: folder.y, width: folder.width, height: 166, rotation: 0, zIndex: 1, hidden: false, isOpen: false,
|
x: folder.x, y: folder.y, width: folder.width, height: 166, rotation: 0, zIndex: 1, hidden: false, isOpen: false,
|
||||||
} as const))
|
} as const))
|
||||||
const claims:ClaimExhibit[]=(manifest.claims || []).map(claim => ({ id:randomUUID(),type:'claim',title:claim.statement,statement:claim.statement,
|
const claims:ClaimExhibit[]=(level.claims || []).map(claim => ({ id:randomUUID(),type:'claim',title:claim.statement,statement:claim.statement,
|
||||||
x:claim.x,y:claim.y,width:claim.width || 310,height:claim.height || 180,rotation:0,zIndex:2,hidden:false }))
|
x:claim.x,y:claim.y,width:claim.width || 310,height:claim.height || 180,rotation:0,zIndex:2,hidden:false }))
|
||||||
state.exhibits = [...documents.values(), ...folders,...claims]
|
const notes:NoteExhibit[]=(level.notes || []).map(note => ({ id:randomUUID(),type:'note',title:note.title || 'NOTE',content:note.content,
|
||||||
state.relations = manifest.folders.flatMap(folder => folder.members.map((key, memberIndex) => {
|
presentation:note.presentation || 'luggage',x:note.x,y:note.y,width:note.width || 230,height:note.height || 180,rotation:0,zIndex:2,hidden:false }))
|
||||||
|
state.exhibits = [...documents.values(), ...folders,...claims,...notes]
|
||||||
|
state.relations = levelFolders.flatMap(folder => folder.members.map((key, memberIndex) => {
|
||||||
const document = documents.get(key)
|
const document = documents.get(key)
|
||||||
if (!document) throw new Error(`Folder ${folder.key} refers to unknown document ${key}`)
|
if (!document) throw new Error(`Folder ${folder.key} refers to unknown document ${key}`)
|
||||||
return {
|
return {
|
||||||
@@ -122,26 +136,26 @@ export async function importMysteryTemplate(manifestPath: string, baseUrl = 'htt
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
state.connections = []
|
state.connections = []
|
||||||
state.report=manifest.report ? { title:manifest.report.title || 'Case Report',investigatorName:'',requiredForCompletion:manifest.report.requiredForCompletion !== false,
|
state.report=level.report ? { title:level.report.title || 'Case Report',investigatorName:'',requiredForCompletion:level.report.requiredForCompletion !== false,
|
||||||
status:'draft',issues:[],claims:[] } : undefined
|
status:'draft',issues:[],claims:[] } : undefined
|
||||||
state.viewport = { x: 0, y: 28, zoom: 0.7 }
|
state.viewport = { x: 0, y: 28, zoom: 0.7 }
|
||||||
|
|
||||||
await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||||
method: 'PUT', headers, body: JSON.stringify(state),
|
method: 'PUT', headers, body: JSON.stringify(state),
|
||||||
}), 'Save authored mystery')
|
}), 'Save authored level')
|
||||||
|
|
||||||
const goalIds = new Map<string,string>()
|
const goalIds = new Map<string,string>()
|
||||||
for (const goal of manifest.goals || []) {
|
for (const goal of level.goals || []) {
|
||||||
const response = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/goals`, {
|
const response = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/goals`, {
|
||||||
method:'POST',headers,body:JSON.stringify(goal),
|
method:'POST',headers,body:JSON.stringify(goal),
|
||||||
}), `Create goal ${goal.key}`)
|
}), `Create goal ${goal.key}`)
|
||||||
const created = await response.json() as { id:string;key:string }
|
const created = await response.json() as { id:string;key:string }
|
||||||
goalIds.set(created.key,created.id)
|
goalIds.set(created.key,created.id)
|
||||||
}
|
}
|
||||||
for (const rule of manifest.evidenceMatchRules || []) await requireOk(await fetch(
|
for (const rule of level.evidenceMatchRules || []) await requireOk(await fetch(
|
||||||
`${baseUrl}/api/levels/${state.id}/evidence-match-rules`, { method:'POST',headers,body:JSON.stringify(rule) }),
|
`${baseUrl}/api/levels/${state.id}/evidence-match-rules`, { method:'POST',headers,body:JSON.stringify(rule) }),
|
||||||
`Create evidence match rule ${rule.name}`)
|
`Create evidence match rule ${rule.name}`)
|
||||||
for (const rule of manifest.evidenceSemanticRules || []) {
|
for (const rule of level.evidenceSemanticRules || []) {
|
||||||
const goalId = goalIds.get(rule.goalKey)
|
const goalId = goalIds.get(rule.goalKey)
|
||||||
if (!goalId) throw new Error(`Semantic evidence rule ${rule.name} refers to unknown goal ${rule.goalKey}`)
|
if (!goalId) throw new Error(`Semantic evidence rule ${rule.name} refers to unknown goal ${rule.goalKey}`)
|
||||||
await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/evidence-semantic-rules`, {
|
await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/evidence-semantic-rules`, {
|
||||||
@@ -149,47 +163,72 @@ export async function importMysteryTemplate(manifestPath: string, baseUrl = 'htt
|
|||||||
}), `Create semantic evidence rule ${rule.name}`)
|
}), `Create semantic evidence rule ${rule.name}`)
|
||||||
}
|
}
|
||||||
const templateResponse = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
|
const templateResponse = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
|
||||||
method: 'POST', headers, body: JSON.stringify({ slug: manifest.slug, name: manifest.name }),
|
method: 'POST', headers, body: JSON.stringify({ slug: level.slug, name: level.name || level.title }),
|
||||||
}), 'Freeze mystery template')
|
}), 'Freeze level template')
|
||||||
const template = await templateResponse.json() as { slug: string; currentVersion: number }
|
const template = await templateResponse.json() as { slug: string; currentVersion: number }
|
||||||
|
|
||||||
// Author the mystery and its NPC cast; the flow lives in the story graph, seeded below.
|
const playableId = `${level.slug}-case-${Date.now()}`
|
||||||
let mystery: { slug: string } | undefined
|
const playableResponse = await requireOk(await fetch(`${baseUrl}/api/templates/${level.slug}/levels?edit=1`, {
|
||||||
if (manifest.narrative) {
|
method: 'POST', headers, body: JSON.stringify({ id: playableId, title: level.title }),
|
||||||
const mysteryResponse = await requireOk(await fetch(`${baseUrl}/api/mysteries?edit=1`, {
|
}), 'Instantiate playable level')
|
||||||
method: 'POST', headers, body: JSON.stringify({ slug: manifest.slug, title: manifest.title, cast: manifest.narrative.cast }),
|
const playable = await playableResponse.json() as CaseState
|
||||||
}), 'Author narrative mystery')
|
return { template, authoringLevelId: state.id, playableLevel: playable }
|
||||||
mystery = await mysteryResponse.json() as { slug: string }
|
}
|
||||||
|
|
||||||
// Seed the story flow graph (default authored content that survives re-imports).
|
// Author the narrative mystery (NPC cast) and seed its story flow graph.
|
||||||
if (manifest.narrative.graph) {
|
async function importNarrative(baseUrl: string, slug: string, title: string, narrative: MysteryNarrative, headers: ImportHeaders, authorization?: string): Promise<{ slug: string }> {
|
||||||
|
const mysteryResponse = await requireOk(await fetch(`${baseUrl}/api/mysteries?edit=1`, {
|
||||||
|
method: 'POST', headers, body: JSON.stringify({ slug, title, cast: narrative.cast }),
|
||||||
|
}), 'Author narrative mystery')
|
||||||
|
const mystery = await mysteryResponse.json() as { slug: string }
|
||||||
|
if (narrative.graph) {
|
||||||
const listResponse = await requireOk(await fetch(`${baseUrl}/api/admin/mysteries`, { headers: authorization ? { authorization } : undefined }), 'List mysteries')
|
const listResponse = await requireOk(await fetch(`${baseUrl}/api/admin/mysteries`, { headers: authorization ? { authorization } : undefined }), 'List mysteries')
|
||||||
const mysteries = await listResponse.json() as { id: string; slug: string }[]
|
const mysteries = await listResponse.json() as { id: string; slug: string }[]
|
||||||
const mysteryId = mysteries.find(m => m.slug === manifest.slug)?.id
|
const mysteryId = mysteries.find(m => m.slug === slug)?.id
|
||||||
if (mysteryId) await requireOk(await fetch(`${baseUrl}/api/admin/mysteries/${mysteryId}/graph`, {
|
if (mysteryId) await requireOk(await fetch(`${baseUrl}/api/admin/mysteries/${mysteryId}/graph`, {
|
||||||
method: 'POST', headers, body: JSON.stringify(manifest.narrative.graph),
|
method: 'POST', headers, body: JSON.stringify(narrative.graph),
|
||||||
}), 'Seed story graph')
|
}), 'Seed story graph')
|
||||||
}
|
}
|
||||||
}
|
return mystery
|
||||||
|
}
|
||||||
|
|
||||||
const playableId = `${manifest.slug}-case-${Date.now()}`
|
// Legacy single-manifest import: one level plus an optional narrative in the same file.
|
||||||
const playableResponse = await requireOk(await fetch(`${baseUrl}/api/templates/${manifest.slug}/levels?edit=1`, {
|
export async function importMysteryTemplate(manifestPath: string, baseUrl = 'http://localhost:8787', adminJwt = process.env.OSINT_ADMIN_JWT) {
|
||||||
method: 'POST', headers, body: JSON.stringify({ id: playableId, title: manifest.title }),
|
const absoluteManifest = path.resolve(manifestPath)
|
||||||
}), 'Instantiate playable mystery')
|
const manifest = JSON.parse(await readFile(absoluteManifest, 'utf8')) as MysteryManifest
|
||||||
const playable = await playableResponse.json() as CaseState
|
const folderDir = path.dirname(absoluteManifest)
|
||||||
return { manifest, template, mystery, authoringLevelId: state.id, playableLevel: playable }
|
const { headers, authorization } = authHeaders(adminJwt)
|
||||||
|
const { narrative, ...level } = manifest
|
||||||
|
const result = await importLevel(baseUrl, folderDir, level, headers, authorization)
|
||||||
|
const mystery = narrative ? await importNarrative(baseUrl, manifest.slug, manifest.title, narrative, headers, authorization) : undefined
|
||||||
|
return { manifest, template: result.template, mystery, authoringLevelId: result.authoringLevelId, playableLevel: result.playableLevel }
|
||||||
|
}
|
||||||
|
|
||||||
|
// New self-contained import: a mystery folder (or single-file) with every level embedded.
|
||||||
|
// Accepts a directory (uses <dir>/mystery.json) or a manifest path; falls back to the
|
||||||
|
// legacy path when the file has no top-level `levels` array.
|
||||||
|
export async function importMystery(inputPath: string, baseUrl = 'http://localhost:8787', adminJwt = process.env.OSINT_ADMIN_JWT) {
|
||||||
|
const resolved = path.resolve(inputPath)
|
||||||
|
const manifestPath = (await stat(resolved)).isDirectory() ? path.join(resolved, 'mystery.json') : resolved
|
||||||
|
const parsed = JSON.parse(await readFile(manifestPath, 'utf8')) as MysteryFile | MysteryManifest
|
||||||
|
if (!Array.isArray((parsed as MysteryFile).levels)) return importMysteryTemplate(manifestPath, baseUrl, adminJwt)
|
||||||
|
const file = parsed as MysteryFile
|
||||||
|
const folderDir = path.dirname(manifestPath)
|
||||||
|
const { headers, authorization } = authHeaders(adminJwt)
|
||||||
|
const levels: LevelResult[] = []
|
||||||
|
for (const level of file.levels) levels.push(await importLevel(baseUrl, folderDir, level, headers, authorization))
|
||||||
|
const mystery = file.narrative ? await importNarrative(baseUrl, file.slug, file.title, file.narrative, headers, authorization) : undefined
|
||||||
|
return { slug: file.slug, title: file.title, levels, mystery }
|
||||||
}
|
}
|
||||||
|
|
||||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ''
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ''
|
||||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||||
const manifestPath = process.argv[2]
|
const inputPath = process.argv[2]
|
||||||
if (!manifestPath) throw new Error('Usage: npm run mystery:import -- <manifest.json>')
|
if (!inputPath) throw new Error('Usage: npm run mystery:import -- <mystery-folder | manifest.json>')
|
||||||
const result = await importMysteryTemplate(manifestPath, process.env.OSINT_BOARD_URL)
|
const result = await importMystery(inputPath, process.env.OSINT_BOARD_URL)
|
||||||
console.log(JSON.stringify({
|
const playUrl = `${process.env.OSINT_BOARD_URL || 'http://localhost:8787'}/`
|
||||||
template: `${result.template.slug}@v${result.template.currentVersion}`,
|
const summary = 'levels' in result
|
||||||
mystery: result.mystery ? result.mystery.slug : undefined,
|
? { mystery: result.mystery?.slug, levels: result.levels.map(l => `${l.template.slug}@v${l.template.currentVersion}`), playUrl }
|
||||||
authoringLevelId: result.authoringLevelId,
|
: { template: `${result.template.slug}@v${result.template.currentVersion}`, mystery: result.mystery?.slug, authoringLevelId: result.authoringLevelId, playableLevelId: result.playableLevel.id, playUrl }
|
||||||
playableLevelId: result.playableLevel.id,
|
console.log(JSON.stringify(summary, null, 2))
|
||||||
playUrl: `${process.env.OSINT_BOARD_URL || 'http://localhost:8787'}/`,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ const levels = createLevelRepository(pool, editingEnabled, objectStorage, eviden
|
|||||||
const narrative = createNarrativeRepository(pool, objectStorage)
|
const narrative = createNarrativeRepository(pool, objectStorage)
|
||||||
const storyGraph = createStoryGraphRepository(pool)
|
const storyGraph = createStoryGraphRepository(pool)
|
||||||
const users = createUserRepository(pool)
|
const users = createUserRepository(pool)
|
||||||
const AUTH_COOKIE = { httpOnly: true, sameSite: 'lax' as const, path: '/', maxAge: 30 * 24 * 60 * 60 * 1000 }
|
const AUTH_COOKIE = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, path: '/', maxAge: 30 * 24 * 60 * 60 * 1000 }
|
||||||
const STORY_NODE_TYPES: StoryNodeType[] = ['cutscene', 'dialogue', 'level', 'det_gate', 'llm_gate', 'merit', 'phone']
|
const STORY_NODE_TYPES: StoryNodeType[] = ['cutscene', 'dialogue', 'level', 'det_gate', 'llm_gate', 'merit', 'phone']
|
||||||
|
|
||||||
function wantsEdit(req: express.Request) {
|
function wantsEdit(req: express.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user