@morphika/andami 0.1.2
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/LICENSE +21 -0
- package/README.md +50 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +212 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RGB Shift (chromatic aberration) fragment shader.
|
|
3
|
+
*
|
|
4
|
+
* Samples R, G, B channels with slightly offset UVs.
|
|
5
|
+
* Offset direction follows cursor position (hover) or scroll progress (scroll).
|
|
6
|
+
*
|
|
7
|
+
* Uniforms:
|
|
8
|
+
* uTexture — image texture
|
|
9
|
+
* uMouse — normalized cursor/scroll position (0–1)
|
|
10
|
+
* uIntensity — config intensity (0.5–2.0)
|
|
11
|
+
* uSpeed — config speed (0.5–3.0) — unused for this preset
|
|
12
|
+
* uTime — elapsed time (seconds) — unused for this preset
|
|
13
|
+
* uHover — lerped hover/scroll state (0→1)
|
|
14
|
+
* uResolution — canvas size in pixels
|
|
15
|
+
*
|
|
16
|
+
* Phase 2 shader — included for completeness, activated in Session 72.
|
|
17
|
+
*/
|
|
18
|
+
export const rgbShiftFragment = /* glsl */ `
|
|
19
|
+
precision highp float;
|
|
20
|
+
|
|
21
|
+
uniform sampler2D uTexture;
|
|
22
|
+
uniform vec2 uMouse;
|
|
23
|
+
uniform float uIntensity;
|
|
24
|
+
uniform float uSpeed;
|
|
25
|
+
uniform float uTime;
|
|
26
|
+
uniform float uHover;
|
|
27
|
+
uniform vec2 uResolution;
|
|
28
|
+
|
|
29
|
+
varying vec2 vUv;
|
|
30
|
+
|
|
31
|
+
void main() {
|
|
32
|
+
vec2 uv = vUv;
|
|
33
|
+
|
|
34
|
+
// Direction from center to mouse/scroll position
|
|
35
|
+
vec2 dir = uMouse - 0.5;
|
|
36
|
+
float strength = length(dir) * 0.02 * uIntensity * uHover;
|
|
37
|
+
|
|
38
|
+
// Offset each channel in a different direction
|
|
39
|
+
float r = texture2D(uTexture, uv + dir * strength).r;
|
|
40
|
+
float g = texture2D(uTexture, uv).g;
|
|
41
|
+
float b = texture2D(uTexture, uv - dir * strength).b;
|
|
42
|
+
|
|
43
|
+
gl_FragColor = vec4(r, g, b, 1.0);
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ripple distortion fragment shader.
|
|
3
|
+
*
|
|
4
|
+
* Circular wave distortion emanating from the cursor position.
|
|
5
|
+
* Trigger: hover only.
|
|
6
|
+
*
|
|
7
|
+
* Uniforms:
|
|
8
|
+
* uTexture — image texture
|
|
9
|
+
* uMouse — normalized cursor position (0–1)
|
|
10
|
+
* uIntensity — config intensity (0.5–2.0)
|
|
11
|
+
* uSpeed — config speed (0.5–3.0)
|
|
12
|
+
* uTime — elapsed time (seconds)
|
|
13
|
+
* uHover — lerped hover state (0→1)
|
|
14
|
+
* uResolution — canvas size in pixels
|
|
15
|
+
*/
|
|
16
|
+
export const rippleFragment = /* glsl */ `
|
|
17
|
+
precision highp float;
|
|
18
|
+
|
|
19
|
+
uniform sampler2D uTexture;
|
|
20
|
+
uniform vec2 uMouse;
|
|
21
|
+
uniform float uIntensity;
|
|
22
|
+
uniform float uSpeed;
|
|
23
|
+
uniform float uTime;
|
|
24
|
+
uniform float uHover;
|
|
25
|
+
uniform vec2 uResolution;
|
|
26
|
+
|
|
27
|
+
varying vec2 vUv;
|
|
28
|
+
|
|
29
|
+
void main() {
|
|
30
|
+
vec2 uv = vUv;
|
|
31
|
+
|
|
32
|
+
// Distance from cursor
|
|
33
|
+
float dist = distance(uv, uMouse);
|
|
34
|
+
|
|
35
|
+
// Ripple wave — frequency and time-based animation
|
|
36
|
+
float ripple = sin(dist * 30.0 * uSpeed - uTime * 4.0) * 0.02 * uIntensity;
|
|
37
|
+
ripple *= smoothstep(0.5, 0.0, dist); // Fade with distance from cursor
|
|
38
|
+
ripple *= uHover; // Only active when hovering
|
|
39
|
+
|
|
40
|
+
// Displacement direction: push outward from cursor
|
|
41
|
+
vec2 direction = dist > 0.001 ? normalize(uv - uMouse) : vec2(0.0);
|
|
42
|
+
vec2 displaced = uv + direction * ripple;
|
|
43
|
+
|
|
44
|
+
gl_FragColor = texture2D(uTexture, displaced);
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vertex shader — pass-through UV coordinates.
|
|
3
|
+
* Used by all shader presets.
|
|
4
|
+
*/
|
|
5
|
+
export const vertexShader = /* glsl */ `
|
|
6
|
+
attribute vec2 uv;
|
|
7
|
+
attribute vec2 position;
|
|
8
|
+
varying vec2 vUv;
|
|
9
|
+
|
|
10
|
+
void main() {
|
|
11
|
+
vUv = uv;
|
|
12
|
+
gl_Position = vec4(position, 0, 1);
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Central entry point for the storage abstraction layer.
|
|
5
|
+
* Reads the active provider from Sanity and returns the
|
|
6
|
+
* appropriate adapter instance.
|
|
7
|
+
*
|
|
8
|
+
* Currently only R2 is implemented. The adapter pattern is
|
|
9
|
+
* preserved so new providers can be added in the future.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const adapter = await getStorageAdapter();
|
|
13
|
+
* const files = await adapter.listFiles();
|
|
14
|
+
* const url = await adapter.resolveUrl("projects/hero.jpg");
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { client } from "../../lib/sanity/client";
|
|
18
|
+
import { R2Adapter } from "./r2-adapter";
|
|
19
|
+
import { logger } from "../../lib/logger";
|
|
20
|
+
import type { StorageAdapter, StorageProvider, StorageProviderConfig } from "./types";
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Adapter singletons (reused across requests)
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
let r2Adapter: R2Adapter | null = null;
|
|
27
|
+
|
|
28
|
+
function getR2Adapter(): R2Adapter {
|
|
29
|
+
if (!r2Adapter) r2Adapter = new R2Adapter();
|
|
30
|
+
return r2Adapter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// Active provider resolution
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the active storage provider from the asset registry.
|
|
39
|
+
* Defaults to "r2". Preserved for multi-provider architecture.
|
|
40
|
+
*/
|
|
41
|
+
export async function getActiveProvider(): Promise<StorageProvider> {
|
|
42
|
+
try {
|
|
43
|
+
const registry = await client.fetch(
|
|
44
|
+
`*[_type == "assetRegistry"][0]{ storage_provider }`
|
|
45
|
+
);
|
|
46
|
+
const provider = registry?.storage_provider;
|
|
47
|
+
if (provider === "r2") return "r2";
|
|
48
|
+
// Default to r2 (only provider currently supported)
|
|
49
|
+
return "r2";
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error("[Storage]", "Failed to read active provider from Sanity, falling back to R2", err);
|
|
52
|
+
return "r2";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================
|
|
57
|
+
// Factory
|
|
58
|
+
// ============================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the storage adapter for the currently active provider.
|
|
62
|
+
*
|
|
63
|
+
* @param provider - Explicit provider override. If omitted, reads from Sanity.
|
|
64
|
+
* Use this when the caller already knows which provider to use
|
|
65
|
+
* (e.g., the storage admin page showing both providers).
|
|
66
|
+
*/
|
|
67
|
+
export async function getStorageAdapter(
|
|
68
|
+
provider?: StorageProvider
|
|
69
|
+
): Promise<StorageAdapter> {
|
|
70
|
+
const activeProvider = provider ?? (await getActiveProvider());
|
|
71
|
+
|
|
72
|
+
switch (activeProvider) {
|
|
73
|
+
case "r2":
|
|
74
|
+
default:
|
|
75
|
+
return getR2Adapter();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get a specific adapter by provider name (does not check active provider).
|
|
81
|
+
* Used by admin UI to interact with a non-active provider (e.g., testing
|
|
82
|
+
* a new provider connection while another is still active).
|
|
83
|
+
*/
|
|
84
|
+
export function getAdapterForProvider(provider: StorageProvider): StorageAdapter {
|
|
85
|
+
switch (provider) {
|
|
86
|
+
case "r2":
|
|
87
|
+
return getR2Adapter();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the status of all configured storage providers.
|
|
93
|
+
* Returns status for each provider, plus which one is active.
|
|
94
|
+
*/
|
|
95
|
+
export async function getAllProviderStatuses(): Promise<{
|
|
96
|
+
activeProvider: StorageProvider;
|
|
97
|
+
providers: StorageProviderConfig[];
|
|
98
|
+
}> {
|
|
99
|
+
const [activeResult, r2Result] = await Promise.allSettled([
|
|
100
|
+
getActiveProvider(),
|
|
101
|
+
getR2Adapter().getStatus(),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const activeProvider = activeResult.status === "fulfilled" ? activeResult.value : "r2";
|
|
105
|
+
|
|
106
|
+
const r2Status: StorageProviderConfig = r2Result.status === "fulfilled"
|
|
107
|
+
? r2Result.value
|
|
108
|
+
: { provider: "r2", connected: false, displayName: "Cloudflare R2 — Error loading status" };
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
activeProvider,
|
|
112
|
+
providers: [r2Status],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// Cached R2 public URL (for proxy routes)
|
|
118
|
+
// ============================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* In-memory cache for the R2 public URL.
|
|
122
|
+
* Used by the asset proxy routes to resolve R2 redirects without
|
|
123
|
+
* hitting Sanity on every request. TTL: 5 minutes.
|
|
124
|
+
*/
|
|
125
|
+
let r2ConfigCache: {
|
|
126
|
+
provider: StorageProvider;
|
|
127
|
+
r2BucketUrl: string | null;
|
|
128
|
+
fetchedAt: number;
|
|
129
|
+
} | null = null;
|
|
130
|
+
|
|
131
|
+
// #4: In-flight promise deduplication — prevents cache stampede race condition
|
|
132
|
+
let inFlightFetch: Promise<ProviderConfig> | null = null;
|
|
133
|
+
|
|
134
|
+
const PROVIDER_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
135
|
+
|
|
136
|
+
export interface ProviderConfig {
|
|
137
|
+
provider: StorageProvider;
|
|
138
|
+
r2BucketUrl: string | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the active provider + config, with in-memory caching.
|
|
143
|
+
*
|
|
144
|
+
* Used by /api/assets and /api/admin/assets/file to decide
|
|
145
|
+
* how to redirect asset requests.
|
|
146
|
+
* Cached for 5 minutes to avoid hitting Sanity on every asset request.
|
|
147
|
+
*
|
|
148
|
+
* #4: Uses promise chaining to deduplicate concurrent requests.
|
|
149
|
+
* #23: Adds AbortController timeout on Sanity fetch.
|
|
150
|
+
*/
|
|
151
|
+
export async function getCachedProviderConfig(): Promise<ProviderConfig> {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (r2ConfigCache && now - r2ConfigCache.fetchedAt < PROVIDER_CACHE_TTL_MS) {
|
|
154
|
+
return r2ConfigCache;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// #4: If there's already an in-flight fetch, reuse it instead of starting a new one
|
|
158
|
+
if (inFlightFetch) return inFlightFetch;
|
|
159
|
+
|
|
160
|
+
inFlightFetch = (async () => {
|
|
161
|
+
try {
|
|
162
|
+
// #23: Add 5-second timeout to prevent hanging on slow Sanity
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
165
|
+
|
|
166
|
+
const registry = await client.fetch(
|
|
167
|
+
`*[_type == "assetRegistry"][0]{
|
|
168
|
+
storage_provider,
|
|
169
|
+
r2_bucket_url
|
|
170
|
+
}`,
|
|
171
|
+
{},
|
|
172
|
+
{ signal: controller.signal } as Record<string, unknown>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
|
|
177
|
+
const config: ProviderConfig = {
|
|
178
|
+
provider: registry?.storage_provider === "r2" ? "r2" : "r2",
|
|
179
|
+
r2BucketUrl: registry?.r2_bucket_url || null,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
r2ConfigCache = { ...config, fetchedAt: Date.now() };
|
|
183
|
+
return config;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// #19: Log errors for detectability
|
|
186
|
+
logger.error("[Storage]", "Failed to fetch provider config from Sanity", err);
|
|
187
|
+
// On error, return cached value if available, otherwise default to R2
|
|
188
|
+
if (r2ConfigCache) return r2ConfigCache as ProviderConfig;
|
|
189
|
+
return { provider: "r2" as StorageProvider, r2BucketUrl: null };
|
|
190
|
+
} finally {
|
|
191
|
+
inFlightFetch = null;
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
194
|
+
|
|
195
|
+
return inFlightFetch;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Invalidate the provider config cache.
|
|
200
|
+
* Call this after switching providers or updating R2 configuration.
|
|
201
|
+
*/
|
|
202
|
+
export function invalidateProviderConfigCache(): void {
|
|
203
|
+
r2ConfigCache = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================
|
|
207
|
+
// Re-exports for convenience
|
|
208
|
+
// ============================================
|
|
209
|
+
|
|
210
|
+
export type { StorageAdapter, StorageProvider, StorageProviderConfig, ScannedFile } from "./types";
|
|
211
|
+
export { getMimeType, isMediaFile, MEDIA_EXTENSIONS } from "./types";
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare R2 Storage Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the StorageAdapter interface for Cloudflare R2.
|
|
5
|
+
*
|
|
6
|
+
* R2 uses direct public URLs for resolveUrl() (no API call needed)
|
|
7
|
+
* and S3-compatible ListObjectsV2Command for file listing.
|
|
8
|
+
* Static API keys — no token refresh complexity.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
12
|
+
import { client } from "../../lib/sanity/client";
|
|
13
|
+
import { decryptToken } from "../../lib/security";
|
|
14
|
+
import { logger } from "../../lib/logger";
|
|
15
|
+
import type { StorageAdapter, StorageProviderConfig, ScannedFile } from "./types";
|
|
16
|
+
import { isMediaFile, getMimeType } from "./types";
|
|
17
|
+
|
|
18
|
+
// #18: Singleton S3Client — lazy initialized, reused across all operations
|
|
19
|
+
let s3Singleton: S3Client | null = null;
|
|
20
|
+
let s3ConfigHash: string = "";
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// R2 configuration (read from Sanity)
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
interface R2Config {
|
|
27
|
+
bucketUrl: string; // Public URL (e.g. https://assets.example.com)
|
|
28
|
+
endpoint: string; // S3 endpoint (e.g. https://<id>.r2.cloudflarestorage.com)
|
|
29
|
+
bucketName: string; // Bucket name (e.g. my-assets)
|
|
30
|
+
accessKeyId: string; // Decrypted
|
|
31
|
+
secretAccessKey: string; // Decrypted
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// Adapter implementation
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
export class R2Adapter implements StorageAdapter {
|
|
39
|
+
readonly provider = "r2" as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List all media files in the R2 bucket.
|
|
43
|
+
*
|
|
44
|
+
* Uses S3-compatible ListObjectsV2Command to paginate through all objects.
|
|
45
|
+
* Filters to supported media extensions only.
|
|
46
|
+
*
|
|
47
|
+
* @param rootPath - Optional key prefix to scan from (e.g. "projects/").
|
|
48
|
+
* Empty or undefined = scan entire bucket.
|
|
49
|
+
*/
|
|
50
|
+
async listFiles(rootPath?: string): Promise<ScannedFile[]> {
|
|
51
|
+
const config = await this.getConfig();
|
|
52
|
+
if (!config.endpoint || !config.bucketName || !config.accessKeyId || !config.secretAccessKey) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"R2 not connected. Go to /admin/storage to connect your R2 bucket."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// #18: Reuse singleton S3Client instead of creating a new one per call
|
|
59
|
+
const s3 = this.getOrCreateS3Client(config);
|
|
60
|
+
|
|
61
|
+
// Normalize prefix: strip leading/trailing slashes, add trailing slash if non-empty
|
|
62
|
+
const prefix = rootPath
|
|
63
|
+
? rootPath.replace(/^\/+|\/+$/g, "") + "/"
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
const files: ScannedFile[] = [];
|
|
67
|
+
let continuationToken: string | undefined;
|
|
68
|
+
|
|
69
|
+
do {
|
|
70
|
+
const response = await s3.send(
|
|
71
|
+
new ListObjectsV2Command({
|
|
72
|
+
Bucket: config.bucketName,
|
|
73
|
+
Prefix: prefix || undefined,
|
|
74
|
+
ContinuationToken: continuationToken,
|
|
75
|
+
MaxKeys: 1000,
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
for (const obj of response.Contents || []) {
|
|
80
|
+
const key = obj.Key;
|
|
81
|
+
if (!key || !isMediaFile(key)) continue;
|
|
82
|
+
|
|
83
|
+
// Skip zero-byte "folder marker" objects
|
|
84
|
+
if (!obj.Size) continue;
|
|
85
|
+
|
|
86
|
+
const filename = key.split("/").pop() || key;
|
|
87
|
+
const extension = filename.includes(".")
|
|
88
|
+
? filename.split(".").pop()!.toLowerCase()
|
|
89
|
+
: "";
|
|
90
|
+
|
|
91
|
+
files.push({
|
|
92
|
+
path: key,
|
|
93
|
+
filename,
|
|
94
|
+
extension,
|
|
95
|
+
file_size: obj.Size,
|
|
96
|
+
mime_type: getMimeType(filename),
|
|
97
|
+
file_hash: obj.ETag?.replace(/"/g, "") || undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
continuationToken = response.NextContinuationToken;
|
|
102
|
+
} while (continuationToken);
|
|
103
|
+
|
|
104
|
+
// Sort by path for consistent ordering
|
|
105
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
106
|
+
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve a relative path to a direct R2 public URL.
|
|
112
|
+
*
|
|
113
|
+
* This is the key advantage of R2: no API call needed.
|
|
114
|
+
* The URL is deterministic: {bucketUrl}/{relativePath}
|
|
115
|
+
*/
|
|
116
|
+
async resolveUrl(relativePath: string): Promise<string> {
|
|
117
|
+
const config = await this.getConfig();
|
|
118
|
+
if (!config.bucketUrl) {
|
|
119
|
+
throw new Error("R2 public bucket URL not configured");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const base = config.bucketUrl.replace(/\/$/, "");
|
|
123
|
+
const path = relativePath.replace(/^\//, "");
|
|
124
|
+
return `${base}/${path}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Test R2 connectivity.
|
|
129
|
+
* #29: Tests both public URL reachability AND S3 API credentials.
|
|
130
|
+
*/
|
|
131
|
+
async testConnection(): Promise<{ ok: boolean; error?: string }> {
|
|
132
|
+
try {
|
|
133
|
+
const config = await this.getConfig();
|
|
134
|
+
if (!config.bucketUrl) {
|
|
135
|
+
return { ok: false, error: "R2 public URL not configured" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Test 1: Public URL reachability
|
|
139
|
+
const response = await fetch(config.bucketUrl, {
|
|
140
|
+
method: "HEAD",
|
|
141
|
+
signal: AbortSignal.timeout(5000),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!response.ok && response.status !== 404) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: `R2 bucket returned HTTP ${response.status}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// #29: Test 2: S3 API credentials by listing one key
|
|
152
|
+
if (config.endpoint && config.accessKeyId && config.secretAccessKey) {
|
|
153
|
+
try {
|
|
154
|
+
const s3 = this.getOrCreateS3Client(config);
|
|
155
|
+
await s3.send(new ListObjectsV2Command({
|
|
156
|
+
Bucket: config.bucketName,
|
|
157
|
+
MaxKeys: 1,
|
|
158
|
+
}));
|
|
159
|
+
} catch (s3Err) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: `S3 API test failed: ${s3Err instanceof Error ? s3Err.message : "credential error"}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { ok: true };
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: err instanceof Error ? err.message : "Connection test failed",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get R2 connection status for the admin UI.
|
|
178
|
+
*/
|
|
179
|
+
async getStatus(): Promise<StorageProviderConfig> {
|
|
180
|
+
try {
|
|
181
|
+
const registry = await client.fetch(
|
|
182
|
+
`*[_type == "assetRegistry"][0]{
|
|
183
|
+
r2_bucket_url,
|
|
184
|
+
r2_bucket_name,
|
|
185
|
+
r2_connected_at
|
|
186
|
+
}`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const connected = !!registry?.r2_bucket_url && !!registry?.r2_bucket_name;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
provider: "r2",
|
|
193
|
+
connected,
|
|
194
|
+
connectedAt: registry?.r2_connected_at || undefined,
|
|
195
|
+
displayName: connected
|
|
196
|
+
? `Cloudflare R2 — ${registry?.r2_bucket_name}`
|
|
197
|
+
: "Cloudflare R2 — Not connected",
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
return {
|
|
201
|
+
provider: "r2",
|
|
202
|
+
connected: false,
|
|
203
|
+
displayName: "Cloudflare R2 — Status unavailable",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================
|
|
209
|
+
// Internal helpers
|
|
210
|
+
// ============================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* #18: Get or create a singleton S3Client, reusing it if config hasn't changed.
|
|
214
|
+
*/
|
|
215
|
+
private getOrCreateS3Client(config: R2Config): S3Client {
|
|
216
|
+
const hash = `${config.endpoint}:${config.accessKeyId}:${config.bucketName}`;
|
|
217
|
+
if (s3Singleton && s3ConfigHash === hash) return s3Singleton;
|
|
218
|
+
|
|
219
|
+
// Destroy old client if config changed
|
|
220
|
+
if (s3Singleton) {
|
|
221
|
+
try { s3Singleton.destroy(); } catch { /* ignore */ }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
s3Singleton = new S3Client({
|
|
225
|
+
region: "auto",
|
|
226
|
+
endpoint: config.endpoint,
|
|
227
|
+
credentials: {
|
|
228
|
+
accessKeyId: config.accessKeyId,
|
|
229
|
+
secretAccessKey: config.secretAccessKey,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
s3ConfigHash = hash;
|
|
233
|
+
return s3Singleton;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Read R2 configuration from the asset registry in Sanity.
|
|
238
|
+
* Decrypts stored credentials.
|
|
239
|
+
* #5: Gracefully handles corrupted encrypted tokens.
|
|
240
|
+
*/
|
|
241
|
+
private async getConfig(): Promise<R2Config> {
|
|
242
|
+
const registry = await client.fetch(
|
|
243
|
+
`*[_type == "assetRegistry"][0]{
|
|
244
|
+
r2_bucket_url,
|
|
245
|
+
r2_endpoint,
|
|
246
|
+
r2_bucket_name,
|
|
247
|
+
r2_access_key_id,
|
|
248
|
+
r2_secret_access_key
|
|
249
|
+
}`
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (!registry) {
|
|
253
|
+
return {
|
|
254
|
+
bucketUrl: "",
|
|
255
|
+
endpoint: "",
|
|
256
|
+
bucketName: "",
|
|
257
|
+
accessKeyId: "",
|
|
258
|
+
secretAccessKey: "",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// #5: Wrap decryptToken in try/catch to prevent crash on corrupted tokens
|
|
263
|
+
let accessKeyId = "";
|
|
264
|
+
let secretAccessKey = "";
|
|
265
|
+
try {
|
|
266
|
+
accessKeyId = registry.r2_access_key_id
|
|
267
|
+
? await decryptToken(registry.r2_access_key_id)
|
|
268
|
+
: "";
|
|
269
|
+
secretAccessKey = registry.r2_secret_access_key
|
|
270
|
+
? await decryptToken(registry.r2_secret_access_key)
|
|
271
|
+
: "";
|
|
272
|
+
} catch (err) {
|
|
273
|
+
logger.error("[R2Adapter]", "Failed to decrypt credentials", err);
|
|
274
|
+
// Return config without credentials — callers will see empty strings
|
|
275
|
+
// and handle appropriately (e.g., showing "R2 not connected")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
bucketUrl: registry.r2_bucket_url || "",
|
|
280
|
+
endpoint: registry.r2_endpoint || "",
|
|
281
|
+
bucketName: registry.r2_bucket_name || "",
|
|
282
|
+
accessKeyId,
|
|
283
|
+
secretAccessKey,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|