create-feedbax 0.2.1 → 0.2.2

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 (78) hide show
  1. package/README.md +17 -5
  2. package/dist/cli.js +502 -209
  3. package/dist/index.js +251 -68
  4. package/package.json +7 -7
  5. package/template/.feedbax-template.sha256 +1 -1
  6. package/template/.gitignore.template +1 -0
  7. package/template/.npmignore.template +1 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/AGENTS.md +1 -1
  10. package/template/CONNECTED.md +16 -2
  11. package/template/README.md +52 -13
  12. package/template/feedbax-vite.ts +79 -0
  13. package/template/migrations/0005_github.sql +4 -0
  14. package/template/migrations/0006_integration.sql +3 -0
  15. package/template/migrations/0007_ownership.sql +2 -0
  16. package/template/migrations/0008_integration_recovery.sql +5 -0
  17. package/template/migrations/0009_observed_history.sql +3 -0
  18. package/template/package.json +34 -33
  19. package/template/src/changelog-runtime.ts +2 -1
  20. package/template/src/cloudflare-workers.d.ts +3 -0
  21. package/template/src/comment-creation.test.tsx +53 -6
  22. package/template/src/components/connected-identity-provider.tsx +44 -17
  23. package/template/src/components/follow-post-button.tsx +2 -13
  24. package/template/src/components/post-history.tsx +101 -0
  25. package/template/src/components/profile-menu.tsx +7 -0
  26. package/template/src/configured-feedback.test.ts +82 -0
  27. package/template/src/configured-feedback.ts +61 -0
  28. package/template/src/connected-identity.test.ts +7 -3
  29. package/template/src/connected-identity.ts +6 -1
  30. package/template/src/connected-runtime.test.ts +5 -2
  31. package/template/src/feedback-cache-invalidation.ts +2 -0
  32. package/template/src/feedback-runtime.ts +61 -18
  33. package/template/src/feedbax/changelog/index.test.ts +35 -0
  34. package/template/src/feedbax/changelog/index.ts +5 -1
  35. package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
  36. package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
  37. package/template/src/feedbax/config/index.test.ts +45 -1
  38. package/template/src/feedbax/config/index.ts +112 -9
  39. package/template/src/feedbax/feedback/comment-storage.ts +38 -0
  40. package/template/src/feedbax/feedback/connected-feedback.ts +41 -12
  41. package/template/src/feedbax/feedback/connected.ts +15 -5
  42. package/template/src/feedbax/feedback/feedback-storage.ts +1 -0
  43. package/template/src/feedbax/feedback/github-comments.ts +390 -0
  44. package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
  45. package/template/src/feedbax/feedback/github-feedback.ts +581 -0
  46. package/template/src/feedbax/feedback/github-request.ts +120 -0
  47. package/template/src/feedbax/feedback/github-service-client.ts +56 -0
  48. package/template/src/feedbax/feedback/image-feedback.ts +23 -10
  49. package/template/src/feedbax/feedback/images.ts +26 -0
  50. package/template/src/feedbax/feedback/index.ts +172 -16
  51. package/template/src/feedbax/feedback/integration-auth.ts +138 -0
  52. package/template/src/feedbax/feedback/notion-comments.ts +2 -35
  53. package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
  54. package/template/src/feedbax/feedback/observed-history.ts +219 -0
  55. package/template/src/feedbax/feedback/ownership.ts +170 -0
  56. package/template/src/history-runtime.test.ts +80 -0
  57. package/template/src/history-runtime.ts +37 -0
  58. package/template/src/index.css +6 -0
  59. package/template/src/integration-runtime.test.ts +365 -0
  60. package/template/src/integration-runtime.ts +289 -0
  61. package/template/src/my-feedback-page.test.tsx +152 -0
  62. package/template/src/my-feedback-page.tsx +398 -0
  63. package/template/src/my-feedback-runtime.test.ts +93 -0
  64. package/template/src/my-feedback-runtime.ts +154 -0
  65. package/template/src/participation-runtime.ts +3 -3
  66. package/template/src/portal-comments.ts +11 -0
  67. package/template/src/portal-feedback-form.tsx +11 -3
  68. package/template/src/portal-feedback-submission.ts +9 -0
  69. package/template/src/portal-server-entry.ts +9 -0
  70. package/template/src/public-portal-error.tsx +1 -1
  71. package/template/src/public-post-detail.tsx +332 -256
  72. package/template/src/public-release-links.test.ts +120 -0
  73. package/template/src/public-release-links.ts +130 -0
  74. package/template/src/routeTree.gen.ts +21 -0
  75. package/template/src/routes/my-feedback.tsx +16 -0
  76. package/template/src/server.ts +46 -1
  77. package/template/src/submission-intent.ts +26 -0
  78. package/template/vite.config.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-feedbax",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-feedbax": "./dist/cli.js"
@@ -10,17 +10,17 @@
10
10
  },
11
11
  "scripts": {
12
12
  "check-types": "tsc --noEmit",
13
- "test": "vitest run src/index.test.ts src/template.test.ts",
13
+ "test": "vitest run src",
14
14
  "template:sync": "tsx ../../scripts/assemble-template.ts",
15
15
  "template:check": "tsx ../../scripts/assemble-template.ts --check",
16
16
  "build": "esbuild src/cli.ts src/index.ts --bundle --platform=node --format=esm --alias:@feedbax/config=../config/src/index.ts --alias:@feedbax/feedback=../feedback/src/index.ts --alias:@feedbax/changelog=../changelog/src/index.ts --outdir=dist --external:jsonc-parser --external:@clack/prompts",
17
17
  "prepack": "pnpm build"
18
18
  },
19
19
  "devDependencies": {
20
- "esbuild": "0.28.1",
21
- "tsx": "4.20.6",
20
+ "esbuild": "0.28.2",
21
+ "tsx": "4.23.15",
22
22
  "typescript": "catalog:",
23
- "vitest": "4.1.11"
23
+ "vitest": "5.0.4"
24
24
  },
25
25
  "files": [
26
26
  "dist",
@@ -30,7 +30,7 @@
30
30
  "@clack/prompts": "^1.8.1",
31
31
  "jsonc-parser": "3.3.1"
32
32
  },
33
- "description": "Create a self-hosted Notion-native feedback portal",
33
+ "description": "Create a self-hosted feedback portal with Notion or GitHub",
34
34
  "license": "Apache-2.0",
35
35
  "repository": {
36
36
  "type": "git",
@@ -42,7 +42,7 @@
42
42
  "url": "https://github.com/jessequartey/feedbax/issues"
43
43
  },
44
44
  "engines": {
45
- "node": ">=22.14.0"
45
+ "node": "^24.15.0 || >=26.0.0"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
@@ -1 +1 @@
1
- 9c41cfc9758db8b7b6c022fe973f24e7a94244c96c13b6679010702d3d39a054
1
+ 38ac71db749b85b9900fd636aa95867390517bc9695bc11534a44892e26810b2
@@ -1,5 +1,6 @@
1
1
  .dev.vars
2
2
  .env*
3
+ .feedbax/
3
4
  dist/
4
5
  .wrangler/
5
6
  *.log
@@ -1,5 +1,6 @@
1
1
  .dev.vars
2
2
  .env*
3
+ .feedbax/
3
4
  dist/
4
5
  .wrangler/
5
6
  *.log
@@ -0,0 +1 @@
1
+ 24.22.0
@@ -6,7 +6,7 @@ This is a flat application, not the Feedbax development monorepo. Product settin
6
6
 
7
7
  Use the copied shadcn primitives in `src/components/ui`. Keep the configured Geist font and theme tokens. After changes, run `pnpm lint`, `pnpm type-check`, `pnpm test`, and `pnpm build`, fixing errors. Use `pnpm format:check` to check formatting.
8
8
 
9
- For setup, `pnpm exec feedbax doctor` must pass with real Notion credentials/mappings and enabled bindings. Run `pnpm deploy:dry-run` before deployment and verify the actual live URL after `pnpm exec feedbax deploy`. Distinguish copied source, configured local readiness, and a verified deployment in the result.
9
+ For setup, `pnpm exec feedbax doctor` must pass with the selected provider’s real credentials/mappings and enabled bindings. For GitHub provisioning, pairing, native publication, public-repository exposure, or provider secrets, read the GitHub setup and publication section in README.md. GitHub requires dedicated D1 independently of Connected identity and disables Changelog. Bootstrap deployment remains unpaired until the GitHub authorization flow completes. Run `pnpm deploy:dry-run` before deployment and verify the actual live URL after `pnpm exec feedbax deploy`. Distinguish copied source, configured local readiness, and a verified deployment in the result.
10
10
 
11
11
  ## Connected social features
12
12
 
@@ -1,6 +1,6 @@
1
- # Connected Profile in 0.2.1
1
+ # Connected Profile
2
2
 
3
- Connected Profile is optional. Omit `connected` to retain Notion-only behavior. A Connected Installation requires a dedicated D1 database and an existing Product Team application that authenticates Participants on its server. There is no Feedbax email sign-in. Small processing also uses a dedicated Cloudflare Queue for background work.
3
+ Connected Profile is optional. Omit `connected` to retain the selected provider’s unsigned participation behavior. Published 0.2.1 uses Notion; 0.2.2 also supports GitHub. A Connected Installation requires a dedicated D1 database (reuse the Installation’s required provider D1 for GitHub) and an existing Product Team application that authenticates Participants on its server. There is no Feedbax email sign-in. Small processing also uses a dedicated Cloudflare Queue for background work.
4
4
 
5
5
  ## Configure an Installation
6
6
 
@@ -402,3 +402,17 @@ In a dedicated Installation, apply all D1 migrations, configure the timer and pr
402
402
  5. Reject a forged review link, wrong-owner image association, oversized/unsupported image, unsigned mutation, and cross-origin mutation. Check mobile previews, unread states, empty/error/retry states, and cleanup of abandoned uploads.
403
403
 
404
404
  These checks require your real application, sender, and infrastructure. Local tests and builds do not constitute production delivery or release evidence.
405
+
406
+ ## GitHub and My feedback in 0.2.2
407
+
408
+ Provider setup and Participant authentication are separate. Complete the GitHub App service pairing and use the same Installation D1 database for Connected; GitHub needs no Notion token and disables Changelog. The existing issuer/subject trust pair, signed handoff, consent and session revocation rules remain unchanged. GitHub Participants need no GitHub account. Follow the GitHub setup guide before adding Connected bindings and secrets.
409
+
410
+ My feedback provides private Submitted, Supported and Following views with bounded pagination. New submissions from a server-verified portal session gain ownership independently of their Device Profile. The owner can view eligible pending context and edit or withdraw a New, unpublished draft from another verified device. Publication or advancement beyond New ends edit authority.
411
+
412
+ An older unsigned draft is not automatically owned. While its lifecycle remains eligible, sign in on the browser that still holds its capability, open My feedback, and explicitly link the draft. Both proofs are required. Matching names/email, browser storage alone without verified identity, and public URLs do not establish ownership. Existing ownership cannot be transferred by another Participant's claim.
413
+
414
+ Private owner responses use an explicit projection and `no-store`; no provider handles, contacts, capability hashes or identity subjects are returned. The browser resets private lists and editors before a session refresh, using an opaque session key rather than a display name to separate cached views. Sign-out revokes the session and reloads the page.
415
+
416
+ Apply every included D1 migration before enabling the new views, including `0007_ownership.sql`; GitHub also needs the integration/recovery migrations. These additions do not create ownership for historical Posts or send historical Notifications. Existing 0.2.1 Comment editing limits remain; the new cross-device authority concerns eligible owned Post drafts, not arbitrary provider moderation.
417
+
418
+ GitHub Comments use explicit App-created public records and private attribution mappings. Private image access still follows current publication and Comment visibility. GitHub issues contain no private review links; native Notion's private page-body review links remain specific to Notion. The selected Content Provider stays authoritative for feedback content and publication, and D1 stays authoritative for verified participation.
@@ -1,32 +1,32 @@
1
1
  # Your Feedbax portal
2
2
 
3
- This is an application you own. Product Teams manage Posts, statuses, Comments, and release notes in Notion; the portal runs on Cloudflare Workers.
3
+ This is an application you own. Product Teams manage Posts, statuses, Comments, and release notes in the selected Notion or GitHub Content Provider; the portal runs on Cloudflare Workers.
4
4
 
5
5
  [Feedbax on GitHub](https://github.com/jessequartey/feedbax) · [Setup and operations](https://feedbax-docs.jessefquartey.workers.dev/docs)
6
6
 
7
7
  ## Start locally
8
8
 
9
- Use Node.js 22.14+ and pnpm 10. Guided setup already installs dependencies and configures the app:
9
+ Use Node.js 24.15+ (Node 24 LTS) and pnpm 10. The included `.nvmrc` pins the tested version; with nvm installed, run `nvm install` and `nvm use` before working on the Installation. Guided setup already installs dependencies and configures the app:
10
10
 
11
11
  ```sh
12
12
  pnpm dev
13
13
  ```
14
14
 
15
- Open the local address printed by Vite. If this directory was created with `--template-only`, run `pnpm install` and finish configuration first; copied source is not yet connected to Notion.
15
+ Open the local address printed by Vite. If this directory was created with `--template-only`, run `pnpm install` and finish configuration first; copied source still requires provider configuration.
16
16
 
17
17
  ## Configuration
18
18
 
19
- | File | Purpose |
20
- | ------------------- | -------------------------------------------------------------------------------------------------------------- |
21
- | `src/feedbax.ts` | Public product settings, feature switches, Notion IDs/property mappings, and optional Connected settings |
22
- | `.dev.vars` | Ignored local Notion token, API-key hash, participation signing secret, and enabled Turnstile/identity secrets |
23
- | `wrangler.jsonc` | Worker name, hostnames, rate-limit bindings, and optional dedicated D1 binding |
24
- | `src/components/ui` | Your copied shadcn primitives, utilities, and theme |
25
- | `CONNECTED.md` | Complete existing-application authentication setup and verification |
19
+ | File | Purpose |
20
+ | ------------------- | -------------------------------------------------------------------------------------------------------- |
21
+ | `src/feedbax.ts` | Product settings, feature switches, selected provider mapping, and optional Connected settings |
22
+ | `.dev.vars` | Ignored local provider/pairing credentials, API-key hash, and enabled signing/Turnstile/identity secrets |
23
+ | `wrangler.jsonc` | Worker name, hostnames, rate-limit bindings, and optional dedicated D1 binding |
24
+ | `src/components/ui` | Your copied shadcn primitives, utilities, and theme |
25
+ | `CONNECTED.md` | Complete existing-application authentication setup and verification |
26
26
 
27
27
  The plaintext trusted API key printed during setup belongs on your product server. The portal stores its hash. Never expose either Notion credentials or signing secrets through browser configuration. Existing `src/feedbax.test-fixture.ts` is for tests only.
28
28
 
29
- For template-only output, follow the [agent setup guide](https://github.com/jessequartey/feedbax/blob/main/docs/agent-setup.md) and supply real Notion mappings and secrets before diagnostics. For Connected, follow `CONNECTED.md`, implement the signed application endpoint, bind a dedicated D1 database, and apply the included migration.
29
+ For template-only output, follow the [agent setup guide](https://github.com/jessequartey/feedbax/blob/main/docs/agent-setup.md) and supply real provider mappings and secrets before diagnostics. For Connected, follow `CONNECTED.md`, implement the signed application endpoint, bind a dedicated D1 database, and apply the included migration.
30
30
 
31
31
  ## Check and deploy
32
32
 
@@ -47,12 +47,51 @@ pnpm exec feedbax deploy
47
47
 
48
48
  Keep the included Geist font and shadcn theme tokens consistent. Run `pnpm lint` after UI changes; it checks theme colors, unknown Tailwind classes, and inspectable component classes. Update individual primitives instead of reapplying a full preset.
49
49
 
50
- Use the [feedback block guide](https://github.com/jessequartey/feedbax/blob/main/apps/docs/content/docs/integrations/feedback-block.mdx) to add feedback collection to your product. Its server helper uses a trusted submission key and new Posts remain unpublished until your Product Team moderates them in Notion. The portal’s standard footer links to Feedbax on GitHub.
50
+ Use the [feedback block guide](https://github.com/jessequartey/feedbax/blob/main/apps/docs/content/docs/integrations/feedback-block.mdx) to add feedback collection to your product. Its server helper uses a trusted submission key and new Posts remain unpublished until your Product Team publishes them through its selected provider. The portal’s standard footer links to Feedbax on GitHub.
51
51
 
52
- Keep Comments and browser capabilities distinct from verified identity: Notion-only names/email are unverified, Votes are best-effort, and recent Comment editing uses short-lived browser capabilities. Connected adds signed identity, unique Votes, and durable attribution without moving content out of Notion.
52
+ Keep Device Profiles and browser capabilities distinct from verified identity: names/email supplied by the browser are unverified, Votes without Connected are best-effort, and recent Comment editing uses short-lived browser capabilities. Connected adds signed identity, unique Votes, and durable attribution while keeping content in the selected provider.
53
53
 
54
54
  See `AGENTS.md` for the short agent workflow.
55
55
 
56
56
  ## Connected social features
57
57
 
58
58
  For following, in-app/email notifications, or image attachments, use the full Connected guide (CONNECTED.md in a generated app; docs/connected-profile.md in the source repository). Apply every included D1 migration, including the Follow baseline migration, configure the timer and dedicated processing queue for small mode, choose Cloudflare/Resend/custom email when required, and keep the R2 bucket private for images. Never use a Device Profile as verified email evidence. Run doctor and behavior checks; explicitly distinguish local tests from real sender delivery and production authentication acceptance.
59
+
60
+ ## GitHub setup and publication
61
+
62
+ GitHub requires a dedicated D1 database even without Participant sign-in. It uses no Notion token, and Changelog must be disabled. A GitHub App service holds the App key; this portal stores only its own directional binding credentials. Setup must use the public-fetch compatibility flag for Worker-to-Worker calls, or an explicitly configured Service Binding. Never copy an App private key into the portal.
63
+
64
+ Apply every included D1 migration before deploying updated source:
65
+
66
+ ```sh
67
+ pnpm exec wrangler d1 migrations apply FEEDBAX_DB --local
68
+ pnpm exec wrangler d1 migrations apply FEEDBAX_DB --remote
69
+ pnpm exec feedbax deploy --pairing-bootstrap
70
+ pnpm exec feedbax pair-github GITHUB_APP_INSTALLATION_ID
71
+ ```
72
+
73
+ Complete the printed GitHub authorization flow, then run `pnpm exec feedbax doctor`. Bootstrap deployment intentionally leaves feedback unavailable until authorization finishes. Local development uses local D1; production pairing writes credentials only into remote D1. A production binding does not automatically authorize a local development origin. Use a separately configured development Installation/repository when testing GitHub writes locally.
74
+
75
+ Native GitHub workflow uses exactly one label: `feedbax:new`, `feedbax:reviewing`, `feedbax:planned`, `feedbax:in-progress`, `feedbax:shipped`, or `feedbax:closed`. Add `feedbax:published` independently to display a Post in the portal. Issue closure alone does not change its Feedbax status. Missing/conflicting status labels block draft changes until repaired.
76
+
77
+ The issue body contains a versioned JSON section between `<!-- feedbax:public:v1 -->` and `<!-- /feedbax:public -->`. Edit its title, description, type and optional Team Response/release-notes URL to change approved portal content. Keep its immutable slug. Notes outside this section and ordinary engineering comments are never portal content. Portal Comments are separately marked App-created records with private attribution mappings.
78
+
79
+ **A public repository exposes issue titles and bodies on GitHub immediately, including Posts awaiting portal moderation.** Use a private Feedback Repository for confidential intake. Portal moderation cannot make an existing public GitHub issue private.
80
+
81
+ Connected identity remains independent: only verified application sessions get cross-device ownership, My feedback, unique Votes and Follows. D1 alone does not establish identity. Browser drafts can be explicitly linked only while New/unpublished and while the browser still possesses their capability.
82
+
83
+ ## Integration checks
84
+
85
+ The [integration checklist](https://github.com/jessequartey/feedbax/blob/main/docs/integration-checklist.md) brings together provider setup, product-server intake, credential locations and authentication.
86
+
87
+ Post details show Comments by default, with observed progress under Activities when enabled. Follow controls appear after Connected sign-in.
88
+
89
+ For an observed public timeline, explicitly add root-level `history: true`, bind `FEEDBAX_DB`, apply all migrations including `0009_observed_history.sql`, and configure a timer. Reuse this Installation's existing GitHub/Connected D1 when present. The first public Post read establishes a baseline; later checks record public status and Team Response changes without backfilling private history. Follow the [history guide](https://github.com/jessequartey/feedbax/blob/main/apps/docs/content/docs/guides/progress-history.mdx). Omit the switch to preserve an existing Notion Installation's storage footprint.
90
+
91
+ Before connecting your product, test a disposable submission from its server, retry with the same `Idempotency-Key`, and confirm one New, unpublished Post. Publish through the selected provider and check that only approved fields appear. Remove publication and verify the Post, Comments and images become unavailable.
92
+
93
+ For Connected, verify two application accounts and two browsers: each account should see only its own My feedback and Notifications. Sign out, then check that private views clear. Test email and images only when enabled; a local build does not verify production sender delivery, identity handoff or private R2 access.
94
+
95
+ Keep the product server's trusted key private. A trusted API submission does not acquire verified ownership from a submitted name, email or subject. Use the signed Identity Handoff for Participant identity. Follow the [submission API guide](https://github.com/jessequartey/feedbax/blob/main/apps/docs/content/docs/integrations/submission-api.mdx) and `CONNECTED.md` for working examples.
96
+
97
+ For an existing customized 0.2.1 Installation, follow the [source upgrade guide](https://github.com/jessequartey/feedbax/blob/main/apps/docs/content/docs/guides/upgrading.mdx). The 0.2.2 CLI previews against the exact published baseline, preserves configuration, and supports source resume/rollback. Review private diffs and back up data separately; source recovery is not a complete archive/restore.
@@ -0,0 +1,79 @@
1
+ import { build } from "esbuild";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import type { Plugin, ResolvedConfig } from "vite";
5
+
6
+ // Only the server receives provider identifiers and operational configuration.
7
+ // The client gets a fixed allowlist, even when copied configuration is customized.
8
+ export function publicFeedbaxConfiguration(): Plugin {
9
+ let configuration: ResolvedConfig;
10
+ return {
11
+ name: "feedbax-public-configuration",
12
+ enforce: "pre",
13
+ configResolved(value) {
14
+ configuration = value;
15
+ },
16
+ async transform(code, id) {
17
+ const root = configuration.root;
18
+ if (
19
+ this.environment.name !== "client" ||
20
+ id.split("?")[0] !== resolve(root, "src/feedbax.ts")
21
+ )
22
+ return;
23
+ const localConfig = resolve(root, "src/feedbax/config/index.ts");
24
+ const result = await build({
25
+ stdin: {
26
+ contents: code,
27
+ loader: "ts",
28
+ resolveDir: resolve(root, "src"),
29
+ },
30
+ bundle: true,
31
+ write: false,
32
+ platform: "node",
33
+ format: "esm",
34
+ alias: {
35
+ "@feedbax/config": existsSync(localConfig)
36
+ ? localConfig
37
+ : resolve(root, "../../packages/config/src/index.ts"),
38
+ },
39
+ define: { "import.meta.env": JSON.stringify(configuration.env) },
40
+ });
41
+ const full = (
42
+ await import(
43
+ `data:text/javascript;base64,${Buffer.from(result.outputFiles[0]!.text).toString("base64")}`
44
+ )
45
+ ).default;
46
+ const publicConfiguration = {
47
+ product: full.product,
48
+ theme: full.theme,
49
+ features: full.features,
50
+ submissions: full.submissions,
51
+ turnstile: full.turnstile,
52
+ history: full.history,
53
+ ...(full.connected
54
+ ? {
55
+ connected: {
56
+ issuer: full.connected.issuer,
57
+ loginUrl: full.connected.loginUrl,
58
+ images: full.connected.images,
59
+ ...(full.connected.email
60
+ ? { email: { provider: full.connected.email.provider } }
61
+ : {}),
62
+ },
63
+ }
64
+ : {}),
65
+ };
66
+ const publicCode = `export default ${JSON.stringify(publicConfiguration)};`;
67
+ return {
68
+ code: publicCode,
69
+ map: {
70
+ version: 3,
71
+ sources: ["feedbax-public-configuration.ts"],
72
+ sourcesContent: [publicCode],
73
+ names: [],
74
+ mappings: "",
75
+ },
76
+ };
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,4 @@
1
+ CREATE TABLE IF NOT EXISTS github_posts (installation_id TEXT NOT NULL, repository_id INTEGER NOT NULL, post_id TEXT NOT NULL, slug TEXT NOT NULL, external_id TEXT, issue_number INTEGER, issue_id INTEGER, record TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('reserved','creating','ready','uncertain','failed','withdrawn')), last_status TEXT NOT NULL DEFAULT 'New', vote_count INTEGER NOT NULL DEFAULT 0, projection_hash TEXT, public_updated_at INTEGER, PRIMARY KEY(installation_id,post_id), UNIQUE(installation_id,slug), UNIQUE(installation_id,external_id), UNIQUE(installation_id,repository_id,issue_id));
2
+ CREATE INDEX IF NOT EXISTS github_posts_repository ON github_posts(installation_id,repository_id,state);
3
+ CREATE TABLE IF NOT EXISTS github_comments (installation_id TEXT NOT NULL, repository_id INTEGER NOT NULL, comment_id TEXT NOT NULL, post_id TEXT NOT NULL, discussion_id TEXT NOT NULL, remote_id INTEGER, record TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('creating','ready','uncertain','deleted')), PRIMARY KEY(installation_id,comment_id), UNIQUE(installation_id,repository_id,remote_id), FOREIGN KEY(installation_id,post_id) REFERENCES github_posts(installation_id,post_id));
4
+ CREATE INDEX IF NOT EXISTS github_comments_post ON github_comments(installation_id,repository_id,post_id,state);
@@ -0,0 +1,3 @@
1
+ CREATE TABLE IF NOT EXISTS integration_credentials (installation_id TEXT PRIMARY KEY, portal_secret TEXT NOT NULL, delivery_secret TEXT NOT NULL, revoked INTEGER NOT NULL DEFAULT 0, paired_at INTEGER NOT NULL);
2
+ CREATE TABLE IF NOT EXISTS portal_integration_nonces (installation_id TEXT NOT NULL, nonce TEXT NOT NULL, expires_at INTEGER NOT NULL, PRIMARY KEY(installation_id,nonce));
3
+ CREATE TABLE IF NOT EXISTS integration_receipts (installation_id TEXT NOT NULL, event_id TEXT NOT NULL, kind TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending', received_at INTEGER NOT NULL, cursor TEXT NOT NULL DEFAULT '', last_checked_at INTEGER, failure_code TEXT, PRIMARY KEY(installation_id,event_id));
@@ -0,0 +1,2 @@
1
+ CREATE TABLE IF NOT EXISTS post_ownership (post_id TEXT PRIMARY KEY, post_slug TEXT NOT NULL, subject TEXT NOT NULL, title TEXT NOT NULL, type TEXT NOT NULL, created_at INTEGER NOT NULL, withdrawn INTEGER NOT NULL DEFAULT 0);
2
+ CREATE INDEX IF NOT EXISTS owned_posts_subject ON post_ownership(subject,created_at,post_id);
@@ -0,0 +1,5 @@
1
+ ALTER TABLE integration_receipts ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0;
2
+ ALTER TABLE integration_receipts ADD COLUMN next_attempt INTEGER NOT NULL DEFAULT 0;
3
+ ALTER TABLE integration_receipts ADD COLUMN lease_token TEXT;
4
+ ALTER TABLE integration_receipts ADD COLUMN lease_until INTEGER NOT NULL DEFAULT 0;
5
+ CREATE INDEX integration_receipts_due ON integration_receipts(installation_id,state,next_attempt,lease_until);
@@ -0,0 +1,3 @@
1
+ CREATE TABLE IF NOT EXISTS public_post_observations (post_slug TEXT PRIMARY KEY, revision INTEGER NOT NULL DEFAULT 0, projection TEXT, checked_at INTEGER NOT NULL DEFAULT 0, lease_until INTEGER NOT NULL DEFAULT 0, lease_token TEXT);
2
+ CREATE TABLE IF NOT EXISTS public_post_history (id TEXT PRIMARY KEY, post_slug TEXT NOT NULL REFERENCES public_post_observations(post_slug), revision INTEGER NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('status','update')), status TEXT, message TEXT, observed_at INTEGER NOT NULL, UNIQUE(post_slug,revision,kind));
3
+ CREATE INDEX IF NOT EXISTS public_post_history_order ON public_post_history(post_slug,observed_at,revision,kind);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedbax-installation",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,17 +16,17 @@
16
16
  "dependencies": {
17
17
  "@tailwindcss/vite": "4.3.3",
18
18
  "@tanstack/react-form": "^1.33.5",
19
- "@tanstack/react-query": "5.101.4",
20
- "@tanstack/react-router": "1.170.31",
21
- "@tanstack/react-start": "1.168.48",
22
- "dotenv": "17.4.2",
23
- "lucide-react": "1.33.0",
19
+ "@tanstack/react-query": "5.104.3",
20
+ "@tanstack/react-router": "1.170.44",
21
+ "@tanstack/react-start": "1.169.2",
22
+ "dotenv": "18.0.8",
23
+ "lucide-react": "1.57.0",
24
24
  "next-themes": "0.4.6",
25
- "react": "19.2.8",
26
- "react-dom": "19.2.8",
25
+ "react": "19.3.1",
26
+ "react-dom": "19.3.1",
27
27
  "sonner": "2.0.8",
28
28
  "tailwindcss": "4.3.3",
29
- "zod": "4.4.3",
29
+ "zod": "4.6.7",
30
30
  "@base-ui/react": "1.9.0",
31
31
  "@fontsource-variable/geist": "5.3.0",
32
32
  "@shadcn/react": "0.3.1",
@@ -34,38 +34,39 @@
34
34
  "clsx": "2.1.1",
35
35
  "cmdk": "^1.1.1",
36
36
  "shadcn": "latest",
37
- "tailwind-merge": "3.6.0",
37
+ "tailwind-merge": "3.7.0",
38
38
  "tw-animate-css": "1.4.0",
39
- "drizzle-orm": "0.45.2"
39
+ "drizzle-orm": "0.45.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@cloudflare/vite-plugin": "1.53.0",
43
- "@cloudflare/workers-types": "^5.20261014.1",
44
- "@tanstack/react-router-devtools": "1.167.1",
45
- "@tanstack/router-cli": "1.167.32",
46
- "@testing-library/dom": "10.4.1",
47
- "@testing-library/react": "16.3.2",
48
- "@testing-library/user-event": "14.6.1",
49
- "@types/react": "19.2.18",
50
- "@types/react-dom": "19.2.4",
51
- "@vitejs/plugin-react": "6.1.0",
52
- "jsdom": "29.1.1",
42
+ "@cloudflare/vite-plugin": "1.64.0",
43
+ "@cloudflare/workers-types": "^5.20261015.1",
44
+ "@tanstack/react-router-devtools": "1.167.2",
45
+ "@tanstack/router-cli": "1.167.44",
46
+ "@testing-library/dom": "10.4.2",
47
+ "@testing-library/react": "16.3.3",
48
+ "@testing-library/user-event": "14.6.8",
49
+ "@types/react": "19.3.0",
50
+ "@types/react-dom": "19.3.0",
51
+ "@vitejs/plugin-react": "6.1.2",
52
+ "jsdom": "30.1.2",
53
53
  "miniflare": "4.20260730.0",
54
54
  "typescript": "6.0.3",
55
- "vite": "8.2.2",
56
- "web-vitals": "5.3.0",
57
- "wrangler": "4.124.0",
55
+ "vite": "8.3.4",
56
+ "web-vitals": "6.2.3",
57
+ "wrangler": "4.151.0",
58
+ "esbuild": "0.28.2",
58
59
  "@tailwindcss/postcss": "4.3.3",
59
60
  "tailwindcss": "4.3.3",
60
- "vitest": "4.1.11",
61
+ "vitest": "5.0.4",
61
62
  "@eslint/js": "10.0.1",
62
63
  "@shadcn/lint": "0.2.1",
63
- "@types/node": "22.20.1",
64
- "feedbax": "0.2.1",
65
- "eslint": "10.8.1",
66
- "globals": "17.11.0",
67
- "prettier": "3.9.6",
68
- "typescript-eslint": "8.67.0"
64
+ "@types/node": "24.19.2",
65
+ "feedbax": "0.2.2",
66
+ "eslint": "10.12.0",
67
+ "globals": "17.13.0",
68
+ "prettier": "3.9.10",
69
+ "typescript-eslint": "8.71.2"
69
70
  },
70
71
  "pnpm": {
71
72
  "onlyBuiltDependencies": [
@@ -73,7 +74,7 @@
73
74
  ]
74
75
  },
75
76
  "engines": {
76
- "node": ">=22.14.0"
77
+ "node": "^24.15.0 || >=26.0.0"
77
78
  },
78
79
  "packageManager": "pnpm@10.29.3",
79
80
  "license": "Apache-2.0"
@@ -8,7 +8,8 @@ import { requiredEnvironmentValue } from "./feedback-runtime";
8
8
 
9
9
  let configuredChangelog: ReturnType<typeof createChangelogModule> | undefined;
10
10
 
11
- export function createConfiguredChangelogModule() {
11
+ export function createConfiguredChangelogModule(fresh = false) {
12
+ if (fresh) return buildConfiguredChangelogModule();
12
13
  configuredChangelog ??= buildConfiguredChangelogModule();
13
14
  return configuredChangelog;
14
15
  }
@@ -6,6 +6,9 @@ declare module "cloudflare:workers" {
6
6
  }>;
7
7
  };
8
8
  export const env: {
9
+ GITHUB_SERVICE_SECRET?: string;
10
+ GITHUB_DELIVERY_SECRET?: string;
11
+ GITHUB_PAIRING_SECRET?: string;
9
12
  FEEDBAX_UPDATES?: import("@cloudflare/workers-types").Queue<string>;
10
13
  FEEDBAX_EMAIL?: import("@feedbax/feedback").CloudflareEmailBinding;
11
14
  EMAIL_PROVIDER_SECRET?: string;
@@ -111,12 +111,15 @@ describe("Comment creation", () => {
111
111
  });
112
112
  fireEvent.click(screen.getByRole("button", { name: "Post comment" }));
113
113
  expect(screen.getByText("Please add shortcuts.")).toBeTruthy();
114
- expect(submitComment).toHaveBeenCalledWith({
115
- slug: "roadmap-search",
116
- body: "Please add shortcuts.",
117
- displayName: "Ari",
118
- email: "private@example.com",
119
- });
114
+ await waitFor(() =>
115
+ expect(submitComment).toHaveBeenCalledWith({
116
+ slug: "roadmap-search",
117
+ body: "Please add shortcuts.",
118
+ displayName: "Ari",
119
+ email: "private@example.com",
120
+ submissionKey: expect.stringMatching(/^[A-Za-z0-9_-]{43}$/),
121
+ }),
122
+ );
120
123
 
121
124
  confirm({
122
125
  id: "notion-comment",
@@ -182,6 +185,7 @@ describe("Comment creation", () => {
182
185
  />
183
186
  </DeviceProfileProvider>,
184
187
  );
188
+ await waitFor(() => expect(submitComment).toHaveBeenCalledTimes(1));
185
189
  finish(confirmed);
186
190
  await waitFor(() =>
187
191
  expect(screen.getAllByText("One comment")).toHaveLength(1),
@@ -205,6 +209,49 @@ describe("Comment creation", () => {
205
209
  await screen.findByRole("alert");
206
210
  expect(screen.queryByText("Temporary message")).toBeNull();
207
211
  expect(screen.getByRole("alert").textContent).toContain("Try again");
212
+ expect(
213
+ (screen.getByLabelText("Add a comment") as HTMLTextAreaElement).value,
214
+ ).toBe("Temporary message");
215
+ });
216
+
217
+ it("retries a lost Comment response with the same intent and preserves the composer", async () => {
218
+ saveDeviceProfile(localStorage, {
219
+ name: "Ari",
220
+ email: "private@example.com",
221
+ });
222
+ const requests: Record<string, unknown>[] = [];
223
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (_url, init) => {
224
+ requests.push(JSON.parse(String(init?.body)));
225
+ if (requests.length === 1) throw new Error("Response lost. Try again.");
226
+ return Response.json({
227
+ comment: {
228
+ id: "recovered-comment",
229
+ discussionId: "recovered-thread",
230
+ body: "Keep this message",
231
+ author: { kind: "participant", displayName: "Ari" },
232
+ createdAt: "2026-08-25T01:00:00.000Z",
233
+ },
234
+ });
235
+ });
236
+ renderCommentDetail({ submitComment: submitPublicComment });
237
+ fireEvent.change(await screen.findByLabelText("Add a comment"), {
238
+ target: { value: "Keep this message" },
239
+ });
240
+ fireEvent.click(screen.getByRole("button", { name: "Post comment" }));
241
+ await screen.findByRole("alert");
242
+ expect(
243
+ (screen.getByLabelText("Add a comment") as HTMLTextAreaElement).value,
244
+ ).toBe("Keep this message");
245
+ fireEvent.click(screen.getByRole("button", { name: "Post comment" }));
246
+ await waitFor(() => expect(requests).toHaveLength(2));
247
+ await waitFor(() =>
248
+ expect(
249
+ (screen.getByLabelText("Add a comment") as HTMLTextAreaElement).value,
250
+ ).toBe(""),
251
+ );
252
+ expect(requests[0]?.submissionKey).toMatch(/^[A-Za-z0-9_-]{43}$/);
253
+ expect(requests[1]?.submissionKey).toBe(requests[0]?.submissionKey);
254
+ expect(screen.getAllByText("Keep this message")).toHaveLength(1);
208
255
  });
209
256
 
210
257
  it("renders a fresh Turnstile challenge after a stale Participation Pass is rejected", async () => {
@@ -19,6 +19,7 @@ type IdentityState = {
19
19
  enabled: boolean;
20
20
  ready: boolean;
21
21
  displayName?: string;
22
+ sessionKey?: string;
22
23
  error?: string;
23
24
  signOut: () => Promise<void>;
24
25
  };
@@ -38,28 +39,54 @@ export function ConnectedIdentityProvider({
38
39
  const [state, setState] = useState<{
39
40
  ready: boolean;
40
41
  displayName?: string;
42
+ sessionKey?: string;
41
43
  error?: string;
42
44
  }>({ ready: !feedbax.connected });
43
45
  useEffect(() => {
44
46
  if (!feedbax.connected) return;
45
- const controller = new AbortController();
46
- fetch("/internal/identity/session", {
47
- signal: controller.signal,
48
- cache: "no-store",
49
- })
50
- .then(async (response) => {
51
- if (!response.ok) throw new Error();
52
- const value = await response.json();
53
- setState({ ready: true, displayName: value.participant?.displayName });
47
+ let controller: AbortController | undefined;
48
+ const refresh = () => {
49
+ controller?.abort();
50
+ const active = new AbortController();
51
+ controller = active;
52
+ // Unmount private views before fetching a possibly different session.
53
+ setState({ ready: false });
54
+ fetch("/internal/identity/session", {
55
+ signal: active.signal,
56
+ cache: "no-store",
54
57
  })
55
- .catch(() => {
56
- if (!controller.signal.aborted)
57
- setState({
58
- ready: true,
59
- error: "Sign-in is temporarily unavailable.",
60
- });
61
- });
62
- return () => controller.abort();
58
+ .then(async (response) => {
59
+ if (!response.ok) throw new Error();
60
+ const value = await response.json();
61
+ if (!active.signal.aborted)
62
+ setState({
63
+ ready: true,
64
+ displayName: value.participant?.displayName,
65
+ sessionKey: value.participant?.sessionKey,
66
+ });
67
+ })
68
+ .catch(() => {
69
+ if (!active.signal.aborted)
70
+ setState({
71
+ ready: true,
72
+ error: "Sign-in is temporarily unavailable.",
73
+ });
74
+ });
75
+ };
76
+ const visibility = () => {
77
+ if (document.visibilityState === "hidden") {
78
+ controller?.abort();
79
+ setState({ ready: false });
80
+ } else refresh();
81
+ };
82
+ refresh();
83
+ window.addEventListener("focus", refresh);
84
+ document.addEventListener("visibilitychange", visibility);
85
+ return () => {
86
+ controller?.abort();
87
+ window.removeEventListener("focus", refresh);
88
+ document.removeEventListener("visibilitychange", visibility);
89
+ };
63
90
  }, []);
64
91
  async function signOut() {
65
92
  const response = await fetch("/internal/identity/logout", {