You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 69aa630
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli,service-storage): the Console CSP's connect-src names the store the server presigns uploads to (#26281)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XE7Tix5uExkLdxanf8sDFR5)_Fixes#26200
## Summary
Since the Console document got its nonce CSP, `connect-src` names only
`'self'`, the default map style host, the deployment's map style and
error-reporting origins, and `OS_CONSOLE_CSP_CONNECT_SRC`. With S3 or an
S3-compatible store, the Console uploads a file by `PUT`ting it straight
to the presigned URL, on the store's host. That host was not in the
policy, so the browser refused every upload with `TypeError: Failed to
fetch`.
The ruling on the card (「另在 objectstack 立卡让 CSP 自动带上存储端点」) asks for the
configured store's origin exactly as the presigner emits it, with one
copy of the endpoint rule and no wildcard. This PR does that in two
parts.
- **`@objectstack/service-storage`.**
`S3StorageAdapter.getPresignedUploadOrigin()` signs a probe `PutObject`
for the adapter's own bucket with the same client options every real
presign uses (factored into one private `clientOptions()`), and answers
the URL's origin. The presigner decides the host, so a virtual-hosted
bucket, a region-templated AWS host, a path-style endpoint and a bucket
name that cannot be a host label all come out as the upload URL carries
them. The probe signs with fixed fake credentials, so it never consults
the AWS credential chain and never opens a socket. The answer is cached
per adapter instance, and a rejection is not cached.
`SwappableStorageService.getPresignedUploadOrigin()` answers the active
adapter's origin, or `undefined` when that adapter has none (the local
adapter's uploads go to the server itself).
- **`@objectstack/cli` (`utils/console.ts`).** The mount reads
`getPresignedUploadOrigin` off the kernel's `storage` service by name,
per document, and adds the answer to `connect-src` after the
error-reporting origin and before `OS_CONSOLE_CSP_CONNECT_SRC`. The
module still imports Node built-ins only. An answer that does not match
the module's existing connect-source grammar (an IPv6 literal, say) is
left out, and so is a failure to answer (an endpoint the SDK refuses);
either is reported once at `warn`, naming `OS_CONSOLE_CSP_CONNECT_SRC`
as the remedy. No storage service, or the local adapter, adds nothing.
**Runtime settings.** Storage can be changed in Setup → Settings without
a restart: `StorageServicePlugin` swaps the adapter inside the one
`SwappableStorageService`. Because the mount reads the origin per
document, not at boot, the next document after a swap names the new
store (pinned below).
**Hosted.** cloud's `objectos-runtime` mounts the Console through
`createConsoleStaticPlugin`, whose signature is unchanged. The origin
arrives through the kernel's `storage` service, so no call-site change
is needed wherever that kernel registers `StorageServicePlugin`. Whether
`objectos-runtime`'s kernel does is outside this repository and not
measured here (see Acceptance notes).
## Tests
- `packages/cli/src/utils/console.content-security-policy.test.ts`, new
describe `connect-src names the host the storage service presigns
browser uploads to`. Each case runs the REAL `StorageServicePlugin.init`
and the REAL console plugin on a REAL Hono app:
- an R2-style store (virtual-hosted) adds
`https://csp-uploads.acct123.r2.cloudflarestorage.com`, the bucket host
and not the endpoint host;
- parity: for an R2 endpoint, AWS `eu-west-2`, AWS with a dotted bucket,
and a path-style `http://127.0.0.1:9000`, the served `connect-src`
equals `'self'`, the map host and the origin of a real
`getPresignedUpload` URL from the same service;
- a settings swap (`storage.swap(...)`) is followed on the next
document;
- local storage adds nothing;
- an endpoint the presigner refuses adds nothing and warns once over two
documents;
- `OS_CONSOLE_CSP_CONNECT_SRC` still follows the storage origin.
-
`packages/services/service-storage/src/s3-storage-adapter.presign-origin.test.ts`:
five configurations (virtual-hosted and path-style S3-compatible
endpoints, a self-hosted port, AWS region-templated, AWS dotted bucket),
each answering the origin of a real presigned upload URL and the literal
expected origin; no credentials needed; two refused endpoints reject;
the swappable service follows a swap and answers `undefined` for the
local adapter.
**Red first** (base `3d50a43bc`, the CLI suite before the fix): 4 failed
/ 40 passed. Each storage case read `connect-src 'self'
https://demotiles.maplibre.org` and nothing else.
**Ablation** (at head `0fd3cd0d`, via `scripts/ablation-replace.mjs`;
the suite imports `console.ts` from source, so there is no dist leg):
deleting the line that pushes `storageUploadOrigin` into `connect-src`
landed (anchor 1 → 0, blob `497ea91966dc` → `1f54438adfbc`), the CSP
suite went 4 failed / 41 passed, and the restore matched `HEAD` with
`git diff HEAD` empty.
Package runs, at head `8990880046` (after merging `origin/main`, which
touched `service-storage`):
- `pnpm --filter @objectstack/service-storage test`: 67 files, 1236
tests passed.
- `pnpm --filter @objectstack/service-storage typecheck` and `pnpm
--filter @objectstack/cli typecheck`: exit 0.
- `vitest run --project unit` for the CSP suite and
`test/published-subpath-console.pin.test.ts` (the packed
`@objectstack/cli/console` still resolves with Node built-ins only): 2
files, 59 tests passed.
- Before that merge (head `0fd3cd0d`): the whole CLI `unit` project, 334
files, 4884 tests passed. The `integration` tier is declared to CI; the
diff touches no spawn entry.
- `eslint --no-inline-config` over the six touched files: exit 0.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 72 commands at head `0fd3cd0d`. Each
ran with its exit code captured by redirect before any pipe. Three first
exited 3 on a prerequisite this worktree lacked
(`check-plugin-teardown-shape --self-test` on a shallow clone, and
`check:dual-build-cjs-loads` and `check:i18n-coverage` on unbuilt
packages outside the diff). They were rerun after unshallowing and
building, and exited 0.
- `--ran` reconciliation: "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN",
exit 0.
- After merging `origin/main` (head `8990880046`) the derivation is the
same list less the two `check-position-name-fold-loaders` rows that main
retired. The ratchet families (`check:type-check-coverage`,
`check:type-check-debt`, `check:query-options-erasure`,
`check:test-source-alias`, `check:cross-package-test-inputs`) and
`check:nul-bytes` were rerun there, and all exited 0. At the final head
`1f201e46b4`, which changes only the changeset's wording, the changeset
gates (`check-changeset-no-major`, `check-adr-0087-registration`,
`check-empty-changeset`) and `check:nul-bytes` exited 0.
- Dogfood battery: not run. The title has no `!` and `Clause-②` has no
`(narrowing)` arm.
## Declarations
Clause-②: yes (widening)
Metadata-protocol: none
Measured on the diff. Two new public methods ship on exported classes of
a published package (`S3StorageAdapter#getPresignedUploadOrigin`,
`SwappableStorageService#getPresignedUploadOrigin`), and the Console
policy accepts one more origin. That origin is always the one the server
itself hands the page. No metadata an author writes moves, so both
packages take a `patch` changeset
(`.changeset/26200-console-csp-storage-upload-origin.md`). Nothing is
removed or narrowed, so ADR-0087 has no disposition to state.
`packages/spec` is untouched, as the claim requires.
## Acceptance notes
- **The hosted half is a verification, not a change.**
`createConsoleStaticPlugin` reads `storage` off the kernel context it is
started in. If cloud's `objectos-runtime` registers
`StorageServicePlugin` on that kernel, hosted uploads work once the
framework pin moves past this PR, and the `OS_CONSOLE_CSP_CONNECT_SRC`
stop-gap can be dropped. If it composes storage elsewhere (another
kernel, or `mountStorageRoutes` over a service the Console's kernel
never sees), the CSP adds nothing there. That cannot be measured from
this repository, so it is reported to the seat for a cloud-lane card.
- **What "endpoint unset" was read as.** AWS S3 with no custom endpoint
still adds its region-templated origin
(`https://BUCKET.s3.REGION.amazonaws.com`). The ruling names
region-templated hosts, and those exist only when no endpoint is set.
"Unset" was read as "no S3 store configured", which adds nothing. The
other reading would leave every AWS-native deployment's uploads blocked.
- **The accessor is not on the `IStorageService` contract.** The claim
forbids `packages/spec/**`, so the Console reads the method by name. An
adapter from outside this repository that presigns to another host is
not covered until it implements the same method, or the contract
declares it.
- **The local adapter's `baseUrl` option** could make its presigned URLs
absolute on another origin. No producer in this repository sets it, and
the ruling says local adds nothing, so the CSP adds nothing for it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JFi7QBsDBsBeLzDyTyerZh
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli,service-storage): the Console document's `connect-src` names the store the server presigns browser uploads to
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: none
10
+
11
+
With S3 or an S3-compatible store (R2, MinIO), the Console uploads a file by `PUT`ting it straight to the URL `POST /api/v1/storage/upload/presigned` answers, on the store's host. The Console document's content-security-policy did not name that host, so the browser refused every such upload with `TypeError: Failed to fetch`.
12
+
13
+
-**`@objectstack/service-storage`.**`S3StorageAdapter.getPresignedUploadOrigin()` answers the origin every presigned upload URL carries. It signs a probe upload with the same client options as every real presign, so a virtual-hosted bucket (`BUCKET.ENDPOINT-HOST`), a region-templated AWS host and a path-style endpoint come out exactly as the upload URL carries them. Signing is local and uses fixed probe credentials, so it never reaches the network or the AWS credential chain. `SwappableStorageService.getPresignedUploadOrigin()` answers the active adapter's origin, or `undefined` for an adapter without one (the local adapter's uploads go to the server itself).
14
+
-**`@objectstack/cli`.** The Console mount reads that origin off the kernel's `storage` service on every document and adds it to `connect-src`, after the error-reporting origin and before `OS_CONSOLE_CSP_CONNECT_SRC`. A storage settings change that swaps the adapter is followed on the next document, with no restart. Local storage, no storage service, or a store whose endpoint the presigner refuses adds nothing. The last case logs one `warn` that names the remedy. `OS_CONSOLE_CSP_CONNECT_SRC` still adds an operator's extra sources, and a deployment that named its store there as a stop-gap can drop it.
0 commit comments