@autumnsgrove/groveengine 0.9.97 → 0.9.98
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/auth/session.js +2 -1
- package/dist/components/admin/FloatingToolbar.svelte +43 -1
- package/dist/components/admin/GutterManager.svelte +2 -2
- package/dist/components/admin/MarkdownEditor.svelte +76 -9
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +2 -0
- package/dist/components/admin/VoiceInput.svelte +7 -0
- package/dist/components/custom/GutterItem.svelte +2 -2
- package/dist/components/custom/InternalsPostViewer.svelte +2 -2
- package/dist/components/custom/MobileTOC.svelte +54 -2
- package/dist/components/custom/TableOfContents.svelte +1 -1
- package/dist/components/quota/UpgradePrompt.svelte +1 -1
- package/dist/config/tiers.d.ts +3 -1
- package/dist/config/tiers.js +16 -6
- package/dist/curios/timeline/Timeline.svelte +14 -1
- package/dist/curios/timeline/secrets.server.d.ts +78 -0
- package/dist/curios/timeline/secrets.server.js +180 -0
- package/dist/data/grove-term-manifest.json +552 -0
- package/dist/email/components/GroveButton.d.ts +10 -0
- package/dist/email/components/GroveButton.tsx +77 -0
- package/dist/email/components/GroveDivider.d.ts +7 -0
- package/dist/email/components/GroveDivider.tsx +80 -0
- package/dist/email/components/GroveEmail.d.ts +12 -0
- package/dist/email/components/GroveEmail.tsx +148 -0
- package/dist/email/components/GroveHighlight.d.ts +9 -0
- package/dist/email/components/GroveHighlight.tsx +71 -0
- package/dist/email/components/GrovePatchNote.d.ts +12 -0
- package/dist/email/components/GrovePatchNote.tsx +114 -0
- package/dist/email/components/GroveText.d.ts +24 -0
- package/dist/email/components/GroveText.tsx +131 -0
- package/dist/email/components/index.d.ts +37 -0
- package/dist/email/components/index.js +43 -0
- package/dist/email/components/styles.d.ts +47 -0
- package/dist/email/components/styles.js +51 -0
- package/dist/email/index.d.ts +31 -0
- package/dist/email/index.js +35 -0
- package/dist/email/lifecycle/GentleNudge.d.ts +7 -0
- package/dist/email/lifecycle/GentleNudge.tsx +52 -0
- package/dist/email/lifecycle/RenewalThankYou.d.ts +7 -0
- package/dist/email/lifecycle/RenewalThankYou.tsx +52 -0
- package/dist/email/lifecycle/index.d.ts +8 -0
- package/dist/email/lifecycle/index.js +8 -0
- package/dist/email/porch/PorchReplyEmail.d.ts +22 -0
- package/dist/email/porch/PorchReplyEmail.tsx +150 -0
- package/dist/email/porch/index.d.ts +7 -0
- package/dist/email/porch/index.js +6 -0
- package/dist/email/render.d.ts +41 -0
- package/dist/email/render.js +74 -0
- package/dist/email/schedule.d.ts +78 -0
- package/dist/email/schedule.js +198 -0
- package/dist/email/seasonal/SeasonalGreeting.d.ts +9 -0
- package/dist/email/seasonal/SeasonalGreeting.tsx +81 -0
- package/dist/email/seasonal/index.d.ts +7 -0
- package/dist/email/seasonal/index.js +7 -0
- package/dist/email/sequences/Day14Email.d.ts +7 -0
- package/dist/email/sequences/Day14Email.tsx +49 -0
- package/dist/email/sequences/Day1Email.d.ts +7 -0
- package/dist/email/sequences/Day1Email.tsx +57 -0
- package/dist/email/sequences/Day30Email.d.ts +7 -0
- package/dist/email/sequences/Day30Email.tsx +45 -0
- package/dist/email/sequences/Day7Email.d.ts +7 -0
- package/dist/email/sequences/Day7Email.tsx +85 -0
- package/dist/email/sequences/WelcomeEmail.d.ts +7 -0
- package/dist/email/sequences/WelcomeEmail.tsx +93 -0
- package/dist/email/sequences/index.d.ts +25 -0
- package/dist/email/sequences/index.js +30 -0
- package/dist/email/types.d.ts +82 -0
- package/dist/email/types.js +39 -0
- package/dist/email/updates/AnnouncementEmail.d.ts +17 -0
- package/dist/email/updates/AnnouncementEmail.tsx +61 -0
- package/dist/email/updates/PatchNotesEmail.d.ts +16 -0
- package/dist/email/updates/PatchNotesEmail.tsx +80 -0
- package/dist/email/updates/index.d.ts +8 -0
- package/dist/email/updates/index.js +8 -0
- package/dist/email/urls.d.ts +60 -0
- package/dist/email/urls.js +83 -0
- package/dist/feature-flags/grafts.d.ts +81 -0
- package/dist/feature-flags/grafts.js +106 -0
- package/dist/feature-flags/index.d.ts +2 -0
- package/dist/feature-flags/index.js +4 -0
- package/dist/feature-flags/tenant-grafts.d.ts +82 -0
- package/dist/feature-flags/tenant-grafts.js +207 -0
- package/dist/grafts/greenhouse/CultivateFlagRow.svelte +12 -11
- package/dist/grafts/greenhouse/CultivateFlagTable.svelte +13 -12
- package/dist/grafts/greenhouse/GraftControlPanel.svelte +374 -0
- package/dist/grafts/greenhouse/GraftControlPanel.svelte.d.ts +21 -0
- package/dist/grafts/greenhouse/GraftToggleRow.svelte +189 -0
- package/dist/grafts/greenhouse/GraftToggleRow.svelte.d.ts +18 -0
- package/dist/grafts/greenhouse/GreenhouseAdminPanel.svelte +491 -0
- package/dist/grafts/greenhouse/GreenhouseAdminPanel.svelte.d.ts +71 -0
- package/dist/grafts/greenhouse/GreenhouseEnrollDialog.svelte +5 -10
- package/dist/grafts/greenhouse/GreenhouseEnrollTable.svelte +24 -20
- package/dist/grafts/greenhouse/GreenhouseToggle.svelte +10 -0
- package/dist/grafts/greenhouse/index.d.ts +4 -1
- package/dist/grafts/greenhouse/index.js +5 -0
- package/dist/grafts/greenhouse/types.d.ts +84 -0
- package/dist/grafts/login/EmailButton.svelte +260 -0
- package/dist/grafts/login/EmailButton.svelte.d.ts +14 -0
- package/dist/grafts/login/LoginGraft.svelte +11 -1
- package/dist/grafts/login/PasskeyButton.svelte +1 -1
- package/dist/grafts/login/config.d.ts +3 -1
- package/dist/grafts/login/config.js +4 -2
- package/dist/grafts/login/index.d.ts +1 -0
- package/dist/grafts/login/index.js +1 -0
- package/dist/grafts/login/passkey-authenticate.js +1 -1
- package/dist/grafts/login/server/callback.js +1 -1
- package/dist/grafts/login/types.d.ts +3 -3
- package/dist/grafts/pricing/PricingFineprint.svelte +1 -1
- package/dist/grafts/pricing/PricingToggle.svelte +2 -2
- package/dist/payments/types.d.ts +23 -22
- package/dist/server/billing.d.ts +17 -0
- package/dist/server/billing.js +40 -0
- package/dist/server/rate-limits/config.js +4 -0
- package/dist/server/services/index.d.ts +1 -1
- package/dist/server/services/index.js +2 -0
- package/dist/server/services/storage.d.ts +32 -0
- package/dist/server/services/storage.js +125 -0
- package/dist/server/services/trace-email.js +1 -1
- package/dist/styles/tokens.css +145 -35
- package/dist/ui/components/charts/ActivityOverview.svelte +8 -6
- package/dist/ui/components/chrome/AdminHeader.svelte +3 -3
- package/dist/ui/components/chrome/Header.svelte +220 -18
- package/dist/ui/components/chrome/Header.svelte.d.ts +27 -1
- package/dist/ui/components/chrome/MobileMenu.svelte +116 -4
- package/dist/ui/components/chrome/MobileMenu.svelte.d.ts +11 -1
- package/dist/ui/components/chrome/defaults.js +7 -5
- package/dist/ui/components/chrome/index.d.ts +2 -0
- package/dist/ui/components/chrome/index.js +2 -0
- package/dist/ui/components/chrome/tenant-nav.d.ts +46 -0
- package/dist/ui/components/chrome/tenant-nav.js +70 -0
- package/dist/ui/components/chrome/types.d.ts +9 -0
- package/dist/ui/components/gallery/ImageGallery.svelte +3 -1
- package/dist/ui/components/gallery/ZoomableImage.svelte +2 -0
- package/dist/ui/components/nature/LogoArchive.svelte +18 -0
- package/dist/ui/components/nature/botanical/Acorn.svelte +1 -1
- package/dist/ui/components/nature/botanical/Berry.svelte +1 -1
- package/dist/ui/components/nature/botanical/DandelionPuff.svelte +8 -1
- package/dist/ui/components/nature/botanical/FallingLeavesLayer.svelte +22 -1
- package/dist/ui/components/nature/botanical/FallingPetalsLayer.svelte +21 -1
- package/dist/ui/components/nature/botanical/Leaf.svelte +1 -1
- package/dist/ui/components/nature/botanical/LeafFalling.svelte +1 -0
- package/dist/ui/components/nature/botanical/PetalFalling.svelte +12 -0
- package/dist/ui/components/nature/botanical/PineCone.svelte +1 -1
- package/dist/ui/components/nature/botanical/Vine.svelte +8 -1
- package/dist/ui/components/nature/creatures/Bee.svelte +19 -4
- package/dist/ui/components/nature/creatures/Bird.svelte +18 -7
- package/dist/ui/components/nature/creatures/BirdFlying.svelte +8 -0
- package/dist/ui/components/nature/creatures/Bluebird.svelte +13 -3
- package/dist/ui/components/nature/creatures/Butterfly.svelte +13 -1
- package/dist/ui/components/nature/creatures/Cardinal.svelte +13 -3
- package/dist/ui/components/nature/creatures/Chickadee.svelte +16 -5
- package/dist/ui/components/nature/creatures/Deer.svelte +12 -2
- package/dist/ui/components/nature/creatures/Firefly.svelte +38 -6
- package/dist/ui/components/nature/creatures/Owl.svelte +13 -3
- package/dist/ui/components/nature/creatures/Rabbit.svelte +13 -2
- package/dist/ui/components/nature/creatures/Robin.svelte +16 -5
- package/dist/ui/components/nature/creatures/Squirrel.svelte +12 -2
- package/dist/ui/components/nature/ground/Bush.svelte +7 -1
- package/dist/ui/components/nature/ground/Crocus.svelte +7 -1
- package/dist/ui/components/nature/ground/Daffodil.svelte +7 -1
- package/dist/ui/components/nature/ground/Fern.svelte +7 -1
- package/dist/ui/components/nature/ground/FlowerWild.svelte +7 -1
- package/dist/ui/components/nature/ground/GrassTuft.svelte +7 -1
- package/dist/ui/components/nature/ground/Log.svelte +1 -1
- package/dist/ui/components/nature/ground/Mushroom.svelte +1 -1
- package/dist/ui/components/nature/ground/MushroomCluster.svelte +1 -1
- package/dist/ui/components/nature/ground/Rock.svelte +1 -1
- package/dist/ui/components/nature/ground/Stump.svelte +1 -1
- package/dist/ui/components/nature/ground/Tulip.svelte +13 -4
- package/dist/ui/components/nature/palette.d.ts +92 -0
- package/dist/ui/components/nature/palette.js +92 -0
- package/dist/ui/components/nature/sky/Cloud.svelte +40 -23
- package/dist/ui/components/nature/sky/Cloud.svelte.d.ts +1 -0
- package/dist/ui/components/nature/sky/CloudWispy.svelte +7 -0
- package/dist/ui/components/nature/sky/Moon.svelte +41 -6
- package/dist/ui/components/nature/sky/Moon.svelte.d.ts +1 -0
- package/dist/ui/components/nature/sky/Rainbow.svelte +7 -1
- package/dist/ui/components/nature/sky/Star.svelte +7 -0
- package/dist/ui/components/nature/sky/StarCluster.svelte +7 -1
- package/dist/ui/components/nature/sky/StarShooting.svelte +8 -0
- package/dist/ui/components/nature/sky/Sun.svelte +8 -1
- package/dist/ui/components/nature/structural/Birdhouse.svelte +1 -1
- package/dist/ui/components/nature/structural/Bridge.svelte +7 -4
- package/dist/ui/components/nature/structural/FencePost.svelte +1 -1
- package/dist/ui/components/nature/structural/GardenGate.svelte +1 -1
- package/dist/ui/components/nature/structural/Lantern.svelte +9 -1
- package/dist/ui/components/nature/structural/Lattice.svelte +1 -1
- package/dist/ui/components/nature/structural/LatticeWithVine.svelte +1 -1
- package/dist/ui/components/nature/structural/StonePath.svelte +1 -1
- package/dist/ui/components/nature/trees/TreeAspen.svelte +7 -1
- package/dist/ui/components/nature/trees/TreeBirch.svelte +11 -3
- package/dist/ui/components/nature/trees/TreeCherry.svelte +7 -0
- package/dist/ui/components/nature/trees/TreePine.svelte +7 -0
- package/dist/ui/components/nature/water/LilyPad.svelte +17 -4
- package/dist/ui/components/nature/water/Pond.svelte +22 -12
- package/dist/ui/components/nature/water/Reeds.svelte +7 -1
- package/dist/ui/components/nature/water/Stream.svelte +28 -14
- package/dist/ui/components/nature/weather/SnowfallLayer.svelte +21 -1
- package/dist/ui/components/nature/weather/Snowflake.svelte +1 -0
- package/dist/ui/components/nature/weather/SnowflakeFalling.svelte +7 -0
- package/dist/ui/components/primitives/badge/badge.svelte +8 -1
- package/dist/ui/components/primitives/badge/badge.svelte.d.ts +1 -0
- package/dist/ui/components/primitives/dialog/dialog-content.svelte +6 -1
- package/dist/ui/components/primitives/dialog/dialog-overlay.svelte +3 -1
- package/dist/ui/components/terrarium/PlacedAsset.svelte +16 -1
- package/dist/ui/components/terrarium/utils/export.d.ts +1 -1
- package/dist/ui/components/terrarium/utils/export.js +34 -17
- package/dist/ui/components/ui/GlassCarousel.svelte +2 -0
- package/dist/ui/components/ui/GlassConfirmDialog.svelte +76 -167
- package/dist/ui/components/ui/GlassConfirmDialog.svelte.d.ts +3 -5
- package/dist/ui/components/ui/Waystone.svelte +40 -98
- package/dist/ui/components/ui/Waystone.svelte.d.ts +22 -0
- package/dist/ui/components/ui/groveterm/GroveTerm.svelte +301 -0
- package/dist/ui/components/ui/groveterm/GroveTerm.svelte.d.ts +17 -0
- package/dist/ui/components/ui/groveterm/GroveTermPopup.svelte +272 -0
- package/dist/ui/components/ui/groveterm/GroveTermPopup.svelte.d.ts +32 -0
- package/dist/ui/components/ui/groveterm/index.d.ts +15 -0
- package/dist/ui/components/ui/groveterm/index.js +15 -0
- package/dist/ui/components/ui/groveterm/types.d.ts +64 -0
- package/dist/ui/components/ui/groveterm/types.js +42 -0
- package/dist/ui/components/ui/index.d.ts +5 -0
- package/dist/ui/components/ui/index.js +6 -0
- package/dist/ui/components/ui/waystone/Waystone.svelte +266 -0
- package/dist/ui/components/ui/waystone/Waystone.svelte.d.ts +18 -0
- package/dist/ui/components/ui/waystone/WaystonePopup.svelte +244 -0
- package/dist/ui/components/ui/waystone/WaystonePopup.svelte.d.ts +34 -0
- package/dist/ui/components/ui/waystone/index.d.ts +10 -0
- package/dist/ui/components/ui/waystone/index.js +10 -0
- package/dist/ui/components/ui/waystone/types.d.ts +42 -0
- package/dist/ui/components/ui/waystone/types.js +17 -0
- package/dist/ui/stores/index.d.ts +1 -0
- package/dist/ui/stores/index.js +1 -0
- package/dist/ui/stores/sidebar.svelte.d.ts +19 -0
- package/dist/ui/stores/sidebar.svelte.js +37 -0
- package/dist/ui/tailwind.preset.js +54 -56
- package/dist/ui/utils/cn.d.ts +1 -13
- package/dist/ui/utils/cn.js +4 -19
- package/dist/ui/vineyard/FeatureCard.svelte +32 -5
- package/dist/ui/vineyard/FeatureCard.svelte.d.ts +2 -2
- package/dist/utils/grove-url.d.ts +52 -0
- package/dist/utils/grove-url.js +83 -0
- package/dist/utils/imageProcessor.js +53 -4
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/rehype-groveterm.d.ts +82 -0
- package/dist/utils/rehype-groveterm.js +266 -0
- package/dist/utils/upload-validation.d.ts +80 -0
- package/dist/utils/upload-validation.js +186 -23
- package/dist/utils/user.d.ts +22 -0
- package/dist/utils/user.js +33 -0
- package/dist/zephyr/README.md +459 -0
- package/dist/zephyr/client.d.ts +64 -0
- package/dist/zephyr/client.js +156 -0
- package/dist/zephyr/index.d.ts +7 -0
- package/dist/zephyr/index.js +6 -0
- package/dist/zephyr/types.d.ts +38 -0
- package/dist/zephyr/types.js +6 -0
- package/package.json +57 -3
- package/dist/ui/styles/grove.css +0 -922
- package/dist/ui/styles/tokens.css +0 -429
package/dist/auth/session.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Session management is now handled by Heartwood SessionDO.
|
|
6
6
|
* This file only contains tenant verification functions.
|
|
7
7
|
*/
|
|
8
|
+
import { emailsMatch } from "../utils/user.js";
|
|
8
9
|
/**
|
|
9
10
|
* Verify that a user owns/has access to a tenant
|
|
10
11
|
*/
|
|
@@ -21,7 +22,7 @@ export async function verifyTenantOwnership(db, tenantId, userEmail) {
|
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
// Check if user email matches tenant owner email
|
|
24
|
-
return tenant.email
|
|
25
|
+
return emailsMatch(tenant.email, userEmail);
|
|
25
26
|
}
|
|
26
27
|
catch (error) {
|
|
27
28
|
console.error("Error verifying tenant ownership:", error);
|
|
@@ -219,8 +219,50 @@
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
//
|
|
222
|
+
// Track if the textarea is focused to avoid global listener interference
|
|
223
|
+
let isTextareaFocused = $state(false);
|
|
224
|
+
|
|
225
|
+
// Handle focus/blur on textarea to manage global listeners lifecycle
|
|
226
|
+
function handleTextareaFocus() {
|
|
227
|
+
isTextareaFocused = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {FocusEvent} e */
|
|
231
|
+
function handleTextareaBlur(e) {
|
|
232
|
+
// If focus moved to toolbar, keep it open
|
|
233
|
+
if (e.relatedTarget && toolbarRef?.contains(/** @type {Node} */ (e.relatedTarget))) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
isTextareaFocused = false;
|
|
237
|
+
isVisible = false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Set up focus tracking on the textarea
|
|
223
241
|
$effect(() => {
|
|
242
|
+
if (!textareaRef) return;
|
|
243
|
+
|
|
244
|
+
textareaRef.addEventListener("focus", handleTextareaFocus);
|
|
245
|
+
textareaRef.addEventListener("blur", handleTextareaBlur);
|
|
246
|
+
|
|
247
|
+
// Check if already focused
|
|
248
|
+
if (document.activeElement === textareaRef) {
|
|
249
|
+
isTextareaFocused = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return () => {
|
|
253
|
+
textareaRef?.removeEventListener("focus", handleTextareaFocus);
|
|
254
|
+
textareaRef?.removeEventListener("blur", handleTextareaBlur);
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Only add global listeners when textarea is focused
|
|
259
|
+
// This prevents interference with other form elements in the admin panel
|
|
260
|
+
$effect(() => {
|
|
261
|
+
if (!isTextareaFocused) {
|
|
262
|
+
isVisible = false;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
224
266
|
// Use selectionchange for more reliable selection tracking (catches programmatic changes)
|
|
225
267
|
document.addEventListener("selectionchange", handleSelectionChange);
|
|
226
268
|
document.addEventListener("mousedown", handleClickOutside);
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
variant="outline"
|
|
568
568
|
onclick={() => openImagePicker((url) => (itemUrl = url))}
|
|
569
569
|
>
|
|
570
|
-
Browse
|
|
570
|
+
Browse Images
|
|
571
571
|
</Button>
|
|
572
572
|
</div>
|
|
573
573
|
</div>
|
|
@@ -636,7 +636,7 @@
|
|
|
636
636
|
</Dialog>
|
|
637
637
|
|
|
638
638
|
<!-- Image Picker Modal -->
|
|
639
|
-
<Dialog bind:open={showImagePicker} title="Select Image
|
|
639
|
+
<Dialog bind:open={showImagePicker} title="Select Image">
|
|
640
640
|
{#snippet children()}
|
|
641
641
|
<div class="picker-controls">
|
|
642
642
|
<Input
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
* @property {Array<{url: string, alt?: string, caption?: string}>} [images]
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object.<string, boolean>} GraftsRecord
|
|
46
|
+
* Graft flags for this tenant - component reads what it needs.
|
|
47
|
+
* Known flags: fireside_mode (AI-assisted prompts), scribe_mode (voice-to-text)
|
|
48
|
+
*/
|
|
49
|
+
|
|
44
50
|
// Props
|
|
45
51
|
let {
|
|
46
52
|
content = $bindable(""),
|
|
@@ -54,8 +60,14 @@
|
|
|
54
60
|
previewTags = /** @type {string[]} */ ([]),
|
|
55
61
|
gutterItems = /** @type {GutterItemProp[]} */ ([]),
|
|
56
62
|
firesideAssisted = $bindable(false),
|
|
63
|
+
/** All grafts for this tenant - component reads what it needs */
|
|
64
|
+
grafts = /** @type {GraftsRecord} */ ({}),
|
|
57
65
|
} = $props();
|
|
58
66
|
|
|
67
|
+
// Derived graft flags - add new ones here as they're created
|
|
68
|
+
const firesideEnabled = $derived(grafts?.fireside_mode ?? false);
|
|
69
|
+
const scribeEnabled = $derived(grafts?.scribe_mode ?? false);
|
|
70
|
+
|
|
59
71
|
// Core refs and state
|
|
60
72
|
/** @type {HTMLTextAreaElement | null} */
|
|
61
73
|
let textareaRef = $state(null);
|
|
@@ -125,12 +137,43 @@
|
|
|
125
137
|
|
|
126
138
|
// Note: Slash commands and command palette removed for simplified Medium-style UX
|
|
127
139
|
|
|
140
|
+
// Debounced preview HTML - avoid expensive markdown rendering on every keystroke
|
|
141
|
+
// Cache the last rendered HTML to prevent jank during typing
|
|
142
|
+
let debouncedContent = $state(content);
|
|
143
|
+
// NOT $state - these are cleanup handles, not reactive state
|
|
144
|
+
// Using $state here causes infinite loops (effect writes to state it reads)
|
|
145
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
146
|
+
let debounceTimer = null;
|
|
147
|
+
let isMounted = true;
|
|
148
|
+
|
|
149
|
+
// Update debounced content after 150ms of no typing
|
|
150
|
+
$effect(() => {
|
|
151
|
+
// Clear any existing timer
|
|
152
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
153
|
+
|
|
154
|
+
// Capture current content for the closure
|
|
155
|
+
const currentContent = content;
|
|
156
|
+
|
|
157
|
+
debounceTimer = setTimeout(() => {
|
|
158
|
+
// Only update if component is still mounted (prevents race condition)
|
|
159
|
+
if (isMounted) {
|
|
160
|
+
debouncedContent = currentContent;
|
|
161
|
+
}
|
|
162
|
+
}, 150);
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
166
|
+
isMounted = false; // Mark as unmounted on cleanup
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
128
170
|
// Computed values
|
|
129
171
|
let wordCount = $derived(content.trim() ? content.trim().split(/\s+/).length : 0);
|
|
130
172
|
let charCount = $derived(content.length);
|
|
131
173
|
let lineCount = $derived(content.split("\n").length);
|
|
132
|
-
|
|
133
|
-
let
|
|
174
|
+
// Use debounced content for expensive operations (markdown rendering)
|
|
175
|
+
let previewHtml = $derived(debouncedContent ? sanitizeMarkdown(editorMd.render(debouncedContent)) : "");
|
|
176
|
+
let previewHeaders = $derived(debouncedContent ? extractHeaders(debouncedContent) : []);
|
|
134
177
|
|
|
135
178
|
let readingTime = $derived.by(() => {
|
|
136
179
|
const minutes = Math.ceil(wordCount / 200);
|
|
@@ -254,8 +297,8 @@
|
|
|
254
297
|
cycleEditorMode();
|
|
255
298
|
}
|
|
256
299
|
|
|
257
|
-
// Cmd/Ctrl + Shift + F for Fireside mode
|
|
258
|
-
if (e.key === "f" && (e.metaKey || e.ctrlKey) && e.shiftKey) {
|
|
300
|
+
// Cmd/Ctrl + Shift + F for Fireside mode (only if graft enabled)
|
|
301
|
+
if (e.key === "f" && (e.metaKey || e.ctrlKey) && e.shiftKey && firesideEnabled) {
|
|
259
302
|
e.preventDefault();
|
|
260
303
|
toggleFiresideMode();
|
|
261
304
|
}
|
|
@@ -541,6 +584,30 @@
|
|
|
541
584
|
}
|
|
542
585
|
});
|
|
543
586
|
|
|
587
|
+
// Full preview modal focus management
|
|
588
|
+
/** @type {HTMLElement | null} */
|
|
589
|
+
let previouslyFocusedBeforePreview = null;
|
|
590
|
+
/** @type {HTMLDivElement | null} */
|
|
591
|
+
let fullPreviewModalRef = $state(null);
|
|
592
|
+
|
|
593
|
+
$effect(() => {
|
|
594
|
+
if (showFullPreview) {
|
|
595
|
+
// Store the currently focused element to restore on close
|
|
596
|
+
const activeEl = document.activeElement;
|
|
597
|
+
if (activeEl instanceof HTMLElement) {
|
|
598
|
+
previouslyFocusedBeforePreview = activeEl;
|
|
599
|
+
}
|
|
600
|
+
// Focus the modal for keyboard accessibility
|
|
601
|
+
setTimeout(() => {
|
|
602
|
+
fullPreviewModalRef?.focus();
|
|
603
|
+
}, 50);
|
|
604
|
+
} else if (previouslyFocusedBeforePreview) {
|
|
605
|
+
// Restore focus when modal closes
|
|
606
|
+
previouslyFocusedBeforePreview.focus();
|
|
607
|
+
previouslyFocusedBeforePreview = null;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
544
611
|
// Drag and drop handlers
|
|
545
612
|
/** @param {DragEvent} e */
|
|
546
613
|
function handleDragEnter(e) {
|
|
@@ -724,7 +791,7 @@
|
|
|
724
791
|
{#if !isFiresideMode}
|
|
725
792
|
<div class="toolbar">
|
|
726
793
|
<div class="toolbar-left">
|
|
727
|
-
{#if !content.trim()}
|
|
794
|
+
{#if firesideEnabled && !content.trim()}
|
|
728
795
|
<button
|
|
729
796
|
type="button"
|
|
730
797
|
class="fireside-btn"
|
|
@@ -737,8 +804,8 @@
|
|
|
737
804
|
</button>
|
|
738
805
|
<span class="toolbar-divider">|</span>
|
|
739
806
|
{/if}
|
|
740
|
-
<!-- Voice Input (Scribe) -->
|
|
741
|
-
{#if editorMode !== "preview"}
|
|
807
|
+
<!-- Voice Input (Scribe) - gated by scribe_mode graft -->
|
|
808
|
+
{#if scribeEnabled && editorMode !== "preview"}
|
|
742
809
|
<div class="voice-wrapper" title="Voice Input (⌘⇧U) - Hold to record, release to transcribe">
|
|
743
810
|
<VoiceInput
|
|
744
811
|
mode={voiceMode}
|
|
@@ -850,7 +917,7 @@
|
|
|
850
917
|
onkeydown={handleKeydown}
|
|
851
918
|
onscroll={handleScroll}
|
|
852
919
|
onpaste={handlePaste}
|
|
853
|
-
placeholder="Start writing your
|
|
920
|
+
placeholder="Start writing your bloom... (Drag & drop or paste images)"
|
|
854
921
|
spellcheck="true"
|
|
855
922
|
disabled={readonly}
|
|
856
923
|
class="editor-textarea"
|
|
@@ -929,7 +996,7 @@
|
|
|
929
996
|
|
|
930
997
|
<!-- Full Preview Modal -->
|
|
931
998
|
{#if showFullPreview}
|
|
932
|
-
<div class="full-preview-modal" role="dialog" aria-modal="true" aria-label="Full article preview" tabindex="-1" onkeydown={(e) => e.key === 'Escape' && (showFullPreview = false)}>
|
|
999
|
+
<div bind:this={fullPreviewModalRef} class="full-preview-modal" role="dialog" aria-modal="true" aria-label="Full article preview" tabindex="-1" onkeydown={(e) => e.key === 'Escape' && (showFullPreview = false)}>
|
|
933
1000
|
<button type="button" class="full-preview-backdrop" onclick={() => (showFullPreview = false)} aria-label="Close preview"></button>
|
|
934
1001
|
<div class="full-preview-container" class:has-vines={gutterItems.length > 0}>
|
|
935
1002
|
<header class="full-preview-header">
|
|
@@ -23,6 +23,7 @@ declare const MarkdownEditor: import("svelte").Component<{
|
|
|
23
23
|
previewTags?: any;
|
|
24
24
|
gutterItems?: any;
|
|
25
25
|
firesideAssisted?: boolean;
|
|
26
|
+
grafts?: any;
|
|
26
27
|
}, {
|
|
27
28
|
getAvailableAnchors: () => string[];
|
|
28
29
|
insertAnchor: (name: string) => void;
|
|
@@ -44,4 +45,5 @@ type $$ComponentProps = {
|
|
|
44
45
|
previewTags?: any;
|
|
45
46
|
gutterItems?: any;
|
|
46
47
|
firesideAssisted?: boolean;
|
|
48
|
+
grafts?: any;
|
|
47
49
|
};
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
type ScribeRecorderState,
|
|
17
17
|
} from "../../scribe/recorder.js";
|
|
18
18
|
import type { GutterItem, ScribeMode } from "../../lumen/types.js";
|
|
19
|
+
import { getCSRFToken } from "../../utils/api.js";
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Props & Events
|
|
@@ -163,10 +164,16 @@
|
|
|
163
164
|
formData.append("audio", audioBlob);
|
|
164
165
|
formData.append("mode", mode);
|
|
165
166
|
|
|
167
|
+
// Get CSRF token for state-changing request
|
|
168
|
+
const csrfToken = getCSRFToken();
|
|
169
|
+
|
|
166
170
|
const response = await fetch("/api/lumen/transcribe", {
|
|
167
171
|
method: "POST",
|
|
168
172
|
body: formData,
|
|
169
173
|
credentials: "include",
|
|
174
|
+
headers: csrfToken
|
|
175
|
+
? { "X-CSRF-Token": csrfToken, "csrf-token": csrfToken }
|
|
176
|
+
: {},
|
|
170
177
|
});
|
|
171
178
|
|
|
172
179
|
if (!response.ok) {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
{@const imageSrc = item.src || item.url || item.file}
|
|
54
54
|
<figure class="gutter-photo">
|
|
55
55
|
<button class="image-button" onclick={() => openLightbox(imageSrc, item.caption || 'Gutter image', item.caption || '')}>
|
|
56
|
-
<img src={imageSrc} alt={item.caption || 'Gutter image'} />
|
|
56
|
+
<img src={imageSrc} alt={item.caption || 'Gutter image'} loading="lazy" decoding="async" />
|
|
57
57
|
</button>
|
|
58
58
|
{#if item.caption}
|
|
59
59
|
<figcaption>{item.caption}</figcaption>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
{/if}
|
|
77
77
|
{:else if item.type === 'emoji'}
|
|
78
78
|
<div class="gutter-emoji">
|
|
79
|
-
<img src={item.src} alt={item.alt || 'Emoji'} title={item.alt || ''} />
|
|
79
|
+
<img src={item.src} alt={item.alt || 'Emoji'} title={item.alt || ''} loading="lazy" decoding="async" />
|
|
80
80
|
</div>
|
|
81
81
|
{/if}
|
|
82
82
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/**
|
|
3
|
-
* Simple component to display a featured
|
|
3
|
+
* Simple component to display a featured garden post
|
|
4
4
|
* @prop {{ title: string; description?: string; slug: string; date?: string }} post - Post data
|
|
5
5
|
* @prop {string} [caption] - Optional caption text
|
|
6
6
|
*/
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{#if caption}
|
|
18
18
|
<span class="caption">{caption}</span>
|
|
19
19
|
{/if}
|
|
20
|
-
<a href="/
|
|
20
|
+
<a href="/garden/{post.slug}" class="post-link">
|
|
21
21
|
<h3 class="title">{post.title}</h3>
|
|
22
22
|
{#if post.description}
|
|
23
23
|
<p class="description">{post.description}</p>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
let menuRef = $state<HTMLDivElement>();
|
|
20
20
|
let buttonRef = $state<HTMLButtonElement>();
|
|
21
21
|
let activeId = $state('');
|
|
22
|
+
let previouslyFocusedElement: HTMLElement | null = null;
|
|
22
23
|
|
|
23
24
|
function toggleMenu() {
|
|
24
25
|
isOpen = !isOpen;
|
|
@@ -28,6 +29,29 @@
|
|
|
28
29
|
isOpen = false;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Focus trap: Tab key cycles within menu
|
|
33
|
+
function handleFocusTrap(event: KeyboardEvent) {
|
|
34
|
+
if (event.key === 'Tab' && isOpen && menuRef) {
|
|
35
|
+
const focusableElements = menuRef.querySelectorAll<HTMLElement>(
|
|
36
|
+
'button, a, [tabindex]:not([tabindex="-1"])'
|
|
37
|
+
);
|
|
38
|
+
if (focusableElements.length === 0) return;
|
|
39
|
+
|
|
40
|
+
const firstElement = focusableElements[0];
|
|
41
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
42
|
+
|
|
43
|
+
if (event.shiftKey && document.activeElement === firstElement) {
|
|
44
|
+
// Shift+Tab on first element: wrap to last
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
lastElement?.focus();
|
|
47
|
+
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
|
48
|
+
// Tab on last element: wrap to first
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
firstElement?.focus();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
function scrollToHeader(id: string) {
|
|
32
56
|
const element = document.getElementById(id);
|
|
33
57
|
if (element) {
|
|
@@ -55,11 +79,14 @@
|
|
|
55
79
|
}
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
// Handle escape key
|
|
82
|
+
// Handle escape key and focus trap
|
|
59
83
|
function handleKeydown(event: KeyboardEvent) {
|
|
60
84
|
if (event.key === 'Escape' && isOpen) {
|
|
61
85
|
closeMenu();
|
|
86
|
+
// Restore focus to button when closing
|
|
87
|
+
buttonRef?.focus();
|
|
62
88
|
}
|
|
89
|
+
handleFocusTrap(event);
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
// Set up intersection observer to track active section
|
|
@@ -90,6 +117,25 @@
|
|
|
90
117
|
return () => observer.disconnect();
|
|
91
118
|
}
|
|
92
119
|
|
|
120
|
+
// Handle focus management when menu opens/closes
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (isOpen) {
|
|
123
|
+
// Store the previously focused element to restore later
|
|
124
|
+
previouslyFocusedElement = document.activeElement as HTMLElement;
|
|
125
|
+
// Focus the first TOC item when menu opens
|
|
126
|
+
requestAnimationFrame(() => {
|
|
127
|
+
const firstLink = menuRef?.querySelector<HTMLButtonElement>('.toc-link');
|
|
128
|
+
firstLink?.focus();
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
if (previouslyFocusedElement && previouslyFocusedElement !== buttonRef) {
|
|
132
|
+
// Restore focus when menu closes (unless already on button)
|
|
133
|
+
previouslyFocusedElement.focus();
|
|
134
|
+
}
|
|
135
|
+
previouslyFocusedElement = null;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
93
139
|
$effect(() => {
|
|
94
140
|
const cleanup = setupScrollTracking();
|
|
95
141
|
|
|
@@ -124,7 +170,13 @@
|
|
|
124
170
|
|
|
125
171
|
<!-- Floating Menu -->
|
|
126
172
|
{#if isOpen}
|
|
127
|
-
<div
|
|
173
|
+
<div
|
|
174
|
+
class="toc-menu"
|
|
175
|
+
bind:this={menuRef}
|
|
176
|
+
role="dialog"
|
|
177
|
+
aria-modal="true"
|
|
178
|
+
aria-label="Table of contents"
|
|
179
|
+
>
|
|
128
180
|
<h3 class="toc-title">{title}</h3>
|
|
129
181
|
<ul class="toc-list">
|
|
130
182
|
{#each headers as header (header.id)}
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
|
|
262
262
|
<div class="flex gap-3">
|
|
263
263
|
<a
|
|
264
|
-
href="/
|
|
264
|
+
href="/arbor/posts"
|
|
265
265
|
class="flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 text-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
|
|
266
266
|
>
|
|
267
267
|
Manage Posts
|
package/dist/config/tiers.d.ts
CHANGED
|
@@ -18,9 +18,11 @@ export type TierIcon = "user" | "sprout" | "tree-deciduous" | "trees" | "crown";
|
|
|
18
18
|
export interface TierLimits {
|
|
19
19
|
posts: number;
|
|
20
20
|
storage: number;
|
|
21
|
+
storageDisplay: string;
|
|
21
22
|
themes: number;
|
|
22
23
|
navPages: number;
|
|
23
24
|
commentsPerWeek: number;
|
|
25
|
+
aiWordsPerMonth: number;
|
|
24
26
|
}
|
|
25
27
|
export interface TierFeatures {
|
|
26
28
|
blog: boolean;
|
|
@@ -121,7 +123,7 @@ export declare function tierHasFeature(tier: TierKey, feature: keyof TierFeature
|
|
|
121
123
|
/**
|
|
122
124
|
* Get a specific limit for a tier.
|
|
123
125
|
*/
|
|
124
|
-
export declare function getTierLimit(tier: TierKey, limit: keyof TierLimits): number;
|
|
126
|
+
export declare function getTierLimit(tier: TierKey, limit: keyof TierLimits): number | string;
|
|
125
127
|
/**
|
|
126
128
|
* Get rate limits for a tier.
|
|
127
129
|
*/
|
package/dist/config/tiers.js
CHANGED
|
@@ -22,9 +22,11 @@ export const TIERS = {
|
|
|
22
22
|
limits: {
|
|
23
23
|
posts: 0,
|
|
24
24
|
storage: 0,
|
|
25
|
+
storageDisplay: "0 MB",
|
|
25
26
|
themes: 0,
|
|
26
27
|
navPages: 0,
|
|
27
28
|
commentsPerWeek: 20,
|
|
29
|
+
aiWordsPerMonth: 0,
|
|
28
30
|
},
|
|
29
31
|
features: {
|
|
30
32
|
blog: false,
|
|
@@ -55,14 +57,14 @@ export const TIERS = {
|
|
|
55
57
|
display: {
|
|
56
58
|
name: "Free",
|
|
57
59
|
tagline: "Just visiting",
|
|
58
|
-
description: "Hang out in Meadow, follow
|
|
60
|
+
description: "Hang out in Meadow, follow gardens, react and comment.",
|
|
59
61
|
icon: "user",
|
|
60
62
|
bestFor: "Readers",
|
|
61
63
|
featureStrings: [
|
|
62
64
|
"Meadow access",
|
|
63
65
|
"20 comments/week",
|
|
64
|
-
"Follow
|
|
65
|
-
"React to
|
|
66
|
+
"Follow gardens",
|
|
67
|
+
"React to blooms",
|
|
66
68
|
],
|
|
67
69
|
},
|
|
68
70
|
support: { level: "help_center", displayString: "Help Center" },
|
|
@@ -74,9 +76,11 @@ export const TIERS = {
|
|
|
74
76
|
limits: {
|
|
75
77
|
posts: 50,
|
|
76
78
|
storage: 1 * 1024 * 1024 * 1024, // 1 GB
|
|
79
|
+
storageDisplay: "1 GB",
|
|
77
80
|
themes: 3,
|
|
78
81
|
navPages: 0,
|
|
79
82
|
commentsPerWeek: Infinity,
|
|
83
|
+
aiWordsPerMonth: 750, // ~25/day * 30 days
|
|
80
84
|
},
|
|
81
85
|
features: {
|
|
82
86
|
blog: true,
|
|
@@ -111,7 +115,7 @@ export const TIERS = {
|
|
|
111
115
|
icon: "sprout",
|
|
112
116
|
bestFor: "Curious",
|
|
113
117
|
featureStrings: [
|
|
114
|
-
"50
|
|
118
|
+
"50 blooms",
|
|
115
119
|
"1 GB storage",
|
|
116
120
|
"3 curated themes",
|
|
117
121
|
"Meadow access",
|
|
@@ -128,9 +132,11 @@ export const TIERS = {
|
|
|
128
132
|
limits: {
|
|
129
133
|
posts: 250,
|
|
130
134
|
storage: 5 * 1024 * 1024 * 1024, // 5 GB
|
|
135
|
+
storageDisplay: "5 GB",
|
|
131
136
|
themes: 10,
|
|
132
137
|
navPages: 3,
|
|
133
138
|
commentsPerWeek: Infinity,
|
|
139
|
+
aiWordsPerMonth: 3000, // ~100/day * 30 days
|
|
134
140
|
},
|
|
135
141
|
features: {
|
|
136
142
|
blog: true,
|
|
@@ -165,7 +171,7 @@ export const TIERS = {
|
|
|
165
171
|
icon: "tree-deciduous",
|
|
166
172
|
bestFor: "Hobbyists",
|
|
167
173
|
featureStrings: [
|
|
168
|
-
"250
|
|
174
|
+
"250 blooms",
|
|
169
175
|
"5 GB storage",
|
|
170
176
|
"10 themes",
|
|
171
177
|
"3 nav pages",
|
|
@@ -183,9 +189,11 @@ export const TIERS = {
|
|
|
183
189
|
limits: {
|
|
184
190
|
posts: Infinity,
|
|
185
191
|
storage: 20 * 1024 * 1024 * 1024, // 20 GB
|
|
192
|
+
storageDisplay: "20 GB",
|
|
186
193
|
themes: Infinity,
|
|
187
194
|
navPages: 5,
|
|
188
195
|
commentsPerWeek: Infinity,
|
|
196
|
+
aiWordsPerMonth: 15000, // ~500/day * 30 days
|
|
189
197
|
},
|
|
190
198
|
features: {
|
|
191
199
|
blog: true,
|
|
@@ -220,7 +228,7 @@ export const TIERS = {
|
|
|
220
228
|
icon: "trees",
|
|
221
229
|
bestFor: "Serious Bloggers",
|
|
222
230
|
featureStrings: [
|
|
223
|
-
"Unlimited
|
|
231
|
+
"Unlimited blooms",
|
|
224
232
|
"20 GB storage",
|
|
225
233
|
"Theme customizer",
|
|
226
234
|
"5 nav pages",
|
|
@@ -238,9 +246,11 @@ export const TIERS = {
|
|
|
238
246
|
limits: {
|
|
239
247
|
posts: Infinity,
|
|
240
248
|
storage: 100 * 1024 * 1024 * 1024, // 100 GB
|
|
249
|
+
storageDisplay: "100 GB",
|
|
241
250
|
themes: Infinity,
|
|
242
251
|
navPages: 8,
|
|
243
252
|
commentsPerWeek: Infinity,
|
|
253
|
+
aiWordsPerMonth: 75000, // ~2500/day * 30 days
|
|
244
254
|
},
|
|
245
255
|
features: {
|
|
246
256
|
blog: true,
|
|
@@ -79,6 +79,19 @@
|
|
|
79
79
|
let loadingMore = $state(false);
|
|
80
80
|
let expandedCards = $state(new Set<string>());
|
|
81
81
|
|
|
82
|
+
// Cache rendered markdown by summary ID (prevents re-rendering on every reactive update)
|
|
83
|
+
const renderedHtmlCache = new Map<string, string>();
|
|
84
|
+
|
|
85
|
+
function getRenderedHtml(summary: Summary): string {
|
|
86
|
+
const cacheKey = summary.id;
|
|
87
|
+
if (renderedHtmlCache.has(cacheKey)) {
|
|
88
|
+
return renderedHtmlCache.get(cacheKey)!;
|
|
89
|
+
}
|
|
90
|
+
const gutterItems = summary.gutter_content ?? [];
|
|
91
|
+
const html = renderMarkdownWithGutter(summary.detailed_timeline ?? '', gutterItems);
|
|
92
|
+
renderedHtmlCache.set(cacheKey, html);
|
|
93
|
+
return html;
|
|
94
|
+
}
|
|
82
95
|
|
|
83
96
|
// Fun rest day messages
|
|
84
97
|
const REST_DAY_MESSAGES = [
|
|
@@ -320,7 +333,7 @@
|
|
|
320
333
|
{#if summary.detailed_timeline && isExpanded}
|
|
321
334
|
<div class="detailed-section">
|
|
322
335
|
<div class="detailed-timeline markdown-content">
|
|
323
|
-
{@html
|
|
336
|
+
{@html getRenderedHtml(summary)}
|
|
324
337
|
</div>
|
|
325
338
|
</div>
|
|
326
339
|
{/if}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Curio Secrets Helper
|
|
3
|
+
*
|
|
4
|
+
* Provides token retrieval with graceful migration from legacy encryption
|
|
5
|
+
* (TOKEN_ENCRYPTION_KEY) to envelope encryption (SecretsManager).
|
|
6
|
+
*
|
|
7
|
+
* Migration strategy:
|
|
8
|
+
* 1. Try SecretsManager first (new system, per-tenant isolation)
|
|
9
|
+
* 2. Fall back to legacy column + TOKEN_ENCRYPTION_KEY
|
|
10
|
+
* 3. Auto-migrate legacy tokens to SecretsManager on successful read
|
|
11
|
+
*/
|
|
12
|
+
import { type SecretsManager } from "../../server/secrets";
|
|
13
|
+
/** Secret key names for Timeline tokens in SecretsManager */
|
|
14
|
+
export declare const TIMELINE_SECRET_KEYS: {
|
|
15
|
+
readonly GITHUB_TOKEN: "timeline_github_token";
|
|
16
|
+
readonly OPENROUTER_KEY: "timeline_openrouter_key";
|
|
17
|
+
};
|
|
18
|
+
export type TimelineSecretKey = (typeof TIMELINE_SECRET_KEYS)[keyof typeof TIMELINE_SECRET_KEYS];
|
|
19
|
+
/**
|
|
20
|
+
* Environment bindings needed for token operations
|
|
21
|
+
*/
|
|
22
|
+
interface TokenEnv {
|
|
23
|
+
DB: D1Database;
|
|
24
|
+
GROVE_KEK?: {
|
|
25
|
+
get(): Promise<string>;
|
|
26
|
+
};
|
|
27
|
+
TOKEN_ENCRYPTION_KEY?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Result of a token retrieval operation
|
|
31
|
+
*/
|
|
32
|
+
export interface TokenResult {
|
|
33
|
+
token: string | null;
|
|
34
|
+
source: "secrets_manager" | "legacy" | "none";
|
|
35
|
+
migrated: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get a Timeline token with graceful migration from legacy encryption.
|
|
39
|
+
*
|
|
40
|
+
* Priority:
|
|
41
|
+
* 1. SecretsManager (envelope encryption) - preferred, per-tenant isolation
|
|
42
|
+
* 2. Legacy column + TOKEN_ENCRYPTION_KEY - fallback with auto-migrate
|
|
43
|
+
*
|
|
44
|
+
* @param env - Platform environment with DB and encryption bindings
|
|
45
|
+
* @param tenantId - The tenant ID
|
|
46
|
+
* @param keyName - Which secret to retrieve
|
|
47
|
+
* @param legacyColumnValue - Value from the legacy encrypted column (may be null)
|
|
48
|
+
* @returns TokenResult with the decrypted token and metadata
|
|
49
|
+
*/
|
|
50
|
+
export declare function getTimelineToken(env: TokenEnv, tenantId: string, keyName: TimelineSecretKey, legacyColumnValue: string | null): Promise<TokenResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Save a Timeline token using SecretsManager (preferred) or legacy encryption.
|
|
53
|
+
*
|
|
54
|
+
* @param env - Platform environment with DB and encryption bindings
|
|
55
|
+
* @param tenantId - The tenant ID
|
|
56
|
+
* @param keyName - Which secret to store
|
|
57
|
+
* @param plainToken - The plaintext token value
|
|
58
|
+
* @returns Object indicating which system was used
|
|
59
|
+
*/
|
|
60
|
+
export declare function setTimelineToken(env: TokenEnv, tenantId: string, keyName: TimelineSecretKey, plainToken: string): Promise<{
|
|
61
|
+
system: "secrets_manager" | "legacy";
|
|
62
|
+
legacyValue: string | null;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Delete a Timeline token from SecretsManager.
|
|
66
|
+
* Legacy column should be cleared separately via SQL.
|
|
67
|
+
*/
|
|
68
|
+
export declare function deleteTimelineToken(env: TokenEnv, tenantId: string, keyName: TimelineSecretKey): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a Timeline token exists in either system.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hasTimelineToken(env: TokenEnv, tenantId: string, keyName: TimelineSecretKey, legacyColumnValue: string | null): Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Create a SecretsManager instance, handling missing GROVE_KEK gracefully.
|
|
75
|
+
* Returns null if GROVE_KEK is not configured.
|
|
76
|
+
*/
|
|
77
|
+
export declare function maybeCreateSecretsManager(env: TokenEnv): Promise<SecretsManager | null>;
|
|
78
|
+
export {};
|