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
@@ -0,0 +1,120 @@
1
+ import { expect, it } from "vitest";
2
+ import { createFeedbackModule } from "@feedbax/feedback";
3
+ import {
4
+ createChangelogModule,
5
+ createInMemoryChangelogStorage,
6
+ } from "@feedbax/changelog";
7
+ import { createPublicReleaseFeedbackModule } from "./public-release-links";
8
+
9
+ it("links only published local Changelog Entries or safe explicit external release notes", async () => {
10
+ const entry = {
11
+ id: "entry",
12
+ slug: "released-export",
13
+ title: "Export",
14
+ summary: "Public",
15
+ body: "Public",
16
+ date: "2026-10-15" as const,
17
+ labels: [],
18
+ images: [],
19
+ published: true,
20
+ createdAt: new Date(1000),
21
+ updatedAt: new Date(1000),
22
+ };
23
+ const changelog = createChangelogModule({
24
+ storage: createInMemoryChangelogStorage([
25
+ entry,
26
+ { ...entry, id: "draft", slug: "private-release", published: false },
27
+ ]),
28
+ });
29
+ const post = {
30
+ id: "p",
31
+ slug: "export",
32
+ title: "Export",
33
+ description: "Public",
34
+ type: "Feature Request" as const,
35
+ status: "Shipped" as const,
36
+ published: true,
37
+ createdAt: new Date(1000),
38
+ updatedAt: new Date(1000),
39
+ };
40
+ const module = (url: string) =>
41
+ createPublicReleaseFeedbackModule({
42
+ feedback: createFeedbackModule({
43
+ initialItems: [{ ...post, releaseNotesUrl: url }],
44
+ }),
45
+ siteUrl: "https://portal.example",
46
+ changelog,
47
+ });
48
+ expect(
49
+ (await module("/changelog#released-export").getPublicPost(post.slug))
50
+ ?.releaseNotesUrl,
51
+ ).toBe("/changelog#released-export");
52
+ for (const url of [
53
+ "/changelog#private-release",
54
+ "/internal/private",
55
+ "//evil.example",
56
+ "https://user:password@release.example",
57
+ "javascript:alert(1)",
58
+ "https://portal.example/changelog#private-release",
59
+ "https://github.com/private-team/private-repo/issues/1",
60
+ "https://github.com/private-team/private-repo/pull/2",
61
+ "https://github.com/private-team/private-repo/%69ssues/1",
62
+ "https://github.com./private-team/private-repo/issues/1",
63
+ ])
64
+ expect(
65
+ (await module(url).getPublicPost(post.slug))?.releaseNotesUrl,
66
+ ).toBeUndefined();
67
+ expect(
68
+ (
69
+ await module("https://example.com/releases/export").getPublicPost(
70
+ post.slug,
71
+ )
72
+ )?.releaseNotesUrl,
73
+ ).toBe("https://example.com/releases/export");
74
+ expect(
75
+ (await module("/changelog#private-release").listPublicPosts()).items[0]
76
+ ?.releaseNotesUrl,
77
+ ).toBeUndefined();
78
+ });
79
+
80
+ it("checks current public visibility before exposing a GitHub release URL", async () => {
81
+ const post = {
82
+ id: "p",
83
+ slug: "export",
84
+ title: "Export",
85
+ description: "Public",
86
+ type: "Feature Request" as const,
87
+ status: "Shipped" as const,
88
+ published: true,
89
+ createdAt: new Date(1000),
90
+ updatedAt: new Date(1000),
91
+ };
92
+ const module = (owner: string) =>
93
+ createPublicReleaseFeedbackModule({
94
+ feedback: createFeedbackModule({
95
+ initialItems: [
96
+ {
97
+ ...post,
98
+ releaseNotesUrl: `https://github.com/${owner}/feedback/releases/tag/v1`,
99
+ },
100
+ ],
101
+ }),
102
+ siteUrl: "https://portal.example",
103
+ request: async (input, init) => {
104
+ expect(String(input)).toBe(
105
+ `https://api.github.com/repos/${owner}/feedback`,
106
+ );
107
+ expect(init?.redirect).toBe("manual");
108
+ expect(new Headers(init?.headers).has("Authorization")).toBe(false);
109
+ return owner === "public-team"
110
+ ? Response.json({ id: 123, private: false })
111
+ : new Response(null, { status: 404 });
112
+ },
113
+ });
114
+ expect(
115
+ (await module("public-team").getPublicPost(post.slug))?.releaseNotesUrl,
116
+ ).toBe("https://github.com/public-team/feedback/releases/tag/v1");
117
+ expect(
118
+ (await module("private-team").getPublicPost(post.slug))?.releaseNotesUrl,
119
+ ).toBeUndefined();
120
+ });
@@ -0,0 +1,130 @@
1
+ import type { FeedbackModule, PublicPost } from "@feedbax/feedback";
2
+ import type { ChangelogModule } from "@feedbax/changelog";
3
+
4
+ export function createPublicReleaseFeedbackModule(options: {
5
+ feedback: FeedbackModule;
6
+ siteUrl?: string;
7
+ changelog?: ChangelogModule;
8
+ request?: typeof fetch;
9
+ }): FeedbackModule {
10
+ const { feedback } = options;
11
+ let published: Promise<Set<string>> | undefined;
12
+ const publishedSlugs = () =>
13
+ (published ??= (async () => {
14
+ const slugs = new Set<string>();
15
+ if (!options.changelog) return slugs;
16
+ let cursor: string | undefined;
17
+ for (let page = 0; page < 10; page++) {
18
+ const result = await options.changelog.listPublishedEntries({ cursor });
19
+ for (const entry of result.items) slugs.add(entry.slug);
20
+ cursor = result.nextCursor;
21
+ if (!cursor) break;
22
+ }
23
+ return slugs;
24
+ })());
25
+ const resolve = async (value: string) => {
26
+ if (
27
+ /[\\\s]/u.test(value) ||
28
+ Array.from(value).some(
29
+ (character) =>
30
+ character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
31
+ )
32
+ )
33
+ return;
34
+ try {
35
+ const url = new URL(value, options.siteUrl ?? "https://feedbax.invalid");
36
+ if (
37
+ url.protocol !== "https:" ||
38
+ url.username ||
39
+ url.password ||
40
+ value.startsWith("//")
41
+ )
42
+ return;
43
+ const local =
44
+ value.startsWith("/") ||
45
+ (options.siteUrl && url.origin === new URL(options.siteUrl).origin);
46
+ if (!local) {
47
+ const host = url.hostname.toLowerCase().replace(/\.+$/, "");
48
+ if (
49
+ [
50
+ "api.github.com",
51
+ "gist.github.com",
52
+ "raw.githubusercontent.com",
53
+ ].includes(host)
54
+ )
55
+ return;
56
+ if (host === "github.com" || host === "www.github.com") {
57
+ const path = decodeURIComponent(url.pathname);
58
+ const release = path.match(
59
+ /^\/([A-Za-z0-9-]+)\/([A-Za-z0-9_.-]+)\/releases(?:\/|$)/,
60
+ );
61
+ if (!release || path.includes("%")) return;
62
+ const response = await (options.request ?? fetch)(
63
+ `https://api.github.com/repos/${release[1]}/${release[2]}`,
64
+ {
65
+ redirect: "manual",
66
+ signal: AbortSignal.timeout(5000),
67
+ headers: {
68
+ Accept: "application/vnd.github+json",
69
+ "User-Agent": "Feedbax",
70
+ "X-GitHub-Api-Version": "2026-03-10",
71
+ },
72
+ },
73
+ );
74
+ if (!response.ok) return;
75
+ const repository: unknown = await response.json();
76
+ if (
77
+ !repository ||
78
+ typeof repository !== "object" ||
79
+ Reflect.get(repository, "private") !== false
80
+ )
81
+ return;
82
+ }
83
+ return value;
84
+ }
85
+ if (
86
+ url.pathname !== "/changelog" ||
87
+ url.search ||
88
+ !/^#[a-z0-9]+(?:-[a-z0-9]+)*$/.test(url.hash)
89
+ )
90
+ return;
91
+ return (await publishedSlugs()).has(url.hash.slice(1))
92
+ ? value
93
+ : undefined;
94
+ } catch {
95
+ return;
96
+ }
97
+ };
98
+ const project = async <T extends PublicPost>(post: T): Promise<T> => {
99
+ if (!post.releaseNotesUrl) return post;
100
+ const url = await resolve(post.releaseNotesUrl);
101
+ const result = { ...post };
102
+ if (url) result.releaseNotesUrl = url;
103
+ else delete result.releaseNotesUrl;
104
+ return result;
105
+ };
106
+ const roadmap: FeedbackModule["listPublicRoadmapPosts"] = async (query) => {
107
+ const page = await feedback.listPublicRoadmapPosts(query);
108
+ return { ...page, items: await Promise.all(page.items.map(project)) };
109
+ };
110
+ return {
111
+ ...feedback,
112
+ async getPublicPost(slug) {
113
+ const post = await feedback.getPublicPost(slug);
114
+ return post ? project(post) : undefined;
115
+ },
116
+ async listPublicPosts(query) {
117
+ const page = await feedback.listPublicPosts(query);
118
+ return { ...page, items: await Promise.all(page.items.map(project)) };
119
+ },
120
+ listPublicRoadmapPosts: roadmap,
121
+ async getPublicPostRoadmap() {
122
+ const [planned, progress, shipped] = await Promise.all([
123
+ roadmap({ status: "Planned" }),
124
+ roadmap({ status: "In Progress" }),
125
+ roadmap({ status: "Shipped" }),
126
+ ]);
127
+ return { Planned: planned, "In Progress": progress, Shipped: shipped };
128
+ },
129
+ };
130
+ }
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
12
12
  import { Route as IndexRouteImport } from './routes/index'
13
13
  import { Route as ChangelogRouteImport } from './routes/changelog'
14
14
  import { Route as EmailPreferencesRouteImport } from './routes/email-preferences'
15
+ import { Route as MyFeedbackRouteImport } from './routes/my-feedback'
15
16
  import { Route as RoadmapRouteImport } from './routes/roadmap'
16
17
  import { Route as SubmitRouteImport } from './routes/submit'
17
18
  import { Route as PSlugRouteImport } from './routes/p.$slug'
@@ -31,6 +32,11 @@ const EmailPreferencesRoute = EmailPreferencesRouteImport.update({
31
32
  path: '/email-preferences',
32
33
  getParentRoute: () => rootRouteImport,
33
34
  } as any)
35
+ const MyFeedbackRoute = MyFeedbackRouteImport.update({
36
+ id: '/my-feedback',
37
+ path: '/my-feedback',
38
+ getParentRoute: () => rootRouteImport,
39
+ } as any)
34
40
  const RoadmapRoute = RoadmapRouteImport.update({
35
41
  id: '/roadmap',
36
42
  path: '/roadmap',
@@ -51,6 +57,7 @@ export interface FileRoutesByFullPath {
51
57
  '/': typeof IndexRoute
52
58
  '/changelog': typeof ChangelogRoute
53
59
  '/email-preferences': typeof EmailPreferencesRoute
60
+ '/my-feedback': typeof MyFeedbackRoute
54
61
  '/roadmap': typeof RoadmapRoute
55
62
  '/submit': typeof SubmitRoute
56
63
  '/p/$slug': typeof PSlugRoute
@@ -59,6 +66,7 @@ export interface FileRoutesByTo {
59
66
  '/': typeof IndexRoute
60
67
  '/changelog': typeof ChangelogRoute
61
68
  '/email-preferences': typeof EmailPreferencesRoute
69
+ '/my-feedback': typeof MyFeedbackRoute
62
70
  '/roadmap': typeof RoadmapRoute
63
71
  '/submit': typeof SubmitRoute
64
72
  '/p/$slug': typeof PSlugRoute
@@ -68,6 +76,7 @@ export interface FileRoutesById {
68
76
  '/': typeof IndexRoute
69
77
  '/changelog': typeof ChangelogRoute
70
78
  '/email-preferences': typeof EmailPreferencesRoute
79
+ '/my-feedback': typeof MyFeedbackRoute
71
80
  '/roadmap': typeof RoadmapRoute
72
81
  '/submit': typeof SubmitRoute
73
82
  '/p/$slug': typeof PSlugRoute
@@ -78,6 +87,7 @@ export interface FileRouteTypes {
78
87
  | '/'
79
88
  | '/changelog'
80
89
  | '/email-preferences'
90
+ | '/my-feedback'
81
91
  | '/roadmap'
82
92
  | '/submit'
83
93
  | '/p/$slug'
@@ -86,6 +96,7 @@ export interface FileRouteTypes {
86
96
  | '/'
87
97
  | '/changelog'
88
98
  | '/email-preferences'
99
+ | '/my-feedback'
89
100
  | '/roadmap'
90
101
  | '/submit'
91
102
  | '/p/$slug'
@@ -94,6 +105,7 @@ export interface FileRouteTypes {
94
105
  | '/'
95
106
  | '/changelog'
96
107
  | '/email-preferences'
108
+ | '/my-feedback'
97
109
  | '/roadmap'
98
110
  | '/submit'
99
111
  | '/p/$slug'
@@ -103,6 +115,7 @@ export interface RootRouteChildren {
103
115
  IndexRoute: typeof IndexRoute
104
116
  ChangelogRoute: typeof ChangelogRoute
105
117
  EmailPreferencesRoute: typeof EmailPreferencesRoute
118
+ MyFeedbackRoute: typeof MyFeedbackRoute
106
119
  RoadmapRoute: typeof RoadmapRoute
107
120
  SubmitRoute: typeof SubmitRoute
108
121
  PSlugRoute: typeof PSlugRoute
@@ -131,6 +144,13 @@ declare module '@tanstack/react-router' {
131
144
  preLoaderRoute: typeof EmailPreferencesRouteImport
132
145
  parentRoute: typeof rootRouteImport
133
146
  }
147
+ '/my-feedback': {
148
+ id: '/my-feedback'
149
+ path: '/my-feedback'
150
+ fullPath: '/my-feedback'
151
+ preLoaderRoute: typeof MyFeedbackRouteImport
152
+ parentRoute: typeof rootRouteImport
153
+ }
134
154
  '/roadmap': {
135
155
  id: '/roadmap'
136
156
  path: '/roadmap'
@@ -159,6 +179,7 @@ const rootRouteChildren: RootRouteChildren = {
159
179
  IndexRoute: IndexRoute,
160
180
  ChangelogRoute: ChangelogRoute,
161
181
  EmailPreferencesRoute: EmailPreferencesRoute,
182
+ MyFeedbackRoute: MyFeedbackRoute,
162
183
  RoadmapRoute: RoadmapRoute,
163
184
  SubmitRoute: SubmitRoute,
164
185
  PSlugRoute: PSlugRoute,
@@ -0,0 +1,16 @@
1
+ import { createFileRoute, notFound } from "@tanstack/react-router";
2
+ import feedbax from "../feedbax";
3
+ import { MyFeedbackPage } from "../my-feedback-page";
4
+ export const Route = createFileRoute("/my-feedback")({
5
+ beforeLoad: () => {
6
+ if (!feedbax.connected) throw notFound();
7
+ },
8
+ head: () => ({
9
+ meta: [
10
+ { title: "My feedback" },
11
+ { name: "robots", content: "noindex,nofollow" },
12
+ { name: "referrer", content: "no-referrer" },
13
+ ],
14
+ }),
15
+ component: MyFeedbackPage,
16
+ });
@@ -1,3 +1,5 @@
1
+ import { createMyFeedbackRequestHandler } from "./my-feedback-runtime";
2
+ import { createPublicHistoryRequestHandler } from "./history-runtime";
1
3
  import { handleImageRequest } from "./images-runtime";
2
4
  import {
3
5
  handleParticipationRequest,
@@ -8,7 +10,13 @@ import { configuredTurnstileSecret } from "./turnstile-configuration";
8
10
  import { createPortalServerEntry } from "./portal-server-entry";
9
11
  import { createConfiguredTrustedFeedbackHandler } from "./trusted-feedback-runtime";
10
12
  import { env } from "cloudflare:workers";
11
- import { createConfiguredFeedbackModule } from "./feedback-runtime";
13
+ import {
14
+ configuredIntegration,
15
+ configuredHistory,
16
+ createConfiguredContentFeedbackModule,
17
+ createConfiguredFeedbackModule,
18
+ requestParticipant,
19
+ } from "./feedback-runtime";
12
20
  import { createPortalVoteHandler } from "./portal-voting";
13
21
  import { createCloudflareTurnstileVerifier } from "./cloudflare-turnstile";
14
22
  import feedbax from "./feedbax";
@@ -20,7 +28,39 @@ import {
20
28
  } from "./portal-comments";
21
29
 
22
30
  const application = createPortalServerEntry({
31
+ cachePublicReads: feedbax.provider !== "github" && !feedbax.history,
23
32
  connectedHandler: async (request) =>
33
+ (await createPublicHistoryRequestHandler({
34
+ enabled: Boolean(feedbax.history),
35
+ history: feedbax.history ? configuredHistory() : undefined,
36
+ rateLimit: async (input) =>
37
+ (
38
+ await env.TRUSTED_SUBMISSIONS_RATE_LIMITER.limit({
39
+ key: `history:${input.headers.get("CF-Connecting-IP") ?? "unknown"}`,
40
+ })
41
+ ).success,
42
+ })(request)) ??
43
+ (feedbax.connected &&
44
+ env.FEEDBAX_DB &&
45
+ request.url.includes("/internal/my-feedback")
46
+ ? await createMyFeedbackRequestHandler({
47
+ enabled: true,
48
+ siteUrl: feedbax.product.siteUrl!,
49
+ database: env.FEEDBAX_DB,
50
+ getParticipant: requestParticipant,
51
+ getFeedback: createConfiguredFeedbackModule,
52
+ rateLimit: async (input) =>
53
+ (
54
+ await env.TRUSTED_SUBMISSIONS_RATE_LIMITER.limit({
55
+ key: `my-feedback:${input.headers.get("CF-Connecting-IP") ?? "unknown"}`,
56
+ })
57
+ ).success,
58
+ limits: feedbax.submissions,
59
+ })(request)
60
+ : undefined) ??
61
+ (feedbax.provider === "github"
62
+ ? await configuredIntegration().handle(request)
63
+ : undefined) ??
24
64
  (await handleImageRequest(request)) ??
25
65
  (await handleParticipationRequest(request)) ??
26
66
  handleConnectedRequest(request),
@@ -123,6 +163,11 @@ const application = createPortalServerEntry({
123
163
  export default {
124
164
  ...application,
125
165
  async scheduled() {
166
+ if (feedbax.history) await configuredHistory().synchronize();
167
+ if (feedbax.provider === "github")
168
+ await configuredIntegration().reconcile(
169
+ createConfiguredContentFeedbackModule(),
170
+ );
126
171
  if (!feedbax.connected) return;
127
172
  if (feedbax.connected.processing === "standard") {
128
173
  await synchronizeConnectedUpdates();
@@ -0,0 +1,26 @@
1
+ // Session storage is tab-scoped. A failed request reuses the same secret intent
2
+ // across a retry/reload; successful completion makes a later submission distinct.
3
+ export async function submissionIntent(
4
+ storage: Pick<Storage, "getItem" | "setItem" | "removeItem">,
5
+ input: unknown,
6
+ ) {
7
+ const bytes = await crypto.subtle.digest(
8
+ "SHA-256",
9
+ new TextEncoder().encode(JSON.stringify(input)),
10
+ );
11
+ const digest = Array.from(new Uint8Array(bytes), (value) =>
12
+ value.toString(16).padStart(2, "0"),
13
+ ).join("");
14
+ const slot = `feedbax:pending-submission:${digest}`;
15
+ let key = storage.getItem(slot);
16
+ if (!key || !/^[A-Za-z0-9_-]{43}$/.test(key)) {
17
+ key = btoa(
18
+ String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))),
19
+ )
20
+ .replaceAll("+", "-")
21
+ .replaceAll("/", "_")
22
+ .replace(/=+$/, "");
23
+ storage.setItem(slot, key);
24
+ }
25
+ return { key, complete: () => storage.removeItem(slot) };
26
+ }
@@ -3,6 +3,7 @@ import tailwindcss from "@tailwindcss/vite";
3
3
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
4
4
  import viteReact from "@vitejs/plugin-react";
5
5
  import { defineConfig } from "vite";
6
+ import { publicFeedbaxConfiguration } from "./feedbax-vite";
6
7
 
7
8
  export default defineConfig({
8
9
  server: {
@@ -12,6 +13,7 @@ export default defineConfig({
12
13
  tsconfigPaths: true,
13
14
  },
14
15
  plugins: [
16
+ publicFeedbaxConfiguration(),
15
17
  cloudflare({ viteEnvironment: { name: "ssr" } }),
16
18
  tailwindcss(),
17
19
  tanstackStart({ server: { entry: "./server.ts" } }),