diff --git a/docs/external-access-and-websites.md b/docs/external-access-and-websites.md index 4c8e523e..abd4e867 100644 --- a/docs/external-access-and-websites.md +++ b/docs/external-access-and-websites.md @@ -69,12 +69,38 @@ run only through `scripts/test-backend-isolated.sh`; use a worktree-local target ### Current integration checkpoint +The operator reaffirmed the existing Setup walkthrough design during UAT. +The follow-up UI uses the existing numbered goal cards, progress styling and +Back/Continue navigation, with one expanded step. Previously saved connections +are reused; installed Blossom omits the installation step. Blossom installation +uses the normal app-store installer. Navigation itself never saves, signs or +publishes. This UI revision is now active on Framework. The actual dashboard walkthrough +saved the synthetic draft, archived it in local Blossom with a profile identity, +fetched it back to verify exact bytes, and published it through FIPS port 32000. +The 390-pixel mobile layout passed the overflow check. Browser request monitoring +recorded no external requests during this journey. + +Live archive acceptance exposed an older `node-*` identity whose `is_node` flag +was false. The container correctly rejected its upload because the canonical +signer allowlist excludes legacy node records. The website identity filter now +matches that rule, including node-name fallbacks and public-key validation, and +checks it again before signing. No public Nostr event or external replica was +created during this failure. The selected 20-test suite covers the regression and +walkthrough navigation; the production typecheck and Vite build passed. A further +new-project connection-inheritance check passed, giving eight Setup and thirteen +Nostr tests for the revised UI. + Blossom is installed and healthy on Framework through the normal app installer. Protocol, real HTTP/HTTPS tab signing and lifecycle/data-preservation evidence is -recorded in `apps/blossom/README.md`. The combined dashboard/backend candidate has -not yet been deployed. No public Nostr test events or external file replicas have -been created. The temporary public proxy route and certificate were removed after -their standalone acceptance checks. +recorded in `apps/blossom/README.md`. The combined dashboard/backend candidate +from local commit `28a92fcc` is now deployed privately on Framework. The +authenticated publishing status probe passes; native Bitcoin/LND process IDs and +start times are unchanged. Complete browser acceptance is still in progress. No public Nostr test events or external file replicas have +been created. The earlier standalone proxy route/certificate were removed. A new owned UAT +route now connects the dashboard-published synthetic site to +`https://free.archipelago.builders` through Yaya. Trusted TLS, exact page bytes, +`/rpc` returning 404, and traversal rejection (400) pass. The route remains for UAT; +full revoke/restart qualification is still in progress. New source work includes local Blossom website archives, explicit app-only guest credentials, and an on-demand HTTPS check against exact published page bytes. @@ -93,15 +119,19 @@ remain unfinished. Source validation and standalone routes must not be described as acceptance of those features or of the complete dashboard journey. The current dashboard production build and supported AIUI build both pass and -are staged separately on Framework. The original backend and full web tree are -backed up for rollback; the live dashboard has not been switched. The selected +are activated on Framework. The original backend and full web tree remain +backed up for rollback. The selected dashboard suite passed 36 tests; subsequent HTTPS UI coverage passed six tests, and tightened Nostr signing/receipt coverage passed 12 tests. The latest combined 18-test run, TypeScript check and dashboard rebuild passed. Catalog drift is zero (37 catalog entries, 64 manifests). Full isolated backend validation now passes 1,699 tests, zero failures and four explicit ignores. The focused app-gate run passes 53 tests, and all three credential tests pass. The deployable backend build -is still pending at this checkpoint; passing tests is not live-node acceptance. +passed. Its stripped deployment artifact SHA-256 is +`11e571a7636779d7a956f9e98dab951f19de12262cf89e5ea478cdc8ba864eae`. +Passing tests and the initial authenticated activation probe do not establish +complete live-node acceptance. The private catalogue signing ceremony remains +pending; six app-sharing policies have not yet been activated. ## Development evidence (2026-10-08, not release acceptance) diff --git a/neode-ui/src/components/SetupWalkthrough.vue b/neode-ui/src/components/SetupWalkthrough.vue new file mode 100644 index 00000000..b18e481c --- /dev/null +++ b/neode-ui/src/components/SetupWalkthrough.vue @@ -0,0 +1,50 @@ + + + diff --git a/neode-ui/src/services/__tests__/nsitePublishing.test.ts b/neode-ui/src/services/__tests__/nsitePublishing.test.ts index e6f353d6..33afdde6 100644 --- a/neode-ui/src/services/__tests__/nsitePublishing.test.ts +++ b/neode-ui/src/services/__tests__/nsitePublishing.test.ts @@ -64,6 +64,19 @@ describe('named nsite publishing', () => { expect(relayAddresses('wss://relay.example wss://relay.example')).toEqual(['wss://relay.example/']) for (const value of ['ws://relay.example', 'wss://user:secret@relay.example', 'wss://relay.example/#key']) expect(() => relayAddresses(value)).toThrow() }) + it('excludes legacy node identities before any local upload signing', async () => { + const hidden = [ + { ...identity, id: ' Node-legacy ', is_node: false }, + { ...identity, name: '\uFEFFNode ', is_node: false }, + { ...identity, nostr_pubkey: 'invalid' }, + ] + vi.mocked(rpcClient.call).mockResolvedValueOnce({ identities: [identity, ...hidden] } as never) + expect(await nsiteIdentities()).toEqual([identity]) + vi.mocked(rpcClient.call).mockClear() + for (const candidate of hidden) await expect(storeLocalWebsite('project', 4, candidate)).rejects.toThrow('profile identity') + expect(rpcClient.call).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() + }) it('preparation never signs, uploads or broadcasts', async () => { await prepareNsite('project', 4, receipt.server, '

Private draft

') expect(fetch).not.toHaveBeenCalled() diff --git a/neode-ui/src/services/nsitePublishing.ts b/neode-ui/src/services/nsitePublishing.ts index b1599408..b229a08c 100644 --- a/neode-ui/src/services/nsitePublishing.ts +++ b/neode-ui/src/services/nsitePublishing.ts @@ -7,6 +7,13 @@ export interface NsiteReceipt { identity_id: string; server: string; event: Sign export interface NsiteIdentity { id: string; name: string; nostr_pubkey: string; is_node: boolean } export interface PreparedNsite { html: string; sha256: string; server: string; identifier: string; authorization: Record; manifest: Record } +// Match the platform app signer and its derived Blossom upload allowlist, +// including older node records that do not carry the explicit is_node flag. +function isProfileIdentity(identity: NsiteIdentity): boolean { + return !identity.is_node && !identity.id.trim().toLowerCase().startsWith('node-') + && identity.name.trim().toLowerCase() !== 'node' && /^[a-f0-9]{64}$/.test(identity.nostr_pubkey) +} + export function relayAddresses(raw: string): string[] { const list = [...new Set(raw.split(/[\s,]+/).filter(Boolean).map(value => { const url = new URL(value) @@ -18,9 +25,10 @@ export function relayAddresses(raw: string): string[] { } export async function nsiteIdentities(): Promise { const data = await rpcClient.call<{ identities: NsiteIdentity[] }>({ method: 'identity.list', maxRetries: 0 }) - return data.identities.filter(i => !i.is_node && i.nostr_pubkey) + return data.identities.filter(isProfileIdentity) } async function sign(identity: NsiteIdentity, event: Record): Promise { + if (!isProfileIdentity(identity)) throw new Error('Choose a profile identity for website files') const signed = await rpcClient.call({ method: 'identity.nostr-sign', params: { id: identity.id, event }, maxRetries: 0 }) if (signed.pubkey !== identity.nostr_pubkey || signed.kind !== event.kind) throw new Error('Signer returned a different identity or event kind') for (const key of ['created_at', 'content', 'tags'] as const) { @@ -29,7 +37,7 @@ async function sign(identity: NsiteIdentity, event: Record): Pr return signed } export async function storeLocalWebsite(projectId: string, version: number, identity: NsiteIdentity): Promise { - if (identity.is_node) throw new Error('Choose a profile identity for local files') + if (!isProfileIdentity(identity)) throw new Error('Choose a profile identity for local files') const prepared = await rpcClient.call<{ authorization: Record }>({ method: 'publishing.blossom-prepare', params: { id: projectId, version }, maxRetries: 0 }) const authorization = await sign(identity, prepared.authorization) await rpcClient.call({ method: 'publishing.blossom-store', params: { id: projectId, version, authorization }, timeout: 70000, maxRetries: 0 }) diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index 8acc733f..2cd06cd6 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -79,7 +79,7 @@ export const HTTPS_PROXY_PATHS: Record = { * trusted. Once the signed catalog carries the app, portIsGateFronted is the * normal source of truth. */ -const PRE_CATALOG_GATED_PORTS: Record = { +const PRE_CATALOG_GATED_PORTS: Partial> = { 'archipelago-source': 8337, 'blossom': GENERATED_APP_PORTS.blossom, } diff --git a/neode-ui/src/views/publishing/PublishingSetup.vue b/neode-ui/src/views/publishing/PublishingSetup.vue index e5f3a95f..1187b3c7 100644 --- a/neode-ui/src/views/publishing/PublishingSetup.vue +++ b/neode-ui/src/views/publishing/PublishingSetup.vue @@ -1,6 +1,6 @@