Pastebin

Untitled

text 8 views Created: October 30, 2026 at 13:02:06 EDT
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
SCREECHVR V19: WORKER SECURITY + FIVE-MINUTE HEARTBEAT
Date: 2026-10-30
Scope: Read-only review of local Worker/Unity source, V08 draft and official
Cloudflare/PlayFab documentation. No code or production changes. V08 is draft,
not verified as deployed.

SUMMARY

There is no implemented Worker heartbeat route and no Unity heartbeat caller.
V08 CloudScript `Heartbeat` is not a cryptographic challenge protocol and has
no D1 integration. Current /updateplayers ignores all input. /redeem validates
a PlayFab session ticket but has non-atomic KV rate limiting and multi-step
link/grant behavior. A proposed heartbeat can fit Cloudflare Free at 100 CCU in
isolation (28,800 beats/day), but Workers Free has a shared 100,000 request/day
limit and D1 Free 100,000 rows written/day; existing traffic consumes headroom.
Heartbeat only indicates recent protocol activity, not an unmodified client.

1. CURRENT SOURCE FINDINGS

Worker: `! Screech VR/discord-bot/worker.js`

Route/auth map (router at lines 617-629):
- `POST /photonwh/*` -> `handlePhotonHook` (175-206): requires PHOTON_KEY in
  `X-Secret` or `?key=` (177-180), parses JSON, optionally checks AppId (182).
  Uses Durable Object CCU counter when bound; on failure falls back to KV
  read-modify-write (193-205). Query key can leak via logs/history. This is
  Photon webhook authentication, not player custom authentication.
- `GET /photonwh/state` -> `handlePhotonState` (209-219): requires `?key=`.
  Returns count and game state. Query-string secret is exposed to URL logging.
- `POST /` -> Discord interactions (434-455): verifies Discord Ed25519 signature
  and timestamp within 300 seconds (246-257). Commands use interaction user ID;
  owner checks compare OWNER_ID (263-264); /link may check ROLE_ID (270-275).
- `POST /updateplayers` -> `handleUpdatePlayers` (457-459): returns
  `{ok:true, ignored:true}` without parsing/authenticating input. Unity submits
  player count, but Worker does not use it.
- `POST /redeem` -> `handleRedeem` (461-521): IP-only KV limiter 10/min via
  read/increment/write (463-468), reads code and PlayFab ticket, calls
  `/Server/AuthenticateSessionTicket` and rejects expired/invalid tickets
  (469-480). Then reads/writes link keys, deletes code/pending keys, grants hat
  and writes UserData (483-512). Multi-step KV/PlayFab operations are not atomic;
  result of UserData update is not checked and hat failure still returns success
  with warning (503-517). Rate limit can race; no per-account limiter or
  request idempotency. Some auth error responses default to HTTP 200 (478).
- `GET /motd` -> `handleMotd` (524-530): unauthenticated, reads title data and
  returns MOTD/lockdown/maintenance. Public read route; verify that returned
  fields are intended to be public and avoid leaking internal title data.
- `GET /export` (533-554) and `GET /setup` (556-610): key in query parameter,
  compared to EXPORT_KEY; export returns linked-account PII CSV. Protect with
  non-URL auth if retained, rotate/manage secret, restrict route and audit use.
  `/setup` calls Discord API; source lacks required User-Agent noted in project
  operational guidance, so guild route may fail.
- `/health` (628): unauthenticated `{ok:true,service:"screech-link"}`; low
  sensitivity but method is not constrained. Apply method restriction if desired.
- Unknown routes return 404. No general CORS or method constraints beyond route
  checks; no heartbeat endpoint.

Secrets: defaults at worker.js:1-16/57-83 include blank env-backed credentials;
runtime uses DISCORD_TOKEN, PLAYFAB_SECRET, PHOTON_KEY, EXPORT_KEY and other
settings. No actual secret value is included here. Keep all secrets in Worker
secret bindings, not source/client. Remove query-string secrets; do not log
tickets, link codes, full PlayFab responses, or auth headers. `handleRedeem`
currently logs truncated auth response and bad code details (477, 492); minimize
and redact those logs.

Unity callers:
- `! Screech VR/Screech VR/Assets/PlayerCountReporter.cs`: starts after 5 sec,
  periodic interval default 30 sec, and reports join/leave/player changes
  (49-66, 77-95). Calls are not heartbeat/security signals.
- `Assets/ScreechApi.cs`: `PostPlayers` sends `{"players":count}` to
  `/updateplayers` with 10 sec timeout (56-65, 115-123); no PlayFab ticket/auth
  header. Failures only warn. SessionTicket property (26-38) is used by `/redeem`
  JSON body (67-78), not the player-count route.
- No Unity `/heartbeat` caller found in Assets C#.
- V08 `Heartbeat` exists only in `revisions/SCREECHVR_PLAYFAB_REVISION_V08_2026-10-29.js`
  (447-506); no Unity caller found. It stores sequence/time in PlayFab
  InternalData, requires increasing sequence, checks optional client timestamp,
  rate limits at four minutes; `challengeResponse` is unused. Read-check-write
  is non-atomic; types/ranges weak; no Worker route, D1 state, session start/end,
  or authoritative session expiration. It does not itself prove integrity or
  implement an automatic ban.

2. PROPOSED AUTHENTICATED HEARTBEAT

Purpose and threat limits:
- Use heartbeat for session liveness/operational telemetry only. A modified
  client can continue sending valid beats, so it is not proof of integrity,
  legitimate movement, or fair play. Never auto-ban or kick for missed beats,
  Worker/D1 outage, or weak anomaly signals.
- Obtain a PlayFab ClientSessionTicket from normal login. Start session by
  POSTing ticket to Worker over TLS. Worker calls PlayFab Server
  AuthenticateSessionTicket using server secret; derive PlayFabId from response,
  never trust client-supplied PlayFabId. Check expiration and optional ban state.
  No title secret goes to Unity.
- Worker generates random session capability (e.g. 256-bit opaque token) and
  random one-use challenge; returns session token, challenge, server expiry and
  interval. Store only token hash, session ID/player ID, last sequence/beat,
  current challenge hash, created/expiry, status. Session token is a bearer
  credential, not an anti-cheat secret; TLS, redaction and rotation still matter.
- Every 5 minutes, client POSTs session capability, current challenge, and
  strictly increasing integer sequence. Worker atomically condition-checks
  session active/unexpired, expected sequence/challenge, then updates last_seen,
  sequence and fresh challenge. Compare via one conditional D1 UPDATE/UPSERT
  with `RETURNING`; exactly one concurrent/replayed request can advance state.
  Repeated request gets stale/replayed response; optionally return current state
  only for a narrowly bounded retry mechanism without accepting an old nonce.
- Server-issued nonce prevents replay of a previously accepted beat. It does not
  prevent a malicious client from requesting/answering fresh challenges. HMAC
  using a key stored in a modifiable client would not improve this trust model.
- Session expires after a grace window (recommend 10-15 minutes after last
  accepted beat) and absolute lifetime (for example, bounded by PlayFab ticket
  expiration or periodic ticket revalidation). Stop marking active; do not ban.
  Player reconnects with fresh PlayFab ticket/session, receiving new session ID
  and nonce. Revoke/close old session on re-auth where feasible.

Failure behavior:
- PlayFab auth unavailable: do not create a trusted heartbeat session; return
  retryable service error. Do not accept a client-provided ID as fallback.
- Worker/D1 timeout or 5xx: client retries with jitter/backoff, preserves current
  challenge/sequence until response outcome resolved; if ambiguous, reconnect or
  status-check with idempotent request ID. Mark telemetry stale/unknown, never
  punish the player.
- D1 daily limits reached: degrade heartbeat only, alert operators; core login,
  rooms, /link, MOTD and CCU must not be coupled to heartbeat storage.
- PlayFab ticket expired/revoked: require ordinary reauthentication; close or
  expire session. Use explicit response codes and avoid leaking session data.

3. D1 SCHEMA / ATOMICITY / RETENTION

Minimal table (illustrative, not implemented):
`heartbeat_sessions(session_id TEXT PRIMARY KEY, player_id TEXT NOT NULL,
 token_hash BLOB NOT NULL, seq INTEGER NOT NULL, challenge_hash BLOB NOT NULL,
 last_seen INTEGER NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER
 NOT NULL, status TEXT NOT NULL)`.

- One active row per session; optionally enforce one active session per player
  with a separate unique active-player key/transaction. Avoid needless indexes:
  Cloudflare counts an additional indexed-row write when indexed columns change.
- Heartbeat uses one prepared SQL statement with conditional match on session,
  token hash, nonce hash, expected next sequence, active status and expiry;
  atomically sets new sequence, challenge hash, last_seen. Use RETURNING to
  determine success without read-before-write. Test exact `rows_read` and
  `rows_written` from D1 result metadata because SQLite query accounting and
  indexes affect billable counts.
- Challenge generation can happen before conditional update; on lost response
  client must not assume accepted. Provide safe recovery protocol that does not
  let the old challenge be accepted twice. One option: server endpoint returns
  session status/current challenge only after bearer token auth, with throttling.
- Cleanup: one scheduled bounded DELETE of expired sessions, e.g. daily or every
  few hours; no per-heartbeat evidence insert. For <=100 active rows, a periodic
  scan/delete is cheap. Retain only latest session state for 24-72 hours after
  expiry if operational diagnostics need it, then delete; aggregate anonymous
  counts if longer trends are needed.
- Evidence: heartbeat records only last-seen/sequence/status; store flags only
  for independently useful security events with timestamp, rule, evidence
  reference and retention limit. Do not retain raw tickets, tokens, challenge
  values or high-frequency movement from heartbeat. Restrict access; redact logs.

4. CAPACITY AND COST ESTIMATE (100 CCU TARGET)

One accepted heartbeat every 5 minutes = 12/player/hour = 288/player/day.
Assumptions: steady active player count, one Worker request and one D1 conditional
row update per beat; no per-beat KV write, no per-beat evidence row. The real D1
read count must be measured from query metadata before rollout.

Active players | beats/Worker requests per day | D1 write rows/day* | storage
20            | 5,760                      | ~5,760            | ~10-30 KB
50            | 14,400                     | ~14,400           | ~25-70 KB
100           | 28,800                     | ~28,800           | ~50-140 KB

*Base estimate: one updated table row per heartbeat and no secondary indexes on
changing columns. D1 documentation says each indexed-column update can add an
index row write; counters/table/session lifecycle and cleanup add extra writes.
If an indexed session lookup is required, session_id primary key is already
indexed; avoid an additional index on last_seen. D1 reads are expected to be
bounded to the matching session row (rough planning upper bound: one row read
per beat: 5,760 / 14,400 / 28,800 per day). Conditional UPDATE may report
different accounting; capture actual `meta.rows_read`/`rows_written` in staging.

Monthly approximate beats: 172,800 / 432,000 / 864,000. These are below D1 Free
5M rows read/day, 100K rows written/day and 5GB total storage, if query accounting
is near the stated one-row estimates. Workers Free allows 100K requests/day:
at 100 players heartbeat alone uses 28.8% of this shared daily allowance; at
50 it uses 14.4%, at 20 5.76%. Existing Discord, redeem, webhook and other Worker
traffic also consumes requests, so monitor total and retain headroom. Worker
subrequests include D1 operations; Free limit is 50 per invocation, far above a
single-query beat. D1 usage exceeding Free daily limits returns errors until
reset, so heartbeat must fail gracefully.

Cost target: €0 additional recurring is feasible if current account is on Workers
Free, D1 free usage stays within shared quotas, and no new paid service is needed.
Cloudflare pricing lists included Free usage; do not guarantee €0 until checking
the actual account plan and aggregate usage. Existing Worker functionality may
already consume capacity. No D1 has been provisioned or measured here.

5. DEPENDENCIES / MIGRATION / TESTS

Dependencies and rollout:
- Add D1 binding/database and migration; choose session/token and expiry policy.
- Add Worker start/beat/reconnect/status routes with POST-only, strict JSON size/
  schema, TLS, origin-independent bearer auth, per-session/IP rate limits,
  constant error detail, and no secrets in URLs/logs. Do not use CF IP alone as
  identity. Avoid KV writes for beats.
- Validate PlayFab session ticket at start with Worker secret; consider periodic
  revalidation aligned with ticket lifetime. Determine if Worker is appropriate
  for game traffic latency/availability.
- Add Unity client caller at a 5-minute interval with bounded jitter and retry;
  send only required session/nonce/seq. Avoid simultaneous duplicate coroutines.
- Keep `/updateplayers` behavior as-is or remove after checking consumers; it
  currently does not update CCU and should not be presented as a security route.
  Preserve /redeem, /motd, Photon webhooks and Discord interactions when changing
  router/auth middleware.
- Review PHOTON_KEY/EXPORT_KEY query parameter exposure and rotate only through
  separately approved operational work. Source scan is not evidence any secret
  value leaked.

Regression tests (staging only):
1. Valid PlayFab session starts session; wrong/expired ticket and forged player
   ID rejected; no secret/ticket appears in logs.
2. Valid beat advances sequence and rotates nonce; old nonce, duplicate seq,
   future/negative seq, wrong session/token, malformed body all rejected.
3. Two concurrent requests using same challenge: at most one succeeds. Lost
   response/retry recovery must neither silently lose session nor double-accept.
4. Expired session, reauth/reconnect and ticket expiry behave as documented; no
   duplicate active sessions beyond policy.
5. Worker/D1/PlayFab timeouts, 429, 5xx and daily quota exhaustion do not ban or
   block core gameplay; retry backoff does not create request storms.
6. Load test 20/50/100 clients at five-minute cadence plus synchronized jitter;
   measure Worker requests, D1 rows_read/written, latency, errors, and total
   account quota headroom. Verify no KV operation on each beat.
7. Route regression: /redeem, /motd, /health, /export, /setup, Discord
   interactions and Photon webhook auth retain intended behavior; verify sensitive
   routes reject missing/bad credentials and URL secrets are removed in a future
   approved change.
8. Expiry cleanup and retention delete rows as specified; evidence logs contain
   only minimal non-secret metadata.

6. OFFICIAL DOCUMENTATION

- Cloudflare D1 pricing and row accounting (indexed columns add writes; Free
  5M read/day, 100K write/day, 5GB total storage; inspect query meta):
  https://developers.cloudflare.com/d1/platform/pricing/
- Workers Free limits (100K requests/day, 50 subrequests/request):
  https://developers.cloudflare.com/workers/platform/limits/
- D1 Worker API / prepared statements, batch transactions, session consistency:
  https://developers.cloudflare.com/d1/worker-api/d1-database/
- PlayFab AuthenticateSessionTicket (server-authenticated ticket validation):
  https://learn.microsoft.com/en-us/rest/api/playfab/server/authentication/authenticate-session-ticket

UNRESOLVED

- Actual Cloudflare account plan, aggregate daily Workers/D1 use, bindings, limits,
  current secret rotation and deployed Worker version.
- Whether any V08 CloudScript handler is deployed/callable; V08 heartbeat has no
  Unity caller in inspected source.
- Which Unity scenes run PlayerCountReporter and whether any external client uses
  `/updateplayers` despite the current no-op.
- PlayFab ticket lifetime/revocation policy and acceptable session revalidation.
- Privacy/retention requirement for player-linked liveness metadata; legal review
  and operator alerting threshold.

No implementation, deployment, dashboard, or production changes.

END OF REPORT

Embed this paste

Install this app

  1. Tap the Share button
  2. Scroll down and tap "Add to Home Screen"
  3. Tap "Add" in the top right

Please confirm