Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Update Channel

An authenticated, multi-product software update channel on AWS — the backend that decides who is allowed to download which build, and hands out short-lived, presigned download URLs instead of making artifacts public.

It is built from four small, dependency-free pieces:

  • Feed — an AWS Lambda (behind API Gateway) that authenticates a bearer token, checks that the token is entitled to the requested product, and returns a manifest or a presigned S3 URL.
  • CDK — one reusable stack construct, instantiated twice to produce two IAM-isolated copies of the channel that share zero blast radius.
  • Publisher — a CLI that builds a content-hashed manifest and uploads the artifact + manifest to the right product/channel prefix in S3.
  • Client — a tiny Node feed client that checks for an update and resolves the download URL.

Extracted and generalized from a production update system I built and operated (it shipped several desktop and CLI products across many release channels). Product names and internals have been replaced with generic examples; the architecture and tests are the real thing.


Why it exists

Shipping auto-updates to installed desktop/CLI software raises three problems that a public S3 bucket doesn't solve:

  1. Entitlement — not every client should be able to pull every product. A token must reach only the products it is granted, so one product's credentials can't fetch another's builds.
  2. Private artifacts — builds shouldn't be world-readable. The feed keeps the bucket private and issues presigned URLs with a short TTL per request.
  3. Blast-radius isolation — a shared-workspace collaborator who can publish product A must have no path — via IAM or via a shared Lambda — to product B. That is enforced by running two physically separate stacks, not by application logic alone.

Architecture

                              ┌──────────────────────── AWS ────────────────────────┐
  client ──bearer token──►  API Gateway ──►  Lambda (feed)                           │
      │                                          │                                   │
      │                        auth: token ─► entitled products?  (constant-time)    │
      │                                          │                                   │
      │                        route: /v1/{product}/manifest/{channel}.json          │
      │                               /v1/{product}/config.json                      │
      │                                          │                                   │
      │                        ┌── manifest ─────┴── presigned GET ──► S3 (private) ─┤
      ◄── 200 manifest / 302 presigned URL ──────────────────────────────────────── ┘

  publisher ──build manifest (sha256, size, version)──► S3 put (artifact + manifest)
                                          │
                    Stack B publishes only if the caller's AWS identity
                    is the designated deployer (STS identity guard)
  • Two stacks, one construct. Stack A serves the everyday products a collaborator can publish to. Stack B is isolated: the publisher refuses to upload to it unless the caller's resolved AWS identity is the designated deployer, so an accidental Stack A-shaped publish can never land a build in the isolated bucket.
  • Tokens map to products. The feed reads an ACCESS_TOKENS map of { "token": ["product", …] }. Token comparison is timingSafeEqual over SHA-256 digests, so it leaks nothing through timing — including length.
  • Channels are a product's parallel release streams (e.g. payload, app, config, beta, bin, full). Large channels get a longer presigned-URL TTL so slow downloads don't expire mid-transfer.
  • Manifests are content-addressed: { version, key, sha256, size, notes, minHostVersion }. The client verifies the hash after download.
  • Legacy flat routes (/manifest.json, /config, …) are kept as permanent aliases so already-installed clients that hard-coded them never lose their update path.

Layout

src/feed/         the Lambda feed
  auth.mjs          token → entitled-products map; constant-time check
  routes.mjs        pure URL parsing (no IO), exhaustively unit-tested
  handler.mjs       manifest lookup + presigned-URL issuance
  index.mjs         Lambda entrypoint
src/publish/
  ship.mjs          publisher: build manifest, validate, upload; Stack B identity guard
src/client/node/
  feedclient.js     update check + download-URL resolution (CommonJS-consumable)
cdk/
  update-feed-stack.ts   the reusable stack (S3 + Lambda + API GW + least-privilege IAM)
  bin/app.ts             instantiates it twice → two IAM-isolated stacks
tests/              node:test suites for feed, auth, routes, publisher, client

Design choices worth calling out

  • Zero runtime dependencies. The feed, publisher, and client run on the Node standard library only (node:crypto, node:test, node:assert). The only third-party dependencies are the CDK libraries, and they are isolated in cdk/ so nothing else inherits them.
  • Pure route parsing. URL parsing is a separate, IO-free module — the one piece of logic a deployed client's URL actually hits — so it can be tested exhaustively without mocking AWS.
  • Frozen wire shapes. Response shapes that installed clients already parse are treated as permanent contracts; the tests encode that so a refactor can't silently strand old installs.

Running the tests

# Feed, auth, routes, publisher, client — no network, no AWS, no filesystem outside a temp dir
node --test tests/*.mjs        # 66 tests

# CDK stack assertions (installs aws-cdk-lib locally, then runs synth-based tests)
cd cdk && npm install && npm test

Deploying (sketch)

cd cdk
npm install
# Configure the two stacks (bucket/function/role names, product lists, ACCESS_TOKENS) in bin/app.ts
npx cdk deploy --all

Publish a build:

node src/publish/ship.mjs desktop-app payload ./build/desktop-app-1.4.0.zip
# builds a sha256 manifest and uploads artifact + manifest under desktop-app/ in the Stack A bucket

License

MIT — see LICENSE.

About

Authenticated multi-product software update channel on AWS — S3 + Lambda + API Gateway feed, per-product bearer tokens, presigned URLs, a CDK construct (two IAM-isolated stacks), publisher CLI + client. Zero runtime deps.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages