@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.
- package/README.md +329 -0
- package/bin/cli.js +38 -0
- package/dist/client/assets/index-BQ96LB2s.css +1 -0
- package/dist/client/assets/index-Dclvnqq8.js +100 -0
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/icons/apple-touch-icon.png +0 -0
- package/dist/client/icons/favicon-96x96.png +0 -0
- package/dist/client/icons/favicon.ico +0 -0
- package/dist/client/icons/favicon.svg +12 -0
- package/dist/client/icons/icon.svg +12 -0
- package/dist/client/icons/web-app-manifest-192x192.png +0 -0
- package/dist/client/icons/web-app-manifest-512x512.png +0 -0
- package/dist/client/index.html +77 -0
- package/dist/client/manifest.json +36 -0
- package/dist/client/sw.js +27 -0
- package/dist/client/vite.svg +1 -0
- package/dist/getbusicalapp/.vite/manifest.json +8 -0
- package/dist/getbusicalapp/index.js +2322 -0
- package/dist/getbusicalapp/wrangler.json +1 -0
- package/dist/worker/app.js +224 -0
- package/dist/worker/node.js +18 -0
- package/package.json +87 -0
|
@@ -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
|
+
}
|