diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bcd2956 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +node_modules +.next +.env +.env.* +!.env.production.example +*.md +!content/**/*.md +!content/**/*.mdx +.vscode +tests +playwright.config.ts +playwright-report +test-results +*.tsbuildinfo diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..6c2e178 --- /dev/null +++ b/.env.production.example @@ -0,0 +1,26 @@ +# Self-hosted TinaCMS Production Environment +# Copy this to .env and fill in the values + +# Authentication +NEXTAUTH_SECRET=change-me-to-a-random-secret +NEXTAUTH_URL=https://docs.linumiq.com + +# Gitea Git Provider +GITEA_TOKEN=your-gitea-api-token +GITEA_OWNER=LinumIQ +GITEA_REPO=docs +GITEA_URL=https://git.linumiq.com +TINA_GIT_BRANCH=main + +# Redis (internal docker network) +KV_REST_API_TOKEN=change-me-to-a-random-token +REDIS_PASSWORD=change-me-to-a-random-password + +# Site +NEXT_PUBLIC_SITE_URL=https://docs.linumiq.com + +# Optional +# NEXT_PUBLIC_GTM_ID= +# GITHUB_TOKEN= +# GITHUB_REPO= +# GITHUB_OWNER= diff --git a/.gitignore b/.gitignore index dc1c942..42cef6e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +.pnpm-store # testing /coverage diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36b4bae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +FROM node:22-alpine AS base + +RUN corepack enable +RUN apk add --no-cache libc6-compat + +# --- Dependencies stage --- +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# --- Builder stage --- +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV DOCKER_BUILD=true +ENV NEXT_TELEMETRY_DISABLED=1 +ENV TINA_PUBLIC_IS_LOCAL=false +ENV NODE_OPTIONS="--dns-result-order=ipv4first" + +RUN npx tinacms build && npx next build +RUN npx pagefind --site .next --output-path .next/static/pagefind + +# --- Runner stage --- +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV TINA_PUBLIC_IS_LOCAL=false + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy standalone output +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public +COPY --from=builder /app/content ./content +COPY --from=builder /app/tina ./tina + +# Copy the startup indexing script +COPY --from=builder /app/scripts/index-database.mjs ./scripts/index-database.mjs + +# Pagefind static search index +COPY --from=builder /app/.next/static/pagefind ./.next/static/pagefind + +# Create cache directory with correct permissions +RUN mkdir -p .next/cache && chown -R nextjs:nodejs .next/cache + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Entrypoint: index content into Redis, then start Next.js +CMD ["sh", "-c", "node scripts/index-database.mjs && node server.js"] diff --git a/content/users/index.json b/content/users/index.json new file mode 100644 index 0000000..b79bbd9 --- /dev/null +++ b/content/users/index.json @@ -0,0 +1,13 @@ +{ + "users": [ + { + "name": "Admin", + "email": "admin@linumiq.com", + "username": "admin", + "password": { + "value": "admin", + "passwordChangeRequired": true + } + } + ] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..588f305 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - TINA_PUBLIC_IS_LOCAL=false + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} + - GITEA_TOKEN=${GITEA_TOKEN} + - GITEA_OWNER=${GITEA_OWNER:-LinumIQ} + - GITEA_REPO=${GITEA_REPO:-docs} + - GITEA_URL=${GITEA_URL:-https://git.linumiq.com} + - TINA_GIT_BRANCH=${TINA_GIT_BRANCH:-main} + - KV_REST_API_URL=http://redis-http:80 + - KV_REST_API_TOKEN=${KV_REST_API_TOKEN} + - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://docs.linumiq.com} + depends_on: + redis-http: + condition: service_started + redis: + condition: service_started + networks: + - tinadocs + restart: unless-stopped + + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis-data:/data + networks: + - tinadocs + restart: unless-stopped + + redis-http: + image: hiett/serverless-redis-http:latest + environment: + - SRH_MODE=env + - SRH_TOKEN=${KV_REST_API_TOKEN} + - SRH_CONNECTION_STRING=redis://:${REDIS_PASSWORD}@redis:6379 + depends_on: + redis: + condition: service_started + networks: + - tinadocs + restart: unless-stopped + +volumes: + redis-data: + +networks: + tinadocs: + driver: bridge diff --git a/next.config.js b/next.config.js index 12b9741..5ab9f67 100644 --- a/next.config.js +++ b/next.config.js @@ -7,12 +7,16 @@ const isStatic = process.env.EXPORT_MODE === "static"; const basePath = process.env.NEXT_PUBLIC_BASE_PATH; const assetPrefix = process.env.NEXT_PUBLIC_ASSET_PREFIX || basePath; +const isDocker = process.env.DOCKER_BUILD === "true"; + const extraConfig = {}; if (isStatic) { extraConfig.output = "export"; extraConfig.trailingSlash = true; extraConfig.skipTrailingSlashRedirect = true; +} else if (isDocker) { + extraConfig.output = "standalone"; } module.exports = { diff --git a/package.json b/package.json index 9f399ba..108c9ab 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "predev": "node scripts/check-pagefind.js", - "dev": "node --dns-result-order=ipv4first ./node_modules/.bin/tinacms dev -c \"next dev --turbopack\"", + "dev": "TINA_PUBLIC_IS_LOCAL=true node --dns-result-order=ipv4first ./node_modules/.bin/tinacms dev -c \"next dev --turbopack\"", + "dev:prod": "TINA_PUBLIC_IS_LOCAL=false node --dns-result-order=ipv4first ./node_modules/.bin/tinacms dev -c \"next dev\"", "build": "echo 'Starting TinaCMS build...' && node --dns-result-order=ipv4first ./node_modules/.bin/tinacms build && echo 'TinaCMS build completed. Starting Next.js build...' && next build", "postbuild": "npx pagefind --site .next --output-path .next/static/pagefind && next-sitemap", "build-local-pagefind": "node --dns-result-order=ipv4first ./node_modules/.bin/tinacms build && next build && npx pagefind --site .next --output-subdir ../public/pagefind", @@ -22,15 +23,19 @@ "@next/third-parties": "^16.1.1", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-tabs": "^1.1.12", + "@tinacms/datalayer": "^2.0.14", + "@upstash/redis": "^1.37.0", "copy-to-clipboard": "^3.3.3", "date-fns": "^4.1.0", "fast-glob": "^3.3.3", "html-to-md": "^0.8.8", + "js-base64": "^3.7.8", "lodash": "^4.17.21", "mermaid": "^11.6.0", "monaco-editor": "^0.52.2", "motion": "^12.15.0", "next": "^15.4.10", + "next-auth": "^4.24.13", "next-themes": "^0.4.6", "prism-react-renderer": "^2.4.1", "prismjs": "^1.30.0", @@ -43,8 +48,10 @@ "rehype-pretty-code": "^0.14.1", "shiki": "^3.6.0", "tinacms": "^3.4.1", + "tinacms-authjs": "^21.0.1", "title-case": "^4.3.2", - "typescript": "5.8.3" + "typescript": "5.8.3", + "upstash-redis-level": "^1.1.1" }, "devDependencies": { "@biomejs/biome": "^1.9.4", diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts new file mode 100644 index 0000000..1c42afd --- /dev/null +++ b/pages/api/auth/[...nextauth].ts @@ -0,0 +1,4 @@ +import NextAuth from "next-auth"; +import { authOptions } from "../tina/[...routes]"; + +export default NextAuth(authOptions); diff --git a/pages/api/tina/[...routes].ts b/pages/api/tina/[...routes].ts new file mode 100644 index 0000000..c4a3763 --- /dev/null +++ b/pages/api/tina/[...routes].ts @@ -0,0 +1,124 @@ +import { + TinaNodeBackend, + LocalBackendAuthProvider, +} from "@tinacms/datalayer"; +import NextAuth from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { getServerSession } from "next-auth/next"; + +import databaseClient from "../../../tina/__generated__/databaseClient"; + +const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true"; + +const TINA_CREDENTIALS_PROVIDER_NAME = "TinaCredentials"; + +const authenticate = async ( + dbClient: typeof databaseClient, + username: string, + password: string +) => { + try { + const result = await dbClient.authenticate({ username, password }); + return result.data?.authenticate || null; + } catch (e) { + return null; + } +}; + +const authOptions = { + callbacks: { + jwt: async ({ token: jwt, account }: any) => { + if (account) { + try { + if (jwt?.sub) { + const result = await databaseClient.authorize({ sub: jwt.sub }); + jwt.role = result.data?.authorize ? "user" : "guest"; + jwt.passwordChangeRequired = + result.data?.authorize?._password?.passwordChangeRequired || + false; + } + } catch { + // ignore auth errors + } + if (jwt.role === undefined) { + jwt.role = "guest"; + } + } + return jwt; + }, + session: async ({ session, token: jwt }: any) => { + session.user.role = jwt.role; + session.user.passwordChangeRequired = jwt.passwordChangeRequired; + session.user.sub = jwt.sub; + return session; + }, + }, + session: { strategy: "jwt" as const }, + secret: process.env.NEXTAUTH_SECRET as string, + providers: [ + CredentialsProvider({ + name: TINA_CREDENTIALS_PROVIDER_NAME, + credentials: { + username: { label: "Username", type: "text" }, + password: { label: "Password", type: "password" }, + }, + authorize: async (credentials: any) => + authenticate(databaseClient, credentials.username, credentials.password), + }), + ], +}; + +const handler = TinaNodeBackend({ + authProvider: isLocal + ? LocalBackendAuthProvider() + : { + initialize: async () => {}, + isAuthorized: async (req: any, res: any) => { + const session = await getServerSession(req, res, authOptions); + if (!req.session) { + Object.defineProperty(req, "session", { + value: session, + writable: false, + }); + } + if (!(session as any)?.user) { + return { + errorCode: 401, + errorMessage: "Unauthorized", + isAuthorized: false, + }; + } + if ((session as any)?.user?.role !== "user") { + return { + errorCode: 403, + errorMessage: "Forbidden", + isAuthorized: false, + }; + } + return { isAuthorized: true }; + }, + extraRoutes: { + auth: { + secure: false, + handler: async (req: any, res: any, opts: any) => { + const url = new URL( + req.url, + `http://${req.headers?.host || "localhost"}` + ); + const authSubRoutes = url.pathname + ?.replace(`${opts.basePath}auth/`, "") + ?.split("/"); + req.query.nextauth = authSubRoutes; + await NextAuth(authOptions)(req, res); + }, + }, + }, + }, + databaseClient, +}); + +export default (req: any, res: any) => { + return handler(req, res); +}; + +export { authOptions }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c68236..3c420dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,13 +16,19 @@ importers: version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@next/third-parties': specifier: ^16.1.1 - version: 16.1.1(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 16.1.1(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@radix-ui/react-dropdown-menu': specifier: ^2.1.15 version: 2.1.15(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-tabs': specifier: ^1.1.12 version: 1.1.12(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tinacms/datalayer': + specifier: ^2.0.14 + version: 2.0.14(react@19.2.3)(typescript@5.8.3) + '@upstash/redis': + specifier: ^1.37.0 + version: 1.37.0 copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 @@ -35,6 +41,9 @@ importers: html-to-md: specifier: ^0.8.8 version: 0.8.8 + js-base64: + specifier: ^3.7.8 + version: 3.7.8 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -49,7 +58,10 @@ importers: version: 12.23.1(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: specifier: ^15.4.10 - version: 15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-auth: + specifier: ^4.24.13 + version: 4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -86,12 +98,18 @@ importers: tinacms: specifier: ^3.4.1 version: 3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)) + tinacms-authjs: + specifier: ^21.0.1 + version: 21.0.1(next-auth@4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tinacms@3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)))(yup@1.7.1) title-case: specifier: ^4.3.2 version: 4.3.2 typescript: specifier: 5.8.3 version: 5.8.3 + upstash-redis-level: + specifier: ^1.1.1 + version: 1.1.1 devDependencies: '@biomejs/biome': specifier: ^1.9.4 @@ -122,7 +140,7 @@ importers: version: 7.1.0(monaco-editor@0.52.2)(webpack@5.100.0) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 4.2.3(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) pagefind: specifier: ^1.3.0 version: 1.3.0 @@ -800,24 +818,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -1509,8 +1531,8 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - '@graphql-codegen/plugin-helpers@6.1.0': - resolution: {integrity: sha512-JJypehWTcty9kxKiqH7TQOetkGdOYjY78RHlI+23qB59cV2wxjFFVf8l7kmuXS4cpGVUNfIjFhVr7A1W7JMtdA==} + '@graphql-codegen/plugin-helpers@6.2.0': + resolution: {integrity: sha512-TKm0Q0+wRlg354Qt3PyXc+sy6dCKxmNofBsgmHoFZNVHtzMQSSgNT+rUWdwBwObQ9bFHiUVsDIv8QqxKMiKmpw==} engines: {node: '>=16'} peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -1601,6 +1623,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@11.0.0': + resolution: {integrity: sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@9.2.1': resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} peerDependencies: @@ -1674,89 +1702,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1912,24 +1956,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@15.4.8': resolution: {integrity: sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@15.4.8': resolution: {integrity: sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@15.4.8': resolution: {integrity: sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@15.4.8': resolution: {integrity: sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==} @@ -1986,6 +2034,9 @@ packages: cpu: [x64] os: [win32] + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2782,24 +2833,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.11': resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.11': resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.11': resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.11': resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} @@ -2859,12 +2914,21 @@ packages: react: '>=18.3.1 <20.0.0' react-dom: '>=18.3.1 <20.0.0' + '@tinacms/datalayer@2.0.14': + resolution: {integrity: sha512-5PQX6+4RNFQ8Rh9ISMsJ8pj4Lqm81FLmVHEOyU7+ziJCyF4TT/v3B89BW4VqVmVr0iRqowVfW68Ap+7m8J+RZQ==} + '@tinacms/graphql@2.1.1': resolution: {integrity: sha512-lX2taaOdSEmgEiCEogN8dkf3RH4yxd3RG4bqj4lL4gHD47QPcaFI5/TDNdEzw6y0Zmkg58/KClQpWya4o84wxw==} + '@tinacms/graphql@2.2.2': + resolution: {integrity: sha512-xwjM7WTUtUWodRnIyRozP9qJvqvwW1ZUZPoq8fHuq1kBaJyaEWbq+KWnd6IEG8fzX4w0aLhfwvAA19/9Ucoq/Q==} + '@tinacms/mdx@2.0.5': resolution: {integrity: sha512-IyzUB/9ep+z1VI+ap2ZpqdDYtlMABG40wwoIQviI4iS+bB5ka7B2bTeK1VhN+1FoKjXfPv5e7Qmev6es/hE1Eg==} + '@tinacms/mdx@2.1.0': + resolution: {integrity: sha512-3A8fDUhhRqtchU0M5vmDM9pnvG4sXy4FG2hdiUlfyeytAF0HwBi7EZq7n0Cv7qvgd5E0vXSKqOZ9H/lOl8QQXQ==} + '@tinacms/metrics@2.0.1': resolution: {integrity: sha512-CKRzs3fbuwcwobNOAFGdo94hLfGzLkKeREpKoAmVj/zFJZ40NJQ+p3tEUm/o1xPA4Zwgfq0QYmN3gNNLHymgGA==} peerDependencies: @@ -2876,6 +2940,12 @@ packages: react: '>=16.14.0' yup: ^1.0.0 + '@tinacms/schema-tools@2.7.0': + resolution: {integrity: sha512-uc6XL8xtldoFQ3xHnXkijJTALEB+gEsyvsf7q/1Gs1uvG5Alm6xPmBAdWxM4fUZv1WDQZwH46SW+cXK1Ej+N8w==} + peerDependencies: + react: '>=16.14.0' + yup: ^1.0.0 + '@tinacms/search@1.2.2': resolution: {integrity: sha512-5KbhocVAZvSWj6xoZbAUqju37kZKm1RWlc8xC3L0j6Rg3RGyTyL8NWYTyflw7rjgZrlUMaIxO6Q3CwDpX1fjZQ==} @@ -3262,6 +3332,12 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@upstash/redis@1.24.3': + resolution: {integrity: sha512-gw6d4IA1biB4eye5ESaXc0zOlVQI94aptsBvVcTghYWu1kRmOrJFoMFEDCa8p5uzluyYAOFCuY2GWLR6O4ZoIw==} + + '@upstash/redis@1.37.0': + resolution: {integrity: sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==} + '@vitejs/plugin-react@3.1.0': resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4806,6 +4882,9 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jotai-optics@0.4.0: resolution: {integrity: sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g==} peerDependencies: @@ -4836,6 +4915,9 @@ packages: react: optional: true + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} @@ -4893,6 +4975,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + katex@0.16.22: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true @@ -4980,24 +5067,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -5075,6 +5166,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lucide-react@0.424.0: resolution: {integrity: sha512-x2Nj2aytk1iOyHqt4hKenfVlySq0rYxNeEf8hE0o+Yh0iE36Rqz0rkngVdv2uQtjZ70LAE73eeplhhptYt9x4Q==} peerDependencies: @@ -5410,6 +5505,9 @@ packages: micromark-util-types@1.0.2: resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==} + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} @@ -5561,6 +5659,20 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-auth@4.24.13: + resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} + peerDependencies: + '@auth/core': 0.34.3 + next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 + nodemailer: ^7.0.7 + react: ^17.0.2 || ^18 || ^19 + react-dom: ^17.0.2 || ^18 || ^19 + peerDependenciesMeta: + '@auth/core': + optional: true + nodemailer: + optional: true + next-sitemap@4.2.3: resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} engines: {node: '>=14.18'} @@ -5641,10 +5753,17 @@ packages: nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -5660,6 +5779,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + oidc-token-hash@5.2.0: + resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} + engines: {node: ^10.13.0 || >=12.0.0} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -5676,6 +5799,9 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + openid-client@5.7.1: + resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + optics-ts@2.4.1: resolution: {integrity: sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==} @@ -6044,6 +6170,14 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@5.2.6: + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + + preact@10.29.0: + resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -6054,6 +6188,9 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + prism-react-renderer@2.4.1: resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} peerDependencies: @@ -6814,6 +6951,15 @@ packages: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} + tinacms-authjs@21.0.1: + resolution: {integrity: sha512-BGmaO6fnkj6YLRoATFv0rSgwSxi++cZrygMivarBiK67MDjXXX5Vw11Eumy66ClYDc2xIG8WsKgDqbSN5ltjbQ==} + peerDependencies: + next: ^12.2.5 || ^13 || ^14 || ^15 + next-auth: ^4.22.1 + react: ^17.0.2 || ^18 || ^19 + react-dom: ^17.0.2 || ^18 || ^19 + tinacms: 3.7.1 + tinacms@3.4.1: resolution: {integrity: sha512-vHzBP7Lt0wj7wUZoMsRLvekyLTuPoTA/wGn2BnormLSOj9AXRrxrPwH8qRbz1EyUsrVFbBG2xFCBjlxJh7kj9A==} peerDependencies: @@ -6940,6 +7086,9 @@ packages: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} @@ -7028,6 +7177,9 @@ packages: upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + upstash-redis-level@1.1.1: + resolution: {integrity: sha512-CAtuEJ/t0xaK7ZLY0OKMG5EUYXWx+q+BRPb+mT+1+Dy4x22gzI6wBN28gLnyCwC3NDaEeJifuKx0oohc2UbBQQ==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -7094,6 +7246,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -7245,6 +7401,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -8776,9 +8935,9 @@ snapshots: lodash: 4.17.21 tslib: 2.6.3 - '@graphql-codegen/plugin-helpers@6.1.0(graphql@15.8.0)': + '@graphql-codegen/plugin-helpers@6.2.0(graphql@15.8.0)': dependencies: - '@graphql-tools/utils': 10.11.0(graphql@15.8.0) + '@graphql-tools/utils': 11.0.0(graphql@15.8.0) change-case-all: 1.0.15 common-tags: 1.8.2 graphql: 15.8.0 @@ -8921,6 +9080,14 @@ snapshots: graphql: 15.8.0 tslib: 2.8.1 + '@graphql-tools/utils@11.0.0(graphql@15.8.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 15.8.0 + tslib: 2.8.1 + '@graphql-tools/utils@9.2.1(graphql@15.8.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0) @@ -9231,9 +9398,9 @@ snapshots: '@next/swc-win32-x64-msvc@15.4.8': optional: true - '@next/third-parties@16.1.1(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@next/third-parties@16.1.1(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': dependencies: - next: 15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 third-party-capital: 1.0.20 @@ -9264,6 +9431,8 @@ snapshots: '@pagefind/windows-x64@1.3.0': optional: true + '@panva/hkdf@1.2.1': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -10168,7 +10337,7 @@ snapshots: '@tinacms/cli@2.1.5(@codemirror/language@6.0.0)(@types/node@24.2.1)(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(lightningcss@1.30.1)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@3.29.5)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(terser@5.44.1)(use-sync-external-store@1.6.0(react@19.2.3))(yaml@2.8.1)': dependencies: '@graphql-codegen/core': 2.6.8(graphql@15.8.0) - '@graphql-codegen/plugin-helpers': 6.1.0(graphql@15.8.0) + '@graphql-codegen/plugin-helpers': 6.2.0(graphql@15.8.0) '@graphql-codegen/typescript': 4.1.6(graphql@15.8.0) '@graphql-codegen/typescript-operations': 4.6.1(graphql@15.8.0) '@graphql-codegen/visitor-plugin-common': 4.1.2(graphql@15.8.0) @@ -10249,6 +10418,15 @@ snapshots: - use-sync-external-store - yaml + '@tinacms/datalayer@2.0.14(react@19.2.3)(typescript@5.8.3)': + dependencies: + '@tinacms/graphql': 2.2.2(react@19.2.3)(typescript@5.8.3) + transitivePeerDependencies: + - react + - react-native-b4a + - supports-color + - typescript + '@tinacms/graphql@2.1.1(react@19.2.3)(typescript@5.8.3)': dependencies: '@iarna/toml': 2.2.5 @@ -10277,6 +10455,34 @@ snapshots: - supports-color - typescript + '@tinacms/graphql@2.2.2(react@19.2.3)(typescript@5.8.3)': + dependencies: + '@iarna/toml': 2.2.5 + '@tinacms/mdx': 2.1.0(react@19.2.3)(typescript@5.8.3)(yup@1.7.1) + '@tinacms/schema-tools': 2.7.0(react@19.2.3)(yup@1.7.1) + abstract-level: 1.0.4 + date-fns: 2.30.0 + es-toolkit: 1.42.0 + fast-glob: 3.3.3 + fs-extra: 11.3.2 + glob-parent: 6.0.2 + graphql: 15.8.0 + gray-matter: 4.0.3 + isomorphic-git: 1.35.0 + js-sha1: 0.6.0 + js-yaml: 3.14.2 + jsonpath-plus: 10.4.0 + many-level: 2.0.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + yup: 1.7.1 + transitivePeerDependencies: + - react + - react-native-b4a + - supports-color + - typescript + '@tinacms/mdx@2.0.5(react@19.2.3)(typescript@5.8.3)(yup@1.7.1)': dependencies: '@tinacms/schema-tools': 2.5.0(react@19.2.3)(yup@1.7.1) @@ -10314,6 +10520,43 @@ snapshots: - typescript - yup + '@tinacms/mdx@2.1.0(react@19.2.3)(typescript@5.8.3)(yup@1.7.1)': + dependencies: + '@tinacms/schema-tools': 2.7.0(react@19.2.3)(yup@1.7.1) + acorn: 8.8.2 + ccount: 2.0.1 + estree-util-is-identifier-name: 2.1.0 + mdast-util-compact: 4.1.1 + mdast-util-directive: 2.2.4 + mdast-util-from-markdown: 1.3.0 + mdast-util-gfm: 2.0.2 + mdast-util-mdx-jsx: 2.1.2 + mdast-util-to-markdown: 1.5.0 + micromark-extension-gfm: 2.0.3 + micromark-factory-mdx-expression: 1.0.7 + micromark-factory-space: 1.0.0 + micromark-factory-whitespace: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.1.0 + parse-entities: 4.0.1 + prettier: 2.8.8 + remark: 14.0.2 + remark-gfm: 2.0.0 + remark-mdx: 2.3.0 + stringify-entities: 4.0.3 + typedoc: 0.26.11(typescript@5.8.3) + unist-util-source: 4.0.2 + unist-util-stringify-position: 3.0.3 + unist-util-visit: 4.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + transitivePeerDependencies: + - react + - supports-color + - typescript + - yup + '@tinacms/metrics@2.0.1(fs-extra@11.3.2)': dependencies: fs-extra: 11.3.2 @@ -10326,6 +10569,14 @@ snapshots: yup: 1.7.1 zod: 3.25.76 + '@tinacms/schema-tools@2.7.0(react@19.2.3)(yup@1.7.1)': + dependencies: + picomatch-browser: 2.2.6 + react: 19.2.3 + url-pattern: 1.0.3 + yup: 1.7.1 + zod: 3.25.76 + '@tinacms/search@1.2.2(abstract-level@1.0.4)(react@19.2.3)(sucrase@3.35.0)(typescript@5.8.3)(yup@1.7.1)': dependencies: '@tinacms/graphql': 2.1.1(react@19.2.3)(typescript@5.8.3) @@ -10820,6 +11071,14 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@upstash/redis@1.24.3': + dependencies: + crypto-js: 4.2.0 + + '@upstash/redis@1.37.0': + dependencies: + uncrypto: 0.1.3 + '@vitejs/plugin-react@3.1.0(vite@4.5.14(@types/node@24.2.1)(lightningcss@1.30.1)(terser@5.44.1))': dependencies: '@babel/core': 7.28.5 @@ -12510,6 +12769,8 @@ snapshots: jiti@2.4.2: {} + jose@4.15.9: {} + jotai-optics@0.4.0(jotai@2.8.4(@types/react@19.2.9)(react@19.2.3))(optics-ts@2.4.1): dependencies: jotai: 2.8.4(@types/react@19.2.9)(react@19.2.3) @@ -12527,6 +12788,8 @@ snapshots: '@types/react': 19.2.9 react: 19.2.3 + js-base64@3.7.8: {} + js-cookie@2.2.1: {} js-sha1@0.6.0: {} @@ -12572,6 +12835,12 @@ snapshots: '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) jsep: 1.4.0 + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + katex@0.16.22: dependencies: commander: 8.3.0 @@ -12718,6 +12987,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lucide-react@0.424.0(react@19.2.3): dependencies: react: 19.2.3 @@ -13446,6 +13719,8 @@ snapshots: micromark-util-types@1.0.2: {} + micromark-util-types@1.1.0: {} + micromark-util-types@2.0.2: {} micromark@3.2.0: @@ -13610,20 +13885,35 @@ snapshots: neo-async@2.6.2: {} - next-sitemap@4.2.3(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + next-auth@4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@panva/hkdf': 1.2.1 + cookie: 0.7.1 + jose: 4.15.9 + next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + oauth: 0.9.15 + openid-client: 5.7.1 + preact: 10.29.0 + preact-render-to-string: 5.2.6(preact@10.29.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + uuid: 8.3.2 + + next-sitemap@4.2.3(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 15.4.10 '@swc/helpers': 0.5.15 @@ -13631,7 +13921,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.28.0)(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 15.4.8 '@next/swc-darwin-x64': 15.4.8 @@ -13682,8 +13972,12 @@ snapshots: nullthrows@1.1.1: {} + oauth@0.9.15: {} + object-assign@4.1.1: {} + object-hash@2.2.0: {} + object-hash@3.0.0: {} object-inspect@1.12.3: {} @@ -13692,6 +13986,8 @@ snapshots: object-inspect@1.13.4: {} + oidc-token-hash@5.2.0: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -13714,6 +14010,13 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + openid-client@5.7.1: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.2.0 + optics-ts@2.4.1: {} p-limit@3.1.0: @@ -14144,6 +14447,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@5.2.6(preact@10.29.0): + dependencies: + preact: 10.29.0 + pretty-format: 3.8.0 + + preact@10.29.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -14161,6 +14471,8 @@ snapshots: prettier@2.8.8: {} + pretty-format@3.8.0: {} + prism-react-renderer@2.4.1(react@19.2.3): dependencies: '@types/prismjs': 1.26.5 @@ -14988,12 +15300,12 @@ snapshots: hey-listen: 1.0.8 tslib: 2.8.1 - styled-jsx@5.1.6(@babel/core@7.28.0)(react@19.2.3): + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.5 stylis@4.3.6: {} @@ -15125,6 +15437,17 @@ snapshots: throttle-debounce@3.0.1: {} + tinacms-authjs@21.0.1(next-auth@4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tinacms@3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)))(yup@1.7.1): + dependencies: + '@tinacms/schema-tools': 2.7.0(react@19.2.3)(yup@1.7.1) + next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-auth: 4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tinacms: 3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)) + transitivePeerDependencies: + - yup + tinacms@3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)): dependencies: '@ariakit/react': 0.4.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -15320,6 +15643,8 @@ snapshots: unc-path-regex@0.1.2: {} + uncrypto@0.1.3: {} + undici-types@7.10.0: {} undici-types@7.16.0: {} @@ -15434,6 +15759,12 @@ snapshots: dependencies: tslib: 2.8.1 + upstash-redis-level@1.1.1: + dependencies: + '@upstash/redis': 1.24.3 + abstract-level: 1.0.4 + module-error: 1.0.2 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -15481,6 +15812,8 @@ snapshots: uuid@11.1.0: {} + uuid@8.3.2: {} + uuid@9.0.1: {} uvu@0.5.6: @@ -15646,6 +15979,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yaml@2.8.1: {} diff --git a/run_dev_container.sh b/run_dev_container.sh new file mode 100755 index 0000000..a41d153 --- /dev/null +++ b/run_dev_container.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker run --rm --user 1000:1000 -v $PWD:/files -p 3000:3000 -ti node:lts bash -c "cd /files ; npm run dev" diff --git a/run_in_container.sh b/run_in_container.sh new file mode 100755 index 0000000..70148cd --- /dev/null +++ b/run_in_container.sh @@ -0,0 +1,4 @@ +#!/bin/bash + + docker run --rm --user 1000:1000 -v $PWD:/files -p 3000:3000 -ti node:lts \ + bash -c 'cd /files; "$@"' -- "$@" diff --git a/scripts/index-database.mjs b/scripts/index-database.mjs new file mode 100644 index 0000000..c348c01 --- /dev/null +++ b/scripts/index-database.mjs @@ -0,0 +1,87 @@ +/** + * Startup script: Indexes TinaCMS content into the database (Redis). + * + * In a self-hosted setup, the database (Redis) starts empty. + * This script reads the generated schema files from the filesystem, + * creates a database connection, and indexes all content into Redis. + * + * Run this BEFORE starting the Next.js server. + */ +import fs from "node:fs"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { + createDatabase, + FilesystemBridge, +} from "@tinacms/datalayer"; + +const require = createRequire(import.meta.url); +const { RedisLevel } = require("upstash-redis-level"); + +const branch = process.env.TINA_GIT_BRANCH || "main"; +const kvUrl = process.env.KV_REST_API_URL; +const kvToken = process.env.KV_REST_API_TOKEN; + +if (!kvUrl || !kvToken) { + console.error( + "[index-database] KV_REST_API_URL and KV_REST_API_TOKEN are required" + ); + process.exit(1); +} + +const generatedDir = path.join(process.cwd(), "tina", "__generated__"); + +const graphqlPath = path.join(generatedDir, "_graphql.json"); +const schemaPath = path.join(generatedDir, "_schema.json"); +const lookupPath = path.join(generatedDir, "_lookup.json"); + +// Verify generated files exist +for (const filePath of [graphqlPath, schemaPath, lookupPath]) { + if (!fs.existsSync(filePath)) { + console.error(`[index-database] Missing generated file: ${filePath}`); + process.exit(1); + } +} + +const graphQLSchema = JSON.parse(fs.readFileSync(graphqlPath, "utf-8")); +const tinaSchema = JSON.parse(fs.readFileSync(schemaPath, "utf-8")); +const lookup = JSON.parse(fs.readFileSync(lookupPath, "utf-8")); + +// A no-op git provider since we only need to index, not push +const noopGitProvider = { + onPut: async () => { }, + onDelete: async () => { }, +}; + +const database = createDatabase({ + gitProvider: noopGitProvider, + databaseAdapter: new RedisLevel({ + redis: { url: kvUrl, token: kvToken }, + debug: process.env.DEBUG === "true" || false, + }), + bridge: new FilesystemBridge(process.cwd()), + namespace: branch, +}); + +async function indexContent() { + console.log("[index-database] Starting content indexing..."); + const startTime = Date.now(); + + try { + await database.indexContent({ + graphQLSchema, + tinaSchema: { schema: tinaSchema }, + lookup, + }); + + const elapsed = Date.now() - startTime; + console.log( + `[index-database] Content indexed successfully in ${elapsed}ms` + ); + } catch (error) { + console.error("[index-database] Failed to index content:", error); + process.exit(1); + } +} + +indexContent(); diff --git a/src/app/api/api-schema/route.ts b/src/app/api/api-schema/route.ts index 8799dad..fece108 100644 --- a/src/app/api/api-schema/route.ts +++ b/src/app/api/api-schema/route.ts @@ -1,4 +1,4 @@ -import { client } from "@/tina/__generated__/client"; +import { client } from "@/tina/__generated__/databaseClient"; import { type NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { diff --git a/src/app/docs/[...slug]/page.tsx b/src/app/docs/[...slug]/page.tsx index b519d15..798700b 100644 --- a/src/app/docs/[...slug]/page.tsx +++ b/src/app/docs/[...slug]/page.tsx @@ -3,7 +3,7 @@ import settings from "@/content/siteConfig.json"; import { fetchTinaData } from "@/services/tina/fetch-tina-data"; import { GitHubMetadataProvider } from "@/src/components/page-metadata/github-metadata-context"; import GithubConfig from "@/src/utils/github-client"; -import client from "@/tina/__generated__/client"; +import client from "@/tina/__generated__/databaseClient"; import { getTableOfContents } from "@/utils/docs"; import { getSeo } from "@/utils/metadata/getSeo"; import Document from "."; diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index c02d2f4..d49025e 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -3,7 +3,7 @@ import settings from "@/content/siteConfig.json"; import { fetchTinaData } from "@/services/tina/fetch-tina-data"; import { GitHubMetadataProvider } from "@/src/components/page-metadata/github-metadata-context"; import GitHubClient from "@/src/utils/github-client"; -import client from "@/tina/__generated__/client"; +import client from "@/tina/__generated__/databaseClient"; import { getTableOfContents } from "@/utils/docs"; import { getSeo } from "@/utils/metadata/getSeo"; import Document from "./[...slug]"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 42e6adc..3ef41b2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,7 +3,7 @@ import AdminLink from "@/components/ui/admin-link"; import { TailwindIndicator } from "@/components/ui/tailwind-indicator"; import { ThemeSelector } from "@/components/ui/theme-selector"; import settings from "@/content/settings/config.json"; -import client from "@/tina/__generated__/client"; +import client from "@/tina/__generated__/databaseClient"; import { GoogleTagManager } from "@next/third-parties/google"; import { ThemeProvider } from "next-themes"; import { Inter, Roboto_Flex } from "next/font/google"; @@ -12,6 +12,10 @@ import { TabsLayout } from "@/components/docs/layout/tab-layout"; import type React from "react"; import { TinaClient } from "./tina-client"; +// Force all pages to be server-rendered (not statically generated at build time) +// Required because databaseClient needs a running database connection +export const dynamic = "force-dynamic"; + const isDev = process.env.NODE_ENV === "development"; const body = Inter({ subsets: ["latin"], variable: "--body-font" }); @@ -76,9 +80,13 @@ const Content = ({ children }: { children?: React.ReactNode }) => ( const DocsMenu = async ({ children }: { children?: React.ReactNode }) => { // Fetch navigation data that will be shared across all docs pages - const navigationData = await client.queries.minimisedNavigationBarFetch({ - relativePath: "docs-navigation-bar.json", - }); + const navigationData = JSON.parse( + JSON.stringify( + await client.queries.minimisedNavigationBarFetch({ + relativePath: "docs-navigation-bar.json", + }) + ) + ); return (
diff --git a/src/components/docs/breadcrumbs.tsx b/src/components/docs/breadcrumbs.tsx index 19a223e..1ea62a2 100644 --- a/src/components/docs/breadcrumbs.tsx +++ b/src/components/docs/breadcrumbs.tsx @@ -165,7 +165,7 @@ export const BreadCrumbs = ({ const currentPath = usePathname(); - const breadcrumbs = findBreadcrumbTrail(navigationDocsData, currentPath); + const breadcrumbs = findBreadcrumbTrail(navigationDocsData, currentPath ?? ""); if (!navigationDocsData || breadcrumbs.length === 0) { return null; diff --git a/src/services/tina/fetch-tina-data.ts b/src/services/tina/fetch-tina-data.ts index 460ee0d..5778e41 100644 --- a/src/services/tina/fetch-tina-data.ts +++ b/src/services/tina/fetch-tina-data.ts @@ -24,7 +24,9 @@ export async function fetchTinaData( const response = await queryFunction(variables); - return response; + // Ensure all data is plain objects (databaseClient may return + // objects with null prototypes that can't be passed to Client Components) + return JSON.parse(JSON.stringify(response)); } catch { notFound(); } diff --git a/src/utils/docs/navigation/documentNavigation.ts b/src/utils/docs/navigation/documentNavigation.ts index 96e4ba8..f655b11 100644 --- a/src/utils/docs/navigation/documentNavigation.ts +++ b/src/utils/docs/navigation/documentNavigation.ts @@ -1,5 +1,5 @@ import siteConfig from "@/content/siteConfig.json"; -import client from "@/tina/__generated__/client"; +import client from "@/tina/__generated__/databaseClient"; /** * A single navigation item diff --git a/tina/config.ts b/tina/config.ts index 291d7e8..498c215 100644 --- a/tina/config.ts +++ b/tina/config.ts @@ -1,22 +1,23 @@ -import { defineConfig } from "tinacms"; +import { defineConfig, LocalAuthProvider } from "tinacms"; +import { UsernamePasswordAuthJSProvider } from "tinacms-authjs/dist/tinacms"; import { schema } from "./schema"; +const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true"; + export const config = defineConfig({ - telemetry: 'disabled', + contentApiUrlOverride: "/api/tina/gql", + authProvider: isLocal + ? new LocalAuthProvider() + : new UsernamePasswordAuthJSProvider(), schema, - clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID, branch: + process.env.TINA_GIT_BRANCH || process.env.NEXT_PUBLIC_TINA_BRANCH || // custom branch env override process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF || // Vercel branch env - process.env.HEAD, // Netlify branch env + process.env.HEAD || // Netlify branch env + "main", token: process.env.TINA_TOKEN, media: { - // If you wanted cloudinary do this - // loadCustomStore: async () => { - // const pack = await import("next-tinacms-cloudinary"); - // return pack.TinaCloudCloudinaryMediaStore; - // }, - // this is the config for the tina cloud media store tina: { publicFolder: "public", mediaRoot: "", @@ -24,8 +25,8 @@ export const config = defineConfig({ accept: ["image/*", "video/*", "application/json", ".json"], }, build: { - publicFolder: "public", // The public asset folder for your framework - outputFolder: "admin", // within the public folder + publicFolder: "public", + outputFolder: "admin", basePath: process.env.TINA_BASE_PATH || "", }, }); diff --git a/tina/database.ts b/tina/database.ts new file mode 100644 index 0000000..4e258c4 --- /dev/null +++ b/tina/database.ts @@ -0,0 +1,47 @@ +import { + createDatabase, + createLocalDatabase, + FilesystemBridge, +} from "@tinacms/datalayer"; +import { RedisLevel } from "upstash-redis-level"; +import { Redis } from "@upstash/redis"; +import { GiteaGitProvider } from "./gitea-git-provider"; + +const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true"; +const branch = process.env.TINA_GIT_BRANCH || "main"; + +function createProductionDatabase() { + const giteaUrl = process.env.GITEA_URL; + const giteaToken = process.env.GITEA_TOKEN; + const giteaOwner = process.env.GITEA_OWNER; + const giteaRepo = process.env.GITEA_REPO; + const kvUrl = process.env.KV_REST_API_URL; + const kvToken = process.env.KV_REST_API_TOKEN; + + if (!giteaUrl || !giteaToken || !giteaOwner || !giteaRepo) { + // During tinacms build (schema generation), env vars may not be available. + // Fall back to local database for the build step. + return createLocalDatabase(); + } + + return createDatabase({ + gitProvider: new GiteaGitProvider({ + owner: giteaOwner, + repo: giteaRepo, + token: giteaToken, + branch, + baseUrl: giteaUrl, + }), + databaseAdapter: new RedisLevel>({ + redis: new Redis({ + url: kvUrl || "http://localhost:8079", + token: kvToken || "example_token", + }) as any, + debug: process.env.DEBUG === "true" || false, + }), + bridge: new FilesystemBridge(process.cwd()), + namespace: branch, + }); +} + +export default isLocal ? createLocalDatabase() : createProductionDatabase(); diff --git a/tina/gitea-git-provider.ts b/tina/gitea-git-provider.ts new file mode 100644 index 0000000..435315b --- /dev/null +++ b/tina/gitea-git-provider.ts @@ -0,0 +1,123 @@ +import { Base64 } from "js-base64"; +import type { GitProvider } from "@tinacms/datalayer"; + +export interface GiteaGitProviderOptions { + owner: string; + repo: string; + token: string; + branch: string; + baseUrl: string; + commitMessage?: string; + rootPath?: string; +} + +export class GiteaGitProvider implements GitProvider { + owner: string; + repo: string; + branch: string; + baseUrl: string; + commitMessage: string; + rootPath?: string; + private token: string; + + constructor(args: GiteaGitProviderOptions) { + this.owner = args.owner; + this.repo = args.repo; + this.branch = args.branch; + this.baseUrl = args.baseUrl.replace(/\/$/, ""); + this.commitMessage = args.commitMessage || "Edited with TinaCMS"; + this.rootPath = args.rootPath; + this.token = args.token; + } + + private getApiUrl(path: string): string { + return `${this.baseUrl}/api/v1/repos/${this.owner}/${this.repo}/contents/${path}`; + } + + private getHeaders(): Record { + return { + Authorization: `token ${this.token}`, + "Content-Type": "application/json", + Accept: "application/json", + }; + } + + private getKeyWithPath(key: string): string { + return this.rootPath ? `${this.rootPath}/${key}` : key; + } + + private async getFileSha( + keyWithPath: string + ): Promise { + try { + const url = `${this.getApiUrl(keyWithPath)}?ref=${encodeURIComponent(this.branch)}`; + const response = await fetch(url, { + method: "GET", + headers: this.getHeaders(), + }); + if (!response.ok) { + return undefined; + } + const data = await response.json(); + return data.sha; + } catch { + return undefined; + } + } + + async onPut(key: string, value: string): Promise { + const keyWithPath = this.getKeyWithPath(key); + const sha = await this.getFileSha(keyWithPath); + + const body: Record = { + message: this.commitMessage, + content: Base64.encode(value), + branch: this.branch, + }; + + if (sha) { + body.sha = sha; + } + + const response = await fetch(this.getApiUrl(keyWithPath), { + method: sha ? "PUT" : "POST", + headers: this.getHeaders(), + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to save ${keyWithPath}: ${response.status} ${errorText}` + ); + } + } + + async onDelete(key: string): Promise { + const keyWithPath = this.getKeyWithPath(key); + const sha = await this.getFileSha(keyWithPath); + + if (!sha) { + throw new Error( + `Could not find file ${keyWithPath} in repo ${this.owner}/${this.repo}` + ); + } + + const response = await fetch(this.getApiUrl(keyWithPath), { + method: "DELETE", + headers: this.getHeaders(), + body: JSON.stringify({ + message: this.commitMessage, + branch: this.branch, + sha, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to delete ${keyWithPath}: ${response.status} ${errorText}` + ); + } + } +} diff --git a/tina/schema.tsx b/tina/schema.tsx index 70b6eea..4d8c5c8 100644 --- a/tina/schema.tsx +++ b/tina/schema.tsx @@ -1,4 +1,5 @@ import { type Collection, defineSchema } from "tinacms"; +import { TinaUserCollection } from "tinacms-authjs/dist/tinacms"; import API_Schema_Collection from "./collections/API-schema"; import docsCollection from "./collections/docs"; import docsNavigationBarCollection from "./collections/navigation-bar"; @@ -11,5 +12,6 @@ export const schema = defineSchema({ //TODO: Investigate why casting as unknown works API_Schema_Collection as unknown as Collection, Settings as unknown as Collection, + TinaUserCollection as Collection, ], });