Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1890b5b1ca | ||
|
|
8a5c8b415f | ||
|
|
2589e2126f | ||
|
|
9d6fdc64be | ||
|
|
fc99745672 | ||
|
|
28045d9b9e | ||
|
|
19a51e5882 | ||
|
|
e64cf7003b | ||
|
|
22fda6d12a | ||
|
|
c062ebaee1 |
@@ -132,3 +132,11 @@ Success Criterion 5 remains open until the user reports results.
|
|||||||
**Resume signal:** "device matrix passed", a per-leg failure report, or a
|
**Resume signal:** "device matrix passed", a per-leg failure report, or a
|
||||||
no-device-available report (→ Success Criterion 5 = UNVERIFIED; recommend Phase-7
|
no-device-available report (→ Success Criterion 5 = UNVERIFIED; recommend Phase-7
|
||||||
deferral with GEO-01/GEO-02 noted unverified on hardware).
|
deferral with GEO-01/GEO-02 noted unverified on hardware).
|
||||||
|
|
||||||
|
## Checkpoint resolution (orchestrator, 2026-10-23)
|
||||||
|
|
||||||
|
Task 3 (device-matrix) resolved **PASSED** by the user after debug rounds 1–6
|
||||||
|
(see 03-DEVICE-MATRIX.md). Notable in-gate fixes: telegram-web-app.js
|
||||||
|
integration (round 3), HMAC signature-field erratum (round 6), web_app menu
|
||||||
|
button + settings CTA (rounds 1–4). iOS leg recorded UNVERIFIED — no device.
|
||||||
|
Verification: 03-VERIFICATION.md — phase 3 PASSED.
|
||||||
|
|||||||
@@ -183,3 +183,88 @@ denied card (the one shown at denial) never got the CTA.
|
|||||||
**Fix (deploy `253d7a3e`):** both denied-shaped instances now build with the
|
**Fix (deploy `253d7a3e`):** both denied-shaped instances now build with the
|
||||||
Telegram-only settings CTA. Deployed `index.html` verified to carry
|
Telegram-only settings CTA. Deployed `index.html` verified to carry
|
||||||
telegram-web-app.js (round 3 fix verified live via curl).
|
telegram-web-app.js (round 3 fix verified live via curl).
|
||||||
|
|
||||||
|
## 2026-10-23 debug round 5 (buttons render but all appear dead)
|
||||||
|
|
||||||
|
**Symptom:** LOCATION SETTINGS now visible (round 3 confirmed — the app IS
|
||||||
|
served and parsed), but all three CTAs appear to do nothing; strip shows
|
||||||
|
LOCATION OFF.
|
||||||
|
|
||||||
|
**Working hypothesis:** the user may be viewing the app OUTSIDE a real
|
||||||
|
Telegram webview (plain Chrome address bar). With round 3's script tag,
|
||||||
|
window.Telegram.WebApp now exists even off-Telegram (SDK defines it
|
||||||
|
unconditionally), so LOCATION SETTINGS renders — but initData is empty
|
||||||
|
there → every session POST 401s → TRY AGAIN/ASK VIA CHAT both bounce back
|
||||||
|
to denied instantly (visually indistinguishable). openSettings() no-ops
|
||||||
|
off-Telegram.
|
||||||
|
|
||||||
|
**Fix (deploy `7cbb73f9`, commit `f445399`):** on-screen DIAGNOSTIC PROBE,
|
||||||
|
bottom-left, category-only: `WA <version>/<platform> · q <initData-chars> ·
|
||||||
|
s <session-category> · loc <state>`. No payload, no coordinates, no ids —
|
||||||
|
category-only law (D-14); removed at the device-matrix exit. Expected
|
||||||
|
readings:
|
||||||
|
- real Telegram webview: `WA 8.0+/android|ios` (not `none`), `q` > 0
|
||||||
|
- plain browser: `WA unknown/unknown · q 0ch · s malformed/tampered`
|
||||||
|
|
||||||
|
**Re-test:** the probe line is what to report, verbatim, in the next message.
|
||||||
|
|
||||||
|
## 2026-10-23 debug round 6 (DEFINITIVE — `s tampered` on a real webview)
|
||||||
|
|
||||||
|
**Probe reading:** `WA ... · s tampered · loc denied` — the session gate was
|
||||||
|
rejecting the REAL initData (empty-initData theories were wrong; the webview
|
||||||
|
is live and Telegram-native).
|
||||||
|
|
||||||
|
**Root cause:** `bot/src/security/initdata.ts` excluded the Ed25519
|
||||||
|
`signature` field from the HMAC data-check-string. Per the official spec
|
||||||
|
(core.telegram.org/bots/webapps §Validating data received via the Mini App),
|
||||||
|
the bot's own HMAC check excludes **only** `hash`; the signature exclusion
|
||||||
|
belongs to THIRD-PARTY validation. Modern webviews append `signature`, so
|
||||||
|
every real client failed as "tampered" while all signature-less suite
|
||||||
|
vectors passed.
|
||||||
|
|
||||||
|
**Fix (deploy `bedc8126`, 50 bot tests incl. new T2b signature-field
|
||||||
|
regression vectors):** HMAC dcs = fields minus `hash` only, in
|
||||||
|
initdata.ts + both test-side signing helpers. Erratum header updated.
|
||||||
|
|
||||||
|
**Live post-fix proof:** a minted initData WITH a signature field → 200 at
|
||||||
|
the deployed origin (pre-fix equivalent → 401/tampered).
|
||||||
|
|
||||||
|
**Re-test:** app should now validate → chain runs → permission ask /
|
||||||
|
LocationManager read → LIVE. Probe should read `s ok · loc live` on
|
||||||
|
success.
|
||||||
|
|
||||||
|
## 2026-10-23 round 7 — probe readings (legs largely passing)
|
||||||
|
|
||||||
|
- **Android Telegram:** `s ok`, app live — leg 1 core WORKS.
|
||||||
|
- **Telegram Desktop:** `s ok · loc live` — tdesktop webview delivered a real
|
||||||
|
geolocation fix natively (better than the assumed issue #77 denial class);
|
||||||
|
GEO-02's denied path remains provable on the plain-browser leg / clients
|
||||||
|
where geolocation is unavailable.
|
||||||
|
- **Plain Chrome:** `WA 6.0/unknown · q 0ch · s malformed · loc denied` — the
|
||||||
|
DESIGNED off-Telegram state; leg 5's pass condition (denial is deliberate,
|
||||||
|
not blank). No webview can work without Telegram identity — correct.
|
||||||
|
|
||||||
|
**Cosmetic fix (`19a51e5`, deploy `1a6947ef`):** LOCATION SETTINGS was visible
|
||||||
|
off-Telegram (SDK stub defines LocationManager and reports 6.0) where
|
||||||
|
`openSettings()` is a no-op. CTA eligibility now shares the chain's
|
||||||
|
`isVersionAtLeast("8.0")` gate → the button hides on unsupported clients.
|
||||||
|
|
||||||
|
**Remaining for the exit:** Android boot-state choreography + reopen
|
||||||
|
(no second prompt), Android stale-fix walk (A1), iOS (device permitting),
|
||||||
|
mid-session theme flip leg, then verdicts on all six legs + probe removal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FINAL VERDICT — 2026-10-23: **PASSED** (user: "device matrix passed")
|
||||||
|
|
||||||
|
| Leg | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| 0. BotFather origin prerequisite | ✅ workers.dev origin |
|
||||||
|
| 1. Android boot + persistence | ✅ LIVE, no second prompt |
|
||||||
|
| 2. Android stale-fix (A1) | ✅ per user pass; A1 notes stand in 03-RESEARCH.md |
|
||||||
|
| 3. iOS | ⚠ UNVERIFIED — no device (honest; Phase 7 hardening item) |
|
||||||
|
| 4. Desktop Telegram | ✅ native fix (`s ok · loc live`) — issue #77 denial class not hit on this client; chat-KB route remains test-gated |
|
||||||
|
| 5. Plain browser | ✅ deliberate denied state — evidence: `leg5-plain-browser-evidence.png` |
|
||||||
|
| 6. Theme flip mid-session | ✅ per user verdict |
|
||||||
|
|
||||||
|
Probe removed at exit (commit `fc99745`, deploy `dc0307ea`). Verification: 03-VERIFICATION.md.
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Phase 3: Mini App Shell + Geolocation — Verification
|
||||||
|
|
||||||
|
**Verified:** 2026-10-23
|
||||||
|
**Method:** goal-backward against ROADMAP §Phase 3 success criteria, after in-execution debug rounds 1–6 (see 03-DEVICE-MATRIX.md) and the user's final device-matrix verdict ("device matrix passed").
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### SC1 (GEO-01) — Android: grant, live position on Rams basemap, permission persists — ✅ PASS
|
||||||
|
- Real Android phone, Telegram webview: boot reaches LIVE, `s ok` (probe round 7), position marker on the Rams basemap.
|
||||||
|
- Probe round 6 root-caused the last blocker: the server's HMAC validator excluded the Ed25519 `signature` field from the data-check-string (spec: exclude ONLY `hash` for the bot's own check). Fixed + regression-tested (T2b vectors, 50 bot tests) + live-verified (signed-with-signature initData → 200).
|
||||||
|
- User-confirmed reopen behavior: no second permission prompt.
|
||||||
|
|
||||||
|
### SC2 (GEO-02) — fallback chain recovers origin — ✅ PASS (with a recorded upgrade)
|
||||||
|
- Telegram Desktop delivered a **native** geolocation fix (`s ok · loc live`) — stronger than the designed denial-class fallback (research's issue-#77 assumption retired on that client).
|
||||||
|
- The chat-keyboard fallback route is fully implemented and gated: `POST /api/location-fallback` (claim-scoped), `GET /api/location` relay, 60s bounded client poll, `?pin=1` harness capture — 48 bot + 33 web tests green. Not exercised live on-device because no tested client landed in the denied class during the matrix; the plain-browser leg proves the deliberate denied surface.
|
||||||
|
|
||||||
|
### SC3 (PX-02) — deliberate denied state, themes, Rams-constant map — ✅ PASS
|
||||||
|
- Plain-browser leg evidence: `leg5-plain-browser-evidence.png` (real-browser capture at the deployed origin) — denied card over the Rams shell, red LOCATION OFF lamp, no blank, no off-Telegram settings CTA (version gate `19a51e5`).
|
||||||
|
- 7/7 capture suite includes light + dark live shots; theme-slot machine tested (PX02 gates); copy en+ms verbatim from the UI-SPEC (COPY1–6 gates).
|
||||||
|
|
||||||
|
### SC4 (GEO-03) — no coordinate persists — ✅ PASS
|
||||||
|
- Automated runtime probe: localStorage/sessionStorage/cookies/IndexedDB all empty after a real boot-to-live cycle (03-04 capture harness).
|
||||||
|
- Session token memory-only (D-03/D-07); KV namespaces TTL-only (PG1/PG2 gates from 03-01).
|
||||||
|
|
||||||
|
### SC5 — device-matrix exit pass — ✅ PASS (user verdict, 2026-10-23)
|
||||||
|
- Android ✅ · Telegram Desktop ✅ (native fix) · plain browser ✅ (denied by design) · theme flip ✅ (per user verdict) · iOS — not available: **UNVERIFIED — no device** (recorded honestly per the matrix protocol; does not block v1 KL-metro scope, flagged for Phase 7 hardening).
|
||||||
|
|
||||||
|
## Debug rounds during execution (summary)
|
||||||
|
|
||||||
|
| Round | Symptom | Root cause | Fix |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | No webview entry; dead CTAs on awaiting card | Menu button was `commands` (phase 2); wiring only on first denied instance | `set-menu-button.mjs` (web_app); wire both instances |
|
||||||
|
| 2 | No prompt / no enable path | `openSettings()` never invoked | LOCATION SETTINGS CTA + `locationManagerUpdated` re-run |
|
||||||
|
| 3 | App behaves as off-Telegram everywhere | `telegram-web-app.js` never loaded (research erratum) | Script tag in `<head>` |
|
||||||
|
| 4 | Settings CTA missing on main denied card | Patch attached to wrong instance | Both instances |
|
||||||
|
| 5 | Buttons "dead" | Diagnostic probe added (removed at exit) | On-screen category-only probe |
|
||||||
|
| 6 | `s tampered` on real clients | HMAC dcs excluded `signature` (spec: hash only) | Validator fix + T2b regressions + live 200 proof |
|
||||||
|
| 7 | Probe readings decoded; CTA visible off-Telegram | SDK stub defines LocationManager at v6.0 | CTA eligibility = chain's 8.0 version gate |
|
||||||
|
|
||||||
|
## Requirements coverage
|
||||||
|
|
||||||
|
- **GEO-01** ✅ · **GEO-02** ✅ (native-desktop upgrade recorded) · **GEO-03** ✅ · **PX-02** ✅
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
**PHASE 3: PASSED** — 4/4 plans executed; 5/5 success criteria met (iOS leg honestly UNVERIFIED, no device). Phase 4 (destination-search-+-kl-gazetteer) is next.
|
||||||
|
|
||||||
|
## Exit addendum (2026-10-23, post-verdict)
|
||||||
|
|
||||||
|
- Diagnostic probe removed (commit `fc99745`, deploy `dc0307ea`) — the probe's
|
||||||
|
exit obligation; final capture suite 7/7 green without it.
|
||||||
|
- GEO-03 runtime probe updated: the official `telegram-web-app.js` (debug
|
||||||
|
round 3) writes exactly one sessionStorage key (`__telegram__initParams`,
|
||||||
|
SDK launch bookkeeping, no coordinates). The probe now allowlists that
|
||||||
|
single platform key and still fails on ANY other write (`8a5c8b4`).
|
||||||
|
Our app's own surfaces remain byte-empty — the D-07 law holds.
|
||||||
|
- Final gates at close: web 33/33 · bot 50/50 · captures 7/7 · theme audit clean.
|
||||||
@@ -10,8 +10,14 @@
|
|||||||
* RED suite's node:crypto signing path, RFC-4231-pinned):
|
* RED suite's node:crypto signing path, RFC-4231-pinned):
|
||||||
* 1. parse `raw` as a Telegram-style querystring (values URL-decoded ONCE;
|
* 1. parse `raw` as a Telegram-style querystring (values URL-decoded ONCE;
|
||||||
* `+` stays literal — do NOT use URLSearchParams)
|
* `+` stays literal — do NOT use URLSearchParams)
|
||||||
* 2. dataCheckString = fields minus `hash`/`signature`, sorted by FIELD
|
* 2. dataCheckString = fields minus `hash` ONLY, sorted by FIELD
|
||||||
* NAME (byte order, JS default sort), `key=value`, joined with "\n"
|
* NAME (byte order, JS default sort), `key=value`, joined with "\n"
|
||||||
|
* (ERRATUM 2026-10-23, device-matrix round 6: the HMAC path must NOT
|
||||||
|
* exclude `signature` — per core.telegram.org/bots/webapps the bot's
|
||||||
|
* own check excludes only `hash`; the signature exclusion belongs to
|
||||||
|
* THIRD-PARTY Ed25519 validation. Real webviews append `signature`,
|
||||||
|
* and excluding it produced "tampered" rejections on live clients
|
||||||
|
* while every synthetic suite vector — signature-less — passed.)
|
||||||
* 3. secretKey bytes = HMAC-SHA256(message = utf8(botToken),
|
* 3. secretKey bytes = HMAC-SHA256(message = utf8(botToken),
|
||||||
* key = utf8(the salt constant, line 179))
|
* key = utf8(the salt constant, line 179))
|
||||||
* checkHex = hex(HMAC-SHA256(message = utf8(dataCheckString),
|
* checkHex = hex(HMAC-SHA256(message = utf8(dataCheckString),
|
||||||
@@ -168,9 +174,11 @@ export async function validateInitData(
|
|||||||
return { ok: false, reason: "malformed" as const };
|
return { ok: false, reason: "malformed" as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
// — data-check-string: hash/signature excluded, sorted by field name —
|
// — data-check-string: hash excluded ONLY (signature participates — the
|
||||||
|
// HMAC spec excludes just `hash`; excluding it broke real clients,
|
||||||
|
// device-matrix round 6) — sorted by field name —
|
||||||
const dataCheckString = [...fields.entries()]
|
const dataCheckString = [...fields.entries()]
|
||||||
.filter(([key]) => key !== "hash" && key !== "signature")
|
.filter(([key]) => key !== "hash")
|
||||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ function loadValidator() {
|
|||||||
|
|
||||||
/* ————— node:crypto signing helpers (test-side, independent path) ————— */
|
/* ————— node:crypto signing helpers (test-side, independent path) ————— */
|
||||||
|
|
||||||
/** Data-check-string: fields sorted by key, key=value, LF-joined (Telegram spec). */
|
/** Data-check-string: fields sorted by key, key=value, LF-joined (Telegram
|
||||||
|
* spec — ALL fields except `hash`; the signature field participates in the
|
||||||
|
* bot's own HMAC check, device-matrix round 6 erratum). */
|
||||||
function dataCheckString(fields) {
|
function dataCheckString(fields) {
|
||||||
return fields
|
return fields
|
||||||
.filter(([key]) => key !== "hash" && key !== "signature")
|
.filter(([key]) => key !== "hash")
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
@@ -104,6 +106,43 @@ test("T1 known-good signed initData → ok:true, user.id 42, languageCode ms", a
|
|||||||
|
|
||||||
/* ————— T2 — flipped hash character → tampered ————— */
|
/* ————— T2 — flipped hash character → tampered ————— */
|
||||||
|
|
||||||
|
/* T2b — ERRATUM regression (device-matrix round 6): real webviews append the
|
||||||
|
* Ed25519 `signature` field; per the official HMAC spec the data-check-string
|
||||||
|
* excludes ONLY `hash`, so a signed body carrying `signature` must validate.
|
||||||
|
* The old implementation excluded `signature` from the HMAC string and
|
||||||
|
* rejected every live client as "tampered" while all signature-less suite
|
||||||
|
* vectors passed — the classic blind spot this test pins. */
|
||||||
|
|
||||||
|
test("T2b real-shaped initData WITH a signature field → ok:true", async () => {
|
||||||
|
const validateInitData = await loadValidator();
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const raw = buildInitData([
|
||||||
|
["query_id", "AAH3xSample"],
|
||||||
|
["signature", "c2FtcGxlX3NpZ25hdHVyZV9icnVmX2Jhc2U2NA"],
|
||||||
|
["user", JSON.stringify({ id: 42, first_name: "Zul", language_code: "ms" })],
|
||||||
|
["auth_date", String(now)],
|
||||||
|
]);
|
||||||
|
const res = await validateInitData(raw, TEST_TOKEN);
|
||||||
|
assert.equal(res.ok, true, `expected ok:true with signature field, got ${JSON.stringify(res)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T2b flipped hash with signature present → still tampered", async () => {
|
||||||
|
const validateInitData = await loadValidator();
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const raw = buildInitData([
|
||||||
|
["query_id", "AAH3xSample"],
|
||||||
|
["signature", "c2FtcGxlX3NpZ25hdHVyZV9icnVmX2Jhc2U2NA"],
|
||||||
|
["user", JSON.stringify({ id: 42, first_name: "Zul", language_code: "ms" })],
|
||||||
|
["auth_date", String(now)],
|
||||||
|
]);
|
||||||
|
const match = /hash=[0-9a-f]{8}/.exec(raw);
|
||||||
|
assert.ok(match !== null);
|
||||||
|
const tampered = raw.slice(0, match.index) + `hash=${(createHmac("sha256", "seed").update("x").digest("hex"))}${raw.slice(match.index + "hash=".length + 64)}`;
|
||||||
|
const res = await validateInitData(tampered, TEST_TOKEN);
|
||||||
|
assert.equal(res.ok, false);
|
||||||
|
assert.equal(res.reason, "tampered");
|
||||||
|
});
|
||||||
|
|
||||||
test("T2 flipped hash character → reason tampered", async () => {
|
test("T2 flipped hash character → reason tampered", async () => {
|
||||||
const validateInitData = await loadValidator();
|
const validateInitData = await loadValidator();
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const TEST_TOKEN = "12345:TESTTOKEN"; // same signer token as initdata.test.mjs
|
|||||||
|
|
||||||
function dataCheckString(fields) {
|
function dataCheckString(fields) {
|
||||||
return fields
|
return fields
|
||||||
.filter(([key]) => key !== "hash" && key !== "signature")
|
.filter(([key]) => key !== "hash")
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
|||||||
+11
-4
@@ -290,17 +290,24 @@ try {
|
|||||||
]);
|
]);
|
||||||
if (signal === 'timeout') throw new Error('GEO-03 probe leg: ARAHARAH-LIVE never arrived within 30000ms');
|
if (signal === 'timeout') throw new Error('GEO-03 probe leg: ARAHARAH-LIVE never arrived within 30000ms');
|
||||||
await page.waitForTimeout(1500);
|
await page.waitForTimeout(1500);
|
||||||
|
/* Platform allowlist (device-matrix exit addendum): the OFFICIAL
|
||||||
|
* telegram-web-app.js (added in debug round 3) writes exactly one
|
||||||
|
* sessionStorage key — `__telegram__initParams`, the SDK's own launch
|
||||||
|
* bookkeeping ({}, or Telegram-supplied launch params). It is not app
|
||||||
|
* data, carries no coordinates, and the D-07 law binds OUR surfaces —
|
||||||
|
* which remain byte-empty. Any OTHER key still fails the probe. */
|
||||||
const probe = await page.evaluate(async () => ({
|
const probe = await page.evaluate(async () => ({
|
||||||
local: localStorage.length,
|
local: localStorage.length,
|
||||||
session: sessionStorage.length,
|
foreignSessionKeys: Object.keys(sessionStorage).filter((k) => k !== '__telegram__initParams'),
|
||||||
|
ssForeignOnly: Object.keys(sessionStorage).every((k) => k === '__telegram__initParams'),
|
||||||
cookie: document.cookie,
|
cookie: document.cookie,
|
||||||
dbs: (await indexedDB.databases()).length,
|
dbs: (await indexedDB.databases()).length,
|
||||||
lsKeys: Object.keys(localStorage),
|
lsKeys: Object.keys(localStorage),
|
||||||
}));
|
}));
|
||||||
if (probe.local !== 0 || probe.session !== 0 || probe.cookie !== "" || probe.dbs !== 0) {
|
if (probe.local !== 0 || !probe.ssForeignOnly || probe.cookie !== "" || probe.dbs !== 0) {
|
||||||
failures.push(`GEO-03 runtime probe FAILED — localStorage=${probe.local} sessionStorage=${probe.session} cookie="${probe.cookie}" indexedDB=${probe.dbs} keys=${JSON.stringify(probe.lsKeys)}`);
|
failures.push(`GEO-03 runtime probe FAILED — localStorage=${probe.local} foreignSessionKeys=${JSON.stringify(probe.foreignSessionKeys)} cookie="${probe.cookie}" indexedDB=${probe.dbs} keys=${JSON.stringify(probe.lsKeys)}`);
|
||||||
} else {
|
} else {
|
||||||
log.push('GEO-03 runtime probe PASSED — localStorage/sessionStorage/cookies/IndexedDB all empty after boot-to-live');
|
log.push('GEO-03 runtime probe PASSED — app storage surfaces empty after boot-to-live (platform allowlist: SDK-owned __telegram__initParams only)');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failures.push(`GEO-03 runtime probe: ${err.message}`);
|
failures.push(`GEO-03 runtime probe: ${err.message}`);
|
||||||
|
|||||||
+3
-10
@@ -28,7 +28,7 @@ import { createPositionMarker } from "./map/marker.ts";
|
|||||||
import { createFollowController } from "./map/follow.ts";
|
import { createFollowController } from "./map/follow.ts";
|
||||||
import { createStatusStrip } from "./ui/strip.ts";
|
import { createStatusStrip } from "./ui/strip.ts";
|
||||||
import { buildBootCard, buildDeniedCard, buildSignalLostCard, startGantry, stopGantry } from "./ui/cards.ts";
|
import { buildBootCard, buildDeniedCard, buildSignalLostCard, startGantry, stopGantry } from "./ui/cards.ts";
|
||||||
import { createProbe } from "./ui/probe.ts";
|
import { telegramLocationSupported } from "./location/telegram.ts";
|
||||||
import { createRecenterControl } from "./ui/recenter.ts";
|
import { createRecenterControl } from "./ui/recenter.ts";
|
||||||
import { getWebApp } from "./telegram/webapp.ts";
|
import { getWebApp } from "./telegram/webapp.ts";
|
||||||
import { watchTheme } from "./telegram/theme.ts";
|
import { watchTheme } from "./telegram/theme.ts";
|
||||||
@@ -98,18 +98,13 @@ async function boot(): Promise<void> {
|
|||||||
if (lastFix !== null && map !== null) follow?.follow(lastFix);
|
if (lastFix !== null && map !== null) follow?.follow(lastFix);
|
||||||
});
|
});
|
||||||
|
|
||||||
// DEBUG-ROUND probe: category-only on-screen telemetry for the device
|
|
||||||
// matrix (webview has no dev-tools); removed at the exit.
|
|
||||||
const probe = createProbe(mainEl);
|
|
||||||
probe.set({ wa: wa ? `${wa.version ?? "?"}/${wa.platform ?? "?"}` : "none", initDataChars: wa ? wa.initData.length : null });
|
|
||||||
|
|
||||||
// Cards are built once at boot (boot-time locale) and reused across re-runs.
|
// Cards are built once at boot (boot-time locale) and reused across re-runs.
|
||||||
const bootCard1 = buildBootCard(locale, 1);
|
const bootCard1 = buildBootCard(locale, 1);
|
||||||
const bootCard2 = buildBootCard(locale, 2);
|
const bootCard2 = buildBootCard(locale, 2);
|
||||||
const deniedCard = buildDeniedCard(
|
const deniedCard = buildDeniedCard(
|
||||||
locale,
|
locale,
|
||||||
// Debug round 2/3: Telegram-only deep-link via LocationManager.openSettings.
|
// Debug round 2/3: Telegram-only deep-link via LocationManager.openSettings.
|
||||||
{ awaitingPin: false, withLocationSettings: wa?.LocationManager !== undefined },
|
{ awaitingPin: false, withLocationSettings: telegramLocationSupported(wa) },
|
||||||
);
|
);
|
||||||
const awaitingCard = buildDeniedCard(
|
const awaitingCard = buildDeniedCard(
|
||||||
locale,
|
locale,
|
||||||
@@ -117,7 +112,7 @@ async function boot(): Promise<void> {
|
|||||||
// is the whole eligibility test — off-Telegram (Desktop/browser) there
|
// is the whole eligibility test — off-Telegram (Desktop/browser) there
|
||||||
// is nothing for openSettings() to open, and navigator.geolocation
|
// is nothing for openSettings() to open, and navigator.geolocation
|
||||||
// denial is handled by the OS settings hint copy instead.
|
// denial is handled by the OS settings hint copy instead.
|
||||||
{ awaitingPin: true, withLocationSettings: wa?.LocationManager !== undefined },
|
{ awaitingPin: true, withLocationSettings: telegramLocationSupported(wa) },
|
||||||
);
|
);
|
||||||
const lostCard = buildSignalLostCard(locale);
|
const lostCard = buildSignalLostCard(locale);
|
||||||
const gantryOf = (card: HTMLElement): HTMLElement => {
|
const gantryOf = (card: HTMLElement): HTMLElement => {
|
||||||
@@ -188,7 +183,6 @@ async function boot(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderState(state: LocationState): void {
|
function renderState(state: LocationState): void {
|
||||||
probe.set({ location: state.kind });
|
|
||||||
const ageSec = lastFix !== null ? fixAgeSeconds(lastFix, Date.now()) : null;
|
const ageSec = lastFix !== null ? fixAgeSeconds(lastFix, Date.now()) : null;
|
||||||
const accuracyM = lastFix !== null ? lastFix.accuracyM : null;
|
const accuracyM = lastFix !== null ? lastFix.accuracyM : null;
|
||||||
strip.render(state, ageSec, accuracyM);
|
strip.render(state, ageSec, accuracyM);
|
||||||
@@ -240,7 +234,6 @@ async function boot(): Promise<void> {
|
|||||||
async function runFromSession(): Promise<void> {
|
async function runFromSession(): Promise<void> {
|
||||||
renderState({ kind: "validating" });
|
renderState({ kind: "validating" });
|
||||||
const session = await createSession(wa?.initData ?? "");
|
const session = await createSession(wa?.initData ?? "");
|
||||||
probe.set({ session: session.ok ? "ok" : session.reason, initDataChars: wa ? wa.initData.length : null });
|
|
||||||
if (!session.ok) {
|
if (!session.ok) {
|
||||||
staleHandled = false; // session-first failure is boot-level, not a stale-fix recovery
|
staleHandled = false; // session-first failure is boot-level, not a stale-fix recovery
|
||||||
renderDenied();
|
renderDenied();
|
||||||
|
|||||||
+1
-5
@@ -91,8 +91,4 @@ main { position: fixed; inset: 0; }
|
|||||||
.pos-group[data-stale="true"] { opacity: 0.4; }
|
.pos-group[data-stale="true"] { opacity: 0.4; }
|
||||||
|
|
||||||
/* No CSS transition or animation anywhere — state changes are instant hard swaps. */
|
/* No CSS transition or animation anywhere — state changes are instant hard swaps. */
|
||||||
|
/* DEBUG-ROUND probe styles removed at the device-matrix exit (2026-10-23). */
|
||||||
/* DEBUG-ROUND probe — on-screen category-only telemetry for the device-matrix
|
|
||||||
loop (no dev-tools in a phone webview). Removed at the exit. Layout-only
|
|
||||||
positioning; the label typography is the existing label-sm token. */
|
|
||||||
.diag-probe { position: absolute; left: var(--spacing-margin); bottom: calc(var(--safe-bottom, 0px) + var(--spacing-space-sm)); font: var(--type-label-sm); letter-spacing: var(--type-label-sm-tracking); color: var(--color-soft-beige); z-index: 10; pointer-events: none; }
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* probe.ts — DEBUG-ROUND diagnostic line (phase 3 device-matrix troubleshooting).
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
* One category-only instrument line, fixed bottom-left: which WebApp the app
|
|
||||||
* sees (version/platform), the character-count of the raw initData (a COUNT,
|
|
||||||
* never its content), the last session-gate category, and the location-chain
|
|
||||||
* category. No coordinates, no payload, no ids — same law as the Worker's
|
|
||||||
* category-only logs (D-14), only surfaced on-screen because a phone
|
|
||||||
* webview has no dev-tools access during the device-matrix pass.
|
|
||||||
*
|
|
||||||
* Rendering uses only machine tokens (letters, digits, separators) — no
|
|
||||||
* human copy, so no strings-module entry and no COPY5 concern. The probe is
|
|
||||||
* expected to be REMOVED or reduced at the device-matrix exit.
|
|
||||||
*/
|
|
||||||
export interface ProbeParts {
|
|
||||||
wa: string;
|
|
||||||
initDataChars: number | null;
|
|
||||||
session: string;
|
|
||||||
location: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Probe {
|
|
||||||
el: HTMLElement;
|
|
||||||
set(parts: Partial<ProbeParts>): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProbe(parent: HTMLElement): Probe {
|
|
||||||
const el = document.createElement("div");
|
|
||||||
el.className = "diag-probe";
|
|
||||||
el.setAttribute("aria-hidden", "true");
|
|
||||||
const parts: ProbeParts = { wa: "-", initDataChars: null, session: "-", location: "-" };
|
|
||||||
const state = { parts };
|
|
||||||
function render(): void {
|
|
||||||
const p = state.parts;
|
|
||||||
const q = p.initDataChars === null ? "-" : `${p.initDataChars}ch`;
|
|
||||||
el.textContent = `WA ${p.wa} · q ${q} · s ${p.session} · loc ${p.location}`;
|
|
||||||
}
|
|
||||||
parent.appendChild(el);
|
|
||||||
render();
|
|
||||||
return {
|
|
||||||
el,
|
|
||||||
set(delta: Partial<ProbeParts>): void {
|
|
||||||
Object.assign(state.parts, delta);
|
|
||||||
render();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user