Skip to content

Commit 4aa8fbe

Browse files
feat(storage): add storage zone and file commands with S3 credentials (#98)
* feat(storage): add storage zone and file commands with S3 credentials * updates to creating storage zones with domains * update table header color * apply fixes * apply code suggestions * add note about flags for secrets * link and fix file listing * apply greptile feedback * replica add warning * improve force cmds * secret fix
1 parent 656b9fb commit 4aa8fbe

42 files changed

Lines changed: 2513 additions & 129 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/tame-pears-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bunny.net/cli": minor
3+
---
4+
5+
feat(storage): add storage zone and file commands with S3-compatible credentials

AGENTS.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,11 @@ bunny-cli/
183183
│ │ ├── format.test.ts # Tests for format utilities
184184
│ │ ├── hostnames/ # Reusable pull-zone hostname feature (mounted by scripts; apps next)
185185
│ │ │ ├── index.ts # Re-exports client helpers, DNS/flow helpers + createHostnamesCommands
186-
│ │ │ ├── client.ts # hostnameUrl(), normalizeHostname(), addHostname(), fetchPullZoneHostnames(), enableSsl() + Hostname/ResolvedPullZone types
186+
│ │ │ ├── client.ts # hostnameUrl(), normalizeHostname(), addHostname(), fetchPullZoneHostnames(), enableSsl(), createPullZone() (storage-zone origin) + Hostname/ResolvedPullZone types
187187
│ │ │ ├── client.test.ts # Tests for hostnameUrl() scheme logic
188188
│ │ │ ├── dns.ts # dnsPointsAt()/anyResolverPointsAt(): DNS checks (CNAME or flattened A records) via system + public (1.1.1.1/8.8.8.8) resolvers, injectable for tests
189189
│ │ │ ├── dns.test.ts # Tests for DNS matching + multi-resolver checks with fake resolvers
190-
│ │ │ ├── flow.ts # offerDnsWaitAndSsl(): poll DNS + opportunistically attempt SSL issuance (~30s) since bunny's resolvers decide validation; printSslHint(). dnsAlreadyLive skips the poll (Bunny DNS record already live). offerBunnyDnsThenSsl() takes an optional onBunnyDnsZone(zone) callback fired when the hostname is on Bunny DNS (lets the command layer link the directory)
190+
│ │ │ ├── flow.ts # offerDnsWaitAndSsl(): poll DNS + opportunistically attempt SSL issuance (~30s) since bunny's resolvers decide validation; printSslHint(). dnsAlreadyLive skips the poll (Bunny DNS record already live). offerBunnyDnsThenSsl() takes an optional onBunnyDnsZone(zone) callback fired when the hostname is on Bunny DNS (lets the command layer link the directory). setupHostname(): resource-agnostic add-hostname -> DNS -> SSL orchestration (caller supplies sslHint/retryHint); used by scripts setupCustomDomain and storage zone add
191191
│ │ │ ├── bunny-dns.ts # findBunnyDnsZone()/offerBunnyDnsRecord(): detect a hostname inside an account Bunny DNS zone, then add/repoint a PullZone record (always confirmed) so SSL can issue immediately
192192
│ │ │ ├── bunny-dns.test.ts # Tests for longest-suffix zone matching + record-name derivation with a fake core client
193193
│ │ │ └── commands.ts # createHostnamesCommands(): add/ssl/list/remove factory parameterized by a pull-zone resolver
@@ -346,6 +346,33 @@ bunny-cli/
346346
│ │ │ ├── attach.ts # Bridge: add a SCRIPT record on a zone pointing at the script. `attach [domain] [name] --script <id>`; always confirms (apex gets a louder root-domain warning, lists existing records at the name), --force skips and is required to write non-interactively
347347
│ │ │ ├── link.ts # Link this directory to an existing DNS script → .bunny/dns-script.json (positional id, else pick interactively; preserves entry)
348348
│ │ │ └── list.ts # List DNS scripts (alias: ls)
349+
│ │ ├── storage/ # Experimental (hidden from help and landing page)
350+
│ │ │ ├── index.ts # defineNamespace("storage", ...): registers zone + file groups + link + regions + docs (+ hidden bucket aliases)
351+
│ │ │ ├── api.ts # CoreClient type, fetchStorageZones/fetchStorageZone, resolveStorageZone (name-or-ID to zone, re-fetched by ID), toSafeStorageZone (strips Password/ReadOnlyPassword; used by every command that emits a raw zone as JSON: show/list/add)
352+
│ │ │ ├── constants.ts # STORAGE_REGIONS (from SDK enum; /storagezone/regions API endpoint is not reliable) + replicationChoices/normalizeReplicationRegions (replication uses the same regions minus the primary; the SDK file ZoneSchema is the physical footprint, NOT the create input) + STORAGE_MANIFEST/StorageZoneManifest (.bunny/storage.json, written by storage link)
353+
│ │ │ ├── interactive.ts # resolveStorageZoneInteractive: explicit name/ID arg → linked manifest (.bunny/storage.json, fetched by ID even when non-interactive) → zone picker
354+
│ │ │ ├── link.ts # Link the current directory to a storage zone (.bunny/storage.json); bunny storage link [zone]
355+
│ │ │ ├── files-api.ts # Adapter over @bunny.net/storage-sdk: connectStorageZone (zone → SDK connection, Region→StorageRegion enum + password), listFiles/uploadFile/downloadFile/deleteFile (deleteFile translates the SDK's boolean return into a UserError)
356+
│ │ │ ├── files-api.test.ts # Tests for region mapping + delete error translation (NOT the SDK's URL building)
357+
│ │ │ ├── s3.ts # S3 (closed preview): isS3Enabled (StorageZoneType===1), s3Endpoint (<region>-s3.storage.bunnycdn.com), s3Credentials (name=access key, password=secret), renderS3ToolConfig (rclone/aws/s3cmd/env formatters)
358+
│ │ │ ├── s3.test.ts # Tests for S3 derivation + tool-config formatters
359+
│ │ │ ├── docs.ts # Open storage docs (bunny storage docs)
360+
│ │ │ ├── regions.ts # List available storage regions (bunny storage regions)
361+
│ │ │ ├── zone/ # `bunny storage zones` (canonical: zones; aliases: zone; hidden: bucket, buckets)
362+
│ │ │ │ ├── index.ts # defineNamespace("zones", ...) + storageZoneHiddenAliases (bucket/buckets)
363+
│ │ │ │ ├── list.ts # List all storage zones (alias: ls)
364+
│ │ │ │ ├── add.ts # Create a storage zone (prompts for name + region when omitted; offers/--pull-zone creates a pull zone via core/hostnames createPullZone, then offers/--domain a custom domain via setupHostname). Under --output json it stays non-interactive: --domain is attached via addHostname (no DNS/SSL prompts) and reported in the CustomDomain field (with cnameTarget or error)
365+
│ │ │ │ ├── show.ts # Show zone details (region, replication, hostname, usage; adds S3 endpoint rows when S3-enabled)
366+
│ │ │ │ ├── credentials.ts # S3 credentials / tool config for the zone (alias: creds; --format, --read-only, --show-secret); table masks the secret unless --show-secret, JSON/--format always emit it in full
367+
│ │ │ │ ├── update.ts # Update zone settings (custom 404, rewrite 404->200, replication); replication is additive (replicas can't be removed, so existing ones are kept and the prompt only offers new regions, confirming before adding); interactive pre-filled editor when no flags, non-interactive under --output json
368+
│ │ │ │ ├── remove.ts # Delete a storage zone and its files (alias: rm)
369+
│ │ │ │ └── hostnames/index.ts # Mounts core/hostnames createHostnamesCommands as "storage zone domains" (alias hostnames); resolver maps a storage zone (name/ID positional, --pull-zone) to its linked pull zone
370+
│ │ │ └── file/ # `bunny storage files` (canonical: files; aliases: file); zone is the --zone/-z flag (defaults to linked zone), the positional is the file/path
371+
│ │ │ ├── index.ts # defineNamespace("files", ...)
372+
│ │ │ ├── list.ts # List files in a directory (alias: ls; directories first; [path] positional, --zone flag)
373+
│ │ │ ├── upload.ts # Upload a local file (<file> positional, --zone, --to, --checksum streams a SHA256, --content-type)
374+
│ │ │ ├── download.ts # Download a file to disk (<path> positional, --zone, --out)
375+
│ │ │ └── remove.ts # Delete a file or directory (alias: rm; <path> positional, --zone, trailing slash = recursive)
349376
│ │ ├── registries/
350377
│ │ │ ├── index.ts # Manual CommandModule (not defineNamespace) — default handler runs list
351378
│ │ │ ├── list.ts # List container registries
@@ -402,6 +429,7 @@ bunny-cli/
402429
- **Config logic lives in `packages/cli/src/config/`** — schema, file resolution, and profile management.
403430
- **Error classes are split.** `UserError` and `ApiError` live in `@bunny.net/openapi-client` (the SDK needs them). `ConfigError` lives in the CLI and extends `UserError`. The CLI's `errors.ts` re-exports `UserError` and `ApiError` from `@bunny.net/openapi-client`.
404431
- **Import API clients from `@bunny.net/openapi-client`**, not relative paths. Import generated types from `@bunny.net/openapi-client/generated/<spec>.d.ts`.
432+
- **Mask secrets in human output; reveal only behind an explicit flag.** Any sensitive value (API keys, passwords, S3 secret keys, auth tokens) must be masked in the default table/text output and shown in full only when the user opts in with a flag (e.g. `--show-secret`). Use `maskSecret()` from `core/format.ts` for the masked form (it keeps the last 4 characters for identification). Machine-readable output is the exception because it exists to be consumed by tools: `--output json` and tool-config `--format` emit full values. Never print a secret the user did not explicitly ask to see. Reference: `storage zones credentials` masks the S3 secret access key by default and reveals it with `--show-secret`, while never leaking it from inspect/list commands (see `toSafeStorageZone`).
405433
- **Pull-zone settings are exposed via "Hybrid D" across surfaces.** Scripts and apps are backed by a pull zone, which has a large settings surface (hostnames, caching, edge rules, origin, security, purge, CORS, optimizer, logging, …). To keep each owner's help legible:
406434
- **Flatten only first-class groups** directly into the owner — picked by user mental model, kept to one or two. `scripts domains` is the flattened group (a custom domain is "my site's address," not a CDN setting).
407435
- **Group the long tail** under a `pullzone` sub-namespace within the owner (e.g. `scripts pullzone <setting>`), so the owner's top-level help gains one line, not ten. Curate per owner — don't expose settings that don't apply (a script _is_ its pull zone's origin, so no origin-URL command under `scripts`).
@@ -927,6 +955,25 @@ bunny
927955
│ ├── enable [domain] [--anonymize-ip] [--anonymization onedigit|drop]
928956
│ │ Enable DNS query logging
929957
│ └── disable [domain] [--force] Disable DNS query logging
958+
├── storage (experimental, hidden from help and landing page)
959+
│ │ Two resource groups: `zones` (the zone, via core API + account key) and `files` (zone contents, via @bunny.net/storage-sdk + the zone password/region host, resolved automatically). The zone is a name or numeric ID; `zones` commands take it as the `[zone]` positional, `files` commands as the `--zone`/`-z` flag (the positional is the file/path). When the zone is omitted it resolves from a linked zone (`bunny storage link`) then an interactive picker.
960+
│ ├── zones (canonical; aliases: zone; hidden: bucket, buckets)
961+
│ │ ├── list List all storage zones (alias: ls)
962+
│ │ ├── add [name] [--region] [--replication] [--pull-zone] [--pull-zone-name] [--domain] [--force/-f] Create a storage zone (prompts for name + region when omitted; offers/--pull-zone creates a pull zone to serve it on the web, then offers/--domain a custom domain via setupHostname; replicas are permanent so adding any is confirmed; --force/--output json skip all prompts and use flag values only)
963+
│ │ ├── show [zone] Show zone details (region, replication, hostname, usage)
964+
│ │ ├── update [zone] [--custom-404-path] [--rewrite-404-to-200] [--replication] [--force/-f] Update zone settings (edits interactively pre-filled when no flags; replication is additive and adding a replica is confirmed unless --force; non-interactive under --output json)
965+
│ │ ├── remove [zone] [--force] Delete a storage zone and its files (alias: rm)
966+
│ │ ├── credentials [zone] [--format rclone|aws|s3cmd|env] [--read-only] [--show-secret] (alias: creds)
967+
│ │ │ S3 credentials for the zone (name = access key, password = secret); --format emits tool config, else table/--output json; table masks the secret unless --show-secret
968+
│ │ └── domains (canonical; alias: hostnames) custom domains on the zone's pull zone; mounts core/hostnames createHostnamesCommands; resolver maps the storage zone to its linked pull zone
969+
│ ├── files (canonical; aliases: file) [--zone|-z] defaults to the linked zone on every file command
970+
│ │ ├── list [path] [--zone] (alias: ls) List files in a directory (trailing slash on path)
971+
│ │ ├── upload <file> [--zone] [--to] [--checksum] [--content-type] Upload a local file
972+
│ │ ├── download <path> [--zone] [--out] Download a file
973+
│ │ └── remove <path> [--zone] [--force] (alias: rm) Delete a file or directory (trailing slash = recursive)
974+
│ ├── link [zone] Link the current directory to a storage zone (.bunny/storage.json); interactive picker when omitted
975+
│ ├── regions List available storage regions (replication uses the same set minus the primary)
976+
│ └── docs Open storage documentation in browser
930977
├── db
931978
│ ├── create [--name] [--primary] [--replicas] [--storage-region]
932979
│ │ Create a new database

0 commit comments

Comments
 (0)