feat(04-03): the /cari command + venue card + OPEN deep link (D-01/D-02/D-10/D-11)
- cari.ts: /cari + /search twin, 128-char pre-search cap (T-04-21), honest no-match refusal (D-13), single sendVenue card with inline OPEN + alternates - deep-link carrier corrected per RESEARCH Pitfall 1: web_app button URL carries ?dest=<plain slug id> — WebAppInfo has only a url field; no crypto - origin from configured MINI_APP_URL only (never user input); blank origin omits the OPEN button instead of guessing (T-04-30) - strings.ts: searchUsage/searchNoMatch/searchQueryTooLong/searchOpenButton/ osmAttribution in en+ms; helpMenu gains the /cari line in both locales - category-only logging law (D-15): four literal rejection categories, query never logged; success path logs nothing - config: MINI_APP_URL in Env + wrangler vars (public, not secret) + .dev.vars.example + docs/CONFIGURATION.md row and sections - bot deps gain @araharah/search (workspace link)
This commit is contained in:
@@ -3,3 +3,6 @@ BOT_TOKEN=
|
|||||||
WEBHOOK_SECRET=
|
WEBHOOK_SECRET=
|
||||||
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
|
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
|
||||||
WEBHOOK_URL=
|
WEBHOOK_URL=
|
||||||
|
# Public Mini App origin (not a secret) — lives in bot/wrangler.jsonc "vars" in production.
|
||||||
|
# The /cari venue card's OPEN button deep link is built from it: <MINI_APP_URL>/?dest=<id>.
|
||||||
|
MINI_APP_URL=
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@
|
|||||||
"commands": "node scripts/register-commands.mjs"
|
"commands": "node scripts/register-commands.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grammy": "^1.46.0"
|
"grammy": "^1.46.0",
|
||||||
|
"@araharah/search": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^4.151.3",
|
"wrangler": "^4.151.3",
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* cari.ts — the /cari command handler: search → venue card (D-01, D-02, D-10, D-11).
|
||||||
|
* --------------------------------------------------------------------------------
|
||||||
|
* Search entry is the /cari command in chat, with its bilingual twin /search
|
||||||
|
* sharing the same handler (D-01; the Phase-2 D-04 alias precedent paired
|
||||||
|
* /start↔/mula and /privacy↔/privasi). /pergi_ke stays RESERVED for Phase 5
|
||||||
|
* and is deliberately NOT registered here — the future-stub ban (CS5b) keeps
|
||||||
|
* it that way.
|
||||||
|
*
|
||||||
|
* D-02 shape: ONE message per search — a native venue card (Bot API
|
||||||
|
* sendVenue) carrying an inline keyboard: row 0 is the OPEN IN ARAHARAH
|
||||||
|
* web_app button, the following row carries 2–4 alternates as callback
|
||||||
|
* buttons. This keyboard is the SECOND deliberate exception to the Phase-2
|
||||||
|
* "plain text only" command-surface rule (the first being the GEO-02 reply
|
||||||
|
* keyboard in handlers/location-fallback.ts); CS4's carve documents it.
|
||||||
|
* The card IS the confirmation (sendVenue has no `text` parameter — its
|
||||||
|
* `title` is the entry's canonical name and `address` the curated address,
|
||||||
|
* verbatim, never the normalized form).
|
||||||
|
*
|
||||||
|
* DEEP-LINK MECHANISM (D-11, carrier corrected per RESEARCH Pitfall 1): a
|
||||||
|
* `web_app` inline button's WebAppInfo carries EXACTLY ONE field — `url`
|
||||||
|
* (@grammyjs/types markup.d.ts) — so `start_param` cannot ride an inline
|
||||||
|
* button. The gazetteer id therefore travels in the URL's OWN query string
|
||||||
|
* (`?dest=<id>`), the webview reads it from location.search, and
|
||||||
|
* `initData.start_param` remains the secondary path (the documented
|
||||||
|
* direct-link / home-screen route). The payload is a PLAIN public place
|
||||||
|
* id — no crypto, no signing (D-11): a hostile id can only send the user
|
||||||
|
* to a place they asked about.
|
||||||
|
*
|
||||||
|
* The OPEN button's origin comes ONLY from the configured MINI_APP_URL env
|
||||||
|
* var — never from user input (an unvalidated origin would be an open
|
||||||
|
* redirect, RESEARCH §Security V4). When the origin is blank the OPEN
|
||||||
|
* button is omitted (never guessed) and the card still confirms the
|
||||||
|
* destination.
|
||||||
|
*
|
||||||
|
* PRIVACY (D-15): the query is user payload. Every log line here is a
|
||||||
|
* category-only literal — the raw query, results, ids, chat ids, and
|
||||||
|
* timestamps NEVER reach a log. Refusal copy names the area, never the
|
||||||
|
* query.
|
||||||
|
*
|
||||||
|
* All copy comes from src/i18n/strings.ts — nothing inline here.
|
||||||
|
*/
|
||||||
|
import { InlineKeyboard } from "grammy";
|
||||||
|
import type { Bot, Context } from "grammy";
|
||||||
|
import { pickLocale, strings } from "../../i18n/strings.ts";
|
||||||
|
import type { Locale } from "../../i18n/strings.ts";
|
||||||
|
import type { GazetteerEntry, SearchSource } from "@araharah/search/src/index.ts";
|
||||||
|
|
||||||
|
/** V5: the query is untrusted input — bounded before ANY processing
|
||||||
|
* (a megabyte argument must never reach normalization/scoring, T-04-21). */
|
||||||
|
export const MAX_QUERY_CHARS = 128;
|
||||||
|
|
||||||
|
/** The Mini App deep link for a gazetteer entry (D-11).
|
||||||
|
* Carrier corrected per RESEARCH Pitfall 1: a `web_app` inline button's
|
||||||
|
* WebAppInfo carries ONLY a `url` — there is no `start_param` field, and
|
||||||
|
* Telegram populates start_param only from an attachment-menu deep link.
|
||||||
|
* The id therefore travels in the URL's own query string and the webview
|
||||||
|
* reads it from location.search. start_param remains the secondary path
|
||||||
|
* (the documented direct-link mechanism, used by the home-screen shortcut).
|
||||||
|
* No crypto: the payload is a public place id (D-11). */
|
||||||
|
export function miniAppUrlFor(origin: string, id: string): string {
|
||||||
|
return `${origin.replace(/\/+$/, "")}/?dest=${encodeURIComponent(id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pure builder — the empty-query usage line. */
|
||||||
|
export function searchUsageText(locale: Locale): string {
|
||||||
|
return strings[locale].searchUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pure builder — the honest out-of-area refusal (D-13). */
|
||||||
|
export function searchNoMatchText(locale: Locale): string {
|
||||||
|
return strings[locale].searchNoMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCari(ctx: Context, source: SearchSource, origin: string): Promise<void> {
|
||||||
|
const locale = pickLocale(ctx.from?.language_code);
|
||||||
|
const raw = (typeof ctx.match === "string" ? ctx.match : "").trim();
|
||||||
|
|
||||||
|
if (raw.length === 0) {
|
||||||
|
console.log("[search] rejected: empty-query");
|
||||||
|
await ctx.reply(searchUsageText(locale), { link_preview_options: { is_disabled: true } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.length > MAX_QUERY_CHARS) {
|
||||||
|
console.log("[search] rejected: query-too-long");
|
||||||
|
await ctx.reply(strings[locale].searchQueryTooLong, { link_preview_options: { is_disabled: true } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hits = await source.search({ text: raw, limit: 5 });
|
||||||
|
if (hits.length === 0) {
|
||||||
|
console.log("[search] rejected: no-match");
|
||||||
|
await ctx.reply(searchNoMatchText(locale), { link_preview_options: { is_disabled: true } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D-02: the best result ships immediately; 2–4 alternates ride on the
|
||||||
|
// SAME message. Exactly ONE sendVenue — no second message round.
|
||||||
|
const entry: GazetteerEntry = hits[0]!.entry;
|
||||||
|
const alternates: GazetteerEntry[] = hits.slice(1).map((hit) => hit.entry);
|
||||||
|
|
||||||
|
const keyboard = new InlineKeyboard();
|
||||||
|
if (origin.trim() !== "") {
|
||||||
|
keyboard.webApp(strings[locale].searchOpenButton, miniAppUrlFor(origin, entry.id)).row();
|
||||||
|
} else {
|
||||||
|
console.log("[search] rejected: mini-app-origin-missing");
|
||||||
|
}
|
||||||
|
for (const alternate of alternates.slice(0, 4)) {
|
||||||
|
keyboard.text(alternate.name, `alt:${alternate.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.replyWithVenue(entry.lat, entry.lng, entry.name, entry.address ?? "", {
|
||||||
|
reply_markup: keyboard,
|
||||||
|
});
|
||||||
|
// No log line on the success path — every log line is a privacy surface (D-15).
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register the search command stack: /cari and its bilingual twin /search.
|
||||||
|
* /pergi_ke is reserved for Phase 5 (D-01) and must never be registered here. */
|
||||||
|
export function registerCari(bot: Bot, source: SearchSource, origin: string): void {
|
||||||
|
bot.command("cari", (ctx) => handleCari(ctx, source, origin));
|
||||||
|
bot.command("search", (ctx) => handleCari(ctx, source, origin));
|
||||||
|
}
|
||||||
@@ -9,4 +9,9 @@ export interface Env {
|
|||||||
// accept-everything.
|
// accept-everything.
|
||||||
WEBHOOK_SECRET: string; // wrangler secret (D-15) — value Telegram echoes in X-Telegram-Bot-Api-Secret-Token
|
WEBHOOK_SECRET: string; // wrangler secret (D-15) — value Telegram echoes in X-Telegram-Bot-Api-Secret-Token
|
||||||
chat_sessions: KVNamespace; // TTL-only namespace — first durable KV namespace (PRIV-02)
|
chat_sessions: KVNamespace; // TTL-only namespace — first durable KV namespace (PRIV-02)
|
||||||
|
// The deployed Mini App ORIGIN (public, not a secret) — used to build the
|
||||||
|
// OPEN button's deep link. NOT derived from user input: the deep-link
|
||||||
|
// origin is a configured value (RESEARCH §Security V4 — an unvalidated
|
||||||
|
// origin would be an open redirect).
|
||||||
|
MINI_APP_URL: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-2
@@ -35,6 +35,20 @@ export interface BotStrings {
|
|||||||
/** GEO-02: confirmation after the user's shared pin has been recorded
|
/** GEO-02: confirmation after the user's shared pin has been recorded
|
||||||
* in the TTL-only location-pin KV (GEO-03). */
|
* in the TTL-only location-pin KV (GEO-03). */
|
||||||
pinReceived: string;
|
pinReceived: string;
|
||||||
|
/** SRCH-03/D-01: the empty-query usage line for /cari. */
|
||||||
|
searchUsage: string;
|
||||||
|
/** D-13: the honest out-of-area refusal — names the v1 area, promises
|
||||||
|
* no pin. Never echoes the query text (D-15). */
|
||||||
|
searchNoMatch: string;
|
||||||
|
/** V5: the over-cap refusal for an over-long /cari argument. */
|
||||||
|
searchQueryTooLong: string;
|
||||||
|
/** D-10: the OPEN label on the venue card's web_app button — a product
|
||||||
|
* affordance, identical in both locales by design. */
|
||||||
|
searchOpenButton: string;
|
||||||
|
/** The OSM data attribution for search-driven surfaces — identical in
|
||||||
|
* both locales (legal attribution, not prose; same string as
|
||||||
|
* web/src/i18n/strings.ts). */
|
||||||
|
osmAttribution: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Locale = "en" | "ms";
|
export type Locale = "en" | "ms";
|
||||||
@@ -48,10 +62,15 @@ export const strings: Record<Locale, BotStrings> = {
|
|||||||
privacyNotStored:
|
privacyNotStored:
|
||||||
"What araharah never keeps: your location, your routes, your trip history, any profile about you.",
|
"What araharah never keeps: your location, your routes, your trip history, any profile about you.",
|
||||||
helpMenu:
|
helpMenu:
|
||||||
"Commands:\n/help — How araharah works and what it keeps\n/privacy — What araharah stores and never stores",
|
"Commands:\n/help — How araharah works and what it keeps\n/privacy — What araharah stores and never stores\n/cari — Search for a destination in the KL metro",
|
||||||
awaitingPin: "Send me a pin in this chat and I'll use it as your starting point.",
|
awaitingPin: "Send me a pin in this chat and I'll use it as your starting point.",
|
||||||
shareLocationButton: "Share my location",
|
shareLocationButton: "Share my location",
|
||||||
pinReceived: "Got it — your starting point is set.",
|
pinReceived: "Got it — your starting point is set.",
|
||||||
|
searchUsage: "/cari <place> — search for a destination in the KL metro.",
|
||||||
|
searchNoMatch: "Not in the v1 KL area. araharah only knows Kuala Lumpur and the Klang Valley for now.",
|
||||||
|
searchQueryTooLong: "That query is too long. Send just the place name.",
|
||||||
|
searchOpenButton: "OPEN IN ARAHARAH",
|
||||||
|
osmAttribution: "© OpenStreetMap contributors",
|
||||||
},
|
},
|
||||||
ms: {
|
ms: {
|
||||||
startIntro:
|
startIntro:
|
||||||
@@ -61,10 +80,15 @@ export const strings: Record<Locale, BotStrings> = {
|
|||||||
privacyNotStored:
|
privacyNotStored:
|
||||||
"Yang araharah tidak pernah simpan: lokasi anda, laluan anda, sejarah perjalanan anda, apa-apa profil tentang anda.",
|
"Yang araharah tidak pernah simpan: lokasi anda, laluan anda, sejarah perjalanan anda, apa-apa profil tentang anda.",
|
||||||
helpMenu:
|
helpMenu:
|
||||||
"Arahan:\n/help — Cara araharah berfungsi dan apa yang ia simpan\n/privacy — Apa yang araharah simpan dan tidak pernah simpan",
|
"Arahan:\n/help — Cara araharah berfungsi dan apa yang ia simpan\n/privacy — Apa yang araharah simpan dan tidak pernah simpan\n/cari — Cari destinasi di kawasan KL",
|
||||||
awaitingPin: "Hantar saya pin dalam chat ini dan saya akan gunakannya sebagai titik mula anda.",
|
awaitingPin: "Hantar saya pin dalam chat ini dan saya akan gunakannya sebagai titik mula anda.",
|
||||||
shareLocationButton: "Kongsi lokasi saya",
|
shareLocationButton: "Kongsi lokasi saya",
|
||||||
pinReceived: "Baik — titik mula anda sudah ditetapkan.",
|
pinReceived: "Baik — titik mula anda sudah ditetapkan.",
|
||||||
|
searchUsage: "/cari <tempat> — cari destinasi dalam kawasan KL.",
|
||||||
|
searchNoMatch: "Bukan dalam kawasan KL v1. araharah hanya tahu Kuala Lumpur dan Lembah Klang buat masa ini.",
|
||||||
|
searchQueryTooLong: "Pertanyaan itu terlalu panjang. Hantar nama tempat sahaja.",
|
||||||
|
searchOpenButton: "OPEN IN ARAHARAH",
|
||||||
|
osmAttribution: "© OpenStreetMap contributors",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,10 @@
|
|||||||
"not_found_handling": "none"
|
"not_found_handling": "none"
|
||||||
},
|
},
|
||||||
"run_worker_first": ["/api/*", "/telegram/*"],
|
"run_worker_first": ["/api/*", "/telegram/*"],
|
||||||
|
// Public vars (NOT secrets): the deployed Mini App ORIGIN used to build
|
||||||
|
// the /cari venue card's OPEN deep link (<MINI_APP_URL>/?dest=<id>).
|
||||||
|
// It must equal the Mini App URL registered in @BotFather (Bot API 10.2
|
||||||
|
// enforces the origin). Never derived from user input.
|
||||||
|
"vars": { "MINI_APP_URL": "https://araharah-bot.araharah-bot.workers.dev" },
|
||||||
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
|
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-2
@@ -24,13 +24,14 @@ Configuration surfaces, by kind:
|
|||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
Three variables reach the Worker (`bot/src/config.ts` declares them as a typed `Env` interface);
|
Four variables reach the Worker (`bot/src/config.ts` declares them as a typed `Env` interface);
|
||||||
a fourth (`WEBHOOK_URL`) is consumed only by the one-time registration scripts, never by the Worker.
|
a fifth (`WEBHOOK_URL`) is consumed only by the one-time registration scripts, never by the Worker.
|
||||||
|
|
||||||
| Variable | Required | Default | Description |
|
| Variable | Required | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `BOT_TOKEN` | **Required** | none | Telegram Bot API token from @BotFather. Used by `createBot()` in `bot/src/bot/bot.ts` to construct the grammY `Bot`, and by `validateInitData()` in `bot/src/security/initdata.ts` as the HMAC key for Mini App `initData` verification. |
|
| `BOT_TOKEN` | **Required** | none | Telegram Bot API token from @BotFather. Used by `createBot()` in `bot/src/bot/bot.ts` to construct the grammY `Bot`, and by `validateInitData()` in `bot/src/security/initdata.ts` as the HMAC key for Mini App `initData` verification. |
|
||||||
| `WEBHOOK_SECRET` | **Required** | none | Value Telegram echoes in the `X-Telegram-Bot-Api-Secret-Token` header on every update. `/telegram/webhook` constant-time-compares it **before** any bot processing or KV access. If missing or empty at request time the route fails closed with `500 Server misconfigured` — grammY's secret check would otherwise accept every request. |
|
| `WEBHOOK_SECRET` | **Required** | none | Value Telegram echoes in the `X-Telegram-Bot-Api-Secret-Token` header on every update. `/telegram/webhook` constant-time-compares it **before** any bot processing or KV access. If missing or empty at request time the route fails closed with `500 Server misconfigured` — grammY's secret check would otherwise accept every request. |
|
||||||
|
| `MINI_APP_URL` | **Required for the deep link** | none | Public origin of the deployed Mini App (e.g. `https://araharah-bot.<account>.workers.dev`) — NOT a secret, so it lives in `bot/wrangler.jsonc`'s `vars`, not in a secret. The `/cari` venue card's OPEN button is built from it: `<MINI_APP_URL>/?dest=<gazetteer id>`. It must equal the Mini App URL registered in @BotFather (Bot API 10.2 enforces the origin). The Worker never derives it from user input — an unvalidated origin would be an open redirect. |
|
||||||
| `WEBHOOK_URL` | **Required for the one-time scripts** | none | Full origin + path of the deployed Worker, used by `bot/scripts/set-webhook.mjs` and `bot/scripts/set-menu-button.mjs`. If the value points at the workers.dev root (no path), `set-webhook.mjs` appends `/telegram/webhook` automatically. The Worker never reads this variable. |
|
| `WEBHOOK_URL` | **Required for the one-time scripts** | none | Full origin + path of the deployed Worker, used by `bot/scripts/set-webhook.mjs` and `bot/scripts/set-menu-button.mjs`. If the value points at the workers.dev root (no path), `set-webhook.mjs` appends `/telegram/webhook` automatically. The Worker never reads this variable. |
|
||||||
| `chat_sessions` | **Required (binding)** | n/a | Not an env var but a Cloudflare KV namespace binding. Wired in `bot/wrangler.jsonc` under `kv_namespaces`; every `kv.put()` in `bot/src/kv/sessions.ts` and `bot/src/kv/location-pin.ts` writes through it with an explicit `expirationTtl`. |
|
| `chat_sessions` | **Required (binding)** | n/a | Not an env var but a Cloudflare KV namespace binding. Wired in `bot/wrangler.jsonc` under `kv_namespaces`; every `kv.put()` in `bot/src/kv/sessions.ts` and `bot/src/kv/location-pin.ts` writes through it with an explicit `expirationTtl`. |
|
||||||
|
|
||||||
@@ -42,6 +43,9 @@ BOT_TOKEN=
|
|||||||
WEBHOOK_SECRET=
|
WEBHOOK_SECRET=
|
||||||
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
|
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
|
||||||
WEBHOOK_URL=
|
WEBHOOK_URL=
|
||||||
|
# Public Mini App origin (not a secret) — lives in bot/wrangler.jsonc "vars" in production.
|
||||||
|
# The /cari venue card's OPEN button deep link is built from it: <MINI_APP_URL>/?dest=<id>.
|
||||||
|
MINI_APP_URL=
|
||||||
```
|
```
|
||||||
|
|
||||||
Script-level precedence: every script in `bot/scripts/` resolves a variable as
|
Script-level precedence: every script in `bot/scripts/` resolves a variable as
|
||||||
@@ -120,6 +124,9 @@ print a missing-variable list to stderr and `process.exit(1)` before any network
|
|||||||
- `KV namespace id` — `bot/wrangler.jsonc` already carries a concrete id for `chat_sessions`; the
|
- `KV namespace id` — `bot/wrangler.jsonc` already carries a concrete id for `chat_sessions`; the
|
||||||
file's leading comment notes it was filled in after `npx wrangler kv namespace create chat_sessions`.
|
file's leading comment notes it was filled in after `npx wrangler kv namespace create chat_sessions`.
|
||||||
- `WEBHOOK_URL` path segment — auto-derived by `set-webhook.mjs` when the configured origin has no path.
|
- `WEBHOOK_URL` path segment — auto-derived by `set-webhook.mjs` when the configured origin has no path.
|
||||||
|
- `MINI_APP_URL` — a non-secret configuration value declared in the Worker `Env` and shipped via
|
||||||
|
`bot/wrangler.jsonc`'s `vars` block (public origin, not a secret). When it is blank the `/cari`
|
||||||
|
OPEN button is omitted rather than guessed; the venue card and its alternates still send.
|
||||||
- KV TTLs — constants in source, not configuration: `SESSION_TTL_SECONDS = 900`
|
- KV TTLs — constants in source, not configuration: `SESSION_TTL_SECONDS = 900`
|
||||||
(`bot/src/kv/sessions.ts`) and `PIN_TTL_SECONDS = 300` (`bot/src/kv/location-pin.ts`). Both are
|
(`bot/src/kv/sessions.ts`) and `PIN_TTL_SECONDS = 300` (`bot/src/kv/location-pin.ts`). Both are
|
||||||
pinned by a grep gate in the test suite so no write path can persist without a TTL.
|
pinned by a grep gate in the test suite so no write path can persist without a TTL.
|
||||||
@@ -141,6 +148,9 @@ print a missing-variable list to stderr and `process.exit(1)` before any network
|
|||||||
"not_found_handling": "none"
|
"not_found_handling": "none"
|
||||||
},
|
},
|
||||||
"run_worker_first": ["/api/*", "/telegram/*"],
|
"run_worker_first": ["/api/*", "/telegram/*"],
|
||||||
|
// Public vars (NOT secrets): the deployed Mini App ORIGIN used to build
|
||||||
|
// the /cari venue card's OPEN deep link (<MINI_APP_URL>/?dest=<id>).
|
||||||
|
"vars": { "MINI_APP_URL": "https://araharah-bot.<account>.workers.dev" },
|
||||||
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
|
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -154,6 +164,9 @@ Key notes:
|
|||||||
- `not_found_handling: "none"` — unmatched paths are not served the SPA `index.html` fallback.
|
- `not_found_handling: "none"` — unmatched paths are not served the SPA `index.html` fallback.
|
||||||
- `workers_dev: true` serves on the `<name>.<account>.workers.dev` URL, which is the `WEBHOOK_URL`
|
- `workers_dev: true` serves on the `<name>.<account>.workers.dev` URL, which is the `WEBHOOK_URL`
|
||||||
target.
|
target.
|
||||||
|
- `vars: { "MINI_APP_URL": ... }` — a **public** origin shipped as a plain var (never a secret):
|
||||||
|
the deployed Mini App origin the `/cari` venue card's OPEN button deep link is built from. It
|
||||||
|
must equal the Mini App URL registered in @BotFather; see the env-var table above.
|
||||||
|
|
||||||
<!-- VERIFY: the Cloudflare account subdomain backing `araharah-bot.<account>.workers.dev` is not recorded in the repository. -->
|
<!-- VERIFY: the Cloudflare account subdomain backing `araharah-bot.<account>.workers.dev` is not recorded in the repository. -->
|
||||||
|
|
||||||
|
|||||||
Generated
+1
@@ -15,6 +15,7 @@
|
|||||||
"bot": {
|
"bot": {
|
||||||
"name": "@araharah/bot",
|
"name": "@araharah/bot",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@araharah/search": "*",
|
||||||
"grammy": "^1.46.0"
|
"grammy": "^1.46.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in New Issue
Block a user