feat: add minimal bun-compiled docker image
This commit is contained in:
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.hermes
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
apps/api/dist
|
||||||
|
apps/web/dist
|
||||||
|
coverage
|
||||||
|
build
|
||||||
|
*.log
|
||||||
|
bun-debug.log*
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
apps/api/prisma/dev.db*
|
||||||
|
README.md
|
||||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM oven/bun:1.3.13 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV DATABASE_URL=file:./dev.db
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
COPY apps/api/package.json apps/api/package.json
|
||||||
|
COPY apps/api/prisma apps/api/prisma
|
||||||
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY packages/shared-types/package.json packages/shared-types/package.json
|
||||||
|
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN bun run --filter @codexdash/web build
|
||||||
|
RUN bun run --filter @codexdash/api bundle
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PORT=3001 \
|
||||||
|
WEB_DIST_DIR=/app/web \
|
||||||
|
CODEX_OAUTH_CALLBACK_BIND_HOST=0.0.0.0
|
||||||
|
|
||||||
|
COPY --from=builder /app/apps/api/dist/codexdash /app/codexdash
|
||||||
|
COPY --from=builder /app/apps/web/dist /app/web
|
||||||
|
|
||||||
|
EXPOSE 3001 1455
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/codexdash"]
|
||||||
34
README.md
34
README.md
@@ -43,6 +43,40 @@ bun run dev:api
|
|||||||
bun run dev:web -- --host 0.0.0.0
|
bun run dev:web -- --host 0.0.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docker image
|
||||||
|
|
||||||
|
The production image uses a multi-stage build:
|
||||||
|
- `bun install` + frontend build in the builder stage
|
||||||
|
- `bun build --compile` to emit a single API executable at `apps/api/dist/codexdash`
|
||||||
|
- a distroless runtime image that only contains the compiled binary and the built web assets
|
||||||
|
|
||||||
|
Build the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t codexdash:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-p 3001:3001 \
|
||||||
|
-p 1455:1455 \
|
||||||
|
-e JWT_SECRET=replace-me \
|
||||||
|
-e ENCRYPTION_SECRET=replace-with-32-plus-chars \
|
||||||
|
-e DATABASE_URL=file:/data/codexdash.db \
|
||||||
|
-e CODEXDASH_FRONTEND_ORIGIN=http://localhost:3001 \
|
||||||
|
-e CODEX_OAUTH_REDIRECT_URI=http://localhost:1455/auth/callback \
|
||||||
|
-v codexdash-data:/data \
|
||||||
|
codexdash:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The container serves the built React app from the same process on port `3001`.
|
||||||
|
- The bundled frontend defaults to `http://localhost:3001` for API calls. If you need a different origin, rebuild the image with `VITE_API_BASE_URL` set at build time.
|
||||||
|
- `CODEX_OAUTH_CALLBACK_BIND_HOST=0.0.0.0` is baked into the image so the callback bridge remains reachable through Docker port publishing while the public redirect URL can still stay on `localhost:1455`.
|
||||||
|
- If the callback bridge is still unreachable in your setup, the manual callback URL paste fallback remains available.
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
### Root `.env`
|
### Root `.env`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
|
"bundle": "bun build --compile ./src/main.ts --target=bun-linux-x64-baseline --minify --outfile ./dist/codexdash -e @nestjs/platform-socket.io -e amqplib -e amqp-connection-manager -e ioredis -e nats -e mqtt -e @grpc/grpc-js -e @grpc/proto-loader -e kafkajs",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
@@ -21,7 +22,9 @@
|
|||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/jwt": "^11.0.1",
|
"@nestjs/jwt": "^11.0.1",
|
||||||
|
"@nestjs/microservices": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/websockets": "^11.0.1",
|
||||||
"@prisma/client": "6.16.2",
|
"@prisma/client": "6.16.2",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { createServer } from 'node:http';
|
import { createServer } from 'node:http';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { CodexService } from './codex/codex.service';
|
import { CodexService } from './codex/codex.service';
|
||||||
|
import {
|
||||||
|
buildSpaFallbackPath,
|
||||||
|
resolveCallbackListenHost,
|
||||||
|
resolveWebDistDir,
|
||||||
|
shouldServeSpaFallback,
|
||||||
|
} from './runtime-assets';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
app.enableCors({ origin: true, credentials: true });
|
app.enableCors({ origin: true, credentials: true });
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@@ -15,6 +23,27 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const webDistDir = resolveWebDistDir({
|
||||||
|
envWebDistDir: process.env.WEB_DIST_DIR,
|
||||||
|
});
|
||||||
|
if (webDistDir) {
|
||||||
|
app.useStaticAssets(webDistDir);
|
||||||
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (
|
||||||
|
shouldServeSpaFallback({
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
acceptHeader: req.headers.accept,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
res.sendFile(buildSpaFallbackPath(webDistDir));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 3001);
|
await app.listen(process.env.PORT ?? 3001);
|
||||||
|
|
||||||
const codexService = app.get(CodexService);
|
const codexService = app.get(CodexService);
|
||||||
@@ -49,6 +78,9 @@ async function bootstrap() {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
callbackServer.listen(Number(callbackUrl.port || 80), callbackUrl.hostname);
|
callbackServer.listen(
|
||||||
|
Number(callbackUrl.port || 80),
|
||||||
|
resolveCallbackListenHost(callbackUrl.hostname),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
87
apps/api/src/runtime-assets.spec.ts
Normal file
87
apps/api/src/runtime-assets.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { join } from 'node:path';
|
||||||
|
import {
|
||||||
|
buildSpaFallbackPath,
|
||||||
|
resolveCallbackListenHost,
|
||||||
|
resolveWebDistDir,
|
||||||
|
shouldServeSpaFallback,
|
||||||
|
} from './runtime-assets';
|
||||||
|
|
||||||
|
describe('runtime asset helpers', () => {
|
||||||
|
it('prefers WEB_DIST_DIR when it points to an existing directory', () => {
|
||||||
|
const webDistDir = resolveWebDistDir({
|
||||||
|
envWebDistDir: '/tmp/codexdash-web',
|
||||||
|
execPath: '/app/codexdash',
|
||||||
|
directoryExists: (candidate) => candidate === '/tmp/codexdash-web',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(webDistDir).toBe('/tmp/codexdash-web');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to a web directory next to the compiled binary', () => {
|
||||||
|
const expected = join('/app', 'web');
|
||||||
|
|
||||||
|
const webDistDir = resolveWebDistDir({
|
||||||
|
execPath: '/app/codexdash',
|
||||||
|
directoryExists: (candidate) => candidate === expected,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(webDistDir).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when no candidate directory exists', () => {
|
||||||
|
expect(
|
||||||
|
resolveWebDistDir({
|
||||||
|
envWebDistDir: '/missing',
|
||||||
|
execPath: '/app/codexdash',
|
||||||
|
directoryExists: () => false,
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves SPA fallback only for browser GET requests outside API routes', () => {
|
||||||
|
expect(
|
||||||
|
shouldServeSpaFallback({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/dashboard',
|
||||||
|
acceptHeader: 'text/html,application/xhtml+xml',
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldServeSpaFallback({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/health',
|
||||||
|
acceptHeader: 'text/html',
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldServeSpaFallback({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/auth/login',
|
||||||
|
acceptHeader: 'text/html',
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldServeSpaFallback({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/dashboard',
|
||||||
|
acceptHeader: 'text/html',
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldServeSpaFallback({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/app.js',
|
||||||
|
acceptHeader: 'text/html',
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds the SPA entrypoint path from the web dist directory', () => {
|
||||||
|
expect(buildSpaFallbackPath('/app/web')).toBe('/app/web/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers an explicit callback bind host override for containers', () => {
|
||||||
|
expect(resolveCallbackListenHost('localhost', '0.0.0.0')).toBe('0.0.0.0');
|
||||||
|
expect(resolveCallbackListenHost('127.0.0.1')).toBe('127.0.0.1');
|
||||||
|
});
|
||||||
|
});
|
||||||
71
apps/api/src/runtime-assets.ts
Normal file
71
apps/api/src/runtime-assets.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { dirname, extname, join } from 'node:path';
|
||||||
|
|
||||||
|
type ResolveWebDistDirOptions = {
|
||||||
|
envWebDistDir?: string | null;
|
||||||
|
execPath?: string;
|
||||||
|
directoryExists?: (candidate: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SpaFallbackRequest = {
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
acceptHeader?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const API_PREFIXES = ['/auth', '/codex', '/health'];
|
||||||
|
|
||||||
|
export function resolveWebDistDir({
|
||||||
|
envWebDistDir,
|
||||||
|
execPath = process.execPath,
|
||||||
|
directoryExists = existsSync,
|
||||||
|
}: ResolveWebDistDirOptions = {}): string | null {
|
||||||
|
const candidates = [
|
||||||
|
envWebDistDir,
|
||||||
|
join(dirname(execPath), 'web'),
|
||||||
|
join(process.cwd(), 'apps/web/dist'),
|
||||||
|
].filter((candidate): candidate is string => Boolean(candidate));
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (directoryExists(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldServeSpaFallback({
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
acceptHeader,
|
||||||
|
}: SpaFallbackRequest): boolean {
|
||||||
|
if (method !== 'GET') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(acceptHeader ?? '').includes('text/html')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
API_PREFIXES.some(
|
||||||
|
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extname(path) === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSpaFallbackPath(webDistDir: string): string {
|
||||||
|
return join(webDistDir, 'index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCallbackListenHost(
|
||||||
|
redirectHostname: string,
|
||||||
|
bindHost = process.env.CODEX_OAUTH_CALLBACK_BIND_HOST,
|
||||||
|
): string {
|
||||||
|
return bindHost || redirectHostname;
|
||||||
|
}
|
||||||
8
bun.lock
8
bun.lock
@@ -14,7 +14,9 @@
|
|||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/jwt": "^11.0.1",
|
"@nestjs/jwt": "^11.0.1",
|
||||||
|
"@nestjs/microservices": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/websockets": "^11.0.1",
|
||||||
"@prisma/client": "6.16.2",
|
"@prisma/client": "6.16.2",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
@@ -330,12 +332,16 @@
|
|||||||
|
|
||||||
"@nestjs/jwt": ["@nestjs/jwt@11.0.2", "", { "dependencies": { "@types/jsonwebtoken": "9.0.10", "jsonwebtoken": "9.0.3" }, "peerDependencies": { "@nestjs/common": "11.1.19" } }, "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA=="],
|
"@nestjs/jwt": ["@nestjs/jwt@11.0.2", "", { "dependencies": { "@types/jsonwebtoken": "9.0.10", "jsonwebtoken": "9.0.3" }, "peerDependencies": { "@nestjs/common": "11.1.19" } }, "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA=="],
|
||||||
|
|
||||||
|
"@nestjs/microservices": ["@nestjs/microservices@11.1.19", "", { "dependencies": { "iterare": "1.2.1", "tslib": "2.8.1" }, "peerDependencies": { "@grpc/grpc-js": "*", "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", "@nestjs/websockets": "^11.0.0", "amqp-connection-manager": "*", "amqplib": "*", "cache-manager": "*", "ioredis": "*", "kafkajs": "*", "mqtt": "*", "nats": "*", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@grpc/grpc-js", "@nestjs/websockets", "amqp-connection-manager", "amqplib", "cache-manager", "ioredis", "kafkajs", "mqtt", "nats"] }, "sha512-3Oja56ydTlSaui19/i7gYM0MMqz/w4UR2aqZeL4K8B+Fq0Ztg3zHb8et76atToJGpSCevJLEsoEMOMaGgzRwfg=="],
|
||||||
|
|
||||||
"@nestjs/platform-express": ["@nestjs/platform-express@11.1.19", "", { "dependencies": { "cors": "2.8.6", "express": "5.2.1", "multer": "2.1.1", "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "11.1.19", "@nestjs/core": "11.1.19" } }, "sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg=="],
|
"@nestjs/platform-express": ["@nestjs/platform-express@11.1.19", "", { "dependencies": { "cors": "2.8.6", "express": "5.2.1", "multer": "2.1.1", "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "11.1.19", "@nestjs/core": "11.1.19" } }, "sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg=="],
|
||||||
|
|
||||||
"@nestjs/schematics": ["@nestjs/schematics@11.1.0", "", { "dependencies": { "@angular-devkit/core": "19.2.24", "@angular-devkit/schematics": "19.2.24", "comment-json": "5.0.0", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" }, "optionalDependencies": { "prettier": "3.8.3" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw=="],
|
"@nestjs/schematics": ["@nestjs/schematics@11.1.0", "", { "dependencies": { "@angular-devkit/core": "19.2.24", "@angular-devkit/schematics": "19.2.24", "comment-json": "5.0.0", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" }, "optionalDependencies": { "prettier": "3.8.3" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw=="],
|
||||||
|
|
||||||
"@nestjs/testing": ["@nestjs/testing@11.1.19", "", { "dependencies": { "tslib": "2.8.1" }, "optionalDependencies": { "@nestjs/platform-express": "11.1.19" }, "peerDependencies": { "@nestjs/common": "11.1.19", "@nestjs/core": "11.1.19" } }, "sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ=="],
|
"@nestjs/testing": ["@nestjs/testing@11.1.19", "", { "dependencies": { "tslib": "2.8.1" }, "optionalDependencies": { "@nestjs/platform-express": "11.1.19" }, "peerDependencies": { "@nestjs/common": "11.1.19", "@nestjs/core": "11.1.19" } }, "sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ=="],
|
||||||
|
|
||||||
|
"@nestjs/websockets": ["@nestjs/websockets@11.1.19", "", { "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", "@nestjs/platform-socket.io": "^11.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/platform-socket.io"] }, "sha512-2qo8jtIwwwgkqAI1BtnJ02EaFLrRkKA39eYXS8IhZCHilhBHCWdjnJ5cLcFq4oF+s+KZ7LcLGD/3stxJy8ijzg=="],
|
||||||
|
|
||||||
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||||
|
|
||||||
"@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "3.4.2" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="],
|
"@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "3.4.2" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="],
|
||||||
@@ -1334,6 +1340,8 @@
|
|||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user