Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/classic-liq/trustline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export async function GET(req: NextRequest) {
try {
const { getCidCacheStats } = await import("@/lib/cid-cache")
const stats = getCidCacheStats()
return NextResponse.json(stats)
return NextResponse.json({ enabled: true, stats })
} catch (e) {
return NextResponse.json({ enabled: true, error: e instanceof Error ? e.message : String(e) }, { status: 500 })
}
Expand Down
1 change: 1 addition & 0 deletions app/api/narrator/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/lib/narrative-world-store"
import { createNotification } from "@/lib/notification-store"
import { checkNarrativeContinuity } from "@/lib/story-arc-continuity"
import { isLoreVersioningEnabled, recordLoreVersion } from "@/lib/lore-versioning"

export const runtime = "nodejs"
export const dynamic = "force-dynamic"
Expand Down
2 changes: 2 additions & 0 deletions app/api/profile/follow/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
getRequestCost,
FollowSuggestionQuerySchema,
validateSep50MetadataBeforePin,
getFollowSuggestions,
FollowSuggestionQuerySchema,
} from "@/lib/follow-store";
import { createNotification } from "@/lib/notification-store";
import { getProfile } from "@/lib/profile-store";
Expand Down
16 changes: 11 additions & 5 deletions components/wallet-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {
return null
} catch (e) {
const pe = parseError(e)
if (pe.code === -1 && pe.message === "No wallet has been connected.") {
if (pe.code === "-1" && pe.message === "No wallet has been connected.") {
setAddress(null)
return null
}
Expand Down Expand Up @@ -134,7 +134,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {

useEffect(() => {
initStellarWalletKit()
const stop = kit.on(KitEventType.STATE_UPDATED, ({ payload }) => {
const stop = kit.on(KitEventType.STATE_UPDATED, ({ payload }: { payload: any }) => {
try {
if (userDisconnectedRef.current) return
setAddress(payload.address ?? null)
Expand Down Expand Up @@ -263,6 +263,9 @@ export function WalletProvider({ children }: { children: ReactNode }) {
setHint(null)
initStellarWalletKit()
try {
if (!kit.authModal) {
throw new Error("authModal not available")
}
const { address: next } = await kit.authModal()
if (!userDisconnectedRef.current) {
setAddress(next)
Expand All @@ -273,7 +276,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {
} catch (e) {
const pe = parseError(e)
setAddress(null)
if (pe.code !== -1) {
if (pe.code !== "-1") {
setHint(pe.message || "Wallet connection failed")
}
} finally {
Expand All @@ -290,9 +293,12 @@ export function WalletProvider({ children }: { children: ReactNode }) {
const openWalletPicker = useCallback((): Promise<string | null> => {
userDisconnectedRef.current = false
initStellarWalletKit()
if (!kit.authModal) {
return Promise.reject(new Error("authModal not available"))
}
return kit
.authModal()
.then(({ address: next }) => {
.then(({ address: next }: { address: any }) => {
const g = typeof next === "string" ? next.trim() : ""
if (!g) {
setAddress(null)
Expand All @@ -305,7 +311,7 @@ export function WalletProvider({ children }: { children: ReactNode }) {
})
.catch((e: unknown) => {
const pe = parseError(e)
if (pe.code !== -1) {
if (pe.code !== "-1") {
setHint(pe.message || "Wallet unavailable")
}
return null
Expand Down
37 changes: 37 additions & 0 deletions diagnose-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,40 @@ if (validation.valid && tokenDiagnostic.isContract && !tokenDiagnostic.errors.le
process.exit(1)
}
console.log("=".repeat(70))


// 7. CSP Header Compliance Verification
console.log("\nπŸ”’ Content Security Policy (CSP) Compliance:")
try {
const nextConfigPath = "./next.config.mjs"
const fs = require("fs")
if (fs.existsSync(nextConfigPath)) {
const configContent = fs.readFileSync(nextConfigPath, "utf8")
const hasCsp = configContent.includes("Content-Security-Policy")
if (hasCsp) {
console.log(" βœ… Content-Security-Policy header configured in next.config.mjs")

// Check for key CSP directives
const hasDefaultSrc = /default-src\s+'self'/.test(configContent)
const hasScriptSrc = /script-src/.test(configContent)
const hasStyleSrc = /style-src/.test(configContent)
const hasImgSrc = /img-src/.test(configContent)

if (hasDefaultSrc) console.log(" βœ… default-src directive present")
if (hasScriptSrc) console.log(" βœ… script-src directive present")
if (hasStyleSrc) console.log(" βœ… style-src directive present")
if (hasImgSrc) console.log(" βœ… img-src directive present")

if (!hasDefaultSrc || !hasScriptSrc) {
console.warn(" ⚠️ WARNING: Some critical CSP directives may be missing")
}
} else {
console.error(" ❌ ERROR: No Content-Security-Policy header found in next.config.mjs")
console.error(" Add CSP headers to protect against XSS and injection attacks")
}
} else {
console.warn(" ⚠️ next.config.mjs not found")
}
} catch (e) {
console.error(" ❌ ERROR checking CSP configuration:", e instanceof Error ? e.message : String(e))
}
2 changes: 1 addition & 1 deletion lib/bulk-listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function parseCSV(csvContent: string): CSVParseResult {

try {
const item: BulkListingItem = {
tokenId: values[tokenIdIdx] || "",
tokenId: Number(values[tokenIdIdx] || "0"),
name: values[nameIdx] || "",
description: descIdx !== -1 ? values[descIdx] : undefined,
price: values[priceIdx] || "0",
Expand Down
6 changes: 5 additions & 1 deletion lib/cid-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ export async function setCachedCid(
if (!cidParsed.success) {
throw new CidIntegrityError("CID_INVALID", cleanCid, `Invalid CID: ${cidParsed.error.message}`)
}
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes instanceof ArrayBuffer ? bytes : bytes)
const buf = Buffer.isBuffer(bytes)
? bytes
: bytes instanceof ArrayBuffer
? Buffer.from(bytes)
: Buffer.from(bytes as Uint8Array)
const sha = sha256Hex(buf)
if (opts.expectedSha256 && !verifyBytesIntegrity(buf, opts.expectedSha256)) {
throw new CidIntegrityError("HASH_MISMATCH", cleanCid, `Bytes hash mismatch for CID ${cleanCid.slice(0, 8)}…`)
Expand Down
1 change: 1 addition & 0 deletions lib/escrow-settlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export function auditEscrowSettlementWiring(): { ok: boolean; note: string } {
amount: "10000000",
tokenId: 1,
collectionId: 0,
timeoutSeconds: 86400,
}

const validation = validateEscrowCreation(probeEscrow)
Expand Down
2 changes: 0 additions & 2 deletions lib/ipfs-pinning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,6 @@ export async function pinWithRedundancy(
if (!isPhase117Enabled()) {
const primary = config.gateways.find((g) => g.name === "pinata") ?? config.gateways[0]!
const singleFile = new Blob([ab], { type: file.type || "application/octet-stream" }) as File & { name?: string }
// @ts-expect-error β€” File name assign
if (!(singleFile as File).name) Object.defineProperty(singleFile, "name", { value: fileName })
const single = await pinToGateway(primary, singleFile as unknown as Blob, jwt, checksum, { signal: opts.signal, fetchImpl: opts.fetchImpl })
const ok = single.ok
Expand All @@ -297,7 +296,6 @@ export async function pinWithRedundancy(
const pinBlob = new Blob([ab], { type: file.type || "application/octet-stream" })
// attach name for FormData
const namedBlob = pinBlob as Blob & { name?: string }
// @ts-expect-error
if (!namedBlob.name) Object.defineProperty(namedBlob, "name", { value: fileName })

const pinResults: PinResult[] = []
Expand Down
189 changes: 189 additions & 0 deletions lib/narrative-world-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type WorldCollectionData = {
created_at: number
narrator_tone?: NarratorTone
creator_wallet?: string
version?: number
}

export type WorldNarrativeData = {
Expand Down Expand Up @@ -197,3 +198,191 @@ export async function getNarrativeForTokenCached(
localizedNarrativeCache.set(key, { value, expiresAt: Date.now() + LOCALIZED_CACHE_TTL_MS })
return value
}

// ─── phase-108: reader progression tracking ──────────────────────────────

type ReaderProgressEntry = {
wallet: string
collection_id: number
read_token_ids: number[]
last_read_at: number
}

type ReaderProgressStore = Record<string, ReaderProgressEntry>

function readerProgressKey(wallet: string, collectionId: number): string {
return `${wallet}:${collectionId}`
}

export async function getReaderProgress(wallet: string, collectionId: number): Promise<number[]> {
const store = await readJsonStore<ReaderProgressStore>(serverDataJsonPath("readerProgress"))
const key = readerProgressKey(wallet, collectionId)
return store[key]?.read_token_ids ?? []
}

export async function markNarrativeRead(wallet: string, collectionId: number, tokenId: number): Promise<void> {
const filePath = serverDataJsonPath("readerProgress")
const store = await readJsonStore<ReaderProgressStore>(filePath)
const key = readerProgressKey(wallet, collectionId)
const existing = store[key] ?? { wallet, collection_id: collectionId, read_token_ids: [], last_read_at: 0 }
if (!existing.read_token_ids.includes(tokenId)) {
existing.read_token_ids.push(tokenId)
}
existing.last_read_at = Date.now()
store[key] = existing
await writeJsonStore(filePath, store)
}

// ─── phase-109: collaborative world permissions ──────────────────────────

export type WorldRole = "editor" | "viewer"

type WorldRolesEntry = {
collection_id: number
owner: string
roles: Record<string, WorldRole>
}

type WorldRolesStore = Record<string, WorldRolesEntry>

export async function getWorldRoles(collectionId: number): Promise<Record<string, WorldRole>> {
const store = await readJsonStore<WorldRolesStore>(serverDataJsonPath("worldRoles"))
return store[String(collectionId)]?.roles ?? {}
}

export async function ensureWorldOwner(collectionId: number, ownerWallet: string): Promise<void> {
const filePath = serverDataJsonPath("worldRoles")
const store = await readJsonStore<WorldRolesStore>(filePath)
const key = String(collectionId)
if (!store[key]) {
store[key] = { collection_id: collectionId, owner: ownerWallet, roles: {} }
await writeJsonStore(filePath, store)
}
}

export async function setWorldRole(
collectionId: number,
actingWallet: string,
targetWallet: string,
role: WorldRole,
): Promise<Record<string, WorldRole>> {
const filePath = serverDataJsonPath("worldRoles")
const store = await readJsonStore<WorldRolesStore>(filePath)
const key = String(collectionId)
const entry = store[key]
if (!entry || entry.owner !== actingWallet) {
throw new Error("Solo el propietario del mundo puede asignar roles")
}
entry.roles[targetWallet] = role
await writeJsonStore(filePath, store)
return entry.roles
}

// ─── phase-112: world export to portable markdown/JSON ───────────────────

export type WorldExportSnapshot = {
collection_id: number
world_name: string
world_prompt: string
narrator_tone?: NarratorTone
created_at: number
narratives: Array<{
token_id: number
narrative: string
lore_input: string
generated_at: number
}>
}

export async function buildWorldExportSnapshot(collectionId: number): Promise<WorldExportSnapshot | null> {
const world = await getWorldForCollection(collectionId)
if (!world) return null

const narrativesStore = await readJsonStore<WorldNarrativesStore>(serverDataJsonPath("worldNarratives"))
const narratives = Object.entries(narrativesStore)
.filter(([_, data]) => data.collection_id === collectionId)
.map(([tokenId, data]) => ({
token_id: Number(tokenId),
narrative: data.narrative,
lore_input: data.lore_input,
generated_at: data.generated_at,
}))
.sort((a, b) => a.token_id - b.token_id)

return {
collection_id: collectionId,
world_name: world.world_name,
world_prompt: world.world_prompt,
narrator_tone: world.narrator_tone,
created_at: world.created_at,
narratives,
}
}

export function renderWorldExportMarkdown(snapshot: WorldExportSnapshot): string {
let md = `# ${snapshot.world_name}\n\n`
md += `**Mundo ID:** ${snapshot.collection_id}\n\n`
md += `**Prompt del Mundo:**\n${snapshot.world_prompt}\n\n`
if (snapshot.narrator_tone) md += `**Tono del Narrador:** ${snapshot.narrator_tone}\n\n`
md += `**Creado:** ${new Date(snapshot.created_at).toISOString()}\n\n`
md += `---\n\n## Narrativas (${snapshot.narratives.length})\n\n`
for (const n of snapshot.narratives) {
md += `### Artefacto #${n.token_id}\n\n`
md += `**Entrada de Lore:** ${n.lore_input}\n\n`
md += `**Narrativa:**\n${n.narrative}\n\n`
md += `*Generado: ${new Date(n.generated_at).toISOString()}*\n\n`
md += `---\n\n`
}
return md
}

// ─── phase-115: cross-artifact lore linking ──────────────────────────────

export type LoreLink = {
from_token_id: number
to_token_id: number
note?: string
created_at: number
}

type LoreLinkStore = LoreLink[]

export async function getLoreLinksForToken(tokenId: number): Promise<{ outgoing: LoreLink[]; incoming: LoreLink[] }> {
const store = await readJsonStore<LoreLinkStore>(serverDataJsonPath("loreLinks"))
const outgoing = store.filter((link) => link.from_token_id === tokenId)
const incoming = store.filter((link) => link.to_token_id === tokenId)
return { outgoing, incoming }
}

export async function addLoreLink(fromTokenId: number, toTokenId: number, note?: string): Promise<LoreLink> {
const filePath = serverDataJsonPath("loreLinks")
const store = await readJsonStore<LoreLinkStore>(filePath)
const existingIndex = store.findIndex((l) => l.from_token_id === fromTokenId && l.to_token_id === toTokenId)
const link: LoreLink = {
from_token_id: fromTokenId,
to_token_id: toTokenId,
note,
created_at: Date.now(),
}
if (existingIndex >= 0) {
store[existingIndex] = link
} else {
store.push(link)
}
await writeJsonStore(filePath, store)
return link
}

// ─── phase-110: narrative search helpers ──────────────────────────────────

export async function getAllNarrativesWithTokenIds(): Promise<
Array<{ tokenId: number; narrative: string; collection_id: number; generated_at: number }>
> {
const store = await readJsonStore<WorldNarrativesStore>(serverDataJsonPath("worldNarratives"))
return Object.entries(store).map(([tokenId, data]) => ({
tokenId: Number(tokenId),
narrative: data.narrative,
collection_id: data.collection_id,
generated_at: data.generated_at,
}))
}
4 changes: 4 additions & 0 deletions lib/server-data-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const FILES = {
watchlists: "watchlists.json",
questRegistry: "quest-registry.json",
distributorHealth: "distributor-health.json",
readerProgress: "reader-progress.json",
loreLinks: "lore-links.json",
blockList: "block-list.json",
trendingSignals: "trending-signals.json",
} as const

export type ServerDataFile = keyof typeof FILES
Expand Down
Loading
Loading