@www.hyperlinks.space/program-kit 18.18.18 → 123.123.123

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.
Files changed (52) hide show
  1. package/.eas/workflows/create-development-builds.yml +21 -0
  2. package/.eas/workflows/create-draft.yml +15 -0
  3. package/.eas/workflows/deploy-to-production.yml +68 -0
  4. package/.gitattributes +48 -0
  5. package/.gitignore +52 -0
  6. package/.nvmrc +1 -0
  7. package/.vercelignore +6 -0
  8. package/README.md +17 -2
  9. package/ai/openai.ts +202 -0
  10. package/ai/transmitter.ts +367 -0
  11. package/backlogs/medium_term_backlog.md +26 -0
  12. package/backlogs/short_term_backlog.md +42 -0
  13. package/eslint.config.cjs +10 -0
  14. package/npmReadMe.md +17 -2
  15. package/npmrc.example +1 -0
  16. package/package.json +3 -28
  17. package/polyfills/buffer.ts +9 -0
  18. package/research & docs/ai_and_search_bar_input.md +94 -0
  19. package/research & docs/ai_bot_messages.md +124 -0
  20. package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
  21. package/research & docs/blue_bar_tackling.md +143 -0
  22. package/research & docs/bot_async_streaming.md +174 -0
  23. package/research & docs/build_and_install.md +129 -0
  24. package/research & docs/database_messages.md +34 -0
  25. package/research & docs/fonts.md +18 -0
  26. package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
  27. package/research & docs/keys-retrieval-console-scripts.js +131 -0
  28. package/research & docs/npm-release.md +46 -0
  29. package/research & docs/releases.md +201 -0
  30. package/research & docs/releases_github_actions.md +188 -0
  31. package/research & docs/scalability.md +34 -0
  32. package/research & docs/security_plan_raw.md +244 -0
  33. package/research & docs/security_raw.md +354 -0
  34. package/research & docs/storage-availability-console-script.js +152 -0
  35. package/research & docs/storage-lifetime.md +33 -0
  36. package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
  37. package/research & docs/timing_raw.md +63 -0
  38. package/research & docs/tma_logo_bar_jump_investigation.md +69 -0
  39. package/research & docs/update.md +205 -0
  40. package/research & docs/wallet_telegram_standalone_multichain_proposal.md +192 -0
  41. package/research & docs/wallets_hosting_architecture.md +403 -0
  42. package/services/wallet/tonWallet.ts +73 -0
  43. package/ui/components/GlobalBottomBar.tsx +447 -0
  44. package/ui/components/GlobalBottomBarWeb.tsx +362 -0
  45. package/ui/components/GlobalLogoBar.tsx +108 -0
  46. package/ui/components/GlobalLogoBarFallback.tsx +66 -0
  47. package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
  48. package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
  49. package/ui/components/Telegram.tsx +677 -0
  50. package/ui/components/telegramWebApp.ts +359 -0
  51. package/ui/fonts.ts +12 -0
  52. package/ui/theme.ts +117 -0
@@ -0,0 +1,154 @@
1
+ # GitHub and GitLab Repository Mirroring
2
+
3
+ This document describes how to keep a Git repository synchronized between GitHub and GitLab, including **bidirectional** setups, the problems that appear in practice, **automation guards** that reduce loops and wasted work, and **automatic escalation** when the automation must not guess (for example, divergent histories).
4
+
5
+ It is guidance for operators and automation authors; it is not specific to application code in this repository.
6
+
7
+ ## Terminology
8
+
9
+ - **Canonical remote:** The single place where humans are expected to push day-to-day changes (or where merge commits land after review). Everything else is a **mirror**.
10
+ - **One-way mirror:** After a push to A, automation updates B. Humans do not push to B for the same branches (or B is protected so only the mirror bot can push).
11
+ - **Bidirectional mirror:** Automation tries to propagate pushes from A→B and B→A. This is workable only with strict guards and a clear policy when histories **diverge**.
12
+ - **Escalate:** Stop silent auto-fix; surface the condition to people or ticketing (failed job, alert, issue). See [Automatic escalation](#automatic-escalation).
13
+
14
+ ## Recommended default: one canonical remote + one-way mirror
15
+
16
+ For most teams, the lowest-risk approach is:
17
+
18
+ 1. Choose **one** platform as the source of truth for each protected branch (usually `main`).
19
+ 2. Mirror to the other platform with **fast-forward-only** pushes and **short-circuit** when tips already match.
20
+
21
+ Typical implementations (pick **one** row: canonical host is where humans push; the arrow is where commits are **copied** next):
22
+
23
+ | Canonical | Replication direction | How |
24
+ |---|---|---|
25
+ | **GitHub** | GitHub → GitLab | GitHub Actions on `push`: fetch GitLab, compare SHAs, `git push` to GitLab using a [GitLab deploy key](https://docs.gitlab.com/ee/user/project/deploy_keys/) or [project access token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html). |
26
+ | **GitLab** | GitHub ← GitLab | [GitLab repository push mirroring](https://docs.gitlab.com/ee/user/project/repository/mirror/) so GitLab pushes to GitHub after receiving commits. |
27
+
28
+ The two directions are **not** used together for the same branch in this pattern: they are **alternatives** depending on which platform is source of truth. (Using both at once without guards is how bidirectional mirroring starts.)
29
+
30
+ Official overview: [GitLab repository mirroring](https://docs.gitlab.com/ee/user/project/repository/mirror/), [GitHub Actions push trigger](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push).
31
+
32
+ This gives **near-instant** propagation from the moment the canonical host accepts the push, without fighting bidirectional edge cases.
33
+
34
+ ## Bidirectional mirroring: when it is used and what breaks
35
+
36
+ Bidirectional setups appear when both platforms must accept pushes (different teams, migration, or CI only on one side). They are **not** “set and forget.”
37
+
38
+ | Risk | What goes wrong | Why it matters |
39
+ |---|---|---|
40
+ | **Loop / ping-pong** | A push to GitHub triggers sync to GitLab; GitLab triggers sync back to GitHub; repeat or duplicate work | Duplicate CI, wasted API calls, flaky pipelines |
41
+ | **New objects from automation** | Mirror job merges, rebases, or force-pushes to “fix” state | New commits or rewritten history; harder audits; can feed loops |
42
+ | **Divergence** | Two different commits on `main` at each host, neither is ancestor of the other | `git push` rejects (non-fast-forward) unless someone force-pushes |
43
+ | **Duplicate CI** | Same tree built twice because both hosts run pipelines on the same logical change | Cost and noise; confusing status checks |
44
+ | **Credential exposure** | Tokens in logs, overly broad PATs, shared user accounts | Security and compliance issues |
45
+
46
+ ## Guards in automation (layered)
47
+
48
+ Use several of these together; one guard rarely covers every failure mode.
49
+
50
+ ### 1. Event and scheduling guards
51
+
52
+ - **SHA equality short-circuit:** After fetching both remotes, if `github/main` and `gitlab/main` are the **same commit OID**, exit successfully without pushing.
53
+ - **Ancestor / fast-forward check before push:** Only push from A to B if B’s tip is an **ancestor** of A’s tip (pure fast-forward). If not, do not force-push; treat as divergence and [escalate](#automatic-escalation).
54
+ - **Mirror bot identity:** Perform mirror pushes with a **dedicated** bot user or deploy key. On the receiving side, webhook or pipeline logic can **skip enqueuing reverse sync** when the actor matches the mirror bot (where the platform exposes this).
55
+ - **Idempotency:** Persist “last mirrored OID per branch” (CI cache, KV, issue comment, etc.) and skip if the incoming event’s `after` SHA was already mirrored.
56
+ - **Debounce / concurrency:** One in-flight sync per branch (GitHub Actions `concurrency`, GitLab `resource_group`, or a lock) to avoid stacked identical jobs from webhook retries.
57
+
58
+ ### 2. Git protocol guards
59
+
60
+ - **No `--force` on mirrored branches** unless you have an explicit, rare break-glass procedure. Non-fast-forward should **fail** and escalate.
61
+ - **Narrow refspecs:** Mirror only agreed branches (for example `refs/heads/main`, `refs/heads/release/*`), not all refs.
62
+ - **Atomic multi-ref push** when supported (`git push --atomic`) to avoid half-updated mirror state.
63
+
64
+ ### 3. Loop-specific logic
65
+
66
+ - **Directional rule:** GitHub→GitLab runs only when GitHub is strictly ahead of GitLab (fast-forward). GitLab→GitHub runs only when GitLab is strictly ahead. If **both** are ahead of the common ancestor (diverged), **neither** direction should force-sync; escalate.
67
+ - **Cooldown:** If the same `(branch, OID)` was mirrored within N seconds, exit (reduces double webhook storms).
68
+
69
+ ### 4. CI noise controls
70
+
71
+ Mirroring duplicates pipelines if both hosts run on every push.
72
+
73
+ - Prefer **one** platform for **required** checks; treat the other as informational or limit runs (for example scheduled smoke only).
74
+ - Where policy allows, use host-specific **skip directives** for mirror-only pushes (conventions vary; confirm org rules).
75
+ - **Path filters** help only when mirror commits change nothing meaningful; usually both sides see the same files.
76
+
77
+ ### 5. Security guards
78
+
79
+ - **Least privilege:** Repo-scoped tokens or deploy keys; no org-wide admin tokens for mirroring.
80
+ - **Branch protection** on both sides: block force-push to mirrored branches; restrict who may push to protected branches.
81
+ - **Secrets hygiene:** Mask remotes in logs; disable interactive prompts (`GIT_TERMINAL_PROMPT=0`); store credentials only in CI secrets.
82
+
83
+ ## Automatic escalation
84
+
85
+ **Escalate** means: the automation **detects a condition it must not resolve alone**, marks the run as failed or blocked, and **notifies** or **files a ticket** with enough context for a human to decide.
86
+
87
+ This is the opposite of silently force-pushing or auto-merging without policy.
88
+
89
+ ### When to escalate automatically
90
+
91
+ Typical conditions:
92
+
93
+ - **Divergence:** `main` on GitHub and GitLab point to different commits and neither is an ancestor of the other (merge-base exists but both tips have unique commits).
94
+ - **Non-fast-forward push rejected:** Mirror push fails with non-FF; do not retry with force.
95
+ - **Repeated failures:** Same branch fails N times in a row (may indicate permission, quota, or hook problems).
96
+ - **Unexpected ref state:** Missing branch, empty repo, or OID mismatch after push (verify step fails).
97
+
98
+ ### What “automatic escalation” can do (concrete patterns)
99
+
100
+ Implement one or more of the following from the same job that detected the problem:
101
+
102
+ 1. **Fail the pipeline** with a clear title and non-zero exit code so the default branch protection and dashboards show breakage.
103
+ 2. **Post a structured comment** on a pinned tracking issue (GitHub Issues / GitLab issue) including: branch name, both OIDs, link to the failed pipeline, and a one-line `git` hint (`git merge-base`, `git log --left-right`) for the on-call engineer.
104
+ 3. **Send chat or email** via webhook (Slack incoming webhook, Microsoft Teams, email SMTP API) with the same summary fields.
105
+ 4. **Open a new issue** (if API token allows) with label `mirror-divergence` so work is tracked and deduplicated (search for open issues with the same branch before creating).
106
+ 5. **Set a repository flag** (for example GitHub Actions variable or GitLab CI variable via API) `MIRROR_HALTED=true` and have sync jobs check it at the start so you do not amplify divergence while humans fix it.
107
+
108
+ ### Content to include in escalation messages
109
+
110
+ - Repository identifiers (both URLs or slugs).
111
+ - Branch (or ref) name.
112
+ - **OID on side A** and **OID on side B** (full hashes avoid ambiguity).
113
+ - **Pipeline run URL** and job name.
114
+ - Short **runbook pointer** (internal doc link) for “how we pick canonical history” and “who may force-push if ever.”
115
+
116
+ ### After humans resolve divergence
117
+
118
+ Runbook should require:
119
+
120
+ 1. Agreement on which tip (or merged result) is correct.
121
+ 2. A single deliberate push to the **canonical** remote (or fast-forward merge on one side then sync).
122
+ 3. Clear `MIRROR_HALTED` reset and verification that both tips match before re-enabling bidirectional automation.
123
+
124
+ ## Implementation sketches (non-prescriptive)
125
+
126
+ ### GitHub Actions (push to GitLab)
127
+
128
+ - Trigger: `on: push` for selected branches.
129
+ - Steps: clone with depth sufficient for ancestry check (or full clone for simplicity), add GitLab remote with token in secret, `fetch` GitLab, compare OIDs / ancestry, `git push` only on fast-forward.
130
+ - On divergence or push failure: exit 1 and call a small script that opens an issue or posts to Slack.
131
+
132
+ ### GitLab push mirror to GitHub
133
+
134
+ - Configure in GitLab project settings; use a GitHub **fine-grained PAT** or deploy key with write access to the target repo.
135
+ - Complement with **branch protection** on GitHub and monitoring for mirror errors in GitLab.
136
+
137
+ ### Webhook-driven bidirectional
138
+
139
+ - Each side receives push webhooks; each handler runs the same **guarded** sync script (SHA short-circuit, bot skip, fast-forward only, escalate on divergence).
140
+ - **Do not** run two independent scripts that both force-push without shared policy.
141
+
142
+ ## Summary
143
+
144
+ - **Best default:** one canonical remote, **one-way** mirror with fast-forward-only pushes and OID short-circuit.
145
+ - **Bidirectional** is possible with **layered guards** (ancestor checks, bot identity, concurrency, no force).
146
+ - **Escalate automatically** on divergence and non-FF: fail visibly, notify, optionally open issues and halt further sync until humans reconcile.
147
+
148
+ ## References
149
+
150
+ - [GitLab: Repository mirroring](https://docs.gitlab.com/ee/user/project/repository/mirror/)
151
+ - [GitHub Actions: Workflow triggers](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows)
152
+ - [GitLab: Deploy keys](https://docs.gitlab.com/ee/user/project/deploy_keys/)
153
+ - [GitLab: Project access tokens](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html)
154
+ - [GitHub: Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Telegram Mini App key retrieval checker (paste into DevTools console).
3
+ *
4
+ * Purpose:
5
+ * - Inspect where wallet keys are currently stored after app flow runs.
6
+ * - Check these keys across available storages:
7
+ * 1) SecureStorage: wallet_master_key
8
+ * 2) DeviceStorage: wallet_master_key
9
+ * 3) CloudStorage: wallet_seed_cipher
10
+ *
11
+ * What it prints:
12
+ * - Per-storage availability (API object + method presence).
13
+ * - Read result for each key (err/value present/length).
14
+ * - Final quick verdict about expected Desktop fallback model:
15
+ * - secureOnly: master key in SecureStorage
16
+ * - deviceFallback: master key in DeviceStorage
17
+ * - none: master key not found in either local store
18
+ *
19
+ * Notes:
20
+ * - This script does not print full secret values; it shows only existence/length.
21
+ * - In some Telegram Desktop builds, SecureStorage methods exist but return UNSUPPORTED.
22
+ */
23
+ (async () => {
24
+ const wa = window.Telegram?.WebApp;
25
+ if (!wa) {
26
+ const out = { ok: false, error: "Telegram.WebApp not found" };
27
+ console.log("[keys-retrieval]", out);
28
+ return out;
29
+ }
30
+
31
+ const readSecure = (key) =>
32
+ new Promise((resolve) => {
33
+ const storage = wa.SecureStorage;
34
+ if (!storage || typeof storage.getItem !== "function") {
35
+ resolve({ present: false, err: "NO_API", value: null, canRestore: null });
36
+ return;
37
+ }
38
+ storage.getItem(key, (err, value, canRestore) => {
39
+ resolve({
40
+ present: true,
41
+ err: err ?? null,
42
+ value: value ?? null,
43
+ canRestore: canRestore ?? null,
44
+ });
45
+ });
46
+ });
47
+
48
+ const readDevice = (key) =>
49
+ new Promise((resolve) => {
50
+ const storage = wa.DeviceStorage;
51
+ if (!storage || typeof storage.getItem !== "function") {
52
+ resolve({ present: false, err: "NO_API", value: null });
53
+ return;
54
+ }
55
+ storage.getItem(key, (err, value) => {
56
+ resolve({
57
+ present: true,
58
+ err: err ?? null,
59
+ value: value ?? null,
60
+ });
61
+ });
62
+ });
63
+
64
+ const readCloud = (key) =>
65
+ new Promise((resolve) => {
66
+ const storage = wa.CloudStorage;
67
+ if (!storage || typeof storage.getItem !== "function") {
68
+ resolve({ present: false, err: "NO_API", value: null });
69
+ return;
70
+ }
71
+ storage.getItem(key, (err, value) => {
72
+ resolve({
73
+ present: true,
74
+ err: err ?? null,
75
+ value: value ?? null,
76
+ });
77
+ });
78
+ });
79
+
80
+ const [secureMaster, deviceMaster, cloudSeedCipher] = await Promise.all([
81
+ readSecure("wallet_master_key"),
82
+ readDevice("wallet_master_key"),
83
+ readCloud("wallet_seed_cipher"),
84
+ ]);
85
+
86
+ const hasSecureMaster = secureMaster.value != null && secureMaster.err == null;
87
+ const hasDeviceMaster = deviceMaster.value != null && deviceMaster.err == null;
88
+ const hasCloudSeedCipher = cloudSeedCipher.value != null && cloudSeedCipher.err == null;
89
+
90
+ const localTier = hasSecureMaster ? "secureOnly" : hasDeviceMaster ? "deviceFallback" : "none";
91
+
92
+ const result = {
93
+ ok: true,
94
+ webApp: {
95
+ platform: wa.platform ?? null,
96
+ version: wa.version ?? null,
97
+ },
98
+ keys: {
99
+ wallet_master_key: {
100
+ secureStorage: {
101
+ available: secureMaster.present,
102
+ err: secureMaster.err,
103
+ exists: hasSecureMaster,
104
+ valueLength: typeof secureMaster.value === "string" ? secureMaster.value.length : 0,
105
+ canRestore: secureMaster.canRestore ?? null,
106
+ },
107
+ deviceStorage: {
108
+ available: deviceMaster.present,
109
+ err: deviceMaster.err,
110
+ exists: hasDeviceMaster,
111
+ valueLength: typeof deviceMaster.value === "string" ? deviceMaster.value.length : 0,
112
+ },
113
+ },
114
+ wallet_seed_cipher: {
115
+ cloudStorage: {
116
+ available: cloudSeedCipher.present,
117
+ err: cloudSeedCipher.err,
118
+ exists: hasCloudSeedCipher,
119
+ valueLength: typeof cloudSeedCipher.value === "string" ? cloudSeedCipher.value.length : 0,
120
+ },
121
+ },
122
+ },
123
+ verdict: {
124
+ localTier,
125
+ modelOkForDesktopFallback: localTier === "deviceFallback" && hasCloudSeedCipher,
126
+ },
127
+ };
128
+
129
+ console.log("[keys-retrieval]", result);
130
+ return result;
131
+ })();
@@ -0,0 +1,46 @@
1
+ # Milestone snapshot package (npm)
2
+
3
+ This repository includes a publishable snapshot package for fast developer bootstrap:
4
+
5
+ - package source: repository root (published directly)
6
+ - **npmjs (public):** `@www.hyperlinks.space/program-kit` — manage org and tokens: [www.hyperlinks.space on npm](https://www.npmjs.com/settings/www.hyperlinks.space/packages)
7
+ - **GitHub Packages:** `@hyperlinksspace/program-kit` (same version; GitHub requires the package scope to match this repo's owner)
8
+
9
+ ## Verify publish payload locally
10
+
11
+ The npm package page uses `README.md` from the published tarball, not `npmReadMe.md`. The published package also includes **`fullREADME.md`**, a copy of the developer readme (saved before the npm readme replaces `README.md`). Match CI before `npm pack`, then restore:
12
+
13
+ ```bash
14
+ cp README.md fullREADME.md
15
+ cp npmReadMe.md README.md
16
+ npm pack --dry-run
17
+ git checkout -- README.md
18
+ rm -f fullREADME.md
19
+ ```
20
+
21
+ ## Install snapshot as a developer
22
+
23
+ ```bash
24
+ npx @www.hyperlinks.space/program-kit ./my-hsp-app
25
+ ```
26
+
27
+ The CLI materializes the bundled package payload into your target folder, then you run:
28
+
29
+ ```bash
30
+ cd my-hsp-app
31
+ npm install
32
+ ```
33
+
34
+ ## Release channels
35
+
36
+ - `latest`: immutable stable snapshots (tag workflow `snapshot-vX.Y.Z`)
37
+ - `next`: rolling snapshots from manual workflow dispatch
38
+
39
+ In the output, you'll find options to open the app in:
40
+
41
+ - [a development build](https://docs.expo.dev/develop/development-builds/introduction/)
42
+ - [an Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
43
+ - [an iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
44
+ - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
45
+
46
+ You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
@@ -0,0 +1,201 @@
1
+ # GitHub Releases Automation Plan (Final)
2
+
3
+ This document defines the production plan for release detection, deduped GitHub Release publishing, and near-instant app update signaling.
4
+
5
+ ## Goals
6
+
7
+ - Build the Windows installer on GitHub Actions (`windows-latest`) with `npm run build:win` (no `.exe` committed to git).
8
+ - Publish the installer to a GitHub Release only when that release tag does not already exist.
9
+ - Notify an app-update service immediately after a new release is published.
10
+ - Keep app-side update detection near-instant using push notification plus fallback polling.
11
+
12
+ ## Source of Truth
13
+
14
+ - The **GitHub Release tag** is the release identity (`release_id`), for example `build_03252026_1929`.
15
+ - Locally, `windows/cleanup.cjs` moves the built installer to `releases/builder/build_MMDDYYYY_HHMM/HyperlinksSpaceProgramInstaller_<stamp>.exe` (other artifacts under `dev/`). In CI you can set `RELEASE_BUILD_ID` to match a chosen tag, or leave it unset so the folder name comes from build time.
16
+
17
+ ## Workflow Trigger
18
+
19
+ - **Manual only:** `workflow_dispatch` (Actions → “Windows release (CI build)”).
20
+ - Optional input **release_id** (`build_MMDDYYYY_HHMM`). If empty, the tag is taken from the build output folder after `cleanup.cjs` runs.
21
+
22
+ Example trigger:
23
+
24
+ ```yaml
25
+ on:
26
+ workflow_dispatch:
27
+ inputs:
28
+ release_id:
29
+ description: "Optional tag, e.g. build_03252026_1929"
30
+ required: false
31
+ default: ""
32
+ ```
33
+
34
+ ## Dedupe Rules (No Duplicate Releases)
35
+
36
+ 1. Run `npm run build:win` (or use optional `RELEASE_BUILD_ID` so the output folder matches the intended tag).
37
+ 2. Resolve `release_id` from the optional input or from `releases/builder/build_*/HyperlinksSpaceProgramInstaller_*.exe`.
38
+ 3. Check whether GitHub Release/tag already exists for that `release_id`.
39
+ 4. If it exists:
40
+ - Exit successfully (`0`)
41
+ - Do not upload assets
42
+ - Do not send webhook notification
43
+ 5. If it does not exist:
44
+ - Create GitHub Release/tag
45
+ - Upload all files from that folder as release assets
46
+ - Continue to webhook notification
47
+
48
+ Recommended extra safety:
49
+
50
+ - Use workflow `concurrency` keyed by `release_id` to avoid race conditions.
51
+
52
+ ## Endpoint Contract
53
+
54
+ - Webhook endpoint path: `api/releases.ts`
55
+ - Route URL: `POST /api/releases`
56
+ - Purpose: accept release-published events from GitHub Actions and fan out update notifications to app clients.
57
+
58
+ ## Security
59
+
60
+ - GitHub Actions sends auth header:
61
+ - `x-release-token: <secret>`
62
+ - Endpoint validates against:
63
+ - `process.env.RELEASE_WEBHOOK_TOKEN`
64
+ - Optional hardening:
65
+ - Add HMAC signature verification for request body.
66
+
67
+ If auth fails:
68
+
69
+ - Return `401` and do not process payload.
70
+
71
+ ## Payload Shape
72
+
73
+ `POST /api/releases` expects JSON:
74
+
75
+ ```json
76
+ {
77
+ "release_id": "build_03252026_1929",
78
+ "version": "1.0.0",
79
+ "published_at": "2026-03-25T19:29:00Z",
80
+ "platform": "windows",
81
+ "assets": [
82
+ {
83
+ "name": "HyperlinksSpaceProgramInstaller.exe",
84
+ "url": "https://github.com/<org>/<repo>/releases/download/build_03252026_1929/HyperlinksSpaceProgramInstaller.exe",
85
+ "sha256": "<optional>"
86
+ }
87
+ ],
88
+ "github_release_url": "https://github.com/<org>/<repo>/releases/tag/build_03252026_1929"
89
+ }
90
+ ```
91
+
92
+ ## Endpoint Behavior (`api/releases.ts`)
93
+
94
+ 1. Accept only `POST`.
95
+ 2. Validate auth token/signature.
96
+ 3. Parse and validate required fields:
97
+ - `release_id`, `published_at`, `assets`.
98
+ 4. Enforce idempotency by `release_id`:
99
+ - If already processed, return `200 { ok: true, duplicate: true }`.
100
+ 5. Store/update latest release metadata in persistent storage.
101
+ 6. Push update signal to clients (WebSocket/SSE/Firebase/Expo push).
102
+ 7. Return quickly with `200 { ok: true }`.
103
+
104
+ ## GitHub Actions Notification Step
105
+
106
+ After successful release creation and asset upload:
107
+
108
+ 1. Read webhook URL and token from repository secrets:
109
+ - `RELEASE_WEBHOOK_URL`
110
+ - `RELEASE_WEBHOOK_TOKEN`
111
+ 2. Send `POST` to `/api/releases` with release payload.
112
+ 3. Retry webhook call with backoff on transient failures.
113
+
114
+ Important:
115
+
116
+ - Do not send webhook if release already existed (dedupe branch).
117
+
118
+ ## When You Must Make a New Installer Release
119
+
120
+ Use this section as the decision rule between OTA update and installer release.
121
+
122
+ ### Native/runtime changes (installer required)
123
+
124
+ Create a new installer release when a change affects native binaries or runtime compatibility, including:
125
+
126
+ - Installing, removing, or updating any package that forces you to rebuild the app.
127
+ - Changing app permissions or native capabilities (camera, notifications, background modes, deep links, etc.).
128
+ - Changing package identifiers, signing, entitlements, or other platform build settings.
129
+ - Changing Expo SDK or React Native version in a way that changes native runtime.
130
+ - Changing `runtimeVersion` policy/behavior or bumping app version when runtime compatibility changes.
131
+ - Any change that requires running a fresh native build to take effect.
132
+
133
+ ### Non-native changes (OTA only, no installer)
134
+
135
+ Do not make a new installer release for:
136
+
137
+ - JavaScript/TypeScript business logic changes.
138
+ - UI/layout/style changes.
139
+ - Text/copy changes and static asset updates that are OTA-compatible.
140
+ - Server/API behavior changes that do not require new native modules in app.
141
+
142
+ ### Exact release checklist
143
+
144
+ Make a new installer release if at least one statement is true:
145
+
146
+ 1. "This change cannot work without rebuilding native binaries."
147
+ 2. "This change modifies runtime compatibility between app binary and updates."
148
+ 3. "This change touches native permissions/capabilities/config."
149
+
150
+ If all three are false, ship via OTA update instead of installer release.
151
+
152
+ ### Team policy
153
+
154
+ - Prefer OTA by default for speed.
155
+ - Use installer releases only for native/runtime boundaries.
156
+ - When uncertain, treat as installer-required and verify in staging before production.
157
+
158
+ ## App Update Strategy
159
+
160
+ Primary strategy:
161
+
162
+ - Real-time push signal from backend triggered by `/api/releases`.
163
+
164
+ Fallback strategy:
165
+
166
+ - App checks latest release on:
167
+ - app foreground/resume
168
+ - periodic interval (for example every 5-15 minutes)
169
+
170
+ Client behavior:
171
+
172
+ 1. Compare local build/version with latest server metadata.
173
+ 2. If newer exists:
174
+ - Show "Update available" prompt or trigger controlled update flow.
175
+
176
+ ## Reliability and Observability
177
+
178
+ - Idempotent handling on `release_id`.
179
+ - Structured logs for each stage:
180
+ - detected -> published/skipped -> webhook sent -> app signal broadcast
181
+ - Alert on partial failure:
182
+ - release created but webhook failed
183
+ - Keep webhook processing fast and non-blocking for external calls.
184
+
185
+ ## Rollout Plan
186
+
187
+ 1. Deploy `api/releases.ts` in staging.
188
+ 2. Run workflow in dry-run mode to validate folder parsing and dedupe checks.
189
+ 3. Enable real release creation for test folder.
190
+ 4. Enable webhook call to staging endpoint.
191
+ 5. Verify end-to-end with one client device.
192
+ 6. Promote to production after stable validation.
193
+
194
+ ## Acceptance Criteria
195
+
196
+ - A new `releases/builder/build_.../` folder produces exactly one GitHub Release.
197
+ - Re-running workflow for the same `release_id` does not create duplicates.
198
+ - Webhook is called only for newly created releases.
199
+ - `POST /api/releases` rejects unauthorized requests.
200
+ - Connected app receives update signal near-instantly in normal conditions.
201
+ - Fallback polling still discovers updates if push delivery fails.