create-feedbax 0.2.0 → 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.
- package/README.md +17 -5
- package/dist/cli.js +2610 -291
- package/dist/index.js +2375 -200
- package/package.json +7 -7
- package/template/.feedbax-template.sha256 +1 -1
- package/template/.gitignore.template +1 -0
- package/template/.npmignore.template +1 -0
- package/template/.nvmrc +1 -0
- package/template/AGENTS.md +13 -0
- package/template/CONNECTED.md +418 -0
- package/template/README.md +97 -0
- package/template/components.json +1 -1
- package/template/eslint.config.mjs +23 -0
- package/template/feedbax-vite.ts +79 -0
- package/template/migrations/0001_connected.sql +6 -0
- package/template/migrations/0002_participation.sql +8 -0
- package/template/migrations/0003_images.sql +3 -0
- package/template/migrations/0004_followed_status.sql +9 -0
- package/template/migrations/0005_github.sql +4 -0
- package/template/migrations/0006_integration.sql +3 -0
- package/template/migrations/0007_ownership.sql +2 -0
- package/template/migrations/0008_integration_recovery.sql +5 -0
- package/template/migrations/0009_observed_history.sql +3 -0
- package/template/package.json +39 -34
- package/template/src/authorized-draft-post.tsx +1 -0
- package/template/src/changelog-page.tsx +63 -54
- package/template/src/changelog-runtime.ts +2 -1
- package/template/src/cloudflare-workers.d.ts +9 -0
- package/template/src/comment-creation.test.tsx +95 -7
- package/template/src/comment-thread-page.ts +11 -1
- package/template/src/components/command-palette.tsx +59 -30
- package/template/src/components/connected-identity-provider.tsx +135 -0
- package/template/src/components/device-profile-provider.tsx +41 -26
- package/template/src/components/follow-post-button.tsx +78 -0
- package/template/src/components/header.tsx +2 -0
- package/template/src/components/image-attachments.tsx +187 -0
- package/template/src/components/loader.tsx +48 -5
- package/template/src/components/notification-inbox.tsx +232 -0
- package/template/src/components/portal-footer.tsx +14 -0
- package/template/src/components/post-history.tsx +101 -0
- package/template/src/components/profile-menu.tsx +37 -0
- package/template/src/components/ui/components/command.tsx +30 -29
- package/template/src/components/ui/components/message-scroller.tsx +2 -2
- package/template/src/components/ui/components/scroll-area.tsx +0 -1
- package/template/src/components/ui/components/switch.tsx +29 -0
- package/template/src/components/ui/styles/globals.css +1 -1
- package/template/src/configured-feedback.test.ts +82 -0
- package/template/src/configured-feedback.ts +61 -0
- package/template/src/connected-identity.test.ts +148 -0
- package/template/src/connected-identity.ts +127 -0
- package/template/src/connected-participation.test.tsx +67 -0
- package/template/src/connected-runtime.test.ts +244 -0
- package/template/src/connected-runtime.ts +176 -0
- package/template/src/connected-social.test.tsx +158 -0
- package/template/src/feedback-cache-invalidation.ts +3 -0
- package/template/src/feedback-runtime.ts +123 -13
- package/template/src/feedbax/changelog/index.test.ts +35 -0
- package/template/src/feedbax/changelog/index.ts +5 -1
- package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
- package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
- package/template/src/feedbax/config/index.test.ts +73 -1
- package/template/src/feedbax/config/index.ts +205 -9
- package/template/src/feedbax/feedback/comment-storage.ts +38 -0
- package/template/src/feedbax/feedback/connected-feedback.ts +235 -0
- package/template/src/feedbax/feedback/connected.test.ts +272 -0
- package/template/src/feedbax/feedback/connected.ts +225 -0
- package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
- package/template/src/feedbax/feedback/email-delivery.ts +66 -0
- package/template/src/feedbax/feedback/feedback-storage.ts +1 -0
- package/template/src/feedbax/feedback/github-comments.ts +390 -0
- package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
- package/template/src/feedbax/feedback/github-feedback.ts +581 -0
- package/template/src/feedbax/feedback/github-request.ts +120 -0
- package/template/src/feedbax/feedback/github-service-client.ts +56 -0
- package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
- package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
- package/template/src/feedbax/feedback/image-feedback.ts +114 -0
- package/template/src/feedbax/feedback/images.test.ts +223 -0
- package/template/src/feedbax/feedback/images.ts +350 -0
- package/template/src/feedbax/feedback/index.ts +226 -16
- package/template/src/feedbax/feedback/integration-auth.ts +138 -0
- package/template/src/feedbax/feedback/notion-comments.ts +2 -35
- package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
- package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
- package/template/src/feedbax/feedback/observed-history.ts +219 -0
- package/template/src/feedbax/feedback/ownership.ts +170 -0
- package/template/src/feedbax/feedback/participation.test.ts +653 -0
- package/template/src/feedbax/feedback/participation.ts +502 -0
- package/template/src/history-runtime.test.ts +80 -0
- package/template/src/history-runtime.ts +37 -0
- package/template/src/images-runtime.test.ts +133 -0
- package/template/src/images-runtime.ts +125 -0
- package/template/src/index.css +103 -440
- package/template/src/integration-runtime.test.ts +365 -0
- package/template/src/integration-runtime.ts +289 -0
- package/template/src/lazy-post-detail-overlay.tsx +13 -2
- package/template/src/my-feedback-page.test.tsx +152 -0
- package/template/src/my-feedback-page.tsx +398 -0
- package/template/src/my-feedback-runtime.test.ts +93 -0
- package/template/src/my-feedback-runtime.ts +154 -0
- package/template/src/overlay-post-detail.test.tsx +59 -8
- package/template/src/overlay-post-detail.tsx +20 -69
- package/template/src/participation-client.ts +63 -0
- package/template/src/participation-runtime.test.ts +35 -0
- package/template/src/participation-runtime.ts +255 -0
- package/template/src/portal-comments.ts +35 -5
- package/template/src/portal-feedback-form.tsx +29 -6
- package/template/src/portal-feedback-runtime.ts +3 -0
- package/template/src/portal-feedback-server-function.ts +6 -3
- package/template/src/portal-feedback-submission.ts +19 -0
- package/template/src/portal-server-entry.ts +13 -0
- package/template/src/post-creation-flow.tsx +2 -2
- package/template/src/post-detail-content.test.tsx +233 -0
- package/template/src/post-detail-content.tsx +162 -0
- package/template/src/post-detail-query.ts +34 -0
- package/template/src/post-results.tsx +1 -1
- package/template/src/public-cache-policy.test.ts +11 -4
- package/template/src/public-cache-policy.ts +1 -1
- package/template/src/public-feedback-index.tsx +9 -39
- package/template/src/public-feedback-page.test.tsx +65 -0
- package/template/src/public-feedback-page.ts +12 -1
- package/template/src/public-portal-error.tsx +1 -1
- package/template/src/public-post-detail-skeleton.tsx +54 -0
- package/template/src/public-post-detail.tsx +437 -271
- package/template/src/public-post-route.test.tsx +39 -3
- package/template/src/public-post-route.tsx +15 -28
- package/template/src/public-release-links.test.ts +120 -0
- package/template/src/public-release-links.ts +130 -0
- package/template/src/public-roadmap-route.test.tsx +38 -2
- package/template/src/public-roadmap-view.tsx +56 -23
- package/template/src/public-shell.test.tsx +26 -0
- package/template/src/routeTree.gen.ts +70 -3
- package/template/src/router.tsx +2 -0
- package/template/src/routes/__root.tsx +3 -5
- package/template/src/routes/email-preferences.tsx +77 -0
- package/template/src/routes/my-feedback.tsx +16 -0
- package/template/src/routes/submit.tsx +1 -1
- package/template/src/safe-public-failure.ts +3 -1
- package/template/src/server.ts +87 -2
- package/template/src/submission-intent.ts +26 -0
- package/template/src/trusted-feedback-handler.test.ts +1 -1
- package/template/src/vote-toggle.tsx +42 -9
- 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.
|
|
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
|
|
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.
|
|
21
|
-
"tsx": "4.
|
|
20
|
+
"esbuild": "0.28.2",
|
|
21
|
+
"tsx": "4.23.15",
|
|
22
22
|
"typescript": "catalog:",
|
|
23
|
-
"vitest": "
|
|
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
|
|
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": ">=
|
|
45
|
+
"node": "^24.15.0 || >=26.0.0"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
38ac71db749b85b9900fd636aa95867390517bc9695bc11534a44892e26810b2
|
package/template/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.22.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Working on this Feedbax installation
|
|
2
|
+
|
|
3
|
+
Read `README.md` for local setup, configuration, and checks. Read `CONNECTED.md` when configuring identity handoff, D1, sessions, verified Votes, or Comment attribution.
|
|
4
|
+
|
|
5
|
+
This is a flat application, not the Feedbax development monorepo. Product settings are in `src/feedbax.ts`; infrastructure is in `wrangler.jsonc`; secrets are in ignored `.dev.vars` and encrypted Worker secrets. `src/feedbax.test-fixture.ts` is never a live configuration.
|
|
6
|
+
|
|
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
|
+
|
|
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
|
+
|
|
11
|
+
## Connected social features
|
|
12
|
+
|
|
13
|
+
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.
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Connected Profile
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Configure an Installation
|
|
6
|
+
|
|
7
|
+
Create a D1 database using `pnpm exec wrangler d1 create <installation-name>`. Review the target account and add the returned identifiers to `wrangler.jsonc`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"d1_databases": [
|
|
12
|
+
{
|
|
13
|
+
"binding": "FEEDBAX_DB",
|
|
14
|
+
"database_name": "your-installation",
|
|
15
|
+
"database_id": "your-database-id",
|
|
16
|
+
"migrations_dir": "migrations"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Apply the included migration using `pnpm exec wrangler d1 migrations apply FEEDBAX_DB --remote` (and `--local` for local testing). Never share this database between Installations. Configure HTTPS for local sign-in testing because identity cookies require Secure.
|
|
23
|
+
|
|
24
|
+
Add the following to `src/feedbax.ts`, alongside an absolute HTTPS `product.siteUrl`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
connected: {
|
|
28
|
+
issuer: "https://app.example.com",
|
|
29
|
+
loginUrl: "https://app.example.com/feedbax-login",
|
|
30
|
+
},
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Generate at least 32 random bytes for `IDENTITY_HANDOFF_SECRET`. Store the same secret only on the Product Team application's server and in the portal's `.dev.vars` / encrypted Worker secrets. Never place it in `feedbax.ts`, frontend code, a query string, or logs. The lifecycle CLI deploys this secret alongside the other configured secrets. `feedbax doctor` checks the configuration and reads the remote D1 schema without changing it.
|
|
34
|
+
|
|
35
|
+
## Product application handoff
|
|
36
|
+
|
|
37
|
+
1. Feedbax's Sign in action visits `/internal/identity/start`, sets a five-minute browser challenge cookie, and redirects to the configured login URL with `state` and `callback` query parameters.
|
|
38
|
+
2. The Product Team application authenticates the current user. It must allow only the exact configured Feedbax callback, retain the supplied state, and derive the stable subject and display name from its authenticated server session. Never sign identity fields supplied by a browser.
|
|
39
|
+
3. Sign the payload below on the application server with HMAC-SHA256. Times are Unix seconds; the token lifetime must be at most 120 seconds. Use an unpredictable nonce. The audience must exactly equal the Feedbax origin and the issuer must exactly equal the configured issuer.
|
|
40
|
+
4. Return an HTML form which posts one `token` field to the allowlisted callback. Use `application/x-www-form-urlencoded` and `Referrer-Policy: strict-origin`. Do not put the token in a redirect URL. HTML-escape interpolated values. The form must retain its application Origin: `no-referrer` makes browser cross-origin form POSTs send `Origin: null`, which the portal rejects. `strict-origin` shares only the application origin, without its path or query.
|
|
41
|
+
5. Feedbax validates the signature, origin, browser challenge, audience, issuer, and expiry, consumes the token nonce and browser challenge once, and creates an opaque seven-day HttpOnly session. Sign out revokes that session in D1. No identity token is stored in browser JavaScript storage.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
45
|
+
|
|
46
|
+
// All identity values come from the application's authenticated server session.
|
|
47
|
+
const now = Math.floor(Date.now() / 1000);
|
|
48
|
+
const payload = Buffer.from(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
purpose: "feedbax-identity-v1",
|
|
51
|
+
subject: authenticatedUser.id,
|
|
52
|
+
displayName: authenticatedUser.displayName,
|
|
53
|
+
issuer: "https://app.example.com",
|
|
54
|
+
audience: "https://feedback.example.com",
|
|
55
|
+
state: validatedBrowserState,
|
|
56
|
+
nonce: randomBytes(32).toString("base64url"),
|
|
57
|
+
issuedAt: now,
|
|
58
|
+
expiresAt: now + 120,
|
|
59
|
+
}),
|
|
60
|
+
).toString("base64url");
|
|
61
|
+
const signature = createHmac("sha256", process.env.IDENTITY_HANDOFF_SECRET)
|
|
62
|
+
.update(`feedbax-identity-v1:${payload}`)
|
|
63
|
+
.digest("base64url");
|
|
64
|
+
const token = `${payload}.${signature}`;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The application must use the same stable subject across browsers; never use display name or an unverified email as the subject. “Verified” means asserted by that trusted application, not legal identity or a guarantee of one account per human. The stored identity is scoped to the configured issuer and subject. Changing the issuer or subject scheme requires deliberate identity migration; do not repurpose identifiers for other people. Secret rotation blocks old handoffs; existing opaque sessions remain valid until expiry, logout, or explicit database revocation.
|
|
68
|
+
|
|
69
|
+
## Data ownership and compatibility
|
|
70
|
+
|
|
71
|
+
Notion remains authoritative for Posts, publication, Team workflow, and Comment content. D1 owns identity, sessions, unique Votes, and verified Comment attribution. A unique database key enforces one Vote per subject and Post; repeated add/remove requests are idempotent. Counts and Top ordering in Connected Profile use D1, including across browsers. Anonymous historical Notion counts are preserved in Notion but excluded from verified counts; they cannot be migrated into verified Votes without identity evidence. The Notion Vote Count property remains the legacy Notion-only counter, not a synchronized Connected counter.
|
|
72
|
+
|
|
73
|
+
Native Comments receive the signed display name on the server. Only Comments with durable D1 attribution are labeled Verified; historical Comments remain Unverified. Notion-authored Comments retain the Product Team label. Resolving/deleting a Comment in Notion still removes it from public reads. Browser-held fifteen-minute Comment capabilities and unpublished Post draft capabilities retain their existing scope; this release does not add cross-device Comment editing or recover draft capabilities. Post submission still uses a private contact Device Profile, editable through Contact profile; it does not gain an identity-verification badge.
|
|
74
|
+
|
|
75
|
+
If D1 is unavailable, Connected operations fail closed. They never fall back to anonymous voting or trust a browser-provided name. Attribution failures reject creation and attempt to remove the just-created Notion Comment; if external cleanup also fails, any surviving Comment remains unverified. The portal does not automatically retry Comment creation, since Notion offers no creation idempotency guarantee.
|
|
76
|
+
|
|
77
|
+
Back up the database and use Cloudflare D1 recovery before destructive changes. Disabling `connected` retains D1 data but switches visible Vote counts back to Notion-only history. Expired sessions/nonces may be purged during maintenance after their expiry; never delete active rows to reset Votes.
|
|
78
|
+
|
|
79
|
+
## Verification and operating limits
|
|
80
|
+
|
|
81
|
+
Before enabling in production, test the real application's sign-in/callback, a second browser, duplicate Votes, sign out, invalid/expired/replayed tokens, and a native Notion Comment with verified attribution. Use the dedicated acceptance Installation; do not turn on the dogfood portal until a real application login endpoint is configured.
|
|
82
|
+
|
|
83
|
+
Connected Top ranking currently reads all matching Published Posts before ranking and pagination. This favors correct ranking for the initial small-team scope; measure larger installations before adoption. Public counts can be briefly cached under the existing public-read policy; private Vote state and identity endpoints are never publicly cached.
|
|
84
|
+
|
|
85
|
+
Implementation references: [D1 database batches](https://developers.cloudflare.com/d1/worker-api/d1-database/) and [Drizzle D1 adapter](https://orm.drizzle.team/docs/sqlite/connect-cloudflare-d1).
|
|
86
|
+
|
|
87
|
+
## Complete application endpoint
|
|
88
|
+
|
|
89
|
+
Copy the following into your application's server code. It returns a standard Web `Response` and works behind a Node-compatible route handler. Configure the app's existing authentication middleware before this endpoint so an unauthenticated Participant is taken through normal app sign-in and returned to the same handoff URL. If your middleware does not redirect, the example returns an explicit 401; sign in to the product first and retry Feedbax.
|
|
90
|
+
|
|
91
|
+
The only app-specific dependency is `authenticate(request)`: call the same trusted session lookup used by your protected app pages. Return a stable internal user ID as `subject` and its server-read display name. Never read these values from form input, query parameters, browser storage, or an unsigned header. Do not hardcode a demonstration user in production.
|
|
92
|
+
|
|
93
|
+
```ts title="server/feedbax-login.ts"
|
|
94
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
95
|
+
|
|
96
|
+
export function createIdentityLoginHandler(options: {
|
|
97
|
+
applicationOrigin: string;
|
|
98
|
+
portalOrigin: string;
|
|
99
|
+
issuer: string;
|
|
100
|
+
secret: string;
|
|
101
|
+
authenticate(request: Request): Promise<{
|
|
102
|
+
subject: string;
|
|
103
|
+
displayName: string;
|
|
104
|
+
email?: string;
|
|
105
|
+
emailVerified?: boolean;
|
|
106
|
+
} | null>;
|
|
107
|
+
}) {
|
|
108
|
+
const app = new URL(options.applicationOrigin).origin;
|
|
109
|
+
const portal = new URL(options.portalOrigin).origin;
|
|
110
|
+
if (
|
|
111
|
+
![app, portal, options.issuer].every(
|
|
112
|
+
(url) => new URL(url).protocol === "https:",
|
|
113
|
+
) ||
|
|
114
|
+
options.secret.length < 32
|
|
115
|
+
)
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Configure HTTPS origins and a server-only handoff secret of at least 32 characters.",
|
|
118
|
+
);
|
|
119
|
+
const headers = {
|
|
120
|
+
"Cache-Control": "no-store",
|
|
121
|
+
"Referrer-Policy": "strict-origin",
|
|
122
|
+
};
|
|
123
|
+
return async (request: Request): Promise<Response> => {
|
|
124
|
+
const url = new URL(request.url);
|
|
125
|
+
if (request.method !== "GET")
|
|
126
|
+
return new Response("Method not allowed", { status: 405, headers });
|
|
127
|
+
const state = url.searchParams.get("state");
|
|
128
|
+
const callback = url.searchParams.get("callback");
|
|
129
|
+
if (
|
|
130
|
+
url.origin !== app ||
|
|
131
|
+
!state ||
|
|
132
|
+
!/^[A-Za-z0-9_-]{43}$/.test(state) ||
|
|
133
|
+
callback !== `${portal}/internal/identity/callback` ||
|
|
134
|
+
url.searchParams.getAll("state").length !== 1 ||
|
|
135
|
+
url.searchParams.getAll("callback").length !== 1
|
|
136
|
+
)
|
|
137
|
+
return new Response("Invalid Feedbax sign-in request", {
|
|
138
|
+
status: 400,
|
|
139
|
+
headers,
|
|
140
|
+
});
|
|
141
|
+
const user = await options.authenticate(request);
|
|
142
|
+
if (!user)
|
|
143
|
+
return new Response(
|
|
144
|
+
"Sign in to your product, then retry Feedbax sign-in.",
|
|
145
|
+
{ status: 401, headers },
|
|
146
|
+
);
|
|
147
|
+
if (
|
|
148
|
+
![user.subject, user.displayName].every(
|
|
149
|
+
(value) =>
|
|
150
|
+
typeof value === "string" &&
|
|
151
|
+
value.trim().length > 0 &&
|
|
152
|
+
value.length <= 512,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
return new Response("Application identity is incomplete", {
|
|
156
|
+
status: 403,
|
|
157
|
+
headers,
|
|
158
|
+
});
|
|
159
|
+
const now = Math.floor(Date.now() / 1000);
|
|
160
|
+
const payload = Buffer.from(
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
purpose: "feedbax-identity-v1",
|
|
163
|
+
subject: user.subject,
|
|
164
|
+
displayName: user.displayName,
|
|
165
|
+
...(user.emailVerified === true && user.email
|
|
166
|
+
? { email: user.email, emailVerified: true }
|
|
167
|
+
: {}),
|
|
168
|
+
issuer: options.issuer,
|
|
169
|
+
audience: portal,
|
|
170
|
+
state,
|
|
171
|
+
nonce: randomBytes(32).toString("base64url"),
|
|
172
|
+
issuedAt: now,
|
|
173
|
+
expiresAt: now + 120,
|
|
174
|
+
}),
|
|
175
|
+
).toString("base64url");
|
|
176
|
+
const signature = createHmac("sha256", options.secret)
|
|
177
|
+
.update(`feedbax-identity-v1:${payload}`)
|
|
178
|
+
.digest("base64url");
|
|
179
|
+
const nonce = randomBytes(24).toString("base64url");
|
|
180
|
+
const html = `<!doctype html><html lang="en"><meta charset="utf-8"><title>Continue to feedback</title><body><form method="post" action="${escapeHtml(callback)}"><input type="hidden" name="token" value="${payload}.${signature}"><button type="submit">Continue to feedback</button></form><script nonce="${nonce}">document.forms[0].requestSubmit()</script></body></html>`;
|
|
181
|
+
return new Response(html, {
|
|
182
|
+
headers: {
|
|
183
|
+
...headers,
|
|
184
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
185
|
+
"Content-Security-Policy": `default-src 'none'; form-action ${portal}; script-src 'nonce-${nonce}'; frame-ancestors 'none'`,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function escapeHtml(value: string) {
|
|
191
|
+
return value.replace(
|
|
192
|
+
/[&<>"']/g,
|
|
193
|
+
(character) =>
|
|
194
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
195
|
+
character
|
|
196
|
+
]!,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Register the handler at the exact URL configured as `connected.loginUrl`. For a framework with Web route handlers, the wiring looks like this (adapt the import paths and session lookup to your application):
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { createIdentityLoginHandler } from "./server/feedbax-login";
|
|
205
|
+
import { readAuthenticatedAppUser } from "./server/auth";
|
|
206
|
+
|
|
207
|
+
export const GET = createIdentityLoginHandler({
|
|
208
|
+
applicationOrigin: "https://app.example.com",
|
|
209
|
+
portalOrigin: "https://feedback.example.com",
|
|
210
|
+
issuer: "https://app.example.com",
|
|
211
|
+
secret: process.env.IDENTITY_HANDOFF_SECRET!,
|
|
212
|
+
authenticate: async (request) => {
|
|
213
|
+
const user = await readAuthenticatedAppUser(request);
|
|
214
|
+
return user ? { subject: user.id, displayName: user.displayName } : null;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`readAuthenticatedAppUser` is your existing application's authentication adapter, not a Feedbax API. On Next.js, the `GET` export belongs in `app/feedbax-login/route.ts`; keep the factory and session lookup in server-only modules. On TanStack Start, call the same handler from a server route's GET handler. Other frameworks can adapt their request/response objects to the Web API. Keep the route behind your current session middleware and rate limits; Feedbax does not create or manage the product's accounts.
|
|
220
|
+
|
|
221
|
+
## Verify the entire sign-in loop
|
|
222
|
+
|
|
223
|
+
1. Run `feedbax doctor`, build, and deploy the configured portal. Confirm its HTTPS origin exactly matches `product.siteUrl` and the application's configured audience/callback.
|
|
224
|
+
2. Sign in to the product, open the portal, and choose Sign in. You should return with your server-provided display name. A handoff form never includes your API key or handoff secret.
|
|
225
|
+
3. Add a Vote, reload, and sign in from another browser with the same product account. The Vote should still be selected. Repeated add requests must not increase it twice.
|
|
226
|
+
4. Post a Comment and confirm Verified appears next to its app-provided name. A historical browser-only Comment still shows Unverified. Reply/resolve from Notion and confirm moderation is reflected after refresh.
|
|
227
|
+
5. Sign out of Feedbax. Its session should stop authorizing Votes/Comments immediately. This signs out only the portal session; product sign-out is managed by the product. An already-issued portal session expires after seven days unless explicitly revoked sooner.
|
|
228
|
+
6. Reuse a captured callback or change its state/audience/signature in a dedicated test environment. Feedbax must reject it. Do not copy production identity tokens into logs or issue reports.
|
|
229
|
+
|
|
230
|
+
## Authentication troubleshooting
|
|
231
|
+
|
|
232
|
+
| Symptom | Check |
|
|
233
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
234
|
+
| Product endpoint returns 401 | Use your existing session middleware and complete the product's login first; the `authenticate` adapter must read that session. |
|
|
235
|
+
| Invalid sign-in request on the product | Preserve the single `state` and `callback` parameters; allow only the configured exact callback. Do not rebuild the callback from arbitrary browser input. |
|
|
236
|
+
| Callback returns 403 | The form must POST directly from the configured login URL's origin. Check reverse-proxy origin handling and sandbox policies. |
|
|
237
|
+
| Callback returns 401 | Check identical handoff secrets, exact issuer/audience, current server clocks, a lifetime of at most 120 seconds, and an unused browser challenge. Start sign-in again after a failed or expired attempt. |
|
|
238
|
+
| Challenge cookie absent | Use HTTPS and the same browser. The challenge uses Secure, HttpOnly, SameSite=None, and the `__Host-` prefix. Cookie-blocking policies may require a normal top-level product/portal flow. |
|
|
239
|
+
| Doctor or participation returns unavailable | Check the dedicated `FEEDBAX_DB` binding, applied migration, remote account/database, and required Worker secrets. The profile fails closed. |
|
|
240
|
+
| Name differs from the current app name | A session reads the last server-asserted name; sign in again after a profile change. Old Comments retain their recorded attribution. |
|
|
241
|
+
| Anonymous vote totals differ after enabling Connected | Historical Notion counters are not verified Vote records; Connected counts only D1 Votes. |
|
|
242
|
+
|
|
243
|
+
## Keep the three server secrets distinct
|
|
244
|
+
|
|
245
|
+
| Secret | Stored where | Purpose |
|
|
246
|
+
| ---------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
|
|
247
|
+
| `IDENTITY_HANDOFF_SECRET` | Product server and portal secret store | Sign/verify identity handoffs. |
|
|
248
|
+
| `PARTICIPATION_SIGNING_SECRET` | Portal secret store | Participation Passes and short-lived Comment capabilities. |
|
|
249
|
+
| `FEEDBAX_API_SECRET` / portal `FEEDBAX_API_KEY_HASH` | Plaintext on product server; hash on portal | Trusted Post submission from the feedback block or another integration. |
|
|
250
|
+
|
|
251
|
+
Generate independent random values. Store production values through your platform's secret manager. When rotating the handoff secret, coordinate both servers; existing portal sessions require separate revocation if they must be ended immediately.
|
|
252
|
+
|
|
253
|
+
## Following and notifications
|
|
254
|
+
|
|
255
|
+
Connected defaults to `processing: "small"`. Its five-minute timer only enqueues a constant work signal into a dedicated Cloudflare Queue; a consumer then prepares one Follow history page, observes one followed Post, attempts one notification email, and cleans up at most five images. This separates the Free timer's 10 ms CPU allowance from Queues' documented default consumer limit of 30 seconds. The queue contains no Participant identity, email, or content. Set `connected.processing: "standard"` for ten preparations/observed Posts, twenty emails, and fifty cleanups directly in the timer; that mode targets Workers Paid and needs no processing queue. Cloudflare Email Service separately requires Workers Paid for arbitrary recipients. Resend/custom delivery supports the small Free setup. See [Workers limits](https://developers.cloudflare.com/workers/platform/limits/), [Queues limits](https://developers.cloudflare.com/queues/platform/limits/), [Queues pricing](https://developers.cloudflare.com/queues/platform/pricing/), and [D1 limits](https://developers.cloudflare.com/d1/platform/limits/).
|
|
256
|
+
|
|
257
|
+
### Create and bind the processing queue
|
|
258
|
+
|
|
259
|
+
Create one queue for this Installation in the same Cloudflare account, before running the guided creator or deploying:
|
|
260
|
+
|
|
261
|
+
```sh
|
|
262
|
+
pnpm exec wrangler queues create your-installation-updates
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The guided creator asks for its name and writes both bindings. For a manual setup, add this beside the existing D1 bindings in `wrangler.jsonc`:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"triggers": { "crons": ["*/5 * * * *"] },
|
|
270
|
+
"queues": {
|
|
271
|
+
"producers": [
|
|
272
|
+
{ "binding": "FEEDBAX_UPDATES", "queue": "your-installation-updates" }
|
|
273
|
+
],
|
|
274
|
+
"consumers": [
|
|
275
|
+
{
|
|
276
|
+
"queue": "your-installation-updates",
|
|
277
|
+
"max_batch_size": 1,
|
|
278
|
+
"max_batch_timeout": 1,
|
|
279
|
+
"max_concurrency": 1,
|
|
280
|
+
"max_retries": 3,
|
|
281
|
+
"retry_delay": 60
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Keep producer and consumer names identical, and use a dedicated queue for each Installation. Small mode fails closed when the binding is absent; it does not run the heavy work inline. `doctor` validates the paired bindings, bounds, remote queue existence, and all D1 migrations. Template-only output needs this configuration added manually. Local Wrangler simulates queues but does not prove remote capacity or email receipt.
|
|
289
|
+
|
|
290
|
+
At five-minute intervals, normal small processing produces 288 signals/day, usually 864 queue operations (write/read/delete). Three queue redeliveries per signal would raise that to 1,728 operations; other queues share the account's Free allowance of 10,000 operations/day. Free retention is 24 hours. Keep the batch and concurrency at one. A failed queue invocation is retried up to three times, at least a minute apart; D1 remains the authority for unfinished work and the next timer signal can resume it. Leases, transactional checkpoints, event deduplication, and provider delivery keys protect repeated attempts. This is not an exactly-once guarantee. Watch timer CPU, queue backlog/failed consumers, D1 quotas, and provider limits on the actual account. No plan upgrade or billing change is performed by these settings.
|
|
291
|
+
|
|
292
|
+
Connected Participants can explicitly Follow a Published Post from its detail view. Following is independent of voting; submitting, commenting, or voting does not silently subscribe a Participant. The header bell opens a private inbox with unread state, Mark read, Mark all read, pagination, and email preferences. Signing out hides these controls and private state is never publicly cached.
|
|
293
|
+
|
|
294
|
+
The timer signals the small consumer every five minutes; standard mode observes directly. Small processing checks one active followed Post, prioritizing the least recently checked; standard checks ten. Each observation reads one 50-Comment cursor page and resumes later pages on subsequent jobs. Normal small cadence budgets 288 observations and email attempts per day, with additional attempts possible after queue redelivery. Busy Installations and long histories have longer latency; delivery older than 24 hours is skipped. Status notifications compare each Follow's recorded status, so minute-rounded Notion timestamps cannot hide an observed transition. Intermediate states between observations may be coalesced.
|
|
295
|
+
|
|
296
|
+
A new Follow snapshots existing Comment IDs from its current minute, preventing both historical notices and missing later Comments with the same rounded timestamp. Small mode prepares every new Follow in the queue, reading one 50-Comment page per run; the request does not read Comment history. Standard mode prepares the first page immediately and resumes longer histories in the background. The button shows preparation with a cancel action until the baseline is complete. Updates begin when preparation completes; earlier Comments and status changes during preparation are excluded. Repeating Follow from another browser preserves the original baseline. Unfollowing cancels preparation and removes its private baseline. Participants do not receive notifications about Comments with their own recorded attribution.
|
|
297
|
+
|
|
298
|
+
In-app notifications are deduplicated by event and Participant, survive browser changes, and retain independent read state. Notifications and outbound delivery recheck that the Post is still Published. Stop following from the same Post to stop future updates. Email is independently opt-in through the inbox switch and can be disabled there or through the unsubscribe link in each email. Unsubscribe links show a confirmation page on GET and accept a one-click POST from supporting mail clients. A link scanner's GET does not change preferences.
|
|
299
|
+
|
|
300
|
+
### Verified delivery address
|
|
301
|
+
|
|
302
|
+
Application identity and email ownership are separate assertions. To offer email notifications, derive both of these additional signed claims from your application's authenticated server record:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
...(user.emailVerified === true
|
|
306
|
+
? { email: user.email, emailVerified: true }
|
|
307
|
+
: {}),
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Never accept these values from the browser or copy the unverified Device Profile. Feedbax ignores an email unless the signed `emailVerified` claim is true. The contact address is stored privately in D1, is refreshed at the next successful handoff, and never appears in public identity, Post, Comment, or notification responses. Existing handoff implementations without these optional claims keep working for in-app notifications. A subsequent handoff without a verified address removes email eligibility.
|
|
311
|
+
|
|
312
|
+
### Choose one email sender
|
|
313
|
+
|
|
314
|
+
All choices share notification content, opt-in, unsubscribe, the D1 delivery queue, and a stable `Idempotency-Key`. The sender address belongs to your verified sending domain. Do not use a Participant's address as the sender.
|
|
315
|
+
|
|
316
|
+
**Cloudflare Email Service** uses the current structured Workers sending API. Onboard and verify your sending domain in Cloudflare Email Service, then configure:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
connected: {
|
|
320
|
+
issuer: "https://app.example.com",
|
|
321
|
+
loginUrl: "https://app.example.com/feedbax-login",
|
|
322
|
+
email: { provider: "cloudflare", from: "updates@example.com" },
|
|
323
|
+
},
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"send_email": [
|
|
329
|
+
{
|
|
330
|
+
"name": "FEEDBAX_EMAIL",
|
|
331
|
+
"allowed_sender_addresses": ["updates@example.com"]
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Use Email Service's outbound sending capability, not a restricted legacy inbound-routing destination. See [Workers sending API](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/) and [domain setup](https://developers.cloudflare.com/email-service/configuration/domains/). Local Wrangler simulation does not deliver real messages. Do not enable a remote email binding during general local testing; it sends real mail.
|
|
338
|
+
|
|
339
|
+
**Resend** requires a verified domain and a sending API key. Set `email: { provider: "resend", from: "updates@example.com" }` and store the key as `EMAIL_PROVIDER_SECRET` in `.dev.vars` and `pnpm exec wrangler secret put EMAIL_PROVIDER_SECRET`. No email binding is required. Feedbax posts to `https://api.resend.com/emails`, passing its stable delivery key in the `Idempotency-Key` header. See [Resend sending](https://resend.com/docs/api-reference/emails/send-email) and [idempotency](https://resend.com/docs/dashboard/emails/idempotency-keys).
|
|
340
|
+
|
|
341
|
+
**Custom HTTPS sender** connects your own backend or mail provider adapter:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
email: {
|
|
345
|
+
provider: "custom",
|
|
346
|
+
from: "updates@example.com",
|
|
347
|
+
endpoint: "https://app.example.com/internal/feedbax-email",
|
|
348
|
+
},
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Store a dedicated shared bearer secret as `EMAIL_PROVIDER_SECRET`. Your endpoint must validate `Authorization: Bearer <secret>`, accept the following JSON, deliver or durably enqueue it, and return 2xx only after acceptance. It must persist `Idempotency-Key` uniqueness for at least 24 hours and return the same successful result for retries without sending again. Keep this endpoint private to the authenticated server contract; a browser must never receive the secret.
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
interface NotificationEmail {
|
|
355
|
+
from: string;
|
|
356
|
+
to: string[]; // exactly one recipient per Feedbax request
|
|
357
|
+
subject: string;
|
|
358
|
+
text: string;
|
|
359
|
+
headers: {
|
|
360
|
+
"List-Unsubscribe": string;
|
|
361
|
+
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click";
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Preserve the unsubscribe headers, disable tracking unless your Product Team has deliberately chosen it, and never return provider credentials or recipient details to the portal. Feedbax requires HTTPS, refuses redirects, and times out custom/Resend requests after ten seconds. This adapter can use SMTP or another provider behind your server; Feedbax does not require a general plugin system or expose SMTP passwords in application configuration.
|
|
367
|
+
|
|
368
|
+
### Delivery limits and recovery
|
|
369
|
+
|
|
370
|
+
Each small queue job attempts at most one email; a standard scheduled invocation attempts at most twenty. D1 leases serialize workers, and failures use bounded exponential retry, starting at one minute and capped at six hours. Delivery is abandoned after eight failed attempts or 24 hours, whichever comes first. Resend's provider deduplication window is 24 hours; Cloudflare's documented binding has no equivalent idempotency parameter, so a crash or ambiguous timeout after acceptance can result in a duplicate email. Custom senders must supply their own durable deduplication. No adapter promises exactly-once inbox delivery, and provider acceptance does not prove final recipient delivery. In-app notifications remain available when email fails. Inspect provider suppression/bounce logs and the private D1 delivery state to investigate delivery failures; never publish these records.
|
|
371
|
+
|
|
372
|
+
## Image attachments
|
|
373
|
+
|
|
374
|
+
Set `connected.images: true` and bind a dedicated private R2 bucket:
|
|
375
|
+
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"r2_buckets": [
|
|
379
|
+
{ "binding": "FEEDBAX_IMAGES", "bucket_name": "your-installation-images" }
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Create the bucket with `pnpm exec wrangler r2 bucket create your-installation-images`. Keep its r2.dev endpoint and public custom-domain access disabled; only Feedbax serves its objects. Apply all included D1 migrations. `PARTICIPATION_SIGNING_SECRET` must contain at least 32 random characters; it already supports participation capabilities and also signs scoped image review and unsubscribe links using independent purposes.
|
|
385
|
+
|
|
386
|
+
Verified Participants can add up to four JPEG, PNG, or WebP images to a new Post or Comment. Each image is at most 5 MiB. The server checks MIME type, image signatures, size, ownership, and association permissions; SVG, video, and general file attachments are rejected. The form shows upload progress, previews, and removal controls and waits for uploads before submission. Text is still required. Images can be added on creation; replacing attachments on an existing Post or Comment is not part of this release.
|
|
387
|
+
|
|
388
|
+
Images attach to the newly created Notion content through D1 records. Pending uploads are private to their authenticated uploader and expire after 24 hours. Scheduled cleanup removes unused uploads and retries interrupted deletions. Public image requests recheck Post publication and, for Comment images, that the Comment still exists in an open Thread. The uploader can inspect pending images and their own existing Draft Post images privately; removing the associated content also removes uploader and review-link access. Unpublishing a Post or resolving/deleting a Comment stops public image access; downloads use no-store and cannot be embedded from another origin.
|
|
389
|
+
|
|
390
|
+
For a Draft Post, Feedbax writes read-only image review links into its private Notion page body. Team Members use them to inspect attachments before publication. These links are bearer capabilities: keep the page body private, do not paste its review URLs into public Description or Team Response fields, and restrict Notion workspace access normally. Public Post projections contain only the ordinary visibility-checked image route, never the review token. Rotating `PARTICIPATION_SIGNING_SECRET` invalidates existing private review links and email unsubscribe links as well as older participation capabilities.
|
|
391
|
+
|
|
392
|
+
Images retain their original bytes; Feedbax does not transcode them or strip EXIF. R2 and D1 need coordinated backups because the association records authorize access to the objects. Removing the R2 binding must be paired with disabling `connected.images`; missing storage fails closed. Images are not included in the stable trusted `/api/v1/posts` submission contract or the copied feedback block in this release.
|
|
393
|
+
|
|
394
|
+
## Expanded production acceptance
|
|
395
|
+
|
|
396
|
+
In a dedicated Installation, apply all D1 migrations, configure the timer and processing queue for small mode, verify the sender domain, and use a private R2 bucket. Run `feedbax doctor`, then verify:
|
|
397
|
+
|
|
398
|
+
1. Sign in from two browsers through the real product app, including a verified email assertion. Follow/unfollow the same Published Post and check continuity. Confirm a Device Profile cannot substitute for identity.
|
|
399
|
+
2. Change a followed Post's status and add a Product Team Comment directly in Notion. Wait for the timer and queue consumer (or the standard scheduled invocation). Inspect their separate outcomes and CPU limits. Confirm exactly one in-app event per observed change, private read state across browsers, and no access from another Participant.
|
|
400
|
+
3. Opt in to email for a test recipient, confirm provider acceptance and actual receipt, force a retry in the dedicated test adapter, and verify its documented deduplication behavior. Check unsubscribe GET is non-mutating and POST disables future email while retaining in-app updates.
|
|
401
|
+
4. Upload an image to a Draft Post. Confirm only the uploader and private Notion review link can access it; publish the Post and confirm public display, then unpublish and confirm public access stops. Repeat for a Comment and resolve it in Notion.
|
|
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
|
+
|
|
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.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Your Feedbax portal
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
[Feedbax on GitHub](https://github.com/jessequartey/feedbax) · [Setup and operations](https://feedbax-docs.jessefquartey.workers.dev/docs)
|
|
6
|
+
|
|
7
|
+
## Start locally
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm dev
|
|
13
|
+
```
|
|
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 still requires provider configuration.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
31
|
+
## Check and deploy
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm exec feedbax doctor
|
|
35
|
+
pnpm format:check
|
|
36
|
+
pnpm lint
|
|
37
|
+
pnpm type-check
|
|
38
|
+
pnpm test
|
|
39
|
+
pnpm build
|
|
40
|
+
pnpm deploy:dry-run
|
|
41
|
+
pnpm exec feedbax deploy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`doctor` checks actual configuration, permissions, and enabled bindings. `deploy:dry-run` checks the bundle. The final command publishes the configured Worker and uploads only supported secrets. Confirm the live URL and enabled flows afterward.
|
|
45
|
+
|
|
46
|
+
## Customize and integrate
|
|
47
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
54
|
+
See `AGENTS.md` for the short agent workflow.
|
|
55
|
+
|
|
56
|
+
## Connected social features
|
|
57
|
+
|
|
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.
|