Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2bf2010
chore: restore flag and counter union members dropped in earlier merges
ExcelDsigN-tech Oct 17, 2026
2fa7dab
fix: undo 0d14b9a encoding corruption and restore the #33 chamber mod…
ExcelDsigN-tech Oct 17, 2026
d2824b5
fix: restore code dropped by earlier merges
ExcelDsigN-tech Oct 17, 2026
515e88f
fix: type errors on main, including three real bugs
ExcelDsigN-tech Oct 17, 2026
c1ca72f
test: fix harness bugs in four suites
ExcelDsigN-tech Oct 17, 2026
d63109d
build: add eslint, eslint-config-next and dotenv as devDependencies
ExcelDsigN-tech Oct 17, 2026
58bb24b
fix(contracts): build on current stable rustc, fix mock-token test
ExcelDsigN-tech Oct 17, 2026
f2185da
ci: make typecheck blocking and run vitest; document the baseline repair
ExcelDsigN-tech Oct 17, 2026
be2c748
docs: CI typecheck and tests now gate PRs
ExcelDsigN-tech Oct 17, 2026
821ee48
fix: every page crashed on hydration (node:crypto randomUUID in the c…
ExcelDsigN-tech Oct 17, 2026
c226dfe
feat(cli): phase-creator command tree behind phase-161 (spike 661, #524)
ExcelDsigN-tech Oct 17, 2026
d8d8ae5
feat(scripts): thin-wrapper preflight, live proof runner and PR draft…
ExcelDsigN-tech Oct 17, 2026
ad22634
test: type the preflight test env
ExcelDsigN-tech Oct 17, 2026
33a42f6
fix: verify wallet signatures per SEP-53 (sha256 of prefix + message)
ExcelDsigN-tech Oct 18, 2026
2b2c0ca
Merge branch 'chore/fix-main-baseline' into feat/creator-cli
ExcelDsigN-tech Oct 18, 2026
cb1c282
Merge upstream main (#573) into chore/fix-main-baseline
ExcelDsigN-tech Oct 18, 2026
cc51921
chore(661): move creator CLI flag to phase-165
ExcelDsigN-tech Oct 18, 2026
9f30cb7
Merge chore/fix-main-baseline (upstream #573) into feat/creator-cli
ExcelDsigN-tech Oct 18, 2026
9cf1472
docs(661): reference baseline PR #574
ExcelDsigN-tech Oct 18, 2026
88289b4
docs: tidy Depends on section
ExcelDsigN-tech Oct 18, 2026
78da52d
docs: replace PR placeholders with current status
ExcelDsigN-tech Oct 18, 2026
ed9cd73
Merge upstream main (11cb3bc) into chore/fix-main-baseline
ExcelDsigN-tech Oct 20, 2026
d484616
refactor(flags): rename phase-165 to phase-creator-cli
ExcelDsigN-tech Oct 20, 2026
8e3fff1
Merge branch 'chore/fix-main-baseline' into feat/creator-cli
ExcelDsigN-tech Oct 20, 2026
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
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ jobs:
continue-on-error: true
run: npm run lint
- name: Type check
# Pre-existing type errors exist outside this change's scope; surfaced but
# non-blocking until tracked and fixed separately.
continue-on-error: true
run: npm run typecheck
- name: Unit tests
run: npm test
- name: Build
run: npm run build
env:
Expand Down
3 changes: 1 addition & 2 deletions PROJECT_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ is a fixed-size digest of the canonical payload, keeping it under wallet
| `phase-167` | `NEXT_PUBLIC_FEATURE_PHASE_167` / `FEATURE_PHASE_167` | WalletConnect v2 mobile session resilience (`lib/walletconnect-resilience.ts`): configurable relay URL, multi-chain support, session persistence, chain-switch guard, and mobile deep-link return. Spike 623 (issue #486) | off | Unset var, restart — WalletConnect banner and reconnect actions disabled, kit falls back to default single-session behavior |
| `phase-168` | `NEXT_PUBLIC_FEATURE_PHASE_168` / `FEATURE_PHASE_168` | xBull wallet full support and parity (`lib/xbull-adapter.ts`): Freighter-difference matrix, xBull module detection, sign result normalization, trustline error translation, and badge surfacing. Spike 624 (issue #487) | off | Unset var, restart — xBull transactions fall through generic kit handlers, badge hidden |
| `phase-169` | `NEXT_PUBLIC_FEATURE_PHASE_169` / `FEATURE_PHASE_169` | Ledger blind-signing to clear-signing review for Soroban (`lib/ledger-soroban-decoder.ts`): invokeHostFunction pre-image decode, verification code, on-device review modal, Gate #13 preemptive validation, and hardware error humanizer. Spike 625 (issue #488) | off | Unset var, restart — Ledger transactions fall back to default blind-signing flow without preflight review modal |


| `phase-creator-cli` | `FEATURE_PHASE_CREATOR_CLI` / `NEXT_PUBLIC_FEATURE_PHASE_CREATOR_CLI` | Creator CLI (spike 661 / #524): `phase-creator` (`npm run creator --`) with `scaffold`/`validate`/`env`/`pin`/`deploy`/`run`, `--json`/`--dry-run`/`--network testnet`, exit codes 0–5; offline validation byte-identical to `str_is_safe_for_json_fragment` (shared vectors in `contracts/phase-protocol/test-vectors/`); headless pin with the JWT scrubbed; `diagnoseCreatorEnv` reused by `diagnose-env.ts`. Validate/env/dry-run work with the flag off | off | Unset var — pin/deploy/run/scaffold exit 5, read-only commands unchanged; nothing persisted |

Flags are read via `lib/feature-flags.ts:isFeatureEnabled`. Client flags use `NEXT_PUBLIC_*`, server also accepts `FEATURE_*`. Zero regression when off.

Expand Down
4 changes: 2 additions & 2 deletions app/api/governance/ve-lock/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ export async function POST(request: NextRequest) {
if (!claimRes.ok) {
return NextResponse.json({ error: claimRes.error }, { status: 400, headers: CORS });
}
return NextResponse.json({ ok: true, ...claimRes }, { status: 200, headers: CORS });
return NextResponse.json({ ...claimRes }, { status: 200, headers: CORS });
}

if (action === "withdraw") {
const withdrawRes = withdrawVeLock(wallet);
if (!withdrawRes.ok) {
return NextResponse.json({ error: withdrawRes.error }, { status: 400, headers: CORS });
}
return NextResponse.json({ ok: true, ...withdrawRes }, { status: 200, headers: CORS });
return NextResponse.json({ ...withdrawRes }, { status: 200, headers: CORS });
}

if (action === "lock") {
Expand Down
2 changes: 1 addition & 1 deletion app/api/ipfs/[...cid]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function GET(
// the 1-year shared cache.
const { verifyCID, cacheControlForCid } = await import("@/lib/cid-verification")
const requestedCid = parsed.data[0] ?? ""
const verification = verifyCID(requestedCid, result.bytes)
const verification = verifyCID(requestedCid, new Uint8Array(result.bytes))
const gated = request.headers.get("x-phase-gated") === "1"

if (!verification.ok && verification.code === "CID_MISMATCH") {
Expand Down
13 changes: 3 additions & 10 deletions app/api/ipfs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isPhase120Enabled,
parseAndValidateChecksumHeader,
pinFileToIpfsWithRetry,
resolvePinataJwt,
resolveRetryConfig,
} from "@/lib/ipfs-upload-retry"

Expand All @@ -13,17 +14,9 @@ const PinataRetryQuerySchema = z.object({
baseDelayMs: z.coerce.number().int().min(100).max(10_000).optional(),
})

function pinataJwt(): string | undefined {
const a = process.env.PINATA_JWT?.trim()
if (a) return a
const b = process.env.PINATA_API_JWT?.trim()
if (b) return b
return undefined
}

/** Returns whether server-side file upload is available (no secrets exposed). */
export async function GET() {
const configured = Boolean(pinataJwt())
const configured = Boolean(resolvePinataJwt())
return NextResponse.json({ configured })
}

Expand All @@ -32,7 +25,7 @@ export async function GET() {
* Fallback (flag off): legacy single-shot path (no retry).
*/
export async function POST(req: NextRequest) {
const jwt = pinataJwt()
const jwt = resolvePinataJwt()
if (!jwt) {
return NextResponse.json({ error: "La subida de imágenes no está configurada en el servidor." }, { status: 503 })
}
Expand Down
7 changes: 7 additions & 0 deletions app/api/market/[id]/offers/[offer_id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export async function POST(

// If accepted, mark listing as sold
let royalty: Awaited<ReturnType<typeof recordRoyaltyPayout>> | null = null
let splits: Awaited<ReturnType<typeof recordCollabSplits>> | null = null
if (action === "accept") {
await soldListing(id)

Expand All @@ -65,6 +66,12 @@ export async function POST(
}
}

// Spike 674 (flag-gated): N-way fan-out at accept (dust → first payee, idempotent per offer).
if (isPhaseCollabSplitsEnabled() && listing.split_payees && listing.split_payees.length > 0) {
const computed = computeCollabSplits(offer.amount_phaselq, listing.split_payees)
splits = await recordCollabSplits(id, offer.id, computed)
}

// phase-140: split proceeds with the original creator on a secondary sale.
if (isPhase140Enabled()) {
const split = computeRoyaltySplit(listing, offer.amount_phaselq)
Expand Down
17 changes: 15 additions & 2 deletions app/api/nft-index/subscribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,21 @@ export function validateWebhookUrl(
if (/^\d+$/.test(host) || /^\[/.test(host) || host.includes(":")) {
return { ok: false, error: "webhookUrl must use a public hostname", reason: "ip_literal" }
}
if (host === "169.254.169.254" || host.startsWith("10.") || host.startsWith("192.168.")) {
return { ok: false, error: "webhookUrl must use a public hostname", reason: "private_network" }
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
if (v4) {
const [a, b] = [Number(v4[1]), Number(v4[2])]
if (a === 127 || a === 0) {
return { ok: false, error: "webhookUrl host is not routable", reason: "loopback" }
}
const isPrivate =
a === 10 ||
(a === 100 && b >= 64 && b <= 127) || // CGNAT
(a === 169 && b === 254) || // link-local, incl. cloud metadata
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168)
if (isPrivate) {
return { ok: false, error: "webhookUrl must use a public hostname", reason: "private_network" }
}
}
return { ok: true, url }
}
Expand Down
14 changes: 5 additions & 9 deletions app/api/notifications/winback/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { isFeatureEnabled } from "@/lib/feature-flags";
import { evaluateWinbackCampaign, recordWinbackDispatch, getWinbackStore } from "@/lib/churn-winback";
import { addNotification } from "@/lib/notification-store";
import { createNotification } from "@/lib/notification-store";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand Down Expand Up @@ -88,16 +88,12 @@ export async function POST(request: NextRequest) {
if (body.dispatch && evaluation.eligible && evaluation.cohort !== "control") {
await recordWinbackDispatch(wallet, evaluation.cohort, evaluation.hook, experimentId);
try {
await addNotification({
recipientWallet: wallet,
type: "system",
await createNotification(wallet, "winback_reactivation", {
title: evaluation.title,
body: evaluation.body,
data: {
cohort: evaluation.cohort,
hook: evaluation.hook,
experimentId,
},
cohort: evaluation.cohort,
hook: evaluation.hook,
experimentId,
});
dispatched = true;
} catch {
Expand Down
12 changes: 3 additions & 9 deletions app/api/profile/avatar/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,7 @@ export async function verifyAvatarOwnershipSignature(
imageUrl: string,
signatureBase64: string,
): Promise<boolean> {
try {
const { Keypair } = await import("@stellar/stellar-sdk")
const { SIGNATURE_PREFIX, sha256Hex } = await import("@/lib/viewer-signature")
const message = `phase-avatar:v1:${await sha256Hex(JSON.stringify({ wallet, imageUrl }))}`
const data = new TextEncoder().encode(SIGNATURE_PREFIX + message)
return Keypair.fromPublicKey(wallet).verify(data, Buffer.from(signatureBase64, "base64"))
} catch {
return false
}
const { sha256Hex, verifySep53Signature } = await import("@/lib/viewer-signature")
const message = `phase-avatar:v1:${await sha256Hex(JSON.stringify({ wallet, imageUrl }))}`
return verifySep53Signature(wallet, message, signatureBase64)
}
1 change: 0 additions & 1 deletion components/chamber/artifact-viewer.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions components/chamber/catalog-thumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client"

// Extracted verbatim from components/fusion-chamber.tsx (refactor #33 imported it but never committed the file).

import { useState } from "react"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

export function ChamberCatalogThumb({ collectionId, uri }: { collectionId: number; uri: string }) {
const [broken, setBroken] = useState(false)
if (!uri.trim() || broken) {
return (
<span className="px-1 text-center text-[7px] font-bold uppercase leading-tight text-cyan-500/80">
#{collectionId}
</span>
)
}
return (
<IpfsDisplayImg
uri={uri}
className="h-full w-full object-cover"
loading="lazy"
onExhausted={() => setBroken(true)}
/>
)
}
1 change: 0 additions & 1 deletion components/chamber/certificate-exporter.tsx

This file was deleted.

11 changes: 11 additions & 0 deletions components/chamber/chamber-chrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Extracted verbatim from components/fusion-chamber.tsx (refactor #33 imported it but never committed the file).

/** Same zinc / violet chrome as `/dashboard` (Phase Market). */
export const chamberChromeNav =
"inline-flex min-h-[36px] items-center rounded-sm border border-zinc-700 bg-zinc-900/60 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-zinc-400 transition-colors hover:border-violet-500/50 hover:text-violet-300"
export const chamberChromeNavHere =
"inline-flex min-h-[36px] items-center rounded-sm border border-violet-600/50 bg-violet-950/50 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-violet-300"
export const chamberChromePrimaryBtn =
"flex min-h-[46px] w-full items-center justify-center bg-gradient-to-r from-violet-600 to-cyan-500 py-3 text-center text-[11px] font-bold uppercase tracking-wide text-white shadow-[0_0_18px_rgba(139,92,246,0.35)] transition-opacity hover:opacity-90 disabled:pointer-events-none"
export const chamberChromeRefreshBtn =
"flex items-center gap-1.5 rounded-sm border border-zinc-700 bg-zinc-900/60 px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest text-zinc-400 transition-colors hover:border-violet-500/50 hover:text-violet-300 disabled:opacity-40"
1 change: 0 additions & 1 deletion components/chamber/energy-gauge-display.tsx

This file was deleted.

36 changes: 36 additions & 0 deletions components/chamber/log-stream.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client"

// Extracted verbatim from components/fusion-chamber.tsx (refactor #33 imported it but never committed the file).

import type { RefObject } from "react"
import { cn } from "@/lib/utils"

export function ChamberLogStream({
lines,
endRef,
}: {
lines: { id: number; text: string }[]
endRef: RefObject<HTMLDivElement | null>
}) {
return (
<>
{lines.map((l, i) => {
const fault = /FAIL|Failed|ERROR|FAULT|ABORT|REJECT|denied|timeout|sequence|desync/i.test(l.text)
return (
<div
key={l.id}
className={cn(
!fault &&
"tactical-log-line mb-2 border-l-2 border-[#39ff14]/35 pl-2.5 font-mono text-[11px] leading-relaxed tracking-wide text-[#7df5d8] sm:text-xs sm:leading-relaxed",
fault && "tactical-log-emergency mb-2 font-mono text-[11px] leading-relaxed sm:text-xs",
)}
style={!fault ? { animationDelay: `${Math.min(i, 8) * 65}ms` } : undefined}
>
<span className="break-words whitespace-pre-wrap">{l.text}</span>
</div>
)
})}
<div ref={endRef} />
</>
)
}
45 changes: 45 additions & 0 deletions components/chamber/phaser-liq-token-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client"

// Extracted verbatim from components/fusion-chamber.tsx (refactor #33 imported it but never committed the file).

import { TokenIcon } from "@/components/token-icon"
import { playTacticalUiClick } from "@/lib/tactical-ui-click"
import { cn } from "@/lib/utils"

/** Enlace al asset PHASELQ en Stellar Expert (icono + símbolo). */
export function PhaserLiqTokenLink({
href,
symbol,
iconClassName,
className,
variant = "default",
}: {
href: string
symbol: string
iconClassName?: string
className?: string
variant?: "default" | "violet"
}) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
title={href}
onClick={(e) => {
e.stopPropagation()
playTacticalUiClick()
}}
className={cn(
"inline-flex items-center gap-1.5 rounded-sm border border-transparent font-mono tracking-normal underline-offset-[3px] transition-colors hover:underline focus:outline-none",
variant === "violet"
? "text-violet-200/95 hover:border-violet-500/45 hover:bg-violet-950/35 hover:text-violet-50 focus-visible:ring-2 focus-visible:ring-violet-400/55"
: "text-cyan-400/95 hover:border-cyan-500/45 hover:bg-cyan-950/35 hover:text-cyan-50 focus-visible:ring-2 focus-visible:ring-cyan-400/60",
className,
)}
>
<TokenIcon className={cn("shrink-0", iconClassName ?? "h-4 w-4")} />
{symbol}
</a>
)
}
1 change: 0 additions & 1 deletion components/chamber/settlement-wizard.tsx

This file was deleted.

1 change: 0 additions & 1 deletion components/chamber/terminal-header.tsx

This file was deleted.

Loading