@autumnsgrove/groveengine 0.1.0

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 (219) hide show
  1. package/README.md +163 -0
  2. package/dist/auth/jwt.d.ts +14 -0
  3. package/dist/auth/jwt.js +109 -0
  4. package/dist/auth/session.d.ts +42 -0
  5. package/dist/auth/session.js +105 -0
  6. package/dist/components/admin/GutterManager.svelte +910 -0
  7. package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
  8. package/dist/components/admin/MarkdownEditor.svelte +3114 -0
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
  10. package/dist/components/custom/CollapsibleSection.svelte +74 -0
  11. package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
  12. package/dist/components/custom/ContentWithGutter.svelte +646 -0
  13. package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
  14. package/dist/components/custom/GutterItem.svelte +201 -0
  15. package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
  16. package/dist/components/custom/LeftGutter.svelte +271 -0
  17. package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
  18. package/dist/components/custom/MobileTOC.svelte +273 -0
  19. package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
  20. package/dist/components/custom/TableOfContents.svelte +163 -0
  21. package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
  22. package/dist/components/gallery/ImageGallery.svelte +681 -0
  23. package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
  24. package/dist/components/gallery/Lightbox.svelte +107 -0
  25. package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
  26. package/dist/components/gallery/LightboxCaption.svelte +25 -0
  27. package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
  28. package/dist/components/gallery/ZoomableImage.svelte +163 -0
  29. package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
  30. package/dist/components/ui/Accordion.svelte +74 -0
  31. package/dist/components/ui/Accordion.svelte.d.ts +42 -0
  32. package/dist/components/ui/Badge.svelte +48 -0
  33. package/dist/components/ui/Badge.svelte.d.ts +26 -0
  34. package/dist/components/ui/Button.svelte +74 -0
  35. package/dist/components/ui/Button.svelte.d.ts +34 -0
  36. package/dist/components/ui/Card.svelte +102 -0
  37. package/dist/components/ui/Card.svelte.d.ts +46 -0
  38. package/dist/components/ui/Dialog.svelte +91 -0
  39. package/dist/components/ui/Dialog.svelte.d.ts +43 -0
  40. package/dist/components/ui/Input.svelte +81 -0
  41. package/dist/components/ui/Input.svelte.d.ts +35 -0
  42. package/dist/components/ui/Select.svelte +69 -0
  43. package/dist/components/ui/Select.svelte.d.ts +36 -0
  44. package/dist/components/ui/Sheet.svelte +98 -0
  45. package/dist/components/ui/Sheet.svelte.d.ts +45 -0
  46. package/dist/components/ui/Skeleton.svelte +31 -0
  47. package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
  48. package/dist/components/ui/Table.svelte +59 -0
  49. package/dist/components/ui/Table.svelte.d.ts +44 -0
  50. package/dist/components/ui/Tabs.svelte +76 -0
  51. package/dist/components/ui/Tabs.svelte.d.ts +41 -0
  52. package/dist/components/ui/Textarea.svelte +81 -0
  53. package/dist/components/ui/Textarea.svelte.d.ts +35 -0
  54. package/dist/components/ui/Toast.svelte +18 -0
  55. package/dist/components/ui/Toast.svelte.d.ts +7 -0
  56. package/dist/components/ui/accordion/accordion-content.svelte +24 -0
  57. package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
  58. package/dist/components/ui/accordion/accordion-item.svelte +12 -0
  59. package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
  60. package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
  61. package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
  62. package/dist/components/ui/accordion/index.d.ts +6 -0
  63. package/dist/components/ui/accordion/index.js +8 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/button/button.svelte +82 -0
  69. package/dist/components/ui/button/button.svelte.d.ts +132 -0
  70. package/dist/components/ui/button/index.d.ts +2 -0
  71. package/dist/components/ui/button/index.js +4 -0
  72. package/dist/components/ui/card/card-content.svelte +16 -0
  73. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  74. package/dist/components/ui/card/card-description.svelte +16 -0
  75. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  76. package/dist/components/ui/card/card-footer.svelte +16 -0
  77. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-header.svelte +16 -0
  79. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-title.svelte +25 -0
  81. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  82. package/dist/components/ui/card/card.svelte +20 -0
  83. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/index.d.ts +7 -0
  85. package/dist/components/ui/card/index.js +9 -0
  86. package/dist/components/ui/dialog/dialog-content.svelte +38 -0
  87. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
  88. package/dist/components/ui/dialog/dialog-description.svelte +16 -0
  89. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  90. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  91. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  92. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  93. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  94. package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
  95. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  96. package/dist/components/ui/dialog/dialog-title.svelte +16 -0
  97. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  98. package/dist/components/ui/dialog/index.d.ts +12 -0
  99. package/dist/components/ui/dialog/index.js +14 -0
  100. package/dist/components/ui/index.d.ts +26 -0
  101. package/dist/components/ui/index.js +29 -0
  102. package/dist/components/ui/input/index.d.ts +2 -0
  103. package/dist/components/ui/input/index.js +4 -0
  104. package/dist/components/ui/input/input.svelte +46 -0
  105. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  106. package/dist/components/ui/select/index.d.ts +11 -0
  107. package/dist/components/ui/select/index.js +13 -0
  108. package/dist/components/ui/select/select-content.svelte +39 -0
  109. package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
  110. package/dist/components/ui/select/select-group-heading.svelte +16 -0
  111. package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
  112. package/dist/components/ui/select/select-item.svelte +37 -0
  113. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  114. package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
  115. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  116. package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
  117. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  118. package/dist/components/ui/select/select-separator.svelte +13 -0
  119. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  120. package/dist/components/ui/select/select-trigger.svelte +24 -0
  121. package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
  122. package/dist/components/ui/separator/index.d.ts +2 -0
  123. package/dist/components/ui/separator/index.js +4 -0
  124. package/dist/components/ui/separator/separator.svelte +22 -0
  125. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  126. package/dist/components/ui/sheet/index.d.ts +12 -0
  127. package/dist/components/ui/sheet/index.js +14 -0
  128. package/dist/components/ui/sheet/sheet-content.svelte +53 -0
  129. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
  130. package/dist/components/ui/sheet/sheet-description.svelte +16 -0
  131. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  132. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  133. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  134. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  135. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  136. package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
  137. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
  138. package/dist/components/ui/sheet/sheet-title.svelte +16 -0
  139. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  140. package/dist/components/ui/skeleton/index.d.ts +2 -0
  141. package/dist/components/ui/skeleton/index.js +4 -0
  142. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  143. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  144. package/dist/components/ui/table/index.d.ts +9 -0
  145. package/dist/components/ui/table/index.js +11 -0
  146. package/dist/components/ui/table/table-body.svelte +16 -0
  147. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  148. package/dist/components/ui/table/table-caption.svelte +16 -0
  149. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  150. package/dist/components/ui/table/table-cell.svelte +20 -0
  151. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  152. package/dist/components/ui/table/table-footer.svelte +16 -0
  153. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  154. package/dist/components/ui/table/table-head.svelte +23 -0
  155. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  156. package/dist/components/ui/table/table-header.svelte +16 -0
  157. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/table/table-row.svelte +23 -0
  159. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  160. package/dist/components/ui/table/table.svelte +18 -0
  161. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  162. package/dist/components/ui/tabs/index.d.ts +6 -0
  163. package/dist/components/ui/tabs/index.js +8 -0
  164. package/dist/components/ui/tabs/tabs-content.svelte +19 -0
  165. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  166. package/dist/components/ui/tabs/tabs-list.svelte +19 -0
  167. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  168. package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
  169. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  170. package/dist/components/ui/textarea/index.d.ts +2 -0
  171. package/dist/components/ui/textarea/index.js +4 -0
  172. package/dist/components/ui/textarea/textarea.svelte +24 -0
  173. package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
  174. package/dist/components/ui/toast.d.ts +86 -0
  175. package/dist/components/ui/toast.js +99 -0
  176. package/dist/db/schema.sql +238 -0
  177. package/dist/index.d.ts +14 -0
  178. package/dist/index.js +20 -0
  179. package/dist/payments/index.d.ts +33 -0
  180. package/dist/payments/index.js +47 -0
  181. package/dist/payments/shop.d.ts +165 -0
  182. package/dist/payments/shop.js +588 -0
  183. package/dist/payments/stripe/client.d.ts +231 -0
  184. package/dist/payments/stripe/client.js +198 -0
  185. package/dist/payments/stripe/index.d.ts +18 -0
  186. package/dist/payments/stripe/index.js +17 -0
  187. package/dist/payments/stripe/provider.d.ts +50 -0
  188. package/dist/payments/stripe/provider.js +530 -0
  189. package/dist/payments/types.d.ts +355 -0
  190. package/dist/payments/types.js +7 -0
  191. package/dist/server/logger.d.ts +53 -0
  192. package/dist/server/logger.js +252 -0
  193. package/dist/styles/content.css +514 -0
  194. package/dist/styles/tokens.css +175 -0
  195. package/dist/utils/api.d.ts +20 -0
  196. package/dist/utils/api.js +109 -0
  197. package/dist/utils/cn.d.ts +15 -0
  198. package/dist/utils/cn.js +18 -0
  199. package/dist/utils/csrf.d.ts +22 -0
  200. package/dist/utils/csrf.js +72 -0
  201. package/dist/utils/debounce.d.ts +7 -0
  202. package/dist/utils/debounce.js +14 -0
  203. package/dist/utils/gallery.d.ts +66 -0
  204. package/dist/utils/gallery.js +181 -0
  205. package/dist/utils/gutter.d.ts +54 -0
  206. package/dist/utils/gutter.js +169 -0
  207. package/dist/utils/imageProcessor.d.ts +58 -0
  208. package/dist/utils/imageProcessor.js +205 -0
  209. package/dist/utils/json.d.ts +17 -0
  210. package/dist/utils/json.js +26 -0
  211. package/dist/utils/markdown.d.ts +101 -0
  212. package/dist/utils/markdown.js +947 -0
  213. package/dist/utils/sanitize.d.ts +25 -0
  214. package/dist/utils/sanitize.js +127 -0
  215. package/dist/utils/validation.d.ts +46 -0
  216. package/dist/utils/validation.js +169 -0
  217. package/dist/utils.d.ts +5 -0
  218. package/dist/utils.js +5 -0
  219. package/package.json +129 -0
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @grove/engine
2
+
3
+ Multi-tenant blog engine for the Grove Platform. Each Grove site runs as its own Cloudflare Worker, powered by this engine.
4
+
5
+ ## Features
6
+
7
+ - **Gutter Annotations** - Unique sidebar annotation system for blog posts
8
+ - **Markdown Editor** - Full-featured editor with live preview, themes, and drag-drop image upload
9
+ - **Magic Code Auth** - Passwordless authentication via email codes
10
+ - **Admin Panel** - Complete CMS for posts, pages, images, and settings
11
+ - **Multi-Tenant Ready** - Designed for username.grove.place architecture
12
+ - **Cloudflare Native** - D1 database, R2 storage, KV caching, Workers deployment
13
+
14
+ ## Architecture
15
+
16
+ ```
17
+ Each Grove Site (Cloudflare Worker)
18
+ ├── src/
19
+ │ ├── routes/
20
+ │ │ ├── admin/ # CMS admin panel
21
+ │ │ ├── api/ # REST API endpoints
22
+ │ │ ├── auth/ # Magic code authentication
23
+ │ │ ├── blog/ # Blog listing and posts
24
+ │ │ ├── about/ # Static about page
25
+ │ │ └── contact/ # Static contact page
26
+ │ └── lib/
27
+ │ ├── auth/ # JWT and session management
28
+ │ ├── components/ # Svelte components
29
+ │ │ ├── admin/ # MarkdownEditor, GutterManager
30
+ │ │ ├── custom/ # ContentWithGutter, TableOfContents
31
+ │ │ ├── gallery/ # ImageGallery, Lightbox, ZoomableImage
32
+ │ │ └── ui/ # shadcn-svelte components
33
+ │ ├── utils/ # Markdown parser, CSRF, sanitization
34
+ │ └── styles/ # CSS and tokens
35
+ ├── UserContent/ # Site-specific content
36
+ ├── migrations/ # D1 database migrations
37
+ └── static/fonts/ # Self-hosted fonts
38
+ ```
39
+
40
+ ## Cloudflare Bindings Required
41
+
42
+ ```toml
43
+ # wrangler.toml
44
+ [[d1_databases]]
45
+ binding = "DB"
46
+ database_name = "your-site-db"
47
+ database_id = "your-database-id"
48
+
49
+ [[r2_buckets]]
50
+ binding = "IMAGES"
51
+ bucket_name = "your-site-images"
52
+
53
+ [[kv_namespaces]]
54
+ binding = "CACHE"
55
+ id = "your-kv-id"
56
+ ```
57
+
58
+ ## Environment Variables
59
+
60
+ ```bash
61
+ # .dev.vars
62
+ JWT_SECRET=your-secret-key
63
+ ALLOWED_ADMIN_EMAILS=admin@example.com
64
+ RESEND_API_KEY=re_xxxxx
65
+ ```
66
+
67
+ ## Quick Start
68
+
69
+ 1. **Clone and install**
70
+ ```bash
71
+ cd packages/engine
72
+ npm install
73
+ ```
74
+
75
+ 2. **Set up Cloudflare resources**
76
+ ```bash
77
+ npx wrangler d1 create your-site-db
78
+ npx wrangler r2 bucket create your-site-images
79
+ npx wrangler kv:namespace create CACHE
80
+ ```
81
+
82
+ 3. **Run migrations**
83
+ ```bash
84
+ npx wrangler d1 execute your-site-db --local --file=migrations/001_magic_codes.sql
85
+ npx wrangler d1 execute your-site-db --local --file=migrations/002_auth_security.sql
86
+ npx wrangler d1 execute your-site-db --local --file=migrations/003_site_settings.sql
87
+ npx wrangler d1 execute your-site-db --local --file=migrations/004_pages_table.sql
88
+ npx wrangler d1 execute your-site-db --local --file=migrations/005_multi_tenant.sql
89
+ ```
90
+
91
+ 4. **Configure environment**
92
+ ```bash
93
+ cp .dev.vars.example .dev.vars
94
+ # Edit .dev.vars with your values
95
+ ```
96
+
97
+ 5. **Start development**
98
+ ```bash
99
+ npm run dev
100
+ ```
101
+
102
+ 6. **Deploy**
103
+ ```bash
104
+ npx wrangler pages deploy
105
+ ```
106
+
107
+ ## Key Components
108
+
109
+ ### Gutter System
110
+ The unique gutter annotation system allows sidebar notes on blog posts:
111
+ - `ContentWithGutter.svelte` - Main content layout
112
+ - `GutterItem.svelte` - Individual annotations
113
+ - `LeftGutter.svelte` - Gutter container
114
+ - `GutterManager.svelte` - Admin UI for managing gutters
115
+
116
+ ### Markdown Editor
117
+ Full-featured editor with:
118
+ - Live preview
119
+ - Multiple themes (Grove, Amber, Matrix, Dracula, Nord, Rose)
120
+ - Drag-drop image upload to R2
121
+ - Mermaid diagram support
122
+ - Slash commands
123
+ - Zen mode
124
+
125
+ ### Authentication
126
+ Passwordless magic code system:
127
+ - Email-based verification codes
128
+ - JWT sessions
129
+ - Rate limiting
130
+ - Constant-time comparison for security
131
+
132
+ ## Multi-Tenant Schema
133
+
134
+ The engine supports multi-tenant deployment where each tenant gets isolated data:
135
+
136
+ ```sql
137
+ -- Each tenant (subdomain)
138
+ CREATE TABLE tenants (
139
+ id TEXT PRIMARY KEY,
140
+ subdomain TEXT UNIQUE NOT NULL,
141
+ plan TEXT DEFAULT 'starter',
142
+ -- ...
143
+ );
144
+
145
+ -- Posts scoped to tenant
146
+ CREATE TABLE posts (
147
+ id TEXT PRIMARY KEY,
148
+ tenant_id TEXT NOT NULL,
149
+ slug TEXT NOT NULL,
150
+ -- ...
151
+ UNIQUE(tenant_id, slug)
152
+ );
153
+ ```
154
+
155
+ ## Related Documentation
156
+
157
+ - [Migration Strategy](../../docs/MIGRATION-STRATEGY.md)
158
+ - [Agent Guide for New Sites](../../docs/guides/AGENT-GUIDE-NEW-GROVE-SITES.md)
159
+ - [Customer Setup Guide](../../docs/guides/customer-setup.md)
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Sign a JWT payload
3
+ * @param {Object} payload - The payload to sign
4
+ * @param {string} secret - The secret key
5
+ * @returns {Promise<string>} - The signed JWT token
6
+ */
7
+ export function signJwt(payload: Object, secret: string): Promise<string>;
8
+ /**
9
+ * Verify and decode a JWT token
10
+ * @param {string} token - The JWT token to verify
11
+ * @param {string} secret - The secret key
12
+ * @returns {Promise<Object|null>} - The decoded payload or null if invalid
13
+ */
14
+ export function verifyJwt(token: string, secret: string): Promise<Object | null>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * JWT utilities using Web Crypto API (Cloudflare Workers compatible)
3
+ */
4
+
5
+ const encoder = new TextEncoder();
6
+ const decoder = new TextDecoder();
7
+
8
+ /**
9
+ * Base64URL encode
10
+ */
11
+ function base64UrlEncode(data) {
12
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(data)));
13
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
+ }
15
+
16
+ /**
17
+ * Base64URL decode
18
+ */
19
+ function base64UrlDecode(str) {
20
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
21
+ const padding = "=".repeat((4 - (base64.length % 4)) % 4);
22
+ const binary = atob(base64 + padding);
23
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
24
+ }
25
+
26
+ /**
27
+ * Create HMAC key from secret
28
+ */
29
+ async function createKey(secret) {
30
+ return await crypto.subtle.importKey(
31
+ "raw",
32
+ encoder.encode(secret),
33
+ { name: "HMAC", hash: "SHA-256" },
34
+ false,
35
+ ["sign", "verify"],
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Sign a JWT payload
41
+ * @param {Object} payload - The payload to sign
42
+ * @param {string} secret - The secret key
43
+ * @returns {Promise<string>} - The signed JWT token
44
+ */
45
+ export async function signJwt(payload, secret) {
46
+ const header = { alg: "HS256", typ: "JWT" };
47
+
48
+ const headerEncoded = base64UrlEncode(encoder.encode(JSON.stringify(header)));
49
+ const payloadEncoded = base64UrlEncode(
50
+ encoder.encode(JSON.stringify(payload)),
51
+ );
52
+
53
+ const message = `${headerEncoded}.${payloadEncoded}`;
54
+ const key = await createKey(secret);
55
+
56
+ const signature = await crypto.subtle.sign(
57
+ "HMAC",
58
+ key,
59
+ encoder.encode(message),
60
+ );
61
+
62
+ const signatureEncoded = base64UrlEncode(signature);
63
+
64
+ return `${message}.${signatureEncoded}`;
65
+ }
66
+
67
+ /**
68
+ * Verify and decode a JWT token
69
+ * @param {string} token - The JWT token to verify
70
+ * @param {string} secret - The secret key
71
+ * @returns {Promise<Object|null>} - The decoded payload or null if invalid
72
+ */
73
+ export async function verifyJwt(token, secret) {
74
+ try {
75
+ const parts = token.split(".");
76
+ if (parts.length !== 3) {
77
+ return null;
78
+ }
79
+
80
+ const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
81
+ const message = `${headerEncoded}.${payloadEncoded}`;
82
+
83
+ const key = await createKey(secret);
84
+ const signature = base64UrlDecode(signatureEncoded);
85
+
86
+ const isValid = await crypto.subtle.verify(
87
+ "HMAC",
88
+ key,
89
+ signature,
90
+ encoder.encode(message),
91
+ );
92
+
93
+ if (!isValid) {
94
+ return null;
95
+ }
96
+
97
+ const payload = JSON.parse(decoder.decode(base64UrlDecode(payloadEncoded)));
98
+
99
+ // Check expiration
100
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
101
+ return null;
102
+ }
103
+
104
+ return payload;
105
+ } catch (error) {
106
+ console.error("JWT verification error:", error);
107
+ return null;
108
+ }
109
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Create a session token for a user
3
+ * @param {Object} user - User data
4
+ * @param {string} user.email - User email address
5
+ * @param {string} secret - Session secret
6
+ * @returns {Promise<string>} - Signed JWT token
7
+ */
8
+ export function createSession(user: {
9
+ email: string;
10
+ }, secret: string): Promise<string>;
11
+ /**
12
+ * Verify a session token and return user data
13
+ * @param {string} token - Session token
14
+ * @param {string} secret - Session secret
15
+ * @returns {Promise<Object|null>} - User data or null if invalid
16
+ */
17
+ export function verifySession(token: string, secret: string): Promise<Object | null>;
18
+ /**
19
+ * Create Set-Cookie header value for session
20
+ * @param {string} token - Session token
21
+ * @param {boolean} isProduction - Whether in production (for secure flag)
22
+ * @returns {string} - Cookie header value
23
+ */
24
+ export function createSessionCookie(token: string, isProduction?: boolean): string;
25
+ /**
26
+ * Create Set-Cookie header value to clear session
27
+ * @returns {string} - Cookie header value
28
+ */
29
+ export function clearSessionCookie(): string;
30
+ /**
31
+ * Parse session token from cookie header
32
+ * @param {string} cookieHeader - Cookie header value
33
+ * @returns {string|null} - Session token or null
34
+ */
35
+ export function parseSessionCookie(cookieHeader: string): string | null;
36
+ /**
37
+ * Check if an email is in the allowed admin list
38
+ * @param {string} email - Email address to check
39
+ * @param {string} allowedList - Comma-separated list of allowed emails
40
+ * @returns {boolean} - Whether the user is allowed
41
+ */
42
+ export function isAllowedAdmin(email: string, allowedList: string): boolean;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Session management utilities
3
+ */
4
+
5
+ import { signJwt, verifyJwt } from "./jwt.js";
6
+
7
+ const SESSION_COOKIE_NAME = "session";
8
+ const SESSION_DURATION_SECONDS = 60 * 60 * 24 * 7; // 7 days
9
+
10
+ /**
11
+ * Create a session token for a user
12
+ * @param {Object} user - User data
13
+ * @param {string} user.email - User email address
14
+ * @param {string} secret - Session secret
15
+ * @returns {Promise<string>} - Signed JWT token
16
+ */
17
+ export async function createSession(user, secret) {
18
+ const payload = {
19
+ sub: user.email,
20
+ email: user.email,
21
+ exp: Math.floor(Date.now() / 1000) + SESSION_DURATION_SECONDS,
22
+ };
23
+
24
+ return await signJwt(payload, secret);
25
+ }
26
+
27
+ /**
28
+ * Verify a session token and return user data
29
+ * @param {string} token - Session token
30
+ * @param {string} secret - Session secret
31
+ * @returns {Promise<Object|null>} - User data or null if invalid
32
+ */
33
+ export async function verifySession(token, secret) {
34
+ const payload = await verifyJwt(token, secret);
35
+
36
+ if (!payload) {
37
+ return null;
38
+ }
39
+
40
+ return {
41
+ email: payload.email,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Create Set-Cookie header value for session
47
+ * @param {string} token - Session token
48
+ * @param {boolean} isProduction - Whether in production (for secure flag)
49
+ * @returns {string} - Cookie header value
50
+ */
51
+ export function createSessionCookie(token, isProduction = true) {
52
+ const parts = [
53
+ `${SESSION_COOKIE_NAME}=${token}`,
54
+ "Path=/",
55
+ `Max-Age=${SESSION_DURATION_SECONDS}`,
56
+ "HttpOnly",
57
+ "SameSite=Lax",
58
+ ];
59
+
60
+ if (isProduction) {
61
+ parts.push("Secure");
62
+ }
63
+
64
+ return parts.join("; ");
65
+ }
66
+
67
+ /**
68
+ * Create Set-Cookie header value to clear session
69
+ * @returns {string} - Cookie header value
70
+ */
71
+ export function clearSessionCookie() {
72
+ return `${SESSION_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`;
73
+ }
74
+
75
+ /**
76
+ * Parse session token from cookie header
77
+ * @param {string} cookieHeader - Cookie header value
78
+ * @returns {string|null} - Session token or null
79
+ */
80
+ export function parseSessionCookie(cookieHeader) {
81
+ if (!cookieHeader) {
82
+ return null;
83
+ }
84
+
85
+ const cookies = cookieHeader.split(";").reduce((acc, cookie) => {
86
+ const [key, value] = cookie.trim().split("=");
87
+ if (key && value) {
88
+ acc[key] = value;
89
+ }
90
+ return acc;
91
+ }, {});
92
+
93
+ return cookies[SESSION_COOKIE_NAME] || null;
94
+ }
95
+
96
+ /**
97
+ * Check if an email is in the allowed admin list
98
+ * @param {string} email - Email address to check
99
+ * @param {string} allowedList - Comma-separated list of allowed emails
100
+ * @returns {boolean} - Whether the user is allowed
101
+ */
102
+ export function isAllowedAdmin(email, allowedList) {
103
+ const allowed = allowedList.split(",").map((e) => e.trim().toLowerCase());
104
+ return allowed.includes(email.toLowerCase());
105
+ }