From 4c5a25ef23ec39e73620b273e72f34d0724dc99a Mon Sep 17 00:00:00 2001 From: sandwich <299465+dskvr@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:14:24 +0100 Subject: [PATCH 1/2] feat: applets upload to the account's file servers through Blossom, asked first The shell offers the upload domain. kernl.files follows the account's file-server list (kind 10063) with well-known defaults, and Settings edits it. createBlossomUploader uses blossom-client-sdk to upload to the first server and mirror to the rest; any server storing the file is a success. The person is asked once per file, with its type, size, servers and who can see it, before anything is signed or sent. Records D-032. Co-Authored-By: Claude Opus 5.5 --- .changeset/blossom-upload.md | 5 + DECISIONS.md | 15 + apps/web/features/preview-kernl.feature | 5 + apps/web/features/steps/kernl.ts | 16 +- apps/web/src/shell/ConsentDialog.svelte | 1 + apps/web/src/shell/FileServerSettings.svelte | 123 ++++++++ apps/web/src/shell/SettingsPanel.svelte | 2 + apps/web/src/shell/preview-kernl.ts | 3 +- apps/web/src/shell/preview-mailboxes.ts | 18 +- docs/packages/core.md | 5 + packages/core/features/file-uploads.feature | 107 +++++++ packages/core/features/kernl.feature | 17 + packages/core/features/steps/catalog.ts | 2 +- packages/core/features/steps/dm-labels.ts | 2 +- packages/core/features/steps/file-uploads.ts | 293 ++++++++++++++++++ packages/core/features/steps/index.ts | 1 + packages/core/features/steps/kernl-network.ts | 34 +- packages/core/features/steps/services.ts | 2 + packages/core/package.json | 1 + packages/core/src/files/file-servers.ts | 143 +++++++++ packages/core/src/files/uploader.ts | 184 +++++++++++ packages/core/src/index.ts | 2 + packages/core/src/kernl-adapter.ts | 4 + packages/core/src/kernl-network.ts | 23 +- packages/core/src/kernl-services.ts | 5 + packages/core/src/kernl.ts | 51 ++- packages/core/src/types.ts | 6 +- pnpm-lock.yaml | 22 ++ tests/e2e/shell-preview.spec.ts | 23 ++ 29 files changed, 1090 insertions(+), 25 deletions(-) create mode 100644 .changeset/blossom-upload.md create mode 100644 apps/web/src/shell/FileServerSettings.svelte create mode 100644 packages/core/features/file-uploads.feature create mode 100644 packages/core/features/steps/file-uploads.ts create mode 100644 packages/core/src/files/file-servers.ts create mode 100644 packages/core/src/files/uploader.ts diff --git a/.changeset/blossom-upload.md b/.changeset/blossom-upload.md new file mode 100644 index 0000000..038b28e --- /dev/null +++ b/.changeset/blossom-upload.md @@ -0,0 +1,5 @@ +--- +"@kernlize/core": minor +--- + +Applets can upload files (NAP-UPLOAD) through Blossom. `kernl.files` follows the account's file-server list (kind 10063) with well-known defaults. `createBlossomUploader` uses `blossom-client-sdk` to upload to the first server and mirror to the rest, succeeds when any server stores the file, and asks the person before each file leaves with a new `public` consent severity. The shell offers the `upload` domain. diff --git a/DECISIONS.md b/DECISIONS.md index 2f20d8f..c801e13 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -463,3 +463,18 @@ NZB, Soulseek and mail stay hand-off. Running them inside kernl needs something **Consequences.** Names appear wherever a key did, from cache at once and from the network as it answers. The store fills from the first relay. The `outbox` tier is no longer edited by hand; it follows the published list. **Reversal cost.** Low. Each is one module behind an interface the preview also implements. + +## D-032 · Applets upload through the shell to the account's own file servers, with Blossom's own client, and the person is asked before each file leaves + +**Context.** Sixteen published applets require `upload` (NAP-UPLOAD), and the shell did not offer it. kehto ships the upload service and a reference HTTP uploader that sends a file to one server. Blossom, the file-server protocol most clients use, has a client SDK by its author that uploads to one server, mirrors to the others, checks first whether a server already holds the file, and handles the protocol's rejection and payment answers. BUD-03 gives every account a published list of its file servers (kind 10063). + +**Decision.** + +- **The wire is kehto's, the transfer is Blossom's.** `createUploadService` answers applets. Behind it, `createBlossomUploader` uses `blossom-client-sdk`'s `multiServerUpload`: the file goes to the first server and is mirrored to the rest. An upload is a success when any server stores it, and the result lists every copy (D-028). When none does it fails with `unavailable:not-accepted:0/`. +- **The account decides where files go.** `kernl.files` follows the account's kind 10063 list and waits for the lookup to finish before falling back to `DEFAULT_FILE_SERVERS` (blossom.primal.net, nostr.download, blossom.band, which answered an upload preflight when chosen). An applet may not name a server. Settings shows the list and publishes changes to the account's outboxes and the lookup servers. +- **Every file is asked about.** Before anything is signed or sent, the person sees "Upload a file", marked `public`, with the file's name and type, its size, the servers it goes to, and "Anyone who has its address". The prompt appears once per file. One authorization (kind 24242) is then signed and covers every server. A read-only session is refused with `denied:read-only`, and an applet the shell cannot name is refused without asking. +- `ConsentSummary.severity` gains `public` for writes that put something where anyone can fetch it. + +**Consequences.** Applets that need uploads now run, and their files land where the account's other clients look for them. A file the person allowed is public for good: Blossom has no private uploads, so the prompt says so. + +**Reversal cost.** Low. The uploader is one module behind kehto's `Uploader` interface, so a host can pass its own. diff --git a/apps/web/features/preview-kernl.feature b/apps/web/features/preview-kernl.feature index afee5ee..18bcd6b 100644 --- a/apps/web/features/preview-kernl.feature +++ b/apps/web/features/preview-kernl.feature @@ -260,6 +260,11 @@ Feature: The preview kernl When the preview kernl saves inbox "" and outbox "" Then the preview kernl save was accepted by 0 servers + Scenario: the preview's uploads go to the default file servers until it saves its own + Then the preview kernl file servers are the defaults + When the preview kernl saves file servers "https://mine.test/" + Then the preview kernl uploads go to "https://mine.test/" + Scenario: the preview's tiers can be replaced at once When the preview kernl sets the "outbox" tier to "wss://a.test,wss://a.test" Then the preview kernl "outbox" tier is "wss://a.test" diff --git a/apps/web/features/steps/kernl.ts b/apps/web/features/steps/kernl.ts index 396c5ce..08c3b9a 100644 --- a/apps/web/features/steps/kernl.ts +++ b/apps/web/features/steps/kernl.ts @@ -3,7 +3,7 @@ import { firstValueFrom, type Observable } from 'rxjs'; import { Given, Then, When } from 'quickpickle'; import type { IAccount } from 'applesauce-accounts'; import { PrivateKeyAccount, ReadonlyAccount } from 'applesauce-accounts/accounts'; -import { BASELINE_DOMAINS } from '@kernlize/core'; +import { BASELINE_DOMAINS, DEFAULT_FILE_SERVERS } from '@kernlize/core'; import { MemoryStorage } from '../../../../packages/core/features/steps/world.ts'; import { abbreviate, type AppletHandle, type AppletPointer, type AppletSummary, type ConsentPrompt, type FrameLike, type HandlerPrompt, type KernlHost, type MountPlan, type ShellKernl } from '../../src/shell/contract.ts'; import { PREVIEW_PUBLISHER, sampleSummary } from '../../src/shell/preview-applet.ts'; @@ -569,3 +569,17 @@ Then('the preview kernl {string} tier is {string}', (world: PreviewWorld, tier: Then('the preview kernl refuses to set the {string} tier to {string}', (world: PreviewWorld, tier: 'outbox', urls: string) => { expect(() => preview(world).kernl.relays.set(tier, list(urls))).toThrow('invalid:url'); }); + +Then('the preview kernl file servers are the defaults', async (world: PreviewWorld) => { + expect(preview(world).kernl.files.state()).toEqual({ status: 'missing', servers: DEFAULT_FILE_SERVERS }); + expect(await preview(world).kernl.files.current()).toEqual(DEFAULT_FILE_SERVERS); +}); + +When('the preview kernl saves file servers {string}', async (world: PreviewWorld, servers: string) => { + expect(await preview(world).kernl.files.save(list(servers))).toMatchObject({ ok: true }); +}); + +Then('the preview kernl uploads go to {string}', async (world: PreviewWorld, servers: string) => { + expect(await preview(world).kernl.files.current()).toEqual(list(servers)); + expect(preview(world).kernl.files.state()).toMatchObject({ status: 'found', servers: list(servers) }); +}); diff --git a/apps/web/src/shell/ConsentDialog.svelte b/apps/web/src/shell/ConsentDialog.svelte index ab75060..f76cbe5 100644 --- a/apps/web/src/shell/ConsentDialog.svelte +++ b/apps/web/src/shell/ConsentDialog.svelte @@ -32,6 +32,7 @@ {shortId(summary.applet.hash)} {appletTitle} wants to {#if summary.severity === 'destructive'}destructive{/if} + {#if summary.severity === 'public'}public{/if}