Metadata-Version: 2.5
Name: gcs-static-site-proxy
Version: 0.1.0
Summary: Serve private GCS static sites securely through a local proxy
Project-URL: Documentation, https://github.com/alexei-ciobanu/gcs-static-site-proxy#readme
Project-URL: Issues, https://github.com/alexei-ciobanu/gcs-static-site-proxy/issues
Project-URL: Source, https://github.com/alexei-ciobanu/gcs-static-site-proxy
Author: Alexei Ciobanu
License-Expression: MIT
License-File: LICENSE
Keywords: gcs,google-cloud,proxy,static-site
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Requires-Python: >=3.11
Requires-Dist: aiohttp<4,>=3.11
Requires-Dist: google-auth[requests]<3,>=2.38
Requires-Dist: truststore<1,>=0.10
Description-Content-Type: text/markdown

# gcs-static-site-proxy

Serve one private Google Cloud Storage static-site prefix, or an explicit
catalog of prefixes, through a renewable, read-only local proxy. The GCS bucket
remains private: the proxy uses the operator's Application Default Credentials
(ADC) and exposes only explicitly configured object prefixes.

> **Status:** alpha. Review the publication-prefix and network-access security
> model before using the proxy with protected data.

## Requirements

- Python 3.11 or newer
- [`uv`](https://docs.astral.sh/uv/) for the recommended isolated invocation
- Google Cloud CLI for initializing or refreshing ADC
- `storage.objects.get` on the configured objects, normally through
  `roles/storage.objectViewer` or a stronger role

Initialize ADC:

```bash
gcloud auth login --update-adc
```

## Install and run on localhost

Run the published package in an isolated environment:

```bash
uvx gcs-static-site-proxy@0.1.0 \
  --bucket private-site-bucket \
  --prefix team/sites/my-site
```

Alternatively, install it with `pip` and use either the command or Python
module entry point:

```bash
python -m pip install gcs-static-site-proxy==0.1.0
python -m gcs_static_site_proxy \
  --bucket private-site-bucket \
  --prefix team/sites/my-site
```

Run directly from a pinned public Git commit during development:

```bash
uvx --from 'git+https://github.com/alexei-ciobanu/gcs-static-site-proxy@COMMIT_SHA' \
  gcs-static-site-proxy \
  --bucket private-site-bucket \
  --prefix team/sites/my-site
```

The default listener is `127.0.0.1:8080`. It accepts only `GET` and `HEAD`,
rejects path traversal, and does not provide a general GCS proxy.
Outbound authentication and GCS requests use the operating system trust store,
including root certificate authorities installed through device management.

## Multiple sites and a landing page

Catalog mode serves a landing-page prefix at `/` and mounts explicitly
allowlisted site prefixes under `/sites/<slug>/`:

```bash
uvx gcs-static-site-proxy@0.1.0 \
  --bucket private-site-bucket \
  --catalog-prefix team/publication/static-sites
```

The catalog prefix must contain this exact object:

```text
team/publication/static-sites/.gcs-static-site-proxy-sites.json
```

Example:

```json
{
  "version": 1,
  "sites": [
    {
      "slug": "service-review",
      "title": "Service review",
      "prefix": "team/projects/service-review/publication/site"
    }
  ]
}
```

The landing page remains under the catalog prefix. Each mount maps only to its
declared prefix; unknown slugs fail closed, and neither catalog nor site
configuration objects are browser-accessible. Catalog editors can expose any
prefix that their proxy operator can read, so treat the catalog as a browser
publication allowlist and never mount a protected artifact prefix.

The proxy loads and validates the catalog and every site's CSP at startup. The
landing page uses the catalog prefix's `.gcs-static-site-proxy.json`; each
mounted site uses the same well-known filename under its own prefix. Explicit
CLI CSP overrides apply to all mounts.

Path-mounted sites share a browser origin. Per-response CSPs remain distinct,
but a script running in one mounted site can make same-origin requests to other
mounted sites. Use one catalog only for sites approved for the same audience;
use separate origins when stronger isolation is required.

## Site security configuration

Unless overridden, the proxy requests this exact object at startup:

```text
<prefix>/.gcs-static-site-proxy.json
```

Example:

```json
{
  "version": 1,
  "contentSecurityPolicy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
}
```

If it is absent, a strict built-in policy is used. An invalid or inaccessible
configuration fails startup. The active source, generation and SHA-256 are
printed.

Without an explicit CLI mode, each prefix uses its own GCS configuration and
falls back to the strict built-in policy when that object is absent. The
mutually exclusive global modes are:

- `--csp-override-file PATH` applies one exact local policy to every route;
- `--strict-csp` applies the strict built-in policy to every route; and
- `--no-csp` disables CSP on every route.

In catalog mode these options affect the landing page and every mounted site.
For a local override of only one site, run that prefix separately in
single-site mode.

`--no-csp` disables only CSP. The proxy continues to send `no-referrer`,
`nosniff`, and frame-denial headers.

## Network access

Loopback is the safe default. Explicit non-loopback binding enables network
mode:

```bash
uvx gcs-static-site-proxy@0.1.0 \
  --bucket private-site-bucket \
  --prefix team/sites/my-site \
  --bind 0.0.0.0
```

Network mode generates a process-lifetime access token. Opening the printed
tokenized URL exchanges it for an HttpOnly, SameSite session cookie. IP-literal
Host values are accepted when they match the local destination address;
machine hostnames are detected automatically. Use `--allow-host HOST` for an
additional DNS alias.

The cookie is intentionally a browser-session cookie rather than a persistent
credential. If the browser is restarted while the proxy remains running, open
the printed tokenized URL again.

Anyone who has the token and can reach the listener can read the site using
the operator's GCS permissions. Plain HTTP does not protect content from
network interception. Supply both options for HTTPS:

```bash
--tls-cert certificate.pem --tls-key private-key.pem
```

Remote visitors cannot launch the local ADC reauthentication command.

## Local development

```bash
uv sync --dev
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run ty check src
uv build --no-sources
```

## License

MIT
