@augurworks/augur 0.15.1
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/CANVAS.md +858 -0
- package/INSTALL.md +603 -0
- package/LICENSE +27 -0
- package/README.md +150 -0
- package/TRADEMARKS.md +72 -0
- package/agents/README.md +65 -0
- package/agents/canon-extract.md +117 -0
- package/agents/canon.md +151 -0
- package/agents/canvas.md +88 -0
- package/agents/currency.md +74 -0
- package/agents/drafts.md +92 -0
- package/agents/identity.md +148 -0
- package/agents/prototype-contract.md +69 -0
- package/agents/publishing.md +324 -0
- package/agents/review-feedback.md +76 -0
- package/agents/space-json.md +132 -0
- package/agents/ui-skill.md +101 -0
- package/agents/working-marks.md +86 -0
- package/brand/augur-eye.svg +3 -0
- package/brand/augur-icon.svg +41 -0
- package/brand/augur-mark.svg +5 -0
- package/brand/lentianova-bulky.otf +0 -0
- package/brand/lentianova-median.otf +0 -0
- package/brand/lentianova.woff2 +0 -0
- package/build.js +8409 -0
- package/changelog.md +133 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/package.json +57 -0
- package/pitis/PETS.md +121 -0
- package/pitis/README.md +117 -0
- package/pitis/index.html +266 -0
- package/pitis/piti.build.js +81 -0
- package/pitis/piti.js +929 -0
- package/pitis/roast-agent.md +185 -0
- package/realtime/src/index.js +119 -0
- package/realtime/wrangler.example.toml +44 -0
- package/scripts/adopt.mjs +106 -0
- package/scripts/board-snapshot.mjs +197 -0
- package/scripts/build-seed-pack.mjs +33 -0
- package/scripts/bundle-rekey.mjs +112 -0
- package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
- package/scripts/canon-extract.mjs +578 -0
- package/scripts/canon.mjs +599 -0
- package/scripts/canvas-screen.mjs +333 -0
- package/scripts/check-local.mjs +136 -0
- package/scripts/clawd-art.mjs +75 -0
- package/scripts/clawd-canvas.mjs +741 -0
- package/scripts/cli.mjs +100 -0
- package/scripts/clone.mjs +422 -0
- package/scripts/close.mjs +30 -0
- package/scripts/connect.mjs +103 -0
- package/scripts/deploy.mjs +157 -0
- package/scripts/dev.mjs +54 -0
- package/scripts/doc-lint.mjs +143 -0
- package/scripts/export.mjs +300 -0
- package/scripts/fork.mjs +64 -0
- package/scripts/freeze.mjs +74 -0
- package/scripts/frontdoor-parity.mjs +180 -0
- package/scripts/hook.mjs +67 -0
- package/scripts/identity-rekey.mjs +112 -0
- package/scripts/init.mjs +122 -0
- package/scripts/land.mjs +40 -0
- package/scripts/lib/adapters.mjs +156 -0
- package/scripts/lib/board-snapshot.mjs +511 -0
- package/scripts/lib/draft.mjs +385 -0
- package/scripts/lib/graduate.mjs +276 -0
- package/scripts/lib/instance.mjs +74 -0
- package/scripts/lib/marks.mjs +107 -0
- package/scripts/lib/materialize.mjs +138 -0
- package/scripts/lib/merge3.mjs +124 -0
- package/scripts/lib/offline-posture.mjs +120 -0
- package/scripts/lib/offline-respawn.mjs +18 -0
- package/scripts/lib/offline-wrangler.mjs +78 -0
- package/scripts/lib/publish-compose.mjs +12 -0
- package/scripts/lib/publish-conflict.mjs +78 -0
- package/scripts/lib/publish-evidence.mjs +144 -0
- package/scripts/lib/refine-compare.mjs +140 -0
- package/scripts/lib/refine-ledger.mjs +150 -0
- package/scripts/lib/refine-png.mjs +149 -0
- package/scripts/lib/seed-pack-build.mjs +141 -0
- package/scripts/lib/state-compare.mjs +68 -0
- package/scripts/lib/store.mjs +122 -0
- package/scripts/login.mjs +84 -0
- package/scripts/mark.mjs +112 -0
- package/scripts/migrate-board-keys.mjs +164 -0
- package/scripts/migrate.mjs +265 -0
- package/scripts/no-foreign-vocabulary.mjs +216 -0
- package/scripts/no-product-names.mjs +157 -0
- package/scripts/no-tenant-globals.mjs +1134 -0
- package/scripts/offline.mjs +247 -0
- package/scripts/og.mjs +170 -0
- package/scripts/one-tenant-resolver.mjs +146 -0
- package/scripts/open.mjs +63 -0
- package/scripts/publish.mjs +1041 -0
- package/scripts/read.mjs +31 -0
- package/scripts/refine.mjs +455 -0
- package/scripts/release-drift.mjs +138 -0
- package/scripts/restore.mjs +322 -0
- package/scripts/review.mjs +125 -0
- package/scripts/save.mjs +21 -0
- package/scripts/shell-lint.mjs +189 -0
- package/scripts/ship.mjs +460 -0
- package/scripts/shoot.mjs +264 -0
- package/scripts/smoke-invite.mjs +113 -0
- package/scripts/state-inventory.mjs +223 -0
- package/scripts/status.mjs +177 -0
- package/scripts/sync.mjs +27 -0
- package/scripts/tenant-do-rehearsal.mjs +1314 -0
- package/scripts/ui-copy-lint.mjs +202 -0
- package/scripts/watch.mjs +32 -0
- package/scripts/wrangler-preflight.mjs +246 -0
- package/seed/CANON.md +43 -0
- package/seed/README.md +119 -0
- package/seed/prototype-status.json +9 -0
- package/seed/registry.json +88 -0
- package/seed/skills/starter-ui/SKILL.md +52 -0
- package/seed/skills/starter-ui/skill.json +11 -0
- package/seed/skills/starter-ui/starter-tokens.css +73 -0
- package/seed/skills/starter-ui/starter-ui.css +318 -0
- package/seed/skills/starter-ui/starter-ui.js +50 -0
- package/seed/space.json +5 -0
- package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
- package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
- package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
- package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
- package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
- package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
- package/seed/threads.json +52 -0
- package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
- package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
- package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
- package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
- package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
- package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
- package/src/_worker.js +12668 -0
- package/src/board-key.mjs +57 -0
- package/src/board-room.mjs +881 -0
- package/src/bundle-keys.mjs +138 -0
- package/src/canon/collect-in-browser.js +215 -0
- package/src/canon/emit.mjs +232 -0
- package/src/canon/observe.mjs +260 -0
- package/src/canon/schema.mjs +451 -0
- package/src/canvas/DSEG-LICENSE.txt +95 -0
- package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
- package/src/canvas/canvas.css +885 -0
- package/src/canvas/canvas.js +5780 -0
- package/src/canvas/capture.js +485 -0
- package/src/chrome/appchrome.mjs +568 -0
- package/src/currency.mjs +203 -0
- package/src/delete-confirmation.mjs +254 -0
- package/src/drafts/drafts.js +326 -0
- package/src/entry.js +58 -0
- package/src/galleries.mjs +400 -0
- package/src/health-cron.mjs +173 -0
- package/src/identity.json +1 -0
- package/src/kv-codec.mjs +135 -0
- package/src/kv-identity.mjs +259 -0
- package/src/mail.mjs +599 -0
- package/src/provenance.mjs +74 -0
- package/src/publish-compose.mjs +363 -0
- package/src/publish-fork.mjs +221 -0
- package/src/publish-units.mjs +44 -0
- package/src/purge.mjs +81 -0
- package/src/review/COMMENTING-UX.md +306 -0
- package/src/review/cat.png +0 -0
- package/src/review/comment-cursor.svg +7 -0
- package/src/review/comments.js +1738 -0
- package/src/room-ticket.mjs +96 -0
- package/src/roster-chip.mjs +32 -0
- package/src/seed-pack.mjs +261 -0
- package/src/state-inventory.mjs +249 -0
- package/src/sw-logic.mjs +48 -0
- package/src/tenant-cache.mjs +141 -0
- package/src/tenant-context.mjs +386 -0
- package/src/tenant-do.js +2940 -0
- package/src/tenant-host.mjs +144 -0
- package/src/tenant-quotas.mjs +140 -0
- package/src/unit-core.mjs +103 -0
- package/src/unit-object.mjs +338 -0
- package/templates/README.md +67 -0
- package/templates/shell/deploy.yml +145 -0
- package/templates/shell/engine-bump.yml +142 -0
- package/templates/shell/health.yml +233 -0
- package/templates/shell/kv-backup.yml +201 -0
- package/templates/shell/roster-update.yml +110 -0
- package/templates/shell/space-preflight.yml +35 -0
- package/templates/shell/space-rebake.yml +79 -0
- package/templates/shell/store-backup.yml +152 -0
- package/templates/shell/wrangler.example.toml +266 -0
- package/templates/space/publish.yml +91 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: Roster update (from Augur Admin)
|
|
2
|
+
|
|
3
|
+
# Receiver for Admin-panel roster changes. Inviting or removing a person in the
|
|
4
|
+
# Augur Admin UI fires a `roster-update` repository_dispatch here (same channel
|
|
5
|
+
# as "Delete forever"), and this job commits the change to identity.json — so
|
|
6
|
+
# the identity file stays the ONE durable record and the KV roster overlay only
|
|
7
|
+
# bridges the minute this takes. The deploy this push triggers sends the fresh
|
|
8
|
+
# config back to the worker, which then drains the superseded overlay entry
|
|
9
|
+
# itself. Both directions are idempotent: an email already in (or already gone
|
|
10
|
+
# from) the file exits green with no commit.
|
|
11
|
+
#
|
|
12
|
+
# The push uses AUGUR_PIN_TOKEN so the commit retriggers deploy.yml — the
|
|
13
|
+
# default GITHUB_TOKEN deliberately would not.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
repository_dispatch:
|
|
17
|
+
types: [roster-update]
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: roster-update
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read # the push below authenticates with AUGUR_PIN_TOKEN
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
update:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
token: ${{ secrets.AUGUR_PIN_TOKEN }}
|
|
33
|
+
submodules: false
|
|
34
|
+
|
|
35
|
+
- name: Apply the change to identity.json and push (retry on races)
|
|
36
|
+
env:
|
|
37
|
+
ACTION: ${{ github.event.client_payload.action }}
|
|
38
|
+
EMAIL: ${{ github.event.client_payload.user.email || github.event.client_payload.email }}
|
|
39
|
+
USER_NAME: ${{ github.event.client_payload.user.name }}
|
|
40
|
+
USER_INITIALS: ${{ github.event.client_payload.user.initials }}
|
|
41
|
+
USER_COLOR: ${{ github.event.client_payload.user.color }}
|
|
42
|
+
USER_ROLE: ${{ github.event.client_payload.user.role }}
|
|
43
|
+
BY: ${{ github.event.client_payload.by }}
|
|
44
|
+
run: |
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
case "$ACTION" in add|remove|role) ;; *) echo "::error::bad action '$ACTION'"; exit 1;; esac
|
|
47
|
+
if ! printf '%s' "$EMAIL" | grep -Eq '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$'; then
|
|
48
|
+
echo "::error::client_payload does not carry a valid email"; exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# The edit, as an idempotent script we can re-run after a lost push race.
|
|
52
|
+
cat > "$RUNNER_TEMP/edit.cjs" <<'EOF'
|
|
53
|
+
const fs = require("fs");
|
|
54
|
+
const { ACTION, EMAIL, USER_NAME, USER_INITIALS, USER_COLOR, USER_ROLE } = process.env;
|
|
55
|
+
const lc = (s) => String(s || "").toLowerCase();
|
|
56
|
+
const users = JSON.parse(fs.readFileSync("identity.json", "utf8"));
|
|
57
|
+
const known = (u) => lc(u.email) === lc(EMAIL) || (u.emails || []).some((e) => lc(e) === lc(EMAIL));
|
|
58
|
+
let out = users, changed = false;
|
|
59
|
+
// `user` is the legacy spelling of `editor` and both mean the same thing, so
|
|
60
|
+
// neither is written out: an absent role IS an editor, and storing one would
|
|
61
|
+
// only give the file two ways to say the same thing.
|
|
62
|
+
const ROLES = ["admin", "editor", "viewer"];
|
|
63
|
+
const wanted = ROLES.includes(USER_ROLE) ? USER_ROLE : "editor";
|
|
64
|
+
if (ACTION === "add" && !users.some(known)) {
|
|
65
|
+
const entry = {
|
|
66
|
+
email: EMAIL,
|
|
67
|
+
name: USER_NAME || EMAIL.split("@")[0],
|
|
68
|
+
initials: USER_INITIALS || "",
|
|
69
|
+
emails: [],
|
|
70
|
+
color: USER_COLOR || "#4f46e5",
|
|
71
|
+
};
|
|
72
|
+
if (wanted !== "editor") entry.role = wanted;
|
|
73
|
+
out = [...users, entry];
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
if (ACTION === "remove") {
|
|
77
|
+
out = users.filter((u) => !known(u));
|
|
78
|
+
changed = out.length !== users.length;
|
|
79
|
+
}
|
|
80
|
+
// A role change from the Admin panel. The KV overlay already made it true;
|
|
81
|
+
// this is what makes it durable, and what lets the overlay entry drain.
|
|
82
|
+
if (ACTION === "role") {
|
|
83
|
+
out = users.map((u) => {
|
|
84
|
+
if (!known(u)) return u;
|
|
85
|
+
const current = u.role === "admin" || u.role === "viewer" ? u.role : "editor";
|
|
86
|
+
if (current === wanted) return u;
|
|
87
|
+
changed = true;
|
|
88
|
+
const { role, ...rest } = u;
|
|
89
|
+
return wanted === "editor" ? rest : { ...rest, role: wanted };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (changed) fs.writeFileSync("identity.json", JSON.stringify(out, null, 2) + "\n");
|
|
93
|
+
console.log(changed ? "identity.json updated" : "already in the desired state");
|
|
94
|
+
EOF
|
|
95
|
+
|
|
96
|
+
git config user.name "augur-deploy"
|
|
97
|
+
git config user.email "augur-deploy@users.noreply.github.com"
|
|
98
|
+
for attempt in 1 2 3 4 5; do
|
|
99
|
+
node "$RUNNER_TEMP/edit.cjs"
|
|
100
|
+
if git diff --quiet identity.json; then
|
|
101
|
+
echo "nothing to commit — the file already agrees"; exit 0
|
|
102
|
+
fi
|
|
103
|
+
git commit -q -m "[roster] $ACTION $EMAIL${USER_ROLE:+ → $USER_ROLE} (via Augur Admin${BY:+, by $BY})" identity.json
|
|
104
|
+
if git push origin HEAD; then exit 0; fi
|
|
105
|
+
echo "push rejected (concurrent commit) — refetching and reapplying"
|
|
106
|
+
git fetch origin main
|
|
107
|
+
git reset --hard --quiet origin/main
|
|
108
|
+
sleep "$attempt"
|
|
109
|
+
done
|
|
110
|
+
echo "::error::push still rejected after 5 attempts — re-run this dispatch"; exit 1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Preflight — can CI read a space repo?
|
|
2
|
+
|
|
3
|
+
# Part of the add-a-space ritual: BEFORE mounting a new space repo as a submodule,
|
|
4
|
+
# verify SUBMODULE_PAT can read it — mounting first would break every deploy at the
|
|
5
|
+
# checkout step until the grant is fixed. Run from the Actions tab (or
|
|
6
|
+
# `gh workflow run space-preflight.yml -f repo=<owner/name>`); green = safe to mount.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
repo:
|
|
12
|
+
description: "Space repo (owner/name)"
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
permissions: {}
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
probe:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Probe SUBMODULE_PAT read access
|
|
22
|
+
env:
|
|
23
|
+
PAT: ${{ secrets.SUBMODULE_PAT }}
|
|
24
|
+
REPO: ${{ inputs.repo }}
|
|
25
|
+
run: |
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
if ! printf '%s' "$REPO" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9._-]+$'; then
|
|
28
|
+
echo "::error::bad repo '$REPO' (expected owner/name)"; exit 1
|
|
29
|
+
fi
|
|
30
|
+
# Output fully discarded so the token-bearing URL can never reach the log.
|
|
31
|
+
if git ls-remote "https://x-access-token:${PAT}@github.com/${REPO}.git" HEAD >/dev/null 2>&1; then
|
|
32
|
+
echo "OK — SUBMODULE_PAT can read ${REPO}; safe to mount at spaces/<id>."
|
|
33
|
+
else
|
|
34
|
+
echo "::error::SUBMODULE_PAT cannot read ${REPO} — add it to the token's repository access (Settings → Developer settings → Fine-grained tokens), then re-run."; exit 1
|
|
35
|
+
fi
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Re-bake any roster space whose LIVE baked chrome (/_build.json builtWithEngine)
|
|
2
|
+
# is behind this shell's pinned engine. Two triggers, one job:
|
|
3
|
+
# - workflow_call: deploy.yml runs it after every deploy (drift converges on
|
|
4
|
+
# every pin bump / config push).
|
|
5
|
+
# - repository_dispatch [space-rebake]: the WORKER fires this the moment a
|
|
6
|
+
# publish arrives baked with a non-current engine — the stale window is
|
|
7
|
+
# minutes, not until-the-next-deploy. The publisher is never involved.
|
|
8
|
+
# Runtime chrome does NOT make this obsolete: serve-time composition fixes
|
|
9
|
+
# marker-wrapped chrome, but pages baked before the markers existed — and all
|
|
10
|
+
# baked generated markup (galleries, indexes) — only converge by republishing.
|
|
11
|
+
# Drift-driven and idempotent: a converged space is a curl + skip, so over-firing
|
|
12
|
+
# is harmless. Publishes from a dirty working tree are someone's live session and
|
|
13
|
+
# are left alone; health.yml alarms if drift outlives its grace.
|
|
14
|
+
name: Space re-bake
|
|
15
|
+
on:
|
|
16
|
+
workflow_call: {}
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
repository_dispatch:
|
|
19
|
+
types: [space-rebake]
|
|
20
|
+
concurrency:
|
|
21
|
+
group: space-rebake
|
|
22
|
+
cancel-in-progress: false
|
|
23
|
+
jobs:
|
|
24
|
+
rebake:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
with:
|
|
29
|
+
submodules: true # the engine, at the pinned sha — bakes exactly it
|
|
30
|
+
token: ${{ secrets.SUBMODULE_PAT }}
|
|
31
|
+
fetch-depth: 0
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: 20
|
|
35
|
+
# For each stale roster space: clone it (full history — publish.mjs dates
|
|
36
|
+
# cards from git), check out the LIVE published sha when the clone has it
|
|
37
|
+
# (a re-bake must republish what IS live, never quietly ship someone's
|
|
38
|
+
# pushed-but-unpublished work), and publish with the star token, the pinned
|
|
39
|
+
# engine, and an explicit AUGUR_ORIGIN. When live's sha isn't in the clone
|
|
40
|
+
# (a direct publish that never pushed), publish main — the client-side
|
|
41
|
+
# reconcile adopts the live bytes, so nothing is lost either way.
|
|
42
|
+
- name: Re-bake roster spaces whose baked chrome is behind the engine
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ secrets.SUBMODULE_PAT }} # clone the space repos
|
|
45
|
+
AUGUR_TOKEN: ${{ secrets.AUGUR_TOKEN }} # *-scoped: publishes any space's content
|
|
46
|
+
run: |
|
|
47
|
+
set -uo pipefail
|
|
48
|
+
if [ -z "${AUGUR_TOKEN:-}" ]; then echo "AUGUR_TOKEN unset (assets-mode) — nothing to re-bake"; exit 0; fi
|
|
49
|
+
NEW="$(git rev-parse 'HEAD:engine' 2>/dev/null || true)"
|
|
50
|
+
[ -n "$NEW" ] || { echo "no engine pin on disk — skipping"; exit 0; }
|
|
51
|
+
ORIGIN="$(jq -r '.siteOrigin // empty' deploy.config.json)"
|
|
52
|
+
[ -n "$ORIGIN" ] || { echo "no siteOrigin in deploy.config.json — skipping"; exit 0; }
|
|
53
|
+
STAMP="$(curl -fsS --max-time 30 "$ORIGIN/_build.json" 2>/dev/null || echo '{}')"
|
|
54
|
+
FAILED=""
|
|
55
|
+
while read -r ID REPO; do
|
|
56
|
+
[ -n "$ID" ] && [ -n "$REPO" ] || continue
|
|
57
|
+
BW="$(printf '%s' "$STAMP" | jq -r --arg id "$ID" '.spaces[$id].builtWithEngine // empty')"
|
|
58
|
+
DIRTY="$(printf '%s' "$STAMP" | jq -r --arg id "$ID" '.spaces[$id].dirty // false')"
|
|
59
|
+
LIVE_SHA="$(printf '%s' "$STAMP" | jq -r --arg id "$ID" '.spaces[$id].sha // empty')"
|
|
60
|
+
if [ "$BW" = "$NEW" ]; then echo "$ID already on ${NEW:0:12} — skip"; continue; fi
|
|
61
|
+
if [ "$DIRTY" = "true" ]; then echo "$ID is a working-tree publish — leaving it"; continue; fi
|
|
62
|
+
DIR="$RUNNER_TEMP/space-$ID"; rm -rf "$DIR"
|
|
63
|
+
if ! git clone --quiet "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$DIR"; then
|
|
64
|
+
echo "::warning::clone of $REPO failed — skipping $ID"; FAILED="$FAILED $ID"; continue
|
|
65
|
+
fi
|
|
66
|
+
if [ -n "$LIVE_SHA" ] && git -C "$DIR" cat-file -e "$LIVE_SHA^{commit}" 2>/dev/null; then
|
|
67
|
+
git -C "$DIR" checkout --quiet "$LIVE_SHA"
|
|
68
|
+
echo "re-baking $ID at live sha ${LIVE_SHA:0:12} (${BW:-<none>}→${NEW:0:12})"
|
|
69
|
+
else
|
|
70
|
+
echo "re-baking $ID from main — live sha ${LIVE_SHA:-<none>} not in clone; reconcile adopts live bytes (${BW:-<none>}→${NEW:0:12})"
|
|
71
|
+
fi
|
|
72
|
+
if ( cd "$DIR" && AUGUR_ORIGIN="$ORIGIN" GV_SPACES_ROOT="$DIR" node "$GITHUB_WORKSPACE/engine/scripts/publish.mjs" ); then
|
|
73
|
+
echo "re-baked $ID"
|
|
74
|
+
else
|
|
75
|
+
echo "::warning::re-bake of $ID failed"; FAILED="$FAILED $ID"
|
|
76
|
+
fi
|
|
77
|
+
done < <(jq -r '.spaces[]? | "\(.id) \(.repo)"' deploy.config.json)
|
|
78
|
+
if [ -n "$FAILED" ]; then echo "::error::re-bake failed for:$FAILED — see warnings above"; exit 1; fi
|
|
79
|
+
echo "re-bake pass complete."
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: Bundle-store backup
|
|
2
|
+
|
|
3
|
+
# Off-Cloudflare copies of everything the live site serves.
|
|
4
|
+
#
|
|
5
|
+
# The store is the only copy of published content, and R2 has no point-in-time
|
|
6
|
+
# restore. Most of it is reproducible — clone a space at the sha /_build.json
|
|
7
|
+
# reports and publish again — but a publish made from an uncommitted working tree
|
|
8
|
+
# serves bytes held in NO repository, and those are gone with the bucket.
|
|
9
|
+
# `augur ship` makes that state rare by committing first; this makes it
|
|
10
|
+
# survivable.
|
|
11
|
+
#
|
|
12
|
+
# The copy is a plain directory: manifests/<id>.json + blobs/<sha256> +
|
|
13
|
+
# export.json. Restore is `augur restore <dir>` from any machine holding a
|
|
14
|
+
# publish token — see the engine's docs/store-recovery.md.
|
|
15
|
+
#
|
|
16
|
+
# TWO SLOTS, ON PURPOSE — one recent copy and one old one:
|
|
17
|
+
#
|
|
18
|
+
# weekly Sunday, kept 10 days → there is always a copy under a week old
|
|
19
|
+
# monthly the 1st, kept 40 days → there is always a copy roughly a month old
|
|
20
|
+
#
|
|
21
|
+
# The old slot is the one that matters against a quiet compromise. Anything that
|
|
22
|
+
# corrupts or empties the store on a Tuesday is in every copy taken after it, so
|
|
23
|
+
# a rolling week of dailies protects you from accidents and not from an attack
|
|
24
|
+
# you notice late. A month-old copy does.
|
|
25
|
+
#
|
|
26
|
+
# Cost: each run uploads a FULL copy (the export's incremental skip saves nothing
|
|
27
|
+
# across runs, because a fresh runner starts empty), and Actions artifact storage
|
|
28
|
+
# is shared across every repo in the account — not per-repo. Two slots is roughly
|
|
29
|
+
# two full copies steady state. Frequency and retention are the only real knobs.
|
|
30
|
+
#
|
|
31
|
+
# Residual risk worth naming: anyone with write access to THIS repo can delete
|
|
32
|
+
# these artifacts as easily as read them. Genuine immutability needs an offsite
|
|
33
|
+
# with object-lock, which is a different job from this one.
|
|
34
|
+
#
|
|
35
|
+
# SET BEFORE USE: AUGUR_ORIGIN below. Secrets: AUGUR_TOKEN, star-scoped — a
|
|
36
|
+
# space-scoped token silently skips the spaces it cannot read, and this job fails
|
|
37
|
+
# if anything was skipped.
|
|
38
|
+
|
|
39
|
+
on:
|
|
40
|
+
schedule:
|
|
41
|
+
- cron: "40 2 * * 0" # weekly — Sunday 02:40 UTC (offset: GitHub drops top-of-hour crons)
|
|
42
|
+
- cron: "40 3 1 * *" # monthly — the 1st, 03:40 UTC
|
|
43
|
+
workflow_dispatch: # take one on demand before anything risky
|
|
44
|
+
inputs:
|
|
45
|
+
slot:
|
|
46
|
+
description: "Which slot this manual copy fills"
|
|
47
|
+
type: choice
|
|
48
|
+
default: weekly
|
|
49
|
+
options: [weekly, monthly]
|
|
50
|
+
|
|
51
|
+
permissions:
|
|
52
|
+
contents: read
|
|
53
|
+
|
|
54
|
+
concurrency:
|
|
55
|
+
group: store-backup
|
|
56
|
+
cancel-in-progress: false
|
|
57
|
+
|
|
58
|
+
# github.event.schedule carries the cron line that fired, so one workflow serves
|
|
59
|
+
# both slots without duplicating the job.
|
|
60
|
+
env:
|
|
61
|
+
SLOT: ${{ github.event.schedule == '40 3 1 * *' && 'monthly' || inputs.slot || 'weekly' }}
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
backup:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
# ⛔ REFUSE TO RUN ON A PUBLIC REPOSITORY. First step, on purpose.
|
|
68
|
+
#
|
|
69
|
+
# A COMMENT IS NOT A GUARD, AND THIS HAS ALREADY HAPPENED. A public fork of the
|
|
70
|
+
# engine ran a workflow of this shape and published a full production KV export of
|
|
71
|
+
# a live instance — 58 keys, 26 internal comment threads, five real names and
|
|
72
|
+
# personal addresses — reachable with no authentication. GitHub shares git objects
|
|
73
|
+
# across a fork network, so the commit resolved through the PARENT repository's own
|
|
74
|
+
# raw URL: deleting the branch, deleting the fork and rewriting the parent's history
|
|
75
|
+
# all leave it fetchable, and only a GitHub Support object purge closes it. The
|
|
76
|
+
# header above this job said "the shell repo must be private" the whole time.
|
|
77
|
+
#
|
|
78
|
+
# IT FAILS, IT DOES NOT SKIP. A skip is worse than having no backup workflow at all,
|
|
79
|
+
# because the operator goes on believing a backup exists.
|
|
80
|
+
#
|
|
81
|
+
# AND IT FAILS WHEN IT CANNOT TELL. `github.event.repository.private` is absent on
|
|
82
|
+
# some event types, and an absent value is not a private repository — it is an
|
|
83
|
+
# unanswered question, and this is the one question that must not have a default.
|
|
84
|
+
- name: Refuse to run on a public repository
|
|
85
|
+
env:
|
|
86
|
+
GH_TOKEN: ${{ github.token }}
|
|
87
|
+
CTX_PRIVATE: ${{ github.event.repository.private }}
|
|
88
|
+
run: |
|
|
89
|
+
set -uo pipefail
|
|
90
|
+
PRIVATE="$(gh api "repos/${GITHUB_REPOSITORY}" --jq .private 2>/dev/null || true)"
|
|
91
|
+
if [ -z "$PRIVATE" ]; then PRIVATE="${CTX_PRIVATE:-}"; fi
|
|
92
|
+
case "$PRIVATE" in
|
|
93
|
+
true)
|
|
94
|
+
echo "OK: ${GITHUB_REPOSITORY} is private"
|
|
95
|
+
;;
|
|
96
|
+
false)
|
|
97
|
+
echo "::error::REFUSING TO RUN: ${GITHUB_REPOSITORY} is a PUBLIC repository. This job exports the instance's whole state and writes it into this repo. On a public repo that publishes it to anyone, permanently — a fork network keeps the objects fetchable through the parent's URL after the branch, the fork and the history are all gone. Make the repository private, or delete this workflow."
|
|
98
|
+
exit 1
|
|
99
|
+
;;
|
|
100
|
+
*)
|
|
101
|
+
echo "::error::REFUSING TO RUN: could not determine whether ${GITHUB_REPOSITORY} is private (got '${PRIVATE}'). An unanswered question is not a private repository."
|
|
102
|
+
exit 1
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
105
|
+
|
|
106
|
+
- uses: actions/checkout@v4
|
|
107
|
+
with:
|
|
108
|
+
submodules: true # the engine carries the export script
|
|
109
|
+
token: ${{ secrets.SUBMODULE_PAT }}
|
|
110
|
+
|
|
111
|
+
- uses: actions/setup-node@v4
|
|
112
|
+
with:
|
|
113
|
+
node-version: 20
|
|
114
|
+
|
|
115
|
+
- name: Export the store
|
|
116
|
+
env:
|
|
117
|
+
AUGUR_TOKEN: ${{ secrets.AUGUR_TOKEN }}
|
|
118
|
+
AUGUR_ORIGIN: https://your-site-origin.example # ← your deploy.config.json siteOrigin
|
|
119
|
+
run: |
|
|
120
|
+
set -euo pipefail
|
|
121
|
+
if [ -z "$AUGUR_TOKEN" ]; then
|
|
122
|
+
echo "::error::AUGUR_TOKEN is not configured — nothing can be backed up."; exit 1
|
|
123
|
+
fi
|
|
124
|
+
node engine/scripts/export.mjs --out backup
|
|
125
|
+
|
|
126
|
+
# A backup that quietly omits a space is the one you discover is short on
|
|
127
|
+
# the day you need it. export.json records anything it could not read;
|
|
128
|
+
# refuse to publish an artifact that claims to be a copy and isn't.
|
|
129
|
+
- name: Refuse an incomplete copy
|
|
130
|
+
run: |
|
|
131
|
+
set -euo pipefail
|
|
132
|
+
SKIPPED="$(jq -r '.skipped | length' backup/export.json)"
|
|
133
|
+
if [ "$SKIPPED" != "0" ]; then
|
|
134
|
+
echo "::error::the export skipped $SKIPPED target(s) — check AUGUR_TOKEN is star-scoped:"
|
|
135
|
+
jq -r '.skipped[] | " - \(.id): \(.reason)"' backup/export.json
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
echo "Spaces copied:"; jq -r '.spaces[] | " \(.id) v\(.version)\(if .source.dirty then " [DIRTY — these bytes exist in no repo]" else "" end)"' backup/export.json
|
|
139
|
+
echo "Blobs: $(jq -r .blobs backup/export.json) · $(du -sh backup | cut -f1)"
|
|
140
|
+
|
|
141
|
+
- name: Stamp the copy
|
|
142
|
+
id: stamp
|
|
143
|
+
run: echo "on=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
|
|
144
|
+
|
|
145
|
+
- uses: actions/upload-artifact@v4
|
|
146
|
+
with:
|
|
147
|
+
name: store-${{ env.SLOT }}-${{ steps.stamp.outputs.on }}
|
|
148
|
+
path: backup
|
|
149
|
+
# Each slot outlives its own interval, so a copy always exists even in
|
|
150
|
+
# the moments around a scheduled run.
|
|
151
|
+
retention-days: ${{ env.SLOT == 'monthly' && 40 || 10 }}
|
|
152
|
+
compression-level: 0 # the payload is already-compressed images and woff2
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# TEMPLATE — copy this into YOUR DEPLOY SHELL as wrangler.toml and fill it in.
|
|
2
|
+
#
|
|
3
|
+
# It is deliberately not a live wrangler.toml: a worker's name, its account, its KV
|
|
4
|
+
# namespace and its R2 bucket belong to ONE instance, so the shared engine must not carry
|
|
5
|
+
# them. `wrangler` ignores this filename, which also means a stray `npx wrangler` run
|
|
6
|
+
# inside an engine clone cannot pick it up. Same pattern as realtime/wrangler.example.toml.
|
|
7
|
+
#
|
|
8
|
+
# It is hand-authored per shell rather than generated from deploy.config.json ON PURPOSE:
|
|
9
|
+
# five of the six mandatory values below (worker name, account id, compatibility date, KV
|
|
10
|
+
# namespace id, R2 bucket) have no source in any deploy.config.json today, so generating
|
|
11
|
+
# it would mean inventing a config block, bumping the shell contract, and committing
|
|
12
|
+
# account and namespace ids into shell repos — for a file each shell writes once.
|
|
13
|
+
#
|
|
14
|
+
# npx wrangler deploy --dry-run -c wrangler.toml # resolves every binding, ships nothing
|
|
15
|
+
# node engine/scripts/wrangler-preflight.mjs -c wrangler.toml
|
|
16
|
+
#
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
# ⛔ READ THIS BEFORE CHANGING [assets]. IT IS THE LOGIN GATE.
|
|
19
|
+
#
|
|
20
|
+
# Pages and Workers invert request precedence, and the inversion is silent.
|
|
21
|
+
#
|
|
22
|
+
# Pages runs _worker.js FIRST for every request; the worker decides what is public
|
|
23
|
+
# and falls through to the static asset only when it chooses to.
|
|
24
|
+
# Workers serves a MATCHING STATIC ASSET FIRST and only invokes the worker on a miss —
|
|
25
|
+
# unless `run_worker_first = true`.
|
|
26
|
+
#
|
|
27
|
+
# `engine/dist` is the asset directory, and it contains `__config/instance.json`, which
|
|
28
|
+
# carries the instance's user roster INCLUDING SEED PASSWORDS (build.js writes `users:
|
|
29
|
+
# IDENTITY`, unstripped, and says so in its own comment). It also contains the admin page
|
|
30
|
+
# and, in assets mode, every built page.
|
|
31
|
+
#
|
|
32
|
+
# So without `run_worker_first = true`, a deploy that passes every test in the engine and
|
|
33
|
+
# answers a health check with "healthy" serves the roster, the admin panel and every gated
|
|
34
|
+
# page to anyone who asks. That is not a hardening flag. It IS the gate.
|
|
35
|
+
#
|
|
36
|
+
# `scripts/wrangler-preflight.mjs` refuses to pass a config whose asset directory holds
|
|
37
|
+
# __config/instance.json without it, and runs in the workflow that gates the deploy.
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
name = "<your-instance>" # the Worker's name, e.g. augur-<instance>
|
|
41
|
+
account_id = "<your-cloudflare-account-id>"
|
|
42
|
+
main = "engine/src/entry.js" # relative to THIS file, i.e. your shell's root
|
|
43
|
+
compatibility_date = "2026-07-01"
|
|
44
|
+
|
|
45
|
+
[assets]
|
|
46
|
+
directory = "engine/dist" # what `node engine/build.js` emits
|
|
47
|
+
binding = "ASSETS" # the worker reads its own config through this
|
|
48
|
+
# The line the gate depends on. See the block above before touching it.
|
|
49
|
+
run_worker_first = true
|
|
50
|
+
# Stated explicitly rather than left to the default. "none" is what keeps the worker's own
|
|
51
|
+
# `asset.status === 404` branches firing; "single-page-application" would answer every
|
|
52
|
+
# unknown path with the index page at status 200, which is the bug engine/dist/404.html
|
|
53
|
+
# exists to prevent.
|
|
54
|
+
not_found_handling = "none"
|
|
55
|
+
# /foo, /foo/ and /foo/index.html all resolve, which is what published prototype folders
|
|
56
|
+
# rely on. Stated explicitly for the same reason as above.
|
|
57
|
+
html_handling = "auto-trailing-slash"
|
|
58
|
+
|
|
59
|
+
# Comments, pins, statuses, rosters, canvas boards, publish tokens. The SAME namespace the
|
|
60
|
+
# Pages project binds as COMMENTS and the realtime worker binds as BOARD_KV — during a
|
|
61
|
+
# migration both front doors read one store, which is what makes the cutover reversible.
|
|
62
|
+
[[kv_namespaces]]
|
|
63
|
+
binding = "COMMENTS"
|
|
64
|
+
id = "<your-kv-namespace-id>"
|
|
65
|
+
|
|
66
|
+
# The bundle store: content-addressed blobs plus per-space manifests. A live instance
|
|
67
|
+
# serves published content from here, never from the asset directory, so an instance with
|
|
68
|
+
# no BUNDLES binding is a site with nothing published in it.
|
|
69
|
+
[[r2_buckets]]
|
|
70
|
+
binding = "BUNDLES"
|
|
71
|
+
bucket_name = "<your-r2-bucket>"
|
|
72
|
+
|
|
73
|
+
# ── Canvas rooms, once this instance serves them from its own worker ────────────────────
|
|
74
|
+
#
|
|
75
|
+
# Uncomment BOTH blocks together, and only when you are ready to retire this instance's
|
|
76
|
+
# standalone `augur-realtime-<instance>` worker. The class already ships in the engine's
|
|
77
|
+
# deploy entry, so wrangler resolves it; what these add is the binding and the migration
|
|
78
|
+
# that creates the Durable Object namespace.
|
|
79
|
+
#
|
|
80
|
+
# The room's KV mirror uses the COMMENTS binding above — the same namespace the standalone
|
|
81
|
+
# worker binds as BOARD_KV, which is why folding realtime in here removes a binding rather
|
|
82
|
+
# than adding one. The room accepts either name for as long as both deployments exist.
|
|
83
|
+
#
|
|
84
|
+
# ⚠️ The rooms in THIS namespace are new and empty: a Durable Object's storage belongs to
|
|
85
|
+
# the script that created it, so boards do not travel from the standalone worker. They
|
|
86
|
+
# migrate on first touch from the KV mirror, which lags by up to 45 seconds — so cut over
|
|
87
|
+
# when boards are quiet, not mid-session.
|
|
88
|
+
#
|
|
89
|
+
# ⚠️ THE BINDING ALSO MOVES THE BOARD KEY, and that is one cutover, not two. With rooms
|
|
90
|
+
# bound, `/__rt` names each room `<workspace>:<path>` and the board document is written to
|
|
91
|
+
# `board:<workspace>:<path>`; without it, both keep the unscoped spelling they have always
|
|
92
|
+
# had. The two ride together because the rail and the room's mirror are one script only
|
|
93
|
+
# here — scoping one of them while the other is in a separate worker would leave the rail
|
|
94
|
+
# reading a key nothing writes. A board written before the cutover is still found: both
|
|
95
|
+
# readers fall back to the unscoped key, serve it, and write it back scoped.
|
|
96
|
+
#
|
|
97
|
+
# So the cutover, in order:
|
|
98
|
+
# 1. deploy with the blocks below uncommented (the worker now serves its own rooms)
|
|
99
|
+
# 2. CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… AUGUR_KV_NS=… \
|
|
100
|
+
# node engine/scripts/migrate-board-keys.mjs --workspace <id>
|
|
101
|
+
# — a DRY RUN, and it has to report `differing 0` before anything else happens
|
|
102
|
+
# 3. the same command with --apply
|
|
103
|
+
# 4. stop the standalone worker only after the boards look right
|
|
104
|
+
# `<id>` is the `tenantId` the build stamped into __config/instance.json, which is the
|
|
105
|
+
# deploy config's `tenantId` if it sets one and otherwise EMPTY — an empty one resolves to
|
|
106
|
+
# the literal `default`, and that is what to pass. Guessing it wrong is silent: the site
|
|
107
|
+
# keeps working on the read-through while a second, stale copy of every board accumulates.
|
|
108
|
+
#
|
|
109
|
+
# [[durable_objects.bindings]]
|
|
110
|
+
# name = "ROOMS"
|
|
111
|
+
# class_name = "BoardRoom"
|
|
112
|
+
#
|
|
113
|
+
# [[migrations]]
|
|
114
|
+
# tag = "v1"
|
|
115
|
+
# new_sqlite_classes = ["BoardRoom"]
|
|
116
|
+
|
|
117
|
+
# ── The workspace store, once membership moves off KV ───────────────────────────────────
|
|
118
|
+
#
|
|
119
|
+
# One Durable Object per workspace, holding the roster, roles, invites, publish tokens and
|
|
120
|
+
# profile photos this instance's KV holds under `users:*` and `publish:tokens` today.
|
|
121
|
+
# Schema and reasoning: engine/src/tenant-do.js.
|
|
122
|
+
#
|
|
123
|
+
# The point of it is that the isolation stops being a rule about key naming: a DO's storage
|
|
124
|
+
# belongs to that object, so there is no key you can construct in one workspace that reads
|
|
125
|
+
# another. Which is why it only pays for itself once a deployment serves more than one —
|
|
126
|
+
# a single-workspace instance gains nothing here and should leave this commented.
|
|
127
|
+
#
|
|
128
|
+
# ⚠️ NOT A CUTOVER ON ITS OWN. Uncommenting creates the namespace and applies the schema;
|
|
129
|
+
# it moves no data and no read. The families move over in their own steps, and until then
|
|
130
|
+
# every read still comes from KV. Do not delete anything from KV on the strength of this
|
|
131
|
+
# block existing.
|
|
132
|
+
#
|
|
133
|
+
# ⚠️ THE MIGRATION TAG IS SHARED. If ROOMS above is already live on `v1`, this needs its
|
|
134
|
+
# own `tag = "v2"` with only the new class in it — tags are applied in order and are never
|
|
135
|
+
# re-run, so amending a tag that has already shipped does nothing at all, silently.
|
|
136
|
+
#
|
|
137
|
+
# [[durable_objects.bindings]]
|
|
138
|
+
# name = "TENANTS"
|
|
139
|
+
# class_name = "TenantStore"
|
|
140
|
+
#
|
|
141
|
+
# [[migrations]]
|
|
142
|
+
# tag = "v1"
|
|
143
|
+
# new_sqlite_classes = ["TenantStore"]
|
|
144
|
+
|
|
145
|
+
# ── The unit store: drafts that land ────────────────────────────────────────────────────
|
|
146
|
+
#
|
|
147
|
+
# One Durable Object per prototype, holding its main revision, open drafts, landing history
|
|
148
|
+
# and the landing lease. Without it `augur open` answers 501 and nothing else changes.
|
|
149
|
+
# Schema and reasoning: engine/src/unit-object.mjs, design: engine/docs/drafts-that-land.md.
|
|
150
|
+
#
|
|
151
|
+
# [[durable_objects.bindings]]
|
|
152
|
+
# name = "UNITS"
|
|
153
|
+
# class_name = "UnitObject"
|
|
154
|
+
#
|
|
155
|
+
# [[migrations]]
|
|
156
|
+
# tag = "v2"
|
|
157
|
+
# new_sqlite_classes = ["UnitObject"]
|
|
158
|
+
|
|
159
|
+
[vars]
|
|
160
|
+
# Bundle mode. Without this the worker falls back to assets mode and every published page
|
|
161
|
+
# answers 404 while the site itself looks healthy.
|
|
162
|
+
GV_ASSET_SOURCE = "r2"
|
|
163
|
+
|
|
164
|
+
# NO tenantId HERE, on purpose. Which workspace a deployment serves already flows
|
|
165
|
+
# deploy.config.json -> build.js -> dist/__config/instance.json -> resolveTenant(), pinned
|
|
166
|
+
# to a single call site by scripts/one-tenant-resolver.mjs. A var would be a second source
|
|
167
|
+
# of truth for the one value the whole isolation model is keyed on.
|
|
168
|
+
#
|
|
169
|
+
# TENANT_HOST_SUFFIX is the exception, and it is not that value — it is the switch between
|
|
170
|
+
# ONE workspace and MANY. Leave it unset and this deployment serves the single workspace
|
|
171
|
+
# its build named, which is what a self-hosted instance is. Set it and the workspace comes
|
|
172
|
+
# from the Host header instead: `TENANT_HOST_SUFFIX = ".example.com"` makes
|
|
173
|
+
# `acme.example.com` the workspace `acme`, and a hostname that names no workspace gets a
|
|
174
|
+
# 404 rather than somebody else's site.
|
|
175
|
+
#
|
|
176
|
+
# RESERVED_LABELS_EXTRA adds this deployment's OWN reserved first labels to the engine's
|
|
177
|
+
# list (src/tenant-host.mjs): names that must never become somebody's workspace address —
|
|
178
|
+
# the fallback origin a certificate product points at, an address you intend to alias to a
|
|
179
|
+
# workspace by operator grant. Comma or space separated; anything that is not a legal label
|
|
180
|
+
# is dropped, never widened. A reserved label resolves nobody literally, which is exactly
|
|
181
|
+
# what lets the alias table answer for it.
|
|
182
|
+
#
|
|
183
|
+
# RESERVED_LABELS_EXTRA = "service"
|
|
184
|
+
#
|
|
185
|
+
# The suffix is LITERAL, which is worth a moment. A dotted suffix puts workspaces one level
|
|
186
|
+
# down and needs a wildcard certificate; a hyphenated one (`-team.example.com`) keeps every
|
|
187
|
+
# workspace on a first-level hostname that a universal certificate already covers. Pick
|
|
188
|
+
# before the first workspace exists — changing it later changes everybody's URL.
|
|
189
|
+
#
|
|
190
|
+
# Set it ONLY together with the TENANTS binding above. Without it there is nowhere for a
|
|
191
|
+
# workspace's own state to live, and the deployment would resolve names it cannot serve.
|
|
192
|
+
#
|
|
193
|
+
# ── TENANT_JURISDICTION — WHERE those workspace objects are, and it is part of their
|
|
194
|
+
# address ──────────────────────────────────────────────────────────────────────────────
|
|
195
|
+
#
|
|
196
|
+
# Leave it unset and workspace objects carry no jurisdictional restriction, which is what
|
|
197
|
+
# every deployment does today. Set it — `TENANT_JURISDICTION = "eu"` — and the platform
|
|
198
|
+
# keeps each workspace's storage inside that jurisdiction and runs it there.
|
|
199
|
+
#
|
|
200
|
+
# ⚠️ IT IS NOT A SETTING ON THE BINDING ABOVE, AND THAT IS THE TRAP. There is no wrangler
|
|
201
|
+
# key for it: the restriction is applied when an object is ADDRESSED, so
|
|
202
|
+
# `idFromName("acme")` and `jurisdiction("eu").idFromName("acme")` are two different ids —
|
|
203
|
+
# two different objects, in two different places, that know nothing about each other. A
|
|
204
|
+
# Durable Object's storage belongs to its id, so this cannot be changed after a workspace
|
|
205
|
+
# exists: it is not a setting you flip, it is an export and a replay into a new object.
|
|
206
|
+
# DECIDE BEFORE THE FIRST WORKSPACE IS CREATED.
|
|
207
|
+
#
|
|
208
|
+
# ⚠️ AND EVERYTHING ELSE THAT ADDRESSES THESE WORKSPACES HAS TO BE TOLD THE SAME VALUE.
|
|
209
|
+
# Anything holding this namespace binding — a provisioning worker, an operator tool —
|
|
210
|
+
# resolves a DIFFERENT object if it applies a different jurisdiction, or none. The symptom
|
|
211
|
+
# is not an error: one side creates a workspace, the other resolves an empty one.
|
|
212
|
+
#
|
|
213
|
+
# The value is handed to the platform as written. It is case-sensitive, `"EU"` is not
|
|
214
|
+
# `"eu"`, and a value the platform does not accept fails EVERY request rather than quietly
|
|
215
|
+
# addressing without a restriction — engine/scripts/wrangler-preflight.mjs refuses the
|
|
216
|
+
# unknown value and the missing binding before the deploy, so the typo costs a re-run
|
|
217
|
+
# rather than an outage.
|
|
218
|
+
#
|
|
219
|
+
# NO SECRETS HERE EITHER — [vars] is plaintext in the repo. These go in with
|
|
220
|
+
# `npx wrangler secret put <NAME> -c wrangler.toml`:
|
|
221
|
+
#
|
|
222
|
+
# SESSION_SECRET REQUIRED. Without it sessions degrade to an unkeyed digest,
|
|
223
|
+
# silently, and anyone who knows an address can forge a cookie.
|
|
224
|
+
# RT_SHARED_SECRET canvas multiplayer; the realtime worker fails closed without it
|
|
225
|
+
# MAIL_API_KEY only if this instance sends mail (with MAIL_PROVIDER/MAIL_FROM)
|
|
226
|
+
# DELETE_DISPATCH_TOKEN only if prototype-delete / roster-update dispatch to a repo
|
|
227
|
+
#
|
|
228
|
+
# And never PUBLISH_BOOTSTRAP_TOKEN on a live instance: it is compared as a bare string
|
|
229
|
+
# and grants star-scope publish. The preflight refuses a config that names it.
|
|
230
|
+
|
|
231
|
+
# ── The health cron, once you want to stop paying GitHub to run your canary ─────────────
|
|
232
|
+
#
|
|
233
|
+
# Uncomment and this deployment checks ITSELF on a schedule: whether any workspace is
|
|
234
|
+
# serving a working-tree publish that has outlived its window, whether any is serving page
|
|
235
|
+
# chrome baked by an older engine than the one deployed, and whether the engine you are
|
|
236
|
+
# running is behind the newest RELEASE. The last one is the point — if your own CI is
|
|
237
|
+
# broken, this is the only thing left that will tell you your engine is months old.
|
|
238
|
+
#
|
|
239
|
+
# The report lands at `/__admin/health`, admin-only. That endpoint READS; it never runs the
|
|
240
|
+
# checks, deliberately, so that "the cron is dead" and "nobody opened this page" cannot
|
|
241
|
+
# produce the same green answer. An absent report says so in words.
|
|
242
|
+
#
|
|
243
|
+
# ⚠️ IT DOES NOT ASK WHETHER YOUR SITE IS GATED, and that omission is deliberate rather
|
|
244
|
+
# than missing. A worker probing its own front door is a detector inside the thing it
|
|
245
|
+
# detects: the request re-enters this deployment, so the misconfiguration that would make
|
|
246
|
+
# the platform serve assets before the worker is exactly the condition under which the
|
|
247
|
+
# worker's own answer stops being worth anything. That question stays with
|
|
248
|
+
# `scripts/frontdoor-parity.mjs`, run from OUTSIDE — health.yml step (g), or by hand.
|
|
249
|
+
#
|
|
250
|
+
# ⚠️ ONE WORKSPACE ONLY. A cron has no Host header, so on a deployment that tells
|
|
251
|
+
# workspaces apart by hostname (TENANT_HOST_SUFFIX above) there is no single workspace to
|
|
252
|
+
# check, and it declines rather than picking one. Nothing is lost — that deployment wants a
|
|
253
|
+
# job that holds the workspace list, not a cron that guesses.
|
|
254
|
+
#
|
|
255
|
+
# [triggers]
|
|
256
|
+
# crons = ["23 */6 * * *"] # every six hours, offset off the hour
|
|
257
|
+
|
|
258
|
+
# The front door. A route attaches to a hostname that already has a proxied DNS record —
|
|
259
|
+
# which a Pages custom domain is — and takes precedence over Pages on the same hostname,
|
|
260
|
+
# so adding this is the cutover and deleting it is the rollback, in seconds, with no DNS
|
|
261
|
+
# propagation and no certificate reissue. Keep the Pages project and its last deployment
|
|
262
|
+
# until the new front door has been watched for a while.
|
|
263
|
+
#
|
|
264
|
+
# [[routes]]
|
|
265
|
+
# pattern = "<your-site-host>/*"
|
|
266
|
+
# zone_name = "<your-zone>"
|