create-feedbax 0.2.0 → 0.2.1
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/dist/cli.js +2042 -16
- package/dist/index.js +1997 -5
- package/package.json +1 -1
- package/template/.feedbax-template.sha256 +1 -1
- package/template/AGENTS.md +13 -0
- package/template/CONNECTED.md +404 -0
- package/template/README.md +58 -0
- package/template/components.json +1 -1
- package/template/eslint.config.mjs +23 -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/package.json +9 -5
- package/template/src/authorized-draft-post.tsx +1 -0
- package/template/src/changelog-page.tsx +63 -54
- package/template/src/cloudflare-workers.d.ts +6 -0
- package/template/src/comment-creation.test.tsx +42 -1
- 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 +108 -0
- package/template/src/components/device-profile-provider.tsx +41 -26
- package/template/src/components/follow-post-button.tsx +89 -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/profile-menu.tsx +30 -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/connected-identity.test.ts +144 -0
- package/template/src/connected-identity.ts +122 -0
- package/template/src/connected-participation.test.tsx +67 -0
- package/template/src/connected-runtime.test.ts +241 -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 +1 -0
- package/template/src/feedback-runtime.ts +79 -12
- package/template/src/feedbax/config/index.test.ts +28 -0
- package/template/src/feedbax/config/index.ts +93 -0
- package/template/src/feedbax/feedback/connected-feedback.ts +206 -0
- package/template/src/feedbax/feedback/connected.test.ts +272 -0
- package/template/src/feedbax/feedback/connected.ts +215 -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/identity-handoff.test.ts +81 -0
- package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
- package/template/src/feedbax/feedback/image-feedback.ts +101 -0
- package/template/src/feedbax/feedback/images.test.ts +223 -0
- package/template/src/feedbax/feedback/images.ts +324 -0
- package/template/src/feedbax/feedback/index.ts +55 -1
- package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
- package/template/src/feedbax/feedback/participation.test.ts +653 -0
- package/template/src/feedbax/feedback/participation.ts +502 -0
- package/template/src/images-runtime.test.ts +133 -0
- package/template/src/images-runtime.ts +125 -0
- package/template/src/index.css +97 -440
- package/template/src/lazy-post-detail-overlay.tsx +13 -2
- 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 +24 -5
- package/template/src/portal-feedback-form.tsx +18 -3
- 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 +10 -0
- package/template/src/portal-server-entry.ts +4 -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-post-detail-skeleton.tsx +54 -0
- package/template/src/public-post-detail.tsx +298 -208
- package/template/src/public-post-route.test.tsx +39 -3
- package/template/src/public-post-route.tsx +15 -28
- 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 +49 -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/submit.tsx +1 -1
- package/template/src/safe-public-failure.ts +3 -1
- package/template/src/server.ts +41 -1
- package/template/src/trusted-feedback-handler.test.ts +1 -1
- package/template/src/vote-toggle.tsx +42 -9
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ImageAttachmentError,
|
|
3
|
+
type createImageModule,
|
|
4
|
+
} from "@feedbax/feedback";
|
|
5
|
+
import { env } from "cloudflare:workers";
|
|
6
|
+
import feedbax from "./feedbax";
|
|
7
|
+
import {
|
|
8
|
+
configuredImages,
|
|
9
|
+
createConfiguredFeedbackModule,
|
|
10
|
+
requestParticipant,
|
|
11
|
+
} from "./feedback-runtime";
|
|
12
|
+
import { sameOrigin } from "./connected-identity";
|
|
13
|
+
|
|
14
|
+
export function createImageRequestHandler(options: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
siteUrl: string;
|
|
17
|
+
getImages(request: Request): Promise<ReturnType<typeof createImageModule>>;
|
|
18
|
+
signedIn(request: Request): Promise<boolean>;
|
|
19
|
+
rateLimit(request: Request): Promise<boolean>;
|
|
20
|
+
}) {
|
|
21
|
+
return async (request: Request): Promise<Response | undefined> => {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
if (
|
|
24
|
+
url.pathname !== "/internal/images" &&
|
|
25
|
+
!url.pathname.startsWith("/internal/images/")
|
|
26
|
+
)
|
|
27
|
+
return;
|
|
28
|
+
const json = (body: unknown, status = 200) =>
|
|
29
|
+
Response.json(body, { status, headers: { "Cache-Control": "no-store" } });
|
|
30
|
+
if (!options.enabled) return json({ error: "Not found." }, 404);
|
|
31
|
+
const id = url.pathname.slice("/internal/images/".length);
|
|
32
|
+
try {
|
|
33
|
+
if (request.method === "GET" && /^[-a-f0-9]{36}$/.test(id)) {
|
|
34
|
+
const image = await (
|
|
35
|
+
await options.getImages(request)
|
|
36
|
+
).read(id, url.searchParams.get("review") ?? undefined);
|
|
37
|
+
if (!image) return json({ error: "Image is unavailable." }, 404);
|
|
38
|
+
return new Response(image.body as BodyInit, {
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": image.contentType,
|
|
41
|
+
"Content-Length": String(image.bytes),
|
|
42
|
+
"Cache-Control": "no-store",
|
|
43
|
+
"X-Content-Type-Options": "nosniff",
|
|
44
|
+
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
45
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
46
|
+
"Referrer-Policy": "no-referrer",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (request.method !== "POST" && request.method !== "DELETE")
|
|
51
|
+
return json({ error: "Method not allowed." }, 405);
|
|
52
|
+
if (!sameOrigin(request, options.siteUrl))
|
|
53
|
+
return json({ error: "Request origin is invalid." }, 403);
|
|
54
|
+
if (!(await options.signedIn(request)))
|
|
55
|
+
return json({ error: "Sign in to add images." }, 401);
|
|
56
|
+
if (!(await options.rateLimit(request)))
|
|
57
|
+
return json(
|
|
58
|
+
{ error: "Too many image requests. Try again shortly." },
|
|
59
|
+
429,
|
|
60
|
+
);
|
|
61
|
+
const images = await options.getImages(request);
|
|
62
|
+
if (request.method === "DELETE") {
|
|
63
|
+
if (!/^[-a-f0-9]{36}$/.test(id))
|
|
64
|
+
return json({ error: "Invalid image." }, 400);
|
|
65
|
+
await images.removePending(id);
|
|
66
|
+
return json({ removed: true });
|
|
67
|
+
}
|
|
68
|
+
if (url.pathname !== "/internal/images")
|
|
69
|
+
return json({ error: "Not found." }, 404);
|
|
70
|
+
const type = request.headers.get("Content-Type") ?? "";
|
|
71
|
+
if (!["image/jpeg", "image/png", "image/webp"].includes(type))
|
|
72
|
+
return json({ error: "Choose a JPEG, PNG, or WebP image." }, 400);
|
|
73
|
+
const max = 5 * 1024 * 1024;
|
|
74
|
+
if (Number(request.headers.get("Content-Length")) > max)
|
|
75
|
+
return json({ error: "Each image must be 5 MiB or smaller." }, 413);
|
|
76
|
+
const reader = request.body?.getReader();
|
|
77
|
+
if (!reader) return json({ error: "Choose an image." }, 400);
|
|
78
|
+
const chunks: Uint8Array[] = [];
|
|
79
|
+
let length = 0;
|
|
80
|
+
while (true) {
|
|
81
|
+
const { value, done } = await reader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
length += value.byteLength;
|
|
84
|
+
if (length > max) {
|
|
85
|
+
await reader.cancel();
|
|
86
|
+
return json({ error: "Each image must be 5 MiB or smaller." }, 413);
|
|
87
|
+
}
|
|
88
|
+
chunks.push(value);
|
|
89
|
+
}
|
|
90
|
+
const bytes = new Uint8Array(length);
|
|
91
|
+
let offset = 0;
|
|
92
|
+
for (const chunk of chunks) {
|
|
93
|
+
bytes.set(chunk, offset);
|
|
94
|
+
offset += chunk.byteLength;
|
|
95
|
+
}
|
|
96
|
+
return json(await images.upload(bytes, type), 201);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return json(
|
|
99
|
+
{
|
|
100
|
+
error:
|
|
101
|
+
error instanceof ImageAttachmentError
|
|
102
|
+
? error.message
|
|
103
|
+
: "Images are temporarily unavailable. Try again.",
|
|
104
|
+
},
|
|
105
|
+
error instanceof ImageAttachmentError ? 400 : 503,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export const handleImageRequest = createImageRequestHandler({
|
|
111
|
+
enabled: Boolean(feedbax.connected?.images),
|
|
112
|
+
siteUrl: feedbax.product.siteUrl ?? "https://not-configured.invalid",
|
|
113
|
+
signedIn: async (request) => Boolean(await requestParticipant(request)),
|
|
114
|
+
getImages: async (request) =>
|
|
115
|
+
configuredImages(
|
|
116
|
+
createConfiguredFeedbackModule(),
|
|
117
|
+
await requestParticipant(request),
|
|
118
|
+
),
|
|
119
|
+
rateLimit: async (request) =>
|
|
120
|
+
(
|
|
121
|
+
await env.PORTAL_SUBMISSIONS_RATE_LIMITER.limit({
|
|
122
|
+
key: `images:${request.headers.get("CF-Connecting-IP") ?? "unknown"}`,
|
|
123
|
+
})
|
|
124
|
+
).success,
|
|
125
|
+
});
|