@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.
- package/README.md +163 -0
- package/dist/auth/jwt.d.ts +14 -0
- package/dist/auth/jwt.js +109 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +105 -0
- package/dist/components/admin/GutterManager.svelte +910 -0
- package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
- package/dist/components/admin/MarkdownEditor.svelte +3114 -0
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
- package/dist/components/custom/CollapsibleSection.svelte +74 -0
- package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
- package/dist/components/custom/ContentWithGutter.svelte +646 -0
- package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
- package/dist/components/custom/GutterItem.svelte +201 -0
- package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
- package/dist/components/custom/LeftGutter.svelte +271 -0
- package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
- package/dist/components/custom/MobileTOC.svelte +273 -0
- package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
- package/dist/components/custom/TableOfContents.svelte +163 -0
- package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
- package/dist/components/gallery/ImageGallery.svelte +681 -0
- package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
- package/dist/components/gallery/Lightbox.svelte +107 -0
- package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
- package/dist/components/gallery/LightboxCaption.svelte +25 -0
- package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
- package/dist/components/gallery/ZoomableImage.svelte +163 -0
- package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
- package/dist/components/ui/Accordion.svelte +74 -0
- package/dist/components/ui/Accordion.svelte.d.ts +42 -0
- package/dist/components/ui/Badge.svelte +48 -0
- package/dist/components/ui/Badge.svelte.d.ts +26 -0
- package/dist/components/ui/Button.svelte +74 -0
- package/dist/components/ui/Button.svelte.d.ts +34 -0
- package/dist/components/ui/Card.svelte +102 -0
- package/dist/components/ui/Card.svelte.d.ts +46 -0
- package/dist/components/ui/Dialog.svelte +91 -0
- package/dist/components/ui/Dialog.svelte.d.ts +43 -0
- package/dist/components/ui/Input.svelte +81 -0
- package/dist/components/ui/Input.svelte.d.ts +35 -0
- package/dist/components/ui/Select.svelte +69 -0
- package/dist/components/ui/Select.svelte.d.ts +36 -0
- package/dist/components/ui/Sheet.svelte +98 -0
- package/dist/components/ui/Sheet.svelte.d.ts +45 -0
- package/dist/components/ui/Skeleton.svelte +31 -0
- package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
- package/dist/components/ui/Table.svelte +59 -0
- package/dist/components/ui/Table.svelte.d.ts +44 -0
- package/dist/components/ui/Tabs.svelte +76 -0
- package/dist/components/ui/Tabs.svelte.d.ts +41 -0
- package/dist/components/ui/Textarea.svelte +81 -0
- package/dist/components/ui/Textarea.svelte.d.ts +35 -0
- package/dist/components/ui/Toast.svelte +18 -0
- package/dist/components/ui/Toast.svelte.d.ts +7 -0
- package/dist/components/ui/accordion/accordion-content.svelte +24 -0
- package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
- package/dist/components/ui/accordion/accordion-item.svelte +12 -0
- package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
- package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
- package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
- package/dist/components/ui/accordion/index.d.ts +6 -0
- package/dist/components/ui/accordion/index.js +8 -0
- package/dist/components/ui/badge/badge.svelte +50 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
- package/dist/components/ui/badge/index.d.ts +2 -0
- package/dist/components/ui/badge/index.js +2 -0
- package/dist/components/ui/button/button.svelte +82 -0
- package/dist/components/ui/button/button.svelte.d.ts +132 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +4 -0
- package/dist/components/ui/card/card-content.svelte +16 -0
- package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-description.svelte +16 -0
- package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-footer.svelte +16 -0
- package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-header.svelte +16 -0
- package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-title.svelte +25 -0
- package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
- package/dist/components/ui/card/card.svelte +20 -0
- package/dist/components/ui/card/card.svelte.d.ts +5 -0
- package/dist/components/ui/card/index.d.ts +7 -0
- package/dist/components/ui/card/index.js +9 -0
- package/dist/components/ui/dialog/dialog-content.svelte +38 -0
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
- package/dist/components/ui/dialog/dialog-description.svelte +16 -0
- package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
- package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-title.svelte +16 -0
- package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/index.d.ts +12 -0
- package/dist/components/ui/dialog/index.js +14 -0
- package/dist/components/ui/index.d.ts +26 -0
- package/dist/components/ui/index.js +29 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +46 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/select/index.d.ts +11 -0
- package/dist/components/ui/select/index.js +13 -0
- package/dist/components/ui/select/select-content.svelte +39 -0
- package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
- package/dist/components/ui/select/select-group-heading.svelte +16 -0
- package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-item.svelte +37 -0
- package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-separator.svelte +13 -0
- package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-trigger.svelte +24 -0
- package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/index.js +4 -0
- package/dist/components/ui/separator/separator.svelte +22 -0
- package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/index.d.ts +12 -0
- package/dist/components/ui/sheet/index.js +14 -0
- package/dist/components/ui/sheet/sheet-content.svelte +53 -0
- package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
- package/dist/components/ui/sheet/sheet-description.svelte +16 -0
- package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
- package/dist/components/ui/sheet/sheet-title.svelte +16 -0
- package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/ui/skeleton/index.d.ts +2 -0
- package/dist/components/ui/skeleton/index.js +4 -0
- package/dist/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
- package/dist/components/ui/table/index.d.ts +9 -0
- package/dist/components/ui/table/index.js +11 -0
- package/dist/components/ui/table/table-body.svelte +16 -0
- package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-caption.svelte +16 -0
- package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-cell.svelte +20 -0
- package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-footer.svelte +16 -0
- package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-head.svelte +23 -0
- package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-header.svelte +16 -0
- package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-row.svelte +23 -0
- package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
- package/dist/components/ui/table/table.svelte +18 -0
- package/dist/components/ui/table/table.svelte.d.ts +5 -0
- package/dist/components/ui/tabs/index.d.ts +6 -0
- package/dist/components/ui/tabs/index.js +8 -0
- package/dist/components/ui/tabs/tabs-content.svelte +19 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +19 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/textarea/index.d.ts +2 -0
- package/dist/components/ui/textarea/index.js +4 -0
- package/dist/components/ui/textarea/textarea.svelte +24 -0
- package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
- package/dist/components/ui/toast.d.ts +86 -0
- package/dist/components/ui/toast.js +99 -0
- package/dist/db/schema.sql +238 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/payments/index.d.ts +33 -0
- package/dist/payments/index.js +47 -0
- package/dist/payments/shop.d.ts +165 -0
- package/dist/payments/shop.js +588 -0
- package/dist/payments/stripe/client.d.ts +231 -0
- package/dist/payments/stripe/client.js +198 -0
- package/dist/payments/stripe/index.d.ts +18 -0
- package/dist/payments/stripe/index.js +17 -0
- package/dist/payments/stripe/provider.d.ts +50 -0
- package/dist/payments/stripe/provider.js +530 -0
- package/dist/payments/types.d.ts +355 -0
- package/dist/payments/types.js +7 -0
- package/dist/server/logger.d.ts +53 -0
- package/dist/server/logger.js +252 -0
- package/dist/styles/content.css +514 -0
- package/dist/styles/tokens.css +175 -0
- package/dist/utils/api.d.ts +20 -0
- package/dist/utils/api.js +109 -0
- package/dist/utils/cn.d.ts +15 -0
- package/dist/utils/cn.js +18 -0
- package/dist/utils/csrf.d.ts +22 -0
- package/dist/utils/csrf.js +72 -0
- package/dist/utils/debounce.d.ts +7 -0
- package/dist/utils/debounce.js +14 -0
- package/dist/utils/gallery.d.ts +66 -0
- package/dist/utils/gallery.js +181 -0
- package/dist/utils/gutter.d.ts +54 -0
- package/dist/utils/gutter.js +169 -0
- package/dist/utils/imageProcessor.d.ts +58 -0
- package/dist/utils/imageProcessor.js +205 -0
- package/dist/utils/json.d.ts +17 -0
- package/dist/utils/json.js +26 -0
- package/dist/utils/markdown.d.ts +101 -0
- package/dist/utils/markdown.js +947 -0
- package/dist/utils/sanitize.d.ts +25 -0
- package/dist/utils/sanitize.js +127 -0
- package/dist/utils/validation.d.ts +46 -0
- package/dist/utils/validation.js +169 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +5 -0
- 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>;
|
package/dist/auth/jwt.js
ADDED
|
@@ -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
|
+
}
|