@morphika/andami 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/app/(site)/[slug]/page.tsx +5 -2
  2. package/app/(site)/layout.tsx +4 -2
  3. package/app/(site)/work/[slug]/page.tsx +5 -2
  4. package/app/admin/layout.tsx +2 -1
  5. package/app/api/admin/assets/health/route.ts +1 -1
  6. package/app/api/admin/assets/register/route.ts +1 -1
  7. package/app/api/admin/assets/registry/route.ts +1 -1
  8. package/app/api/admin/assets/relink/confirm/route.ts +1 -1
  9. package/app/api/admin/assets/relink/route.ts +1 -1
  10. package/app/api/admin/assets/scan/route.ts +1 -1
  11. package/app/api/admin/custom-sections/[slug]/route.ts +1 -1
  12. package/app/api/admin/custom-sections/route.ts +1 -1
  13. package/app/api/admin/database/route.ts +1 -1
  14. package/app/api/admin/pages/[slug]/duplicate/route.ts +1 -1
  15. package/app/api/admin/pages/[slug]/route.ts +1 -1
  16. package/app/api/admin/pages/[slug]/set-home/route.ts +1 -1
  17. package/app/api/admin/pages/route.ts +1 -1
  18. package/app/api/admin/preview/route.ts +1 -1
  19. package/app/api/admin/r2/delete/route.ts +1 -1
  20. package/app/api/admin/r2/rename/route.ts +1 -1
  21. package/app/api/admin/r2/status/route.ts +1 -1
  22. package/app/api/admin/r2/upload-url/route.ts +1 -1
  23. package/app/api/admin/settings/route.ts +1 -1
  24. package/app/api/admin/setup/complete/route.ts +1 -1
  25. package/app/api/admin/setup/route.ts +1 -1
  26. package/app/api/admin/storage/switch/route.ts +1 -1
  27. package/app/api/admin/styles/route.ts +1 -1
  28. package/components/ui/Navbar.tsx +9 -7
  29. package/lib/sanity/client.ts +16 -0
  30. package/lib/storage/index.ts +22 -4
  31. package/lib/version.ts +6 -0
  32. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { cache } from "react";
1
2
  import { notFound } from "next/navigation";
2
3
  import type { Metadata } from "next";
3
4
  import { client } from "../../../lib/sanity/client";
@@ -16,14 +17,16 @@ interface PageProps {
16
17
  params: Promise<{ slug: string }>;
17
18
  }
18
19
 
19
- async function getPageBySlug(slug: string): Promise<Page | null> {
20
+ // CPU Audit Fix 3: React cache() deduplicates this call within a single
21
+ // render pass — generateMetadata() and the page component share one fetch.
22
+ const getPageBySlug = cache(async (slug: string): Promise<Page | null> => {
20
23
  try {
21
24
  return await client.fetch<Page>(publishedPageBySlugQuery, { slug });
22
25
  } catch (error) {
23
26
  console.error(`[Page] Failed to fetch page "${slug}":`, error);
24
27
  return null;
25
28
  }
26
- }
29
+ });
27
30
 
28
31
  export async function generateStaticParams() {
29
32
  try {
@@ -1,3 +1,4 @@
1
+ import { cache } from "react";
1
2
  import { client } from "../../lib/sanity/client";
2
3
  import { siteSettingsQuery } from "../../lib/sanity/queries";
3
4
  import type { SiteSettings } from "../../lib/sanity/types";
@@ -11,14 +12,15 @@ import CustomCursor from "../../components/ui/CustomCursor";
11
12
  import ScrollToTop from "../../components/ui/ScrollToTop";
12
13
  import PortfolioTracker from "../../components/ui/PortfolioTracker";
13
14
 
14
- async function getSiteSettings(): Promise<SiteSettings | null> {
15
+ // CPU Audit Fix 3: React cache() deduplicates within a render pass.
16
+ const getSiteSettings = cache(async (): Promise<SiteSettings | null> => {
15
17
  try {
16
18
  return await client.fetch<SiteSettings>(siteSettingsQuery);
17
19
  } catch (error) {
18
20
  console.error("[SiteLayout] Failed to fetch site settings:", error);
19
21
  return null;
20
22
  }
21
- }
23
+ });
22
24
 
23
25
  export default async function SiteLayout({
24
26
  children,
@@ -1,3 +1,4 @@
1
+ import { cache } from "react";
1
2
  import { notFound } from "next/navigation";
2
3
  import type { Metadata } from "next";
3
4
  import { client } from "../../../../lib/sanity/client";
@@ -17,14 +18,16 @@ interface ProjectPageProps {
17
18
  params: Promise<{ slug: string }>;
18
19
  }
19
20
 
20
- async function getProjectPage(slug: string): Promise<Page | null> {
21
+ // CPU Audit Fix 3: React cache() deduplicates this call within a single
22
+ // render pass — generateMetadata() and the page component share one fetch.
23
+ const getProjectPage = cache(async (slug: string): Promise<Page | null> => {
21
24
  try {
22
25
  return await client.fetch<Page>(publishedProjectBySlugQuery, { slug });
23
26
  } catch (error) {
24
27
  console.error(`[Project] Failed to fetch project "${slug}":`, error);
25
28
  return null;
26
29
  }
27
- }
30
+ });
28
31
 
29
32
  export async function generateStaticParams() {
30
33
  try {
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
4
4
  import Link from "next/link";
5
5
  import { useState, useEffect, useRef } from "react";
6
6
  import { getSiteConfig } from "../../lib/config";
7
+ import { ANDAMI_VERSION } from "../../lib/version";
7
8
 
8
9
  // ============================================
9
10
  // Navigation Configuration — flat list, no sections
@@ -161,7 +162,7 @@ export default function AdminLayout({
161
162
  <div className="flex h-14 items-center justify-between px-4 border-b border-white/[0.06]">
162
163
  {sidebarOpen && (
163
164
  <span className="text-[10px] font-semibold tracking-widest uppercase text-white/90 leading-tight">
164
- {getSiteConfig().adminTitle || "Morphika Andami"}<br />
165
+ Morphika Andami <span className="text-white/40 font-normal">v{ANDAMI_VERSION}</span><br />
165
166
  <span className="text-white/50 font-normal">{getSiteConfig().name}</span>
166
167
  </span>
167
168
  )}
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
- import { client } from "../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
4
  import { isAdminAuthenticated } from "../../../../../lib/auth";
5
5
  import { logger } from "../../../../../lib/logger";
@@ -3,7 +3,7 @@ import { isAdminAuthenticated } from "../../../../../lib/auth";
3
3
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
4
4
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath } from "../../../../../lib/security";
5
5
  import { writeClient } from "../../../../../lib/sanity/writeClient";
6
- import { client } from "../../../../../lib/sanity/client";
6
+ import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { isMediaFile, getMimeType } from "../../../../../lib/storage/types";
8
8
  import { auditLog } from "../../../../../lib/audit";
9
9
  import { logger } from "../../../../../lib/logger";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
4
  import { assetRegistryQuery } from "../../../../../lib/sanity/queries";
5
5
  import { isAdminAuthenticated } from "../../../../../lib/auth";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../../lib/sanity/writeClient";
4
4
  import { isAdminAuthenticated } from "../../../../../../lib/auth";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
5
  import { getStorageAdapter } from "../../../../../lib/storage";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
4
  import { isAdminAuthenticated } from "../../../../../lib/auth";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
5
  import { customSectionBySlugQuery, pagesUsingCustomSectionQuery } from "../../../../../lib/sanity/queries";
6
6
  import { isAdminAuthenticated } from "../../../../../lib/auth";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../lib/sanity/writeClient";
4
4
  import { allCustomSectionsQuery } from "../../../../lib/sanity/queries";
5
5
  import { isAdminAuthenticated } from "../../../../lib/auth";
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
- import { client } from "../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../lib/sanity/client";
4
4
 
5
5
  /**
6
6
  * GET /api/admin/database — Sanity connection status & stats
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../../lib/sanity/writeClient";
5
5
  import { pageBySlugQuery } from "../../../../../../lib/sanity/queries";
6
6
  import { isAdminAuthenticated } from "../../../../../../lib/auth";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
5
  import { pageBySlugQuery } from "../../../../../lib/sanity/queries";
6
6
  import { isAdminAuthenticated } from "../../../../../lib/auth";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../../lib/sanity/writeClient";
5
5
  import { isAdminAuthenticated } from "../../../../../../lib/auth";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../lib/sanity/writeClient";
4
4
  import { allPagesQuery } from "../../../../lib/sanity/queries";
5
5
  import { isAdminAuthenticated } from "../../../../lib/auth";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../lib/sanity/client";
3
3
  import { pageBySlugQuery } from "../../../../lib/sanity/queries";
4
4
  import { isAdminAuthenticated } from "../../../../lib/auth";
5
5
  import { logger } from "../../../../lib/logger";
@@ -3,7 +3,7 @@ import { S3Client, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsComma
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
5
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
6
- import { client } from "../../../../../lib/sanity/client";
6
+ import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { writeClient } from "../../../../../lib/sanity/writeClient";
8
8
  import { decryptToken } from "../../../../../lib/security";
9
9
  import { auditLog } from "../../../../../lib/audit";
@@ -3,7 +3,7 @@ import { S3Client, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command,
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
5
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
6
- import { client } from "../../../../../lib/sanity/client";
6
+ import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { writeClient } from "../../../../../lib/sanity/writeClient";
8
8
  import { decryptToken } from "../../../../../lib/security";
9
9
  import { auditLog } from "../../../../../lib/audit";
@@ -1,7 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
- import { client } from "../../../../../lib/sanity/client";
4
+ import { adminClient as client } from "../../../../../lib/sanity/client";
5
5
  import { decryptToken } from "../../../../../lib/security";
6
6
  import { logger } from "../../../../../lib/logger";
7
7
 
@@ -4,7 +4,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { isAdminAuthenticated } from "../../../../../lib/auth";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
6
6
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
7
- import { client } from "../../../../../lib/sanity/client";
7
+ import { adminClient as client } from "../../../../../lib/sanity/client";
8
8
  import { decryptToken } from "../../../../../lib/security";
9
9
  import { getMimeType, isMediaFile } from "../../../../../lib/storage/types";
10
10
  import { logger } from "../../../../../lib/logger";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
- import { client } from "../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../lib/sanity/writeClient";
5
5
  import { siteSettingsQuery } from "../../../../lib/sanity/queries";
6
6
  import { isSafeUrl, isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../lib/security";
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../../lib/auth";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
- import { client } from "../../../../../lib/sanity/client";
4
+ import { adminClient as client } from "../../../../../lib/sanity/client";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
6
6
  import { auditLog } from "../../../../../lib/audit";
7
7
  import { logger } from "../../../../../lib/logger";
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
3
  import { getSetupStatus } from "../../../../lib/setup/detect";
4
- import { client } from "../../../../lib/sanity/client";
4
+ import { adminClient as client } from "../../../../lib/sanity/client";
5
5
  import { writeClient } from "../../../../lib/sanity/writeClient";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
7
7
 
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
- import { client } from "../../../../../lib/sanity/client";
5
+ import { adminClient as client } from "../../../../../lib/sanity/client";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
7
7
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
8
8
  import { invalidateProviderConfigCache } from "../../../../../lib/storage";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
- import { client } from "../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../lib/sanity/writeClient";
5
5
  import { siteStylesQuery } from "../../../../lib/sanity/queries";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
@@ -537,8 +537,10 @@ export default function Navbar({
537
537
  </div>
538
538
 
539
539
  {/* Mobile: simple flex layout with hamburger — uses independent mobile styles */}
540
+ {/* BG color and icon color only apply when menu is open (overlay visible). */}
541
+ {/* When closed, the bar inherits desktop navbar colors for visual continuity. */}
540
542
  <div
541
- className={`flex lg:hidden items-center justify-between ${mobileBarColorClass}`}
543
+ className={`flex lg:hidden items-center justify-between ${isMenuOpen ? mobileBarColorClass : ""} transition-colors duration-300`}
542
544
  style={{
543
545
  maxWidth: "var(--grid-width, 1445px)",
544
546
  marginLeft: "auto",
@@ -547,16 +549,16 @@ export default function Navbar({
547
549
  paddingRight: `${mobilePaddingH}px`,
548
550
  paddingTop: `${mobilePaddingV}px`,
549
551
  paddingBottom: `${mobilePaddingV}px`,
550
- ...mobileNavBgStyle,
551
- ...mobileBarColorStyle,
552
+ ...(isMenuOpen ? mobileNavBgStyle : {}),
553
+ ...(isMenuOpen ? mobileBarColorStyle : {}),
552
554
  }}
553
555
  >
554
556
  <Link
555
557
  href="/"
556
- className={`tracking-normal ${mobileBarColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
558
+ className={`tracking-normal ${isMenuOpen && mobileBarColorClass ? mobileBarColorClass : ""} transition-colors duration-300 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
557
559
  style={{
558
560
  ...linkStyle,
559
- ...(mobileBarIsHex ? { color: "inherit" } : {}),
561
+ ...(isMenuOpen && mobileBarIsHex ? { color: "inherit" } : {}),
560
562
  }}
561
563
  >
562
564
  {logoLabel}
@@ -565,11 +567,11 @@ export default function Navbar({
565
567
  <button
566
568
  ref={hamburgerButtonRef}
567
569
  onClick={() => setIsMenuOpen(!isMenuOpen)}
568
- className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${mobileBarColorClass} focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
570
+ className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${isMenuOpen ? mobileBarColorClass : ""} transition-colors duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
569
571
  aria-label={isMenuOpen ? "Close menu" : "Open menu"}
570
572
  aria-expanded={isMenuOpen}
571
573
  aria-controls="mobile-nav-menu"
572
- style={mobileBarIsHex ? { color: mobileBarColor } : undefined}
574
+ style={isMenuOpen && mobileBarIsHex ? { color: mobileBarColor } : undefined}
573
575
  >
574
576
  <span
575
577
  className={`block w-5 h-[1.5px] bg-current transition-all duration-300 ${
@@ -1,6 +1,22 @@
1
1
  import { createClient } from "next-sanity";
2
2
 
3
+ /**
4
+ * Public read client — uses Sanity CDN for fast edge-cached responses.
5
+ * Stale window: ~120s (acceptable since ISR revalidates every 1h).
6
+ * Used by: public pages, public API routes, asset proxy.
7
+ */
3
8
  export const client = createClient({
9
+ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
10
+ dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
11
+ apiVersion: "2024-01-01",
12
+ useCdn: true,
13
+ });
14
+
15
+ /**
16
+ * Admin read client — bypasses CDN for always-fresh data.
17
+ * Used by: admin API routes that read content for editing.
18
+ */
19
+ export const adminClient = createClient({
4
20
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
5
21
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
6
22
  apiVersion: "2024-01-01",
@@ -14,7 +14,7 @@
14
14
  * const url = await adapter.resolveUrl("projects/hero.jpg");
15
15
  */
16
16
 
17
- import { client } from "../../lib/sanity/client";
17
+ import { adminClient as client } from "../../lib/sanity/client";
18
18
  import { R2Adapter } from "./r2-adapter";
19
19
  import { logger } from "../../lib/logger";
20
20
  import type { StorageAdapter, StorageProvider, StorageProviderConfig } from "./types";
@@ -120,7 +120,7 @@ export async function getAllProviderStatuses(): Promise<{
120
120
  /**
121
121
  * In-memory cache for the R2 public URL.
122
122
  * Used by the asset proxy routes to resolve R2 redirects without
123
- * hitting Sanity on every request. TTL: 5 minutes.
123
+ * hitting Sanity on every request. TTL: 30 minutes.
124
124
  */
125
125
  let r2ConfigCache: {
126
126
  provider: StorageProvider;
@@ -131,7 +131,9 @@ let r2ConfigCache: {
131
131
  // #4: In-flight promise deduplication — prevents cache stampede race condition
132
132
  let inFlightFetch: Promise<ProviderConfig> | null = null;
133
133
 
134
- const PROVIDER_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
134
+ // CPU Audit Fix 4: Increased from 5min to 30min — R2 bucket URL rarely changes,
135
+ // and invalidateProviderConfigCache() handles immediate resets on config changes.
136
+ const PROVIDER_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
135
137
 
136
138
  export interface ProviderConfig {
137
139
  provider: StorageProvider;
@@ -143,7 +145,11 @@ export interface ProviderConfig {
143
145
  *
144
146
  * Used by /api/assets and /api/admin/assets/file to decide
145
147
  * how to redirect asset requests.
146
- * Cached for 5 minutes to avoid hitting Sanity on every asset request.
148
+ *
149
+ * Resolution order (CPU Audit Fix 2):
150
+ * 1. In-memory cache (30-minute TTL)
151
+ * 2. NEXT_PUBLIC_R2_BUCKET_URL env var (zero-cost, no Sanity query)
152
+ * 3. Sanity assetRegistry query (fallback)
147
153
  *
148
154
  * #4: Uses promise chaining to deduplicate concurrent requests.
149
155
  * #23: Adds AbortController timeout on Sanity fetch.
@@ -154,6 +160,18 @@ export async function getCachedProviderConfig(): Promise<ProviderConfig> {
154
160
  return r2ConfigCache;
155
161
  }
156
162
 
163
+ // CPU Audit Fix 2: Check env var first — avoids Sanity query entirely.
164
+ // The R2 bucket URL almost never changes; env var is the fastest path.
165
+ const envBucketUrl = process.env.NEXT_PUBLIC_R2_BUCKET_URL;
166
+ if (envBucketUrl) {
167
+ const config: ProviderConfig = {
168
+ provider: "r2" as StorageProvider,
169
+ r2BucketUrl: envBucketUrl,
170
+ };
171
+ r2ConfigCache = { ...config, fetchedAt: Date.now() };
172
+ return config;
173
+ }
174
+
157
175
  // #4: If there's already an in-flight fetch, reuse it instead of starting a new one
158
176
  if (inFlightFetch) return inFlightFetch;
159
177
 
package/lib/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Framework version — auto-read from package.json at build time.
3
+ * Kept as a simple constant so it can be imported without reading
4
+ * the full package.json at runtime.
5
+ */
6
+ export const ANDAMI_VERSION = "0.1.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",