create-headroom 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -80,7 +80,11 @@ async function main() {
80
80
 
81
81
  Next steps:
82
82
  cd ${projectName}
83
- ./scripts/setup.sh <stage> # First-time setup (sets secrets + deploys)
83
+ ./scripts/setup.sh <stage> # First-time setup: sets the three SST secrets
84
+ # (image-signing master + rotation fallback +
85
+ # internal service-to-service) and deploys.
86
+ # Re-running is idempotent — already-set
87
+ # secrets are left alone.
84
88
 
85
89
  For subsequent deploys:
86
90
  npx sst deploy --stage <stage>
@@ -88,6 +92,10 @@ async function main() {
88
92
  After deploying, create your first admin user:
89
93
  ./scripts/create-admin.sh admin@example.com 'YourPassword123!'
90
94
 
95
+ To rotate the image-signing master (preserving the grace window for
96
+ already-signed URLs):
97
+ ./scripts/setup.sh <stage> --force-rotate
98
+
91
99
  Documentation: https://github.com/cykod/headroom
92
100
  `);
93
101
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-headroom",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "description": "Create a new Headroom CMS project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,74 @@
1
+ # Headroom CMS Deployment
2
+
3
+ This project deploys a Headroom CMS instance to AWS via SST.
4
+
5
+ ## First-time setup
6
+
7
+ ```bash
8
+ ./scripts/setup.sh <stage>
9
+ ```
10
+
11
+ This generates the three SST secrets required by the stack and then runs
12
+ `npx sst deploy`. Re-running the script is idempotent: already-set secrets
13
+ are left alone.
14
+
15
+ ### Required SST secrets
16
+
17
+ The instance name `CMS` (the first argument to `new HeadroomCMS(...)` in
18
+ `sst.config.ts`) is prepended to each secret name. If you change that
19
+ instance name, also update `INSTANCE` at the top of `scripts/setup.sh`.
20
+
21
+ | Secret | Required? | What breaks if missing |
22
+ |--------|-----------|------------------------|
23
+ | `CMSImageSigningMasterSecret` | **Yes** | All `/img/*` image-transform requests return 403. Per-site signing keys are derived from this master via HMAC-SHA256. |
24
+ | `CMSImageSigningMasterSecretOld` | Optional (set to empty by default) | Rotation fallback. The Sharp Lambda accepts URLs signed with either master, so populating this lets you rotate the primary without invalidating already-issued URLs. Empty disables the fallback path. |
25
+ | `CMSInternalSecret` | **Yes** (if collaboration is enabled) | The collaboration Lambda's calls to the Go API (via the `X-Headroom-Internal` header) are rejected, breaking real-time editing. |
26
+
27
+ ## Rotating the image signing master
28
+
29
+ To rotate while preserving signed URLs that may still be in CDN / browser
30
+ caches:
31
+
32
+ ```bash
33
+ ./scripts/setup.sh <stage> --force-rotate
34
+ ```
35
+
36
+ This moves the current `CMSImageSigningMasterSecret` value to
37
+ `CMSImageSigningMasterSecretOld`, generates a new primary, and redeploys.
38
+ `CMSInternalSecret` is **not** rotated by this flag — it has no OLD-fallback
39
+ pair and rotating it would reject every in-flight collab Lambda → Go API
40
+ request until the new deploy completes.
41
+
42
+ Manual equivalent:
43
+
44
+ ```bash
45
+ npx sst secret set CMSImageSigningMasterSecretOld <current-value> --stage <stage>
46
+ npx sst secret set CMSImageSigningMasterSecret <new-value> --stage <stage>
47
+ npx sst deploy --stage <stage>
48
+ ```
49
+
50
+ See the root project's `CLAUDE.md` "Rotating the image signing master"
51
+ section for guidance on how long to leave `MasterSecretOld` populated.
52
+
53
+ ## Rotating the internal secret
54
+
55
+ `CMSInternalSecret` has no rotation grace window — rotating it causes a
56
+ brief collab outage until the next deploy completes. Plan a deploy window,
57
+ then rotate manually:
58
+
59
+ ```bash
60
+ npx sst secret set CMSInternalSecret "$(openssl rand -hex 32)" --stage <stage>
61
+ npx sst deploy --stage <stage>
62
+ ```
63
+
64
+ ## Subsequent deploys
65
+
66
+ ```bash
67
+ npx sst deploy --stage <stage>
68
+ ```
69
+
70
+ ## Creating an admin user
71
+
72
+ ```bash
73
+ ./scripts/create-admin.sh admin@example.com 'YourPassword123!'
74
+ ```
@@ -9,7 +9,7 @@
9
9
  "remove": "sst remove"
10
10
  },
11
11
  "dependencies": {
12
- "headroom-cms": "^0.1.10"
12
+ "headroom-cms": "^0.2.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "sst": "^3.17.0",
@@ -1,37 +1,209 @@
1
1
  #!/bin/bash
2
2
  # First-time setup for a Headroom CMS deployment stage.
3
3
  #
4
- # Generates and sets required secrets, then deploys.
4
+ # Sets the three SST secrets required by Headroom and then deploys.
5
+ #
6
+ # The instance name (INSTANCE below) MUST match the first argument passed
7
+ # to `new HeadroomCMS(...)` in sst.config.ts — the secret resources are
8
+ # named `${INSTANCE}ImageSigningMasterSecret`, etc.
9
+ #
10
+ # Secrets set:
11
+ # 1. ${INSTANCE}ImageSigningMasterSecret — REQUIRED. Master KDF input
12
+ # used to derive per-site image signing keys. Without it, every
13
+ # /img/* request 403s.
14
+ # 2. ${INSTANCE}ImageSigningMasterSecretOld — OPTIONAL fallback master,
15
+ # initialised empty. The Sharp Lambda accepts URLs signed with either
16
+ # master, so populating this lets you rotate the primary without
17
+ # invalidating already-issued URLs.
18
+ # 3. ${INSTANCE}InternalSecret — REQUIRED for collaboration.
19
+ # Shared secret on the X-Headroom-Internal header between the collab
20
+ # Lambda and the Go API.
21
+ #
22
+ # Idempotency: re-running the script does NOT overwrite already-set
23
+ # secrets. Pass --force-rotate to rotate the image-signing master only
24
+ # (moves the current value to MasterSecretOld before generating a new
25
+ # primary, preserving the rotation grace window for in-the-wild signed
26
+ # URLs). InternalSecret rotation is NOT covered by --force-rotate — it
27
+ # has no OLD-fallback and rotating it breaks in-flight collab connections
28
+ # without grace. Rotate InternalSecret manually if needed:
29
+ # npx sst secret set CMSInternalSecret <new-value> --stage <stage>
5
30
  #
6
31
  # Usage:
7
32
  # ./scripts/setup.sh <stage>
33
+ # ./scripts/setup.sh <stage> --force-rotate
8
34
  #
9
35
  # Example:
10
36
  # ./scripts/setup.sh production
11
37
 
12
38
  set -euo pipefail
13
39
 
14
- STAGE="${1:-}"
40
+ INSTANCE="CMS" # Must match the instance name in sst.config.ts
41
+
42
+ FORCE_ROTATE=0
43
+ STAGE=""
44
+ for arg in "$@"; do
45
+ case "$arg" in
46
+ --force-rotate|--rotate)
47
+ FORCE_ROTATE=1
48
+ ;;
49
+ -*)
50
+ echo "Unknown flag: $arg" >&2
51
+ echo "Usage: $0 <stage> [--force-rotate]" >&2
52
+ exit 1
53
+ ;;
54
+ *)
55
+ if [ -z "$STAGE" ]; then
56
+ STAGE="$arg"
57
+ else
58
+ echo "Unexpected positional argument: $arg" >&2
59
+ echo "Usage: $0 <stage> [--force-rotate]" >&2
60
+ exit 1
61
+ fi
62
+ ;;
63
+ esac
64
+ done
15
65
 
16
66
  if [ -z "$STAGE" ]; then
17
- echo "Usage: $0 <stage>" >&2
67
+ echo "Usage: $0 <stage> [--force-rotate]" >&2
18
68
  echo "" >&2
19
69
  echo "Run this once per stage before your first deploy." >&2
20
70
  echo "Example: $0 production" >&2
21
71
  exit 1
22
72
  fi
23
73
 
74
+ # Sanity-check: INSTANCE here must match `new HeadroomCMS("...", ...)` in
75
+ # sst.config.ts. If they disagree, the script would set secrets under the
76
+ # wrong name and the deploy would fail at apply time with an opaque "secret
77
+ # not set" error. Catch the mismatch up front with a helpful message.
78
+ if [ -f sst.config.ts ]; then
79
+ ACTUAL_INSTANCE=$(grep -oE 'new HeadroomCMS\("[^"]+"' sst.config.ts | head -1 | sed 's/.*"\([^"]*\)".*/\1/' || true)
80
+ if [ -n "$ACTUAL_INSTANCE" ] && [ "$ACTUAL_INSTANCE" != "$INSTANCE" ]; then
81
+ echo "ERROR: sst.config.ts uses 'new HeadroomCMS(\"$ACTUAL_INSTANCE\", ...)' but INSTANCE=\"$INSTANCE\" in this script." >&2
82
+ echo " Edit the INSTANCE= line at the top of this script to match \"$ACTUAL_INSTANCE\"." >&2
83
+ echo " (Or rename the instance in sst.config.ts back to \"$INSTANCE\".)" >&2
84
+ exit 1
85
+ fi
86
+ fi
87
+
24
88
  echo "==> Setting up stage: $STAGE"
89
+ if [ "$FORCE_ROTATE" -eq 1 ]; then
90
+ echo " (--force-rotate: image-signing master will be rotated)"
91
+ fi
92
+
93
+ # secret_get <name> — print the current value of an SST secret to stdout, or
94
+ # empty string if the secret has never been set.
95
+ #
96
+ # This must distinguish two failure modes:
97
+ # (A) secret unset → expected on first run; treat as empty result.
98
+ # (B) any other SST/AWS failure (wrong stage, missing creds, transient
99
+ # error, etc.) → hard-fail. We MUST NOT proceed: a silent failure
100
+ # here on a real stage would regenerate the image-signing master
101
+ # and destroy every signed /img/* URL in the wild.
102
+ #
103
+ # SST exits non-zero with a "not set" / "not found" / "does not exist"
104
+ # message on (A); other failures produce different stderr. We match
105
+ # substrings as a defensible-but-imperfect proxy for distinct exit codes
106
+ # (SST does not document distinct codes per failure class).
107
+ secret_get() {
108
+ local name="$1"
109
+ local err_file out
110
+ err_file=$(mktemp 2>/dev/null || echo "/tmp/setup-sst-err.$$")
111
+ if out=$(npx sst secret get "$name" --stage "$STAGE" 2>"$err_file"); then
112
+ rm -f "$err_file"
113
+ printf '%s' "$out"
114
+ return 0
115
+ fi
116
+ # Non-zero exit. Classify.
117
+ local err
118
+ err=$(cat "$err_file" 2>/dev/null || true)
119
+ rm -f "$err_file"
120
+ if echo "$err" | grep -qiE "(not (set|found)|does not exist|no such secret)"; then
121
+ # (A) Secret has never been set — return empty stdout, exit 0.
122
+ return 0
123
+ fi
124
+ # (B) Anything else — surface and abort. Refusing to proceed prevents
125
+ # destroying production secrets when the user typed the wrong --stage
126
+ # or AWS credentials have expired.
127
+ echo "ERROR: querying ${name} (stage=${STAGE}) failed:" >&2
128
+ if [ -n "$err" ]; then
129
+ echo "$err" | sed 's/^/ /' >&2
130
+ fi
131
+ echo "" >&2
132
+ echo " Refusing to proceed — re-run with the correct --stage, or fix" >&2
133
+ echo " AWS credentials. If you believe this is a transient SST error," >&2
134
+ echo " re-run the script." >&2
135
+ exit 1
136
+ }
137
+
138
+ # 1. Image signing master (REQUIRED)
139
+ EXISTING_MASTER=$(secret_get "${INSTANCE}ImageSigningMasterSecret")
140
+ if [ "$FORCE_ROTATE" -eq 1 ]; then
141
+ if [ -n "$EXISTING_MASTER" ]; then
142
+ echo "==> Rotating ${INSTANCE}ImageSigningMasterSecret — moving current value to ${INSTANCE}ImageSigningMasterSecretOld"
143
+ npx sst secret set "${INSTANCE}ImageSigningMasterSecretOld" "$EXISTING_MASTER" --stage "$STAGE"
144
+ fi
145
+ SECRET=$(openssl rand -hex 32)
146
+ echo "==> Setting new ${INSTANCE}ImageSigningMasterSecret..."
147
+ npx sst secret set "${INSTANCE}ImageSigningMasterSecret" "$SECRET" --stage "$STAGE"
148
+ elif [ -n "$EXISTING_MASTER" ]; then
149
+ echo "==> ${INSTANCE}ImageSigningMasterSecret already set, leaving alone (pass --force-rotate to rotate)"
150
+ else
151
+ SECRET=$(openssl rand -hex 32)
152
+ echo "==> Setting ${INSTANCE}ImageSigningMasterSecret..."
153
+ npx sst secret set "${INSTANCE}ImageSigningMasterSecret" "$SECRET" --stage "$STAGE"
154
+ fi
25
155
 
26
- # Generate a random image signing secret
27
- SECRET=$(openssl rand -hex 32)
28
- echo "==> Setting HeadroomImageSigningSecret..."
29
- npx sst secret set HeadroomImageSigningSecret "$SECRET" --stage "$STAGE"
156
+ # 2. Image signing master OLD (rotation fallback; empty disables it)
157
+ #
158
+ # We always touch this secret on first setup so the resource exists in SST
159
+ # state that way the eventual rotation step ("move current to OLD before
160
+ # setting a new primary") is a single command rather than a create-or-update
161
+ # branch. Force-rotate above may have populated it with the previous master;
162
+ # only initialize to empty if no prior value (and no rotation) set it.
163
+ if [ "$FORCE_ROTATE" -eq 0 ]; then
164
+ EXISTING_OLD=$(secret_get "${INSTANCE}ImageSigningMasterSecretOld")
165
+ # `sst secret get` returns empty for both "never set" and "set to empty
166
+ # string". That's fine — setting it to empty again is a no-op-ish call
167
+ # that ensures the resource exists. We only call it when nothing else
168
+ # has populated it in this run.
169
+ if [ -z "$EXISTING_OLD" ]; then
170
+ echo "==> Initializing ${INSTANCE}ImageSigningMasterSecretOld (empty)..."
171
+ npx sst secret set "${INSTANCE}ImageSigningMasterSecretOld" "" --stage "$STAGE"
172
+ else
173
+ echo "==> ${INSTANCE}ImageSigningMasterSecretOld already set, leaving alone"
174
+ fi
175
+ fi
176
+
177
+ # 3. Internal service-to-service secret (REQUIRED if collab is enabled)
178
+ #
179
+ # NOT rotated by --force-rotate: it has no OLD-fallback pair, so rotating
180
+ # it would reject every in-flight collab Lambda → Go API request with 401
181
+ # until the new deploy completes. Rotate manually only when you can tolerate
182
+ # a brief collab outage:
183
+ # npx sst secret set ${INSTANCE}InternalSecret <new> --stage <stage>
184
+ EXISTING_INTERNAL=$(secret_get "${INSTANCE}InternalSecret")
185
+ if [ -z "$EXISTING_INTERNAL" ]; then
186
+ INTERNAL=$(openssl rand -hex 32)
187
+ echo "==> Setting ${INSTANCE}InternalSecret..."
188
+ npx sst secret set "${INSTANCE}InternalSecret" "$INTERNAL" --stage "$STAGE"
189
+ else
190
+ echo "==> ${INSTANCE}InternalSecret already set, leaving alone"
191
+ fi
30
192
 
31
193
  echo ""
32
194
  echo "==> Deploying stage: $STAGE"
33
195
  npx sst deploy --stage "$STAGE"
34
196
 
35
197
  echo ""
36
- echo "==> Setup complete! Next steps:"
37
- echo " ./scripts/create-admin.sh admin@example.com 'YourPassword123!'"
198
+ echo "==> Setup complete!"
199
+ echo ""
200
+ echo " To rotate the image-signing master later, preserving the grace"
201
+ echo " window for already-issued signed URLs:"
202
+ echo " ./scripts/setup.sh $STAGE --force-rotate"
203
+ echo ""
204
+ echo " Or manually:"
205
+ echo " npx sst secret set ${INSTANCE}ImageSigningMasterSecretOld <current> --stage $STAGE"
206
+ echo " npx sst secret set ${INSTANCE}ImageSigningMasterSecret <new> --stage $STAGE"
207
+ echo ""
208
+ echo " Next: create your first admin user"
209
+ echo " ./scripts/create-admin.sh admin@example.com 'YourPassword123!'"