@dirathea/busical 0.1.3

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.
@@ -0,0 +1 @@
1
+ {"configPath":"/home/runner/work/getbusicalapp/getbusicalapp/wrangler.jsonc","userConfigPath":"/home/runner/work/getbusicalapp/getbusicalapp/wrangler.jsonc","topLevelName":"getbusicalapp","definedEnvironments":[],"legacy_env":true,"compatibility_date":"2025-04-03","compatibility_flags":[],"jsx_factory":"React.createElement","jsx_fragment":"React.Fragment","rules":[{"type":"ESModule","globs":["**/*.js","**/*.mjs"]}],"name":"getbusicalapp","main":"index.js","triggers":{},"assets":{"directory":"../client","not_found_handling":"single-page-application"},"vars":{},"durable_objects":{"bindings":[]},"workflows":[],"migrations":[],"kv_namespaces":[],"cloudchamber":{},"send_email":[],"queues":{"producers":[],"consumers":[]},"r2_buckets":[],"d1_databases":[],"vectorize":[],"hyperdrive":[],"services":[],"analytics_engine_datasets":[],"dispatch_namespaces":[],"mtls_certificates":[],"pipelines":[],"secrets_store_secrets":[],"unsafe_hello_world":[],"worker_loaders":[],"ratelimits":[],"vpc_services":[],"logfwdr":{"bindings":[]},"python_modules":{"exclude":["**/*.pyc"]},"dev":{"ip":"localhost","local_protocol":"http","upstream_protocol":"http","enable_containers":true,"generate_types":false},"no_bundle":true}
@@ -0,0 +1,224 @@
1
+ import { cors } from "hono/cors";
2
+ const setup = (app, env) => {
3
+ // Get allowed origin from environment variable or fallback to wildcard for dev
4
+ const allowedOrigin = env?.ALLOWED_ORIGIN || "*";
5
+ // Enable CORS for all routes
6
+ app.use("*", cors({
7
+ origin: allowedOrigin,
8
+ allowMethods: ["GET", "POST", "OPTIONS"],
9
+ allowHeaders: ["Content-Type", "Authorization", "Accept", "User-Agent"],
10
+ exposeHeaders: ["Content-Length", "Content-Type"],
11
+ maxAge: 86400, // 24 hours
12
+ credentials: false,
13
+ }));
14
+ // Security headers middleware (applied to all routes)
15
+ app.use("*", async (c, next) => {
16
+ await next();
17
+ // Prevent MIME type sniffing
18
+ c.header('X-Content-Type-Options', 'nosniff');
19
+ // Prevent clickjacking
20
+ c.header('X-Frame-Options', 'DENY');
21
+ // Enable XSS protection
22
+ c.header('X-XSS-Protection', '1; mode=block');
23
+ // Referrer policy
24
+ c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
25
+ // Note: CSP is handled by index.html meta tag for static files
26
+ // Proxy endpoints have their own strict CSP below
27
+ });
28
+ // Strict CSP for proxy endpoint only (API responses don't need scripts/styles)
29
+ app.use("/proxy", async (c, next) => {
30
+ await next();
31
+ c.header('Content-Security-Policy', "default-src 'none'; connect-src 'self'");
32
+ });
33
+ // Health check endpoint
34
+ app.get("/health", (c) => {
35
+ return c.json({ status: "ok", timestamp: new Date().toISOString() });
36
+ });
37
+ // GET endpoint for ICS proxy (primary method)
38
+ app.get("/proxy", async (c) => {
39
+ try {
40
+ const url = c.req.query("url");
41
+ if (!url) {
42
+ return c.json({
43
+ success: false,
44
+ error: "Missing url query parameter",
45
+ }, 400);
46
+ }
47
+ // Validate URL to prevent SSRF attacks
48
+ let urlObj;
49
+ try {
50
+ urlObj = new URL(url);
51
+ }
52
+ catch {
53
+ return c.json({
54
+ success: false,
55
+ error: "Invalid URL format",
56
+ }, 400);
57
+ }
58
+ if (!["http:", "https:"].includes(urlObj.protocol)) {
59
+ return c.json({
60
+ success: false,
61
+ error: "Invalid URL protocol. Only http and https are allowed.",
62
+ }, 400);
63
+ }
64
+ // Fetch the ICS file
65
+ const response = await fetch(url, {
66
+ method: "GET",
67
+ headers: {
68
+ "User-Agent": "BusiCal-Proxy/1.0 (Calendar Sync App)",
69
+ Accept: "text/calendar, text/plain, */*",
70
+ },
71
+ });
72
+ // Check file size limit (5MB max)
73
+ const MAX_ICS_SIZE = 5 * 1024 * 1024; // 5MB
74
+ const contentLength = response.headers.get('content-length');
75
+ if (contentLength && parseInt(contentLength) > MAX_ICS_SIZE) {
76
+ return c.json({
77
+ success: false,
78
+ error: "Calendar file too large (max 5MB)",
79
+ }, 413 // Payload Too Large
80
+ );
81
+ }
82
+ if (!response.ok) {
83
+ return c.json({
84
+ success: false,
85
+ error: "Failed to fetch calendar",
86
+ upstreamStatus: response.status,
87
+ upstreamStatusText: response.statusText,
88
+ }, 502); // Bad Gateway
89
+ }
90
+ // Get the ICS data as plain text
91
+ const icsData = await response.text();
92
+ // Double-check actual size (in case Content-Length header was missing)
93
+ if (icsData.length > MAX_ICS_SIZE) {
94
+ return c.json({
95
+ success: false,
96
+ error: "Calendar file too large (max 5MB)",
97
+ }, 413);
98
+ }
99
+ // Validate that we got ICS-like content
100
+ if (!icsData.includes("BEGIN:VCALENDAR")) {
101
+ return c.json({
102
+ success: false,
103
+ error: "Response does not appear to be a valid ICS file",
104
+ }, 400);
105
+ }
106
+ // Return success with ICS data
107
+ return c.json({
108
+ success: true,
109
+ data: icsData,
110
+ contentType: response.headers.get("content-type") || "text/calendar",
111
+ });
112
+ }
113
+ catch (error) {
114
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
115
+ // No logging - privacy first
116
+ return c.json({
117
+ success: false,
118
+ error: "Proxy request failed",
119
+ details: errorMessage,
120
+ }, 500);
121
+ }
122
+ });
123
+ // POST endpoint for ICS proxy (alternative method)
124
+ app.post("/proxy", async (c) => {
125
+ try {
126
+ const body = await c.req.json();
127
+ const { url } = body;
128
+ if (!url) {
129
+ return c.json({
130
+ success: false,
131
+ error: "Missing url parameter",
132
+ }, 400);
133
+ }
134
+ // Validate URL
135
+ let urlObj;
136
+ try {
137
+ urlObj = new URL(url);
138
+ }
139
+ catch {
140
+ return c.json({
141
+ success: false,
142
+ error: "Invalid URL format",
143
+ }, 400);
144
+ }
145
+ if (!["http:", "https:"].includes(urlObj.protocol)) {
146
+ return c.json({
147
+ success: false,
148
+ error: "Invalid URL protocol",
149
+ }, 400);
150
+ }
151
+ const response = await fetch(url, {
152
+ method: "GET",
153
+ headers: {
154
+ "User-Agent": "BusiCal-Proxy/1.0",
155
+ Accept: "text/calendar, text/plain, */*",
156
+ },
157
+ });
158
+ // Check file size limit (5MB max)
159
+ const MAX_ICS_SIZE = 5 * 1024 * 1024; // 5MB
160
+ const contentLength = response.headers.get('content-length');
161
+ if (contentLength && parseInt(contentLength) > MAX_ICS_SIZE) {
162
+ return c.json({
163
+ success: false,
164
+ error: "Calendar file too large (max 5MB)",
165
+ }, 413 // Payload Too Large
166
+ );
167
+ }
168
+ if (!response.ok) {
169
+ return c.json({
170
+ success: false,
171
+ error: "Failed to fetch calendar",
172
+ upstreamStatus: response.status,
173
+ upstreamStatusText: response.statusText,
174
+ }, 502); // Bad Gateway
175
+ }
176
+ const icsData = await response.text();
177
+ // Double-check actual size
178
+ if (icsData.length > MAX_ICS_SIZE) {
179
+ return c.json({
180
+ success: false,
181
+ error: "Calendar file too large (max 5MB)",
182
+ }, 413);
183
+ }
184
+ if (!icsData.includes("BEGIN:VCALENDAR")) {
185
+ return c.json({
186
+ success: false,
187
+ error: "Response does not appear to be a valid ICS file",
188
+ }, 400);
189
+ }
190
+ return c.json({
191
+ success: true,
192
+ data: icsData,
193
+ });
194
+ }
195
+ catch (error) {
196
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
197
+ // No logging - privacy first
198
+ return c.json({
199
+ success: false,
200
+ error: "Proxy request failed",
201
+ details: errorMessage,
202
+ }, 500);
203
+ }
204
+ });
205
+ // 404 handler
206
+ app.notFound((c) => {
207
+ return c.json({
208
+ success: false,
209
+ error: "Not found",
210
+ path: c.req.path,
211
+ }, 404);
212
+ });
213
+ // Error handler
214
+ app.onError((err, c) => {
215
+ // No logging - privacy first
216
+ return c.json({
217
+ success: false,
218
+ error: "Internal server error",
219
+ message: err.message,
220
+ }, 500);
221
+ });
222
+ return app;
223
+ };
224
+ export { setup };
@@ -0,0 +1,18 @@
1
+ import { Hono } from "hono";
2
+ import { serve } from "@hono/node-server";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { setup } from "./app.js";
5
+ const app = new Hono();
6
+ const port = parseInt(process.env.PORT || "3000", 10);
7
+ const allowedOrigin = process.env.ALLOWED_ORIGIN || "*";
8
+ console.log("Starting BusiCal server...");
9
+ console.log(`Port: ${port}`);
10
+ console.log(`CORS Origin: ${allowedOrigin}`);
11
+ setup(app, { ALLOWED_ORIGIN: allowedOrigin });
12
+ // Serve static files from dist/client directory
13
+ app.use("/*", serveStatic({ root: "./dist/client" }));
14
+ // SPA fallback
15
+ app.get("*", serveStatic({ path: "./dist/client/index.html" }));
16
+ serve({ fetch: app.fetch, port }, (info) => {
17
+ console.log(`BusiCal server ready on http://localhost:${info.port}`);
18
+ });
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@dirathea/busical",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "description": "Sync calendar events to your work calendar - with privacy",
6
+ "license": "MIT",
7
+ "author": "dirathea",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/dirathea/getbusicalapp"
11
+ },
12
+ "homepage": "https://github.com/dirathea/getbusicalapp",
13
+ "bugs": {
14
+ "url": "https://github.com/dirathea/getbusicalapp/issues"
15
+ },
16
+ "keywords": [
17
+ "calendar",
18
+ "ics",
19
+ "sync",
20
+ "privacy",
21
+ "busical"
22
+ ],
23
+ "bin": {
24
+ "busical": "./bin/cli.js"
25
+ },
26
+ "files": [
27
+ "bin",
28
+ "dist"
29
+ ],
30
+ "publishConfig": {
31
+ "registry": "https://registry.npmjs.org",
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "dev": "vite",
36
+ "build": "tsc -b && vite build && npm run build:worker",
37
+ "build:worker": "tsc -p tsconfig.worker.json",
38
+ "lint": "eslint .",
39
+ "preview": "vite preview",
40
+ "dev:all": "concurrently 'npm run dev:worker' 'npm run dev'",
41
+ "dev:worker": "wrangler dev --port 8787",
42
+ "dev:node": "node bin/cli.js",
43
+ "deploy": "npm run build && wrangler deploy",
44
+ "changeset": "changeset",
45
+ "version-packages": "changeset version && node scripts/sync-sw-version.cjs",
46
+ "release": "changeset publish"
47
+ },
48
+ "dependencies": {
49
+ "@base-ui/react": "^1.0.0",
50
+ "@fontsource-variable/inter": "^5.2.8",
51
+ "@hono/node-server": "^1.19.9",
52
+ "@tailwindcss/vite": "^4.1.17",
53
+ "class-variance-authority": "^0.7.1",
54
+ "clsx": "^2.1.1",
55
+ "date-fns": "^4.1.0",
56
+ "hono": "^4.11.3",
57
+ "ical.js": "^2.2.1",
58
+ "lucide-react": "^0.562.0",
59
+ "radix-ui": "^1.4.3",
60
+ "react": "^19.2.0",
61
+ "react-dom": "^19.2.0",
62
+ "react-router": "^7.12.0",
63
+ "shadcn": "^3.6.3",
64
+ "tailwind-merge": "^3.4.0",
65
+ "tailwindcss": "^4.1.17",
66
+ "tw-animate-css": "^1.4.0"
67
+ },
68
+ "devDependencies": {
69
+ "@changesets/changelog-github": "^0.5.2",
70
+ "@changesets/cli": "^2.29.8",
71
+ "@cloudflare/vite-plugin": "^1.20.1",
72
+ "@eslint/js": "^9.39.1",
73
+ "@types/node": "^24.10.1",
74
+ "@types/react": "^19.2.5",
75
+ "@types/react-dom": "^19.2.3",
76
+ "@vitejs/plugin-react": "^5.1.1",
77
+ "concurrently": "^9.2.1",
78
+ "eslint": "^9.39.1",
79
+ "eslint-plugin-react-hooks": "^7.0.1",
80
+ "eslint-plugin-react-refresh": "^0.4.24",
81
+ "globals": "^16.5.0",
82
+ "typescript": "~5.9.3",
83
+ "typescript-eslint": "^8.46.4",
84
+ "vite": "^7.3.1",
85
+ "wrangler": "^4.58.0"
86
+ }
87
+ }