+72
.env.grafana.example
+72
.env.grafana.example
···
···
1
+
# Grafana Cloud Configuration for wisp.place monorepo
2
+
# Copy these variables to your .env file to enable Grafana integration
3
+
# The observability package will automatically pick up these environment variables
4
+
5
+
# ============================================================================
6
+
# Grafana Loki (for logs)
7
+
# ============================================================================
8
+
# Get this from your Grafana Cloud portal under Loki โ Details
9
+
# Example: https://logs-prod-012.grafana.net
10
+
GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net
11
+
12
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
13
+
GRAFANA_LOKI_TOKEN=glc_xxx
14
+
15
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
16
+
# GRAFANA_LOKI_USERNAME=your-username
17
+
# GRAFANA_LOKI_PASSWORD=your-password
18
+
19
+
# ============================================================================
20
+
# Grafana Prometheus (for metrics)
21
+
# ============================================================================
22
+
# Get this from your Grafana Cloud portal under Prometheus โ Details
23
+
# Note: You need to add /api/prom to the base URL for OTLP export
24
+
# Example: https://prometheus-prod-10-prod-us-central-0.grafana.net/api/prom
25
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom
26
+
27
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
28
+
GRAFANA_PROMETHEUS_TOKEN=glc_xxx
29
+
30
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
31
+
# GRAFANA_PROMETHEUS_USERNAME=your-username
32
+
# GRAFANA_PROMETHEUS_PASSWORD=your-password
33
+
34
+
# ============================================================================
35
+
# Optional Configuration
36
+
# ============================================================================
37
+
# These will be used by both main-app and hosting-service if not overridden
38
+
39
+
# Service metadata (optional - defaults are provided in code)
40
+
# SERVICE_NAME=wisp-app
41
+
# SERVICE_VERSION=1.0.0
42
+
43
+
# Batching configuration (optional)
44
+
# GRAFANA_BATCH_SIZE=100 # Flush after this many entries
45
+
# GRAFANA_FLUSH_INTERVAL=5000 # Flush every 5 seconds
46
+
47
+
# ============================================================================
48
+
# How to get these values:
49
+
# ============================================================================
50
+
# 1. Sign up for Grafana Cloud at https://grafana.com/
51
+
# 2. Go to your Grafana Cloud portal
52
+
# 3. For Loki:
53
+
# - Navigate to "Connections" โ "Loki"
54
+
# - Click "Details"
55
+
# - Copy the Push endpoint URL (without /loki/api/v1/push)
56
+
# - Create an API token with push permissions
57
+
# 4. For Prometheus:
58
+
# - Navigate to "Connections" โ "Prometheus"
59
+
# - Click "Details"
60
+
# - Copy the Remote Write endpoint (add /api/prom for OTLP)
61
+
# - Create an API token with write permissions
62
+
63
+
# ============================================================================
64
+
# Testing the integration:
65
+
# ============================================================================
66
+
# 1. Copy this file's contents to your .env file
67
+
# 2. Fill in the actual values
68
+
# 3. Restart your services (main-app and hosting-service)
69
+
# 4. Check your Grafana Cloud dashboard for incoming data
70
+
# 5. Use Grafana Explore to query:
71
+
# - Loki: {job="main-app"} or {job="hosting-service"}
72
+
# - Prometheus: http_requests_total{service="main-app"}
+1
.gitignore
+1
.gitignore
+54
-42
.tangled/workflows/deploy-wisp.yml
+54
-42
.tangled/workflows/deploy-wisp.yml
···
1
-
# Deploy to Wisp.place
2
-
# This workflow builds your site and deploys it to Wisp.place using the wisp-cli
3
when:
4
-
- event: ['push']
5
-
branch: ['main']
6
-
- event: ['manual']
7
-
engine: 'nixery'
8
clone:
9
-
skip: false
10
-
depth: 1
11
-
submodules: true
12
dependencies:
13
-
nixpkgs:
14
-
- git
15
-
- gcc
16
-
github:NixOS/nixpkgs/nixpkgs-unstable:
17
-
- rustc
18
-
- cargo
19
environment:
20
-
# Customize these for your project
21
-
SITE_PATH: 'testDeploy'
22
-
SITE_NAME: 'wispPlaceDocs'
23
steps:
24
-
- name: 'Initialize submodules'
25
-
command: |
26
-
git submodule update --init --recursive
27
28
-
- name: 'Build wisp-cli'
29
-
command: |
30
-
cd cli
31
-
export PATH="$HOME/.nix-profile/bin:$PATH"
32
-
nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
33
-
nix-channel --update
34
-
nix-shell -p pkg-config openssl --run '
35
-
export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)"
36
-
export OPENSSL_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.dev)"
37
-
export OPENSSL_NO_VENDOR=1
38
-
export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.out)/lib"
39
-
cargo build --release
40
-
'
41
-
cd ..
42
43
-
- name: 'Deploy to Wisp.place'
44
-
command: |
45
-
./cli/target/release/wisp-cli \
46
-
"$WISP_HANDLE" \
47
-
--path "$SITE_PATH" \
48
-
--site "$SITE_NAME" \
49
-
--password "$WISP_APP_PASSWORD"
···
1
+
---
2
when:
3
+
- event:
4
+
- push
5
+
branch:
6
+
- main
7
+
- event:
8
+
- manual
9
+
engine: nixery
10
clone:
11
+
skip: false
12
+
depth: 1
13
+
submodules: true
14
dependencies:
15
+
nixpkgs:
16
+
- git
17
+
- gcc
18
+
github:NixOS/nixpkgs/nixpkgs-unstable:
19
+
- rustc
20
+
- cargo
21
+
- bun
22
environment:
23
+
WISP_HANDLE: wisp.place
24
+
SITE_PATH: docs/dist
25
+
SITE_NAME: docs
26
steps:
27
+
- name: Initialize submodules
28
+
command: |
29
+
git submodule update --init --recursive
30
+
- name: Build wisp-cli
31
+
command: |
32
+
cd cli
33
34
+
export PATH="$HOME/.nix-profile/bin:$PATH"
35
36
+
nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
37
+
38
+
nix-channel --update
39
+
40
+
nix-shell -p pkg-config openssl --run '
41
+
export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)"
42
+
export OPENSSL_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.dev)"
43
+
export OPENSSL_NO_VENDOR=1
44
+
export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.out)/lib"
45
+
cargo build --release
46
+
'
47
+
48
+
cd ..
49
+
- name: Build docs
50
+
command: |
51
+
cd docs
52
+
export PATH="$HOME/.nix-profile/bin:$PATH"
53
+
bun install
54
+
bun run build
55
+
- name: Deploy to Wisp.place
56
+
command: |
57
+
./cli/target/release/wisp-cli \
58
+
"$WISP_HANDLE" \
59
+
--path "$SITE_PATH" \
60
+
--site "$SITE_NAME" \
61
+
--password "$WISP_APP_PASSWORD"
+2
-1
.tangled/workflows/test.yml
+2
-1
.tangled/workflows/test.yml
···
7
dependencies:
8
nixpkgs:
9
- git
10
github:NixOS/nixpkgs/nixpkgs-unstable:
11
- bun
12
···
16
export PATH="$HOME/.nix-profile/bin:$PATH"
17
18
# have to regenerate otherwise it wont install necessary dependencies to run
19
-
rm -rf bun.lock package-lock.json
20
bun install
21
22
- name: run all tests
···
7
dependencies:
8
nixpkgs:
9
- git
10
+
- findutils
11
github:NixOS/nixpkgs/nixpkgs-unstable:
12
- bun
13
···
17
export PATH="$HOME/.nix-profile/bin:$PATH"
18
19
# have to regenerate otherwise it wont install necessary dependencies to run
20
+
find . -type f \( -name "bun.lock" -o -name "package-lock.json" \) -delete
21
bun install
22
23
- name: run all tests
+15
-58
Dockerfile
+15
-58
Dockerfile
···
1
-
# Build stage
2
-
FROM oven/bun:1.3 AS build
3
4
WORKDIR /app
5
···
7
COPY package.json bunfig.toml tsconfig.json bun.lock* ./
8
9
# Copy all workspace package.json files first (for dependency resolution)
10
-
COPY packages ./packages
11
COPY apps/main-app/package.json ./apps/main-app/package.json
12
COPY apps/hosting-service/package.json ./apps/hosting-service/package.json
13
14
-
# Install all dependencies (including workspaces)
15
-
RUN bun install --frozen-lockfile
16
17
-
# Copy source files
18
-
COPY apps/main-app ./apps/main-app
19
-
20
-
# Build compiled server
21
-
RUN bun build \
22
-
--compile \
23
-
--target bun \
24
-
--minify \
25
-
--outfile server \
26
-
apps/main-app/src/index.ts
27
-
28
-
# Production dependencies stage
29
-
FROM oven/bun:1.3 AS prod-deps
30
-
31
-
WORKDIR /app
32
-
33
-
COPY package.json bunfig.toml tsconfig.json bun.lock* ./
34
COPY packages ./packages
35
-
COPY apps/main-app/package.json ./apps/main-app/package.json
36
-
COPY apps/hosting-service/package.json ./apps/hosting-service/package.json
37
38
-
# Install only production dependencies
39
-
RUN bun install --frozen-lockfile --production
40
-
41
-
# Remove unnecessary large packages (bun is already in base image, these are dev tools)
42
-
RUN rm -rf /app/node_modules/bun \
43
-
/app/node_modules/@oven \
44
-
/app/node_modules/prettier \
45
-
/app/node_modules/@ts-morph
46
-
47
-
# Final stage - use distroless or slim debian-based image
48
-
FROM debian:bookworm-slim
49
-
50
-
# Install Bun runtime
51
-
COPY --from=oven/bun:1.3 /usr/local/bin/bun /usr/local/bin/bun
52
-
53
-
WORKDIR /app
54
-
55
-
# Copy compiled server
56
-
COPY --from=build /app/server /app/server
57
-
58
-
# Copy public files
59
-
COPY apps/main-app/public apps/main-app/public
60
-
61
-
# Copy production dependencies only
62
-
COPY --from=prod-deps /app/node_modules /app/node_modules
63
-
64
-
# Copy configs
65
-
COPY package.json bunfig.toml tsconfig.json /app/
66
-
COPY apps/main-app/tsconfig.json /app/apps/main-app/tsconfig.json
67
-
COPY apps/main-app/package.json /app/apps/main-app/package.json
68
-
69
-
# Create symlink for module resolution
70
-
RUN ln -s /app/node_modules /app/apps/main-app/node_modules
71
72
ENV PORT=8000
73
74
EXPOSE 8000
75
76
-
CMD ["./server"]
···
1
+
# Production stage
2
+
FROM oven/bun:1.3
3
4
WORKDIR /app
5
···
7
COPY package.json bunfig.toml tsconfig.json bun.lock* ./
8
9
# Copy all workspace package.json files first (for dependency resolution)
10
+
COPY packages/@wisp/atproto-utils/package.json ./packages/@wisp/atproto-utils/package.json
11
+
COPY packages/@wisp/constants/package.json ./packages/@wisp/constants/package.json
12
+
COPY packages/@wisp/database/package.json ./packages/@wisp/database/package.json
13
+
COPY packages/@wisp/fs-utils/package.json ./packages/@wisp/fs-utils/package.json
14
+
COPY packages/@wisp/lexicons/package.json ./packages/@wisp/lexicons/package.json
15
+
COPY packages/@wisp/observability/package.json ./packages/@wisp/observability/package.json
16
+
COPY packages/@wisp/safe-fetch/package.json ./packages/@wisp/safe-fetch/package.json
17
COPY apps/main-app/package.json ./apps/main-app/package.json
18
COPY apps/hosting-service/package.json ./apps/hosting-service/package.json
19
20
+
# Install dependencies
21
+
RUN bun install --frozen-lockfile --production
22
23
+
# Copy workspace source files
24
COPY packages ./packages
25
26
+
# Copy app source and public files
27
+
COPY apps/main-app ./apps/main-app
28
29
ENV PORT=8000
30
31
EXPOSE 8000
32
33
+
CMD ["bun", "run", "apps/main-app/src/index.ts"]
+20
-3
README.md
+20
-3
README.md
···
39
40
```bash
41
# Backend
42
+
# bun install will install packages across the monorepo
43
bun install
44
+
bun run dev
45
46
# Hosting service
47
+
bun run hosting:dev
48
49
# CLI
50
cd cli
···
52
```
53
54
## Features
55
+
56
+
### File Filtering and `.wispignore`
57
+
58
+
Wisp automatically excludes common files that shouldn't be uploaded to your site (like `.git`, `node_modules`, `.env` files, etc.). You can customize this behavior by creating a `.wispignore` file in your site root.
59
+
60
+
The `.wispignore` file uses the same syntax as `.gitignore`:
61
+
```
62
+
# Custom ignore patterns
63
+
*.log
64
+
temp/
65
+
build/
66
+
.secret
67
+
```
68
+
69
+
Default patterns include: `.git`, `.github`, `.gitlab`, `.DS_Store`, `node_modules`, `.env`, cache directories, Python virtual environments, editor swap files, `.tangled`, and more.
70
+
71
+
See [File Filtering Documentation](./docs/src/content/docs/file-filtering.md) for details.
72
73
### URL Redirects and Rewrites
74
+13
-10
apps/hosting-service/package.json
+13
-10
apps/hosting-service/package.json
···
6
"dev": "tsx --env-file=.env src/index.ts",
7
"build": "bun run build.ts",
8
"start": "tsx src/index.ts",
9
"backfill": "tsx src/index.ts --backfill"
10
},
11
"dependencies": {
12
-
"@wisp/lexicons": "workspace:*",
13
-
"@wisp/constants": "workspace:*",
14
-
"@wisp/observability": "workspace:*",
15
-
"@wisp/atproto-utils": "workspace:*",
16
-
"@wisp/database": "workspace:*",
17
-
"@wisp/fs-utils": "workspace:*",
18
-
"@wisp/safe-fetch": "workspace:*",
19
"@atproto/api": "^0.17.4",
20
"@atproto/identity": "^0.4.9",
21
-
"@atproto/lexicon": "^0.5.1",
22
"@atproto/sync": "^0.1.36",
23
"@atproto/xrpc": "^0.7.5",
24
"@hono/node-server": "^1.19.6",
25
"hono": "^4.10.4",
26
"mime-types": "^2.1.35",
27
"multiformats": "^13.4.1",
28
-
"postgres": "^3.4.5"
29
},
30
"devDependencies": {
31
"@types/bun": "^1.3.1",
32
"@types/mime-types": "^2.1.4",
33
"@types/node": "^22.10.5",
34
-
"tsx": "^4.19.2"
35
}
36
}
···
6
"dev": "tsx --env-file=.env src/index.ts",
7
"build": "bun run build.ts",
8
"start": "tsx src/index.ts",
9
+
"check": "tsc --noEmit",
10
"backfill": "tsx src/index.ts --backfill"
11
},
12
"dependencies": {
13
"@atproto/api": "^0.17.4",
14
"@atproto/identity": "^0.4.9",
15
+
"@atproto/lexicon": "^0.5.2",
16
"@atproto/sync": "^0.1.36",
17
"@atproto/xrpc": "^0.7.5",
18
"@hono/node-server": "^1.19.6",
19
+
"@wisp/atproto-utils": "workspace:*",
20
+
"@wisp/constants": "workspace:*",
21
+
"@wisp/database": "workspace:*",
22
+
"@wisp/fs-utils": "workspace:*",
23
+
"@wisp/lexicons": "workspace:*",
24
+
"@wisp/observability": "workspace:*",
25
+
"@wisp/safe-fetch": "workspace:*",
26
"hono": "^4.10.4",
27
"mime-types": "^2.1.35",
28
"multiformats": "^13.4.1",
29
+
"postgres": "^3.4.5",
30
+
"tiered-storage": "1.0.3"
31
},
32
"devDependencies": {
33
"@types/bun": "^1.3.1",
34
"@types/mime-types": "^2.1.4",
35
"@types/node": "^22.10.5",
36
+
"tsx": "^4.19.2",
37
+
"typescript": "^5.9.3"
38
}
39
}
+46
-6
apps/hosting-service/src/index.ts
+46
-6
apps/hosting-service/src/index.ts
···
1
import app from './server';
2
import { serve } from '@hono/node-server';
3
import { FirehoseWorker } from './lib/firehose';
4
-
import { createLogger } from '@wisp/observability';
5
import { mkdirSync, existsSync } from 'fs';
6
import { backfillCache } from './lib/backfill';
7
-
import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode } from './lib/db';
8
9
const logger = createLogger('hosting-service');
10
11
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001;
12
const CACHE_DIR = process.env.CACHE_DIR || './cache/sites';
13
14
// Parse CLI arguments
15
const args = process.argv.slice(2);
···
41
42
firehose.start();
43
44
// Run backfill if requested
45
if (backfillOnStartup) {
46
console.log('๐ Backfill requested, starting cache backfill...');
47
backfillCache({
48
skipExisting: true,
49
-
concurrency: 3,
50
}).then((stats) => {
51
console.log('โ
Cache backfill completed');
52
}).catch((err) => {
···
69
port: PORT,
70
});
71
72
console.log(`
73
-
Wisp Hosting Service
74
75
Server: http://localhost:${PORT}
76
Health: http://localhost:${PORT}/health
77
-
Cache: ${CACHE_DIR}
78
-
Firehose: Connected to Firehose
79
Cache-Only: ${CACHE_ONLY_MODE ? 'ENABLED (no DB writes)' : 'DISABLED'}
80
`);
81
82
// Graceful shutdown
···
84
console.log('\n๐ Shutting down...');
85
firehose.stop();
86
stopDomainCacheCleanup();
87
server.close();
88
process.exit(0);
89
});
···
92
console.log('\n๐ Shutting down...');
93
firehose.stop();
94
stopDomainCacheCleanup();
95
server.close();
96
process.exit(0);
97
});
···
1
import app from './server';
2
import { serve } from '@hono/node-server';
3
import { FirehoseWorker } from './lib/firehose';
4
+
import { createLogger, initializeGrafanaExporters } from '@wisp/observability';
5
import { mkdirSync, existsSync } from 'fs';
6
import { backfillCache } from './lib/backfill';
7
+
import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode, closeDatabase } from './lib/db';
8
+
import { storage, getStorageConfig } from './lib/storage';
9
+
10
+
// Initialize Grafana exporters if configured
11
+
initializeGrafanaExporters({
12
+
serviceName: 'hosting-service',
13
+
serviceVersion: '1.0.0'
14
+
});
15
16
const logger = createLogger('hosting-service');
17
18
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001;
19
const CACHE_DIR = process.env.CACHE_DIR || './cache/sites';
20
+
const BACKFILL_CONCURRENCY = process.env.BACKFILL_CONCURRENCY
21
+
? parseInt(process.env.BACKFILL_CONCURRENCY)
22
+
: undefined; // Let backfill.ts default (10) apply
23
24
// Parse CLI arguments
25
const args = process.argv.slice(2);
···
51
52
firehose.start();
53
54
+
// Optional: Bootstrap hot cache from warm tier on startup
55
+
const BOOTSTRAP_HOT_ON_STARTUP = process.env.BOOTSTRAP_HOT_ON_STARTUP === 'true';
56
+
const BOOTSTRAP_HOT_LIMIT = process.env.BOOTSTRAP_HOT_LIMIT ? parseInt(process.env.BOOTSTRAP_HOT_LIMIT) : 100;
57
+
58
+
if (BOOTSTRAP_HOT_ON_STARTUP) {
59
+
console.log(`๐ฅ Bootstrapping hot cache (top ${BOOTSTRAP_HOT_LIMIT} items)...`);
60
+
storage.bootstrapHot(BOOTSTRAP_HOT_LIMIT)
61
+
.then((loaded: number) => {
62
+
console.log(`โ
Bootstrapped ${loaded} items into hot cache`);
63
+
})
64
+
.catch((err: unknown) => {
65
+
console.error('โ Hot cache bootstrap error:', err);
66
+
});
67
+
}
68
+
69
// Run backfill if requested
70
if (backfillOnStartup) {
71
console.log('๐ Backfill requested, starting cache backfill...');
72
backfillCache({
73
skipExisting: true,
74
+
concurrency: BACKFILL_CONCURRENCY,
75
}).then((stats) => {
76
console.log('โ
Cache backfill completed');
77
}).catch((err) => {
···
94
port: PORT,
95
});
96
97
+
// Get storage configuration for display
98
+
const storageConfig = getStorageConfig();
99
+
100
console.log(`
101
+
Wisp Hosting Service with Tiered Storage
102
103
Server: http://localhost:${PORT}
104
Health: http://localhost:${PORT}/health
105
Cache-Only: ${CACHE_ONLY_MODE ? 'ENABLED (no DB writes)' : 'DISABLED'}
106
+
Backfill: ${backfillOnStartup ? `ENABLED (concurrency: ${BACKFILL_CONCURRENCY || 10})` : 'DISABLED'}
107
+
108
+
Tiered Storage Configuration:
109
+
Hot Cache: ${storageConfig.hotCacheSize} (${storageConfig.hotCacheCount} items max)
110
+
Warm Cache: ${storageConfig.warmCacheSize} (${storageConfig.warmEvictionPolicy} eviction)
111
+
Cold Storage: S3 - ${storageConfig.s3Bucket}
112
+
S3 Region: ${storageConfig.s3Region}
113
+
S3 Endpoint: ${storageConfig.s3Endpoint}
114
+
S3 Prefix: ${storageConfig.s3Prefix}
115
+
Metadata Bucket: ${storageConfig.metadataBucket}
116
+
117
+
Firehose: Connecting...
118
`);
119
120
// Graceful shutdown
···
122
console.log('\n๐ Shutting down...');
123
firehose.stop();
124
stopDomainCacheCleanup();
125
+
await closeDatabase();
126
server.close();
127
process.exit(0);
128
});
···
131
console.log('\n๐ Shutting down...');
132
firehose.stop();
133
stopDomainCacheCleanup();
134
+
await closeDatabase();
135
server.close();
136
process.exit(0);
137
});
+65
-57
apps/hosting-service/src/lib/backfill.ts
+65
-57
apps/hosting-service/src/lib/backfill.ts
···
60
console.log(`โ๏ธ Limited to ${maxSites} sites for backfill`);
61
}
62
63
-
// Process sites in batches
64
-
const batches: typeof sites[] = [];
65
-
for (let i = 0; i < sites.length; i += concurrency) {
66
-
batches.push(sites.slice(i, i + concurrency));
67
-
}
68
-
69
let processed = 0;
70
-
for (const batch of batches) {
71
-
await Promise.all(
72
-
batch.map(async (site) => {
73
-
try {
74
-
// Check if already cached
75
-
if (skipExisting && isCached(site.did, site.rkey)) {
76
-
stats.skipped++;
77
-
processed++;
78
-
logger.debug(`Skipping already cached site`, { did: site.did, rkey: site.rkey });
79
-
console.log(`โญ๏ธ [${processed}/${sites.length}] Skipped (cached): ${site.display_name || site.rkey}`);
80
-
return;
81
-
}
82
83
-
// Fetch site record
84
-
const siteData = await fetchSiteRecord(site.did, site.rkey);
85
-
if (!siteData) {
86
-
stats.failed++;
87
-
processed++;
88
-
logger.error('Site record not found during backfill', null, { did: site.did, rkey: site.rkey });
89
-
console.log(`โ [${processed}/${sites.length}] Failed (not found): ${site.display_name || site.rkey}`);
90
-
return;
91
-
}
92
-
93
-
// Get PDS endpoint
94
-
const pdsEndpoint = await getPdsForDid(site.did);
95
-
if (!pdsEndpoint) {
96
-
stats.failed++;
97
-
processed++;
98
-
logger.error('PDS not found during backfill', null, { did: site.did });
99
-
console.log(`โ [${processed}/${sites.length}] Failed (no PDS): ${site.display_name || site.rkey}`);
100
-
return;
101
-
}
102
103
-
// Mark site as being cached to prevent serving stale content during update
104
-
markSiteAsBeingCached(site.did, site.rkey);
105
106
-
try {
107
-
// Download and cache site
108
-
await downloadAndCacheSite(site.did, site.rkey, siteData.record, pdsEndpoint, siteData.cid);
109
-
// Clear redirect rules cache since the site was updated
110
-
clearRedirectRulesCache(site.did, site.rkey);
111
-
stats.cached++;
112
-
processed++;
113
-
logger.info('Successfully cached site during backfill', { did: site.did, rkey: site.rkey });
114
-
console.log(`โ
[${processed}/${sites.length}] Cached: ${site.display_name || site.rkey}`);
115
-
} finally {
116
-
// Always unmark, even if caching fails
117
-
unmarkSiteAsBeingCached(site.did, site.rkey);
118
-
}
119
-
} catch (err) {
120
stats.failed++;
121
processed++;
122
-
logger.error('Failed to cache site during backfill', err, { did: site.did, rkey: site.rkey });
123
-
console.log(`โ [${processed}/${sites.length}] Failed: ${site.display_name || site.rkey}`);
124
}
125
-
})
126
-
);
127
}
128
129
stats.duration = Date.now() - startTime;
130
···
60
console.log(`โ๏ธ Limited to ${maxSites} sites for backfill`);
61
}
62
63
+
// Process sites with sliding window concurrency pool
64
+
const executing = new Set<Promise<void>>();
65
let processed = 0;
66
67
+
for (const site of sites) {
68
+
// Create task for this site
69
+
const processSite = async () => {
70
+
try {
71
+
// Check if already cached
72
+
if (skipExisting && await isCached(site.did, site.rkey)) {
73
+
stats.skipped++;
74
+
processed++;
75
+
logger.debug(`Skipping already cached site`, { did: site.did, rkey: site.rkey });
76
+
console.log(`โญ๏ธ [${processed}/${sites.length}] Skipped (cached): ${site.display_name || site.rkey}`);
77
+
return;
78
+
}
79
80
+
// Fetch site record
81
+
const siteData = await fetchSiteRecord(site.did, site.rkey);
82
+
if (!siteData) {
83
+
stats.failed++;
84
+
processed++;
85
+
logger.error('Site record not found during backfill', null, { did: site.did, rkey: site.rkey });
86
+
console.log(`โ [${processed}/${sites.length}] Failed (not found): ${site.display_name || site.rkey}`);
87
+
return;
88
+
}
89
90
+
// Get PDS endpoint
91
+
const pdsEndpoint = await getPdsForDid(site.did);
92
+
if (!pdsEndpoint) {
93
stats.failed++;
94
processed++;
95
+
logger.error('PDS not found during backfill', null, { did: site.did });
96
+
console.log(`โ [${processed}/${sites.length}] Failed (no PDS): ${site.display_name || site.rkey}`);
97
+
return;
98
+
}
99
+
100
+
// Mark site as being cached to prevent serving stale content during update
101
+
markSiteAsBeingCached(site.did, site.rkey);
102
+
103
+
try {
104
+
// Download and cache site
105
+
await downloadAndCacheSite(site.did, site.rkey, siteData.record, pdsEndpoint, siteData.cid);
106
+
// Clear redirect rules cache since the site was updated
107
+
clearRedirectRulesCache(site.did, site.rkey);
108
+
stats.cached++;
109
+
processed++;
110
+
logger.info('Successfully cached site during backfill', { did: site.did, rkey: site.rkey });
111
+
console.log(`โ
[${processed}/${sites.length}] Cached: ${site.display_name || site.rkey}`);
112
+
} finally {
113
+
// Always unmark, even if caching fails
114
+
unmarkSiteAsBeingCached(site.did, site.rkey);
115
}
116
+
} catch (err) {
117
+
stats.failed++;
118
+
processed++;
119
+
logger.error('Failed to cache site during backfill', err, { did: site.did, rkey: site.rkey });
120
+
console.log(`โ [${processed}/${sites.length}] Failed: ${site.display_name || site.rkey}`);
121
+
}
122
+
};
123
+
124
+
// Add to executing pool and remove when done
125
+
const promise = processSite().finally(() => executing.delete(promise));
126
+
executing.add(promise);
127
+
128
+
// When pool is full, wait for at least one to complete
129
+
if (executing.size >= concurrency) {
130
+
await Promise.race(executing);
131
+
}
132
}
133
+
134
+
// Wait for all remaining tasks to complete
135
+
await Promise.all(executing);
136
137
stats.duration = Date.now() - startTime;
138
+35
-43
apps/hosting-service/src/lib/cache.ts
+35
-43
apps/hosting-service/src/lib/cache.ts
···
1
-
// In-memory LRU cache for file contents and metadata
2
3
interface CacheEntry<T> {
4
value: T;
5
size: number;
···
96
return true;
97
}
98
99
-
// Invalidate all entries for a specific site
100
-
invalidateSite(did: string, rkey: string): number {
101
-
const prefix = `${did}:${rkey}:`;
102
-
let count = 0;
103
-
104
-
for (const key of Array.from(this.cache.keys())) {
105
-
if (key.startsWith(prefix)) {
106
-
this.delete(key);
107
-
count++;
108
-
}
109
-
}
110
-
111
-
return count;
112
-
}
113
-
114
-
// Get cache size
115
size(): number {
116
return this.cache.size;
117
}
···
127
return { ...this.stats };
128
}
129
130
-
// Get cache hit rate
131
getHitRate(): number {
132
const total = this.stats.hits + this.stats.misses;
133
return total === 0 ? 0 : (this.stats.hits / total) * 100;
134
}
135
}
136
137
-
// File metadata cache entry
138
-
export interface FileMetadata {
139
-
encoding?: 'gzip';
140
-
mimeType: string;
141
-
}
142
-
143
-
// Global cache instances
144
-
const FILE_CACHE_SIZE = 100 * 1024 * 1024; // 100MB
145
-
const FILE_CACHE_COUNT = 500;
146
-
const METADATA_CACHE_COUNT = 2000;
147
-
148
-
export const fileCache = new LRUCache<Buffer>(FILE_CACHE_SIZE, FILE_CACHE_COUNT);
149
-
export const metadataCache = new LRUCache<FileMetadata>(1024 * 1024, METADATA_CACHE_COUNT); // 1MB for metadata
150
export const rewrittenHtmlCache = new LRUCache<Buffer>(50 * 1024 * 1024, 200); // 50MB for rewritten HTML
151
152
-
// Helper to generate cache keys
153
export function getCacheKey(did: string, rkey: string, filePath: string, suffix?: string): string {
154
const base = `${did}:${rkey}:${filePath}`;
155
return suffix ? `${base}:${suffix}` : base;
156
}
157
158
-
// Invalidate all caches for a site
159
-
export function invalidateSiteCache(did: string, rkey: string): void {
160
-
const fileCount = fileCache.invalidateSite(did, rkey);
161
-
const metaCount = metadataCache.invalidateSite(did, rkey);
162
-
const htmlCount = rewrittenHtmlCache.invalidateSite(did, rkey);
163
164
-
console.log(`[Cache] Invalidated site ${did}:${rkey} - ${fileCount} files, ${metaCount} metadata, ${htmlCount} HTML`);
165
}
166
167
// Track sites currently being cached (to prevent serving stale cache during updates)
···
183
}
184
185
// Get overall cache statistics
186
-
export function getCacheStats() {
187
return {
188
-
files: fileCache.getStats(),
189
-
fileHitRate: fileCache.getHitRate(),
190
-
metadata: metadataCache.getStats(),
191
-
metadataHitRate: metadataCache.getHitRate(),
192
rewrittenHtml: rewrittenHtmlCache.getStats(),
193
rewrittenHtmlHitRate: rewrittenHtmlCache.getHitRate(),
194
sitesBeingCached: sitesBeingCached.size,
···
1
+
/**
2
+
* Cache management for wisp-hosting-service
3
+
*
4
+
* With tiered storage, most caching is handled transparently.
5
+
* This module tracks sites being cached and manages rewritten HTML cache.
6
+
*/
7
8
+
import { storage } from './storage';
9
+
10
+
// In-memory LRU cache for rewritten HTML (for path rewriting in subdomain routes)
11
interface CacheEntry<T> {
12
value: T;
13
size: number;
···
104
return true;
105
}
106
107
size(): number {
108
return this.cache.size;
109
}
···
119
return { ...this.stats };
120
}
121
122
getHitRate(): number {
123
const total = this.stats.hits + this.stats.misses;
124
return total === 0 ? 0 : (this.stats.hits / total) * 100;
125
}
126
}
127
128
+
// Rewritten HTML cache: stores HTML after path rewriting for subdomain routes
129
export const rewrittenHtmlCache = new LRUCache<Buffer>(50 * 1024 * 1024, 200); // 50MB for rewritten HTML
130
131
+
// Helper to generate cache keys for rewritten HTML
132
export function getCacheKey(did: string, rkey: string, filePath: string, suffix?: string): string {
133
const base = `${did}:${rkey}:${filePath}`;
134
return suffix ? `${base}:${suffix}` : base;
135
}
136
137
+
/**
138
+
* Invalidate site cache via tiered storage
139
+
* Also invalidates locally cached rewritten HTML
140
+
*/
141
+
export async function invalidateSiteCache(did: string, rkey: string): Promise<void> {
142
+
// Invalidate in tiered storage
143
+
const prefix = `${did}/${rkey}/`;
144
+
const deleted = await storage.invalidate(prefix);
145
146
+
// Invalidate rewritten HTML cache for this site
147
+
const sitePrefix = `${did}:${rkey}:`;
148
+
let htmlCount = 0;
149
+
const cacheKeys = Array.from((rewrittenHtmlCache as any).cache?.keys() || []) as string[];
150
+
for (const key of cacheKeys) {
151
+
if (key.startsWith(sitePrefix)) {
152
+
rewrittenHtmlCache.delete(key);
153
+
htmlCount++;
154
+
}
155
+
}
156
+
157
+
console.log(`[Cache] Invalidated site ${did}:${rkey} - ${deleted} files in tiered storage, ${htmlCount} rewritten HTML`);
158
}
159
160
// Track sites currently being cached (to prevent serving stale cache during updates)
···
176
}
177
178
// Get overall cache statistics
179
+
export async function getCacheStats() {
180
+
const tieredStats = await storage.getStats();
181
+
182
return {
183
+
tieredStorage: tieredStats,
184
rewrittenHtml: rewrittenHtmlCache.getStats(),
185
rewrittenHtmlHitRate: rewrittenHtmlCache.getHitRate(),
186
sitesBeingCached: sitesBeingCached.size,
+32
-1
apps/hosting-service/src/lib/db.ts
+32
-1
apps/hosting-service/src/lib/db.ts
···
183
return hashNum & 0x7FFFFFFFFFFFFFFFn;
184
}
185
186
/**
187
* Acquire a distributed lock using PostgreSQL advisory locks
188
* Returns true if lock was acquired, false if already held by another instance
···
193
194
try {
195
const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`;
196
-
return result[0]?.acquired === true;
197
} catch (err) {
198
console.error('Failed to acquire lock', { key, error: err });
199
return false;
···
208
209
try {
210
await sql`SELECT pg_advisory_unlock(${Number(lockId)})`;
211
} catch (err) {
212
console.error('Failed to release lock', { key, error: err });
213
}
214
}
215
···
183
return hashNum & 0x7FFFFFFFFFFFFFFFn;
184
}
185
186
+
// Track active locks for cleanup on shutdown
187
+
const activeLocks = new Set<string>();
188
+
189
/**
190
* Acquire a distributed lock using PostgreSQL advisory locks
191
* Returns true if lock was acquired, false if already held by another instance
···
196
197
try {
198
const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`;
199
+
const acquired = result[0]?.acquired === true;
200
+
if (acquired) {
201
+
activeLocks.add(key);
202
+
}
203
+
return acquired;
204
} catch (err) {
205
console.error('Failed to acquire lock', { key, error: err });
206
return false;
···
215
216
try {
217
await sql`SELECT pg_advisory_unlock(${Number(lockId)})`;
218
+
activeLocks.delete(key);
219
} catch (err) {
220
console.error('Failed to release lock', { key, error: err });
221
+
// Still remove from tracking even if unlock fails
222
+
activeLocks.delete(key);
223
+
}
224
+
}
225
+
226
+
/**
227
+
* Close all database connections
228
+
* Call this during graceful shutdown
229
+
*/
230
+
export async function closeDatabase(): Promise<void> {
231
+
try {
232
+
// Release all active advisory locks before closing connections
233
+
if (activeLocks.size > 0) {
234
+
console.log(`[DB] Releasing ${activeLocks.size} active advisory locks before shutdown`);
235
+
for (const key of activeLocks) {
236
+
await releaseLock(key);
237
+
}
238
+
}
239
+
240
+
await sql.end({ timeout: 5 });
241
+
console.log('[DB] Database connections closed');
242
+
} catch (err) {
243
+
console.error('[DB] Error closing database connections:', err);
244
}
245
}
246
+64
-76
apps/hosting-service/src/lib/file-serving.ts
+64
-76
apps/hosting-service/src/lib/file-serving.ts
···
7
import { lookup } from 'mime-types';
8
import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings';
9
import { shouldCompressMimeType } from '@wisp/atproto-utils/compression';
10
-
import { fileCache, metadataCache, rewrittenHtmlCache, getCacheKey, isSiteBeingCached } from './cache';
11
import { getCachedFilePath, getCachedSettings } from './utils';
12
import { loadRedirectRules, matchRedirectRule, parseCookies, parseQueryString } from './redirects';
13
import { rewriteHtmlPaths, isHtmlContent } from './html-rewriter';
14
import { generate404Page, generateDirectoryListing, siteUpdatingResponse } from './page-generators';
15
import { getIndexFiles, applyCustomHeaders, fileExists } from './request-utils';
16
import { getRedirectRulesFromCache, setRedirectRulesInCache } from './site-cache';
17
18
/**
19
* Helper to serve files from cache (for custom domains and subdomains)
···
176
177
// Not a directory, try to serve as a file
178
const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html';
179
-
const cacheKey = getCacheKey(did, rkey, fileRequestPath);
180
-
const cachedFile = getCachedFilePath(did, rkey, fileRequestPath);
181
182
-
// Check in-memory cache first
183
-
let content = fileCache.get(cacheKey);
184
-
let meta = metadataCache.get(cacheKey);
185
186
-
if (!content && await fileExists(cachedFile)) {
187
-
// Read from disk and cache
188
-
content = await readFile(cachedFile);
189
-
fileCache.set(cacheKey, content, content.length);
190
191
-
const metaFile = `${cachedFile}.meta`;
192
-
if (await fileExists(metaFile)) {
193
-
const metaJson = await readFile(metaFile, 'utf-8');
194
-
meta = JSON.parse(metaJson);
195
-
metadataCache.set(cacheKey, meta!, JSON.stringify(meta).length);
196
-
}
197
-
}
198
-
199
-
if (content) {
200
// Build headers with caching
201
-
const headers: Record<string, string> = {};
202
203
-
if (meta && meta.encoding === 'gzip' && meta.mimeType) {
204
const shouldServeCompressed = shouldCompressMimeType(meta.mimeType);
205
206
if (!shouldServeCompressed) {
···
233
}
234
235
// Non-compressed files
236
-
const mimeType = lookup(cachedFile) || 'application/octet-stream';
237
headers['Content-Type'] = mimeType;
238
headers['Cache-Control'] = mimeType.startsWith('text/html')
239
? 'public, max-age=300'
···
246
if (!fileRequestPath.includes('.')) {
247
for (const indexFileName of indexFiles) {
248
const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName;
249
-
const indexCacheKey = getCacheKey(did, rkey, indexPath);
250
-
const indexFile = getCachedFilePath(did, rkey, indexPath);
251
252
-
let indexContent = fileCache.get(indexCacheKey);
253
-
let indexMeta = metadataCache.get(indexCacheKey);
254
255
-
if (!indexContent && await fileExists(indexFile)) {
256
-
indexContent = await readFile(indexFile);
257
-
fileCache.set(indexCacheKey, indexContent, indexContent.length);
258
259
-
const indexMetaFile = `${indexFile}.meta`;
260
-
if (await fileExists(indexMetaFile)) {
261
-
const metaJson = await readFile(indexMetaFile, 'utf-8');
262
-
indexMeta = JSON.parse(metaJson);
263
-
metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length);
264
-
}
265
-
}
266
-
267
-
if (indexContent) {
268
const headers: Record<string, string> = {
269
'Content-Type': 'text/html; charset=utf-8',
270
'Cache-Control': 'public, max-age=300',
271
};
272
273
-
if (indexMeta && indexMeta.encoding === 'gzip') {
274
headers['Content-Encoding'] = 'gzip';
275
}
276
···
556
557
// Not a directory, try to serve as a file
558
const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html';
559
-
const cacheKey = getCacheKey(did, rkey, fileRequestPath);
560
-
const cachedFile = getCachedFilePath(did, rkey, fileRequestPath);
561
562
// Check for rewritten HTML in cache first (if it's HTML)
563
const mimeTypeGuess = lookup(fileRequestPath) || 'application/octet-stream';
···
565
const rewrittenKey = getCacheKey(did, rkey, fileRequestPath, `rewritten:${basePath}`);
566
const rewrittenContent = rewrittenHtmlCache.get(rewrittenKey);
567
if (rewrittenContent) {
568
const headers: Record<string, string> = {
569
'Content-Type': 'text/html; charset=utf-8',
570
'Content-Encoding': 'gzip',
571
'Cache-Control': 'public, max-age=300',
572
};
573
applyCustomHeaders(headers, fileRequestPath, settings);
574
return new Response(rewrittenContent, { headers });
575
}
576
}
577
578
-
// Check in-memory file cache
579
-
let content = fileCache.get(cacheKey);
580
-
let meta = metadataCache.get(cacheKey);
581
582
-
if (!content && await fileExists(cachedFile)) {
583
-
// Read from disk and cache
584
-
content = await readFile(cachedFile);
585
-
fileCache.set(cacheKey, content, content.length);
586
587
-
const metaFile = `${cachedFile}.meta`;
588
-
if (await fileExists(metaFile)) {
589
-
const metaJson = await readFile(metaFile, 'utf-8');
590
-
meta = JSON.parse(metaJson);
591
-
metadataCache.set(cacheKey, meta!, JSON.stringify(meta).length);
592
-
}
593
-
}
594
-
595
-
if (content) {
596
-
const mimeType = meta?.mimeType || lookup(cachedFile) || 'application/octet-stream';
597
-
const isGzipped = meta?.encoding === 'gzip';
598
599
// Check if this is HTML content that needs rewriting
600
if (isHtmlContent(fileRequestPath, mimeType)) {
601
let htmlContent: string;
602
if (isGzipped) {
603
// Verify content is actually gzipped
···
612
} else {
613
htmlContent = content.toString('utf-8');
614
}
615
const rewritten = rewriteHtmlPaths(htmlContent, basePath, fileRequestPath);
616
617
// Recompress and cache the rewritten HTML
618
const { gzipSync } = await import('zlib');
619
const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8'));
···
625
'Content-Type': 'text/html; charset=utf-8',
626
'Content-Encoding': 'gzip',
627
'Cache-Control': 'public, max-age=300',
628
};
629
applyCustomHeaders(htmlHeaders, fileRequestPath, settings);
630
return new Response(recompressed, { headers: htmlHeaders });
···
634
const headers: Record<string, string> = {
635
'Content-Type': mimeType,
636
'Cache-Control': 'public, max-age=31536000, immutable',
637
};
638
639
if (isGzipped) {
···
663
if (!fileRequestPath.includes('.')) {
664
for (const indexFileName of indexFiles) {
665
const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName;
666
-
const indexCacheKey = getCacheKey(did, rkey, indexPath);
667
-
const indexFile = getCachedFilePath(did, rkey, indexPath);
668
669
// Check for rewritten index file in cache
670
const rewrittenKey = getCacheKey(did, rkey, indexPath, `rewritten:${basePath}`);
···
674
'Content-Type': 'text/html; charset=utf-8',
675
'Content-Encoding': 'gzip',
676
'Cache-Control': 'public, max-age=300',
677
};
678
applyCustomHeaders(headers, indexPath, settings);
679
return new Response(rewrittenContent, { headers });
680
}
681
682
-
let indexContent = fileCache.get(indexCacheKey);
683
-
let indexMeta = metadataCache.get(indexCacheKey);
684
685
-
if (!indexContent && await fileExists(indexFile)) {
686
-
indexContent = await readFile(indexFile);
687
-
fileCache.set(indexCacheKey, indexContent, indexContent.length);
688
-
689
-
const indexMetaFile = `${indexFile}.meta`;
690
-
if (await fileExists(indexMetaFile)) {
691
-
const metaJson = await readFile(indexMetaFile, 'utf-8');
692
-
indexMeta = JSON.parse(metaJson);
693
-
metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length);
694
-
}
695
-
}
696
-
697
-
if (indexContent) {
698
const isGzipped = indexMeta?.encoding === 'gzip';
699
700
let htmlContent: string;
···
722
'Content-Type': 'text/html; charset=utf-8',
723
'Content-Encoding': 'gzip',
724
'Cache-Control': 'public, max-age=300',
725
};
726
applyCustomHeaders(headers, indexPath, settings);
727
return new Response(recompressed, { headers });
···
7
import { lookup } from 'mime-types';
8
import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings';
9
import { shouldCompressMimeType } from '@wisp/atproto-utils/compression';
10
+
import { rewrittenHtmlCache, getCacheKey, isSiteBeingCached } from './cache';
11
import { getCachedFilePath, getCachedSettings } from './utils';
12
import { loadRedirectRules, matchRedirectRule, parseCookies, parseQueryString } from './redirects';
13
import { rewriteHtmlPaths, isHtmlContent } from './html-rewriter';
14
import { generate404Page, generateDirectoryListing, siteUpdatingResponse } from './page-generators';
15
import { getIndexFiles, applyCustomHeaders, fileExists } from './request-utils';
16
import { getRedirectRulesFromCache, setRedirectRulesInCache } from './site-cache';
17
+
import { storage } from './storage';
18
+
19
+
/**
20
+
* Helper to retrieve a file with metadata from tiered storage
21
+
*/
22
+
async function getFileWithMetadata(did: string, rkey: string, filePath: string) {
23
+
const key = `${did}/${rkey}/${filePath}`;
24
+
return await storage.getWithMetadata(key);
25
+
}
26
27
/**
28
* Helper to serve files from cache (for custom domains and subdomains)
···
185
186
// Not a directory, try to serve as a file
187
const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html';
188
189
+
// Retrieve from tiered storage
190
+
const result = await getFileWithMetadata(did, rkey, fileRequestPath);
191
192
+
if (result) {
193
+
const content = Buffer.from(result.data);
194
+
const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined;
195
196
// Build headers with caching
197
+
const headers: Record<string, string> = {
198
+
'X-Cache-Tier': result.source,
199
+
};
200
201
+
if (meta?.encoding === 'gzip' && meta.mimeType) {
202
const shouldServeCompressed = shouldCompressMimeType(meta.mimeType);
203
204
if (!shouldServeCompressed) {
···
231
}
232
233
// Non-compressed files
234
+
const mimeType = meta?.mimeType || lookup(fileRequestPath) || 'application/octet-stream';
235
headers['Content-Type'] = mimeType;
236
headers['Cache-Control'] = mimeType.startsWith('text/html')
237
? 'public, max-age=300'
···
244
if (!fileRequestPath.includes('.')) {
245
for (const indexFileName of indexFiles) {
246
const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName;
247
248
+
const indexResult = await getFileWithMetadata(did, rkey, indexPath);
249
250
+
if (indexResult) {
251
+
const indexContent = Buffer.from(indexResult.data);
252
+
const indexMeta = indexResult.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined;
253
254
const headers: Record<string, string> = {
255
'Content-Type': 'text/html; charset=utf-8',
256
'Cache-Control': 'public, max-age=300',
257
+
'X-Cache-Tier': indexResult.source,
258
};
259
260
+
if (indexMeta?.encoding === 'gzip') {
261
headers['Content-Encoding'] = 'gzip';
262
}
263
···
543
544
// Not a directory, try to serve as a file
545
const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html';
546
547
// Check for rewritten HTML in cache first (if it's HTML)
548
const mimeTypeGuess = lookup(fileRequestPath) || 'application/octet-stream';
···
550
const rewrittenKey = getCacheKey(did, rkey, fileRequestPath, `rewritten:${basePath}`);
551
const rewrittenContent = rewrittenHtmlCache.get(rewrittenKey);
552
if (rewrittenContent) {
553
+
console.log(`[HTML Rewrite] Serving from rewritten cache: ${rewrittenKey}`);
554
const headers: Record<string, string> = {
555
'Content-Type': 'text/html; charset=utf-8',
556
'Content-Encoding': 'gzip',
557
'Cache-Control': 'public, max-age=300',
558
+
'X-Cache-Tier': 'local', // Rewritten HTML is stored locally
559
};
560
applyCustomHeaders(headers, fileRequestPath, settings);
561
return new Response(rewrittenContent, { headers });
562
}
563
}
564
565
+
// Retrieve from tiered storage
566
+
const result = await getFileWithMetadata(did, rkey, fileRequestPath);
567
568
+
if (result) {
569
+
const content = Buffer.from(result.data);
570
+
const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined;
571
+
const mimeType = meta?.mimeType || lookup(fileRequestPath) || 'application/octet-stream';
572
+
const isGzipped = meta?.encoding === 'gzip';
573
574
+
console.log(`[File Serve] Serving ${fileRequestPath}, mimeType: ${mimeType}, isHTML: ${isHtmlContent(fileRequestPath, mimeType)}, basePath: ${basePath}`);
575
576
// Check if this is HTML content that needs rewriting
577
if (isHtmlContent(fileRequestPath, mimeType)) {
578
+
console.log(`[HTML Rewrite] Processing ${fileRequestPath}, basePath: ${basePath}, mimeType: ${mimeType}, isGzipped: ${isGzipped}`);
579
let htmlContent: string;
580
if (isGzipped) {
581
// Verify content is actually gzipped
···
590
} else {
591
htmlContent = content.toString('utf-8');
592
}
593
+
// Check for <base> tag which can override paths
594
+
const baseTagMatch = htmlContent.match(/<base\s+[^>]*href=["'][^"']+["'][^>]*>/i);
595
+
if (baseTagMatch) {
596
+
console.warn(`[HTML Rewrite] WARNING: <base> tag found: ${baseTagMatch[0]} - this may override path rewrites`);
597
+
}
598
+
599
+
// Find src/href attributes (quoted and unquoted) to debug
600
+
const allMatches = htmlContent.match(/(?:src|href)\s*=\s*["']?\/[^"'\s>]+/g);
601
+
console.log(`[HTML Rewrite] Found ${allMatches ? allMatches.length : 0} local path attrs`);
602
+
if (allMatches && allMatches.length > 0) {
603
+
console.log(`[HTML Rewrite] Sample paths: ${allMatches.slice(0, 5).join(', ')}`);
604
+
}
605
+
606
const rewritten = rewriteHtmlPaths(htmlContent, basePath, fileRequestPath);
607
608
+
const rewrittenMatches = rewritten.match(/(?:src|href)\s*=\s*["']?\/[^"'\s>]+/g);
609
+
console.log(`[HTML Rewrite] After rewrite, found ${rewrittenMatches ? rewrittenMatches.length : 0} local paths`);
610
+
if (rewrittenMatches && rewrittenMatches.length > 0) {
611
+
console.log(`[HTML Rewrite] Sample rewritten: ${rewrittenMatches.slice(0, 5).join(', ')}`);
612
+
}
613
+
614
// Recompress and cache the rewritten HTML
615
const { gzipSync } = await import('zlib');
616
const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8'));
···
622
'Content-Type': 'text/html; charset=utf-8',
623
'Content-Encoding': 'gzip',
624
'Cache-Control': 'public, max-age=300',
625
+
'X-Cache-Tier': result.source,
626
};
627
applyCustomHeaders(htmlHeaders, fileRequestPath, settings);
628
return new Response(recompressed, { headers: htmlHeaders });
···
632
const headers: Record<string, string> = {
633
'Content-Type': mimeType,
634
'Cache-Control': 'public, max-age=31536000, immutable',
635
+
'X-Cache-Tier': result.source,
636
};
637
638
if (isGzipped) {
···
662
if (!fileRequestPath.includes('.')) {
663
for (const indexFileName of indexFiles) {
664
const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName;
665
666
// Check for rewritten index file in cache
667
const rewrittenKey = getCacheKey(did, rkey, indexPath, `rewritten:${basePath}`);
···
671
'Content-Type': 'text/html; charset=utf-8',
672
'Content-Encoding': 'gzip',
673
'Cache-Control': 'public, max-age=300',
674
+
'X-Cache-Tier': 'local', // Rewritten HTML is stored locally
675
};
676
applyCustomHeaders(headers, indexPath, settings);
677
return new Response(rewrittenContent, { headers });
678
}
679
680
+
const indexResult = await getFileWithMetadata(did, rkey, indexPath);
681
682
+
if (indexResult) {
683
+
const indexContent = Buffer.from(indexResult.data);
684
+
const indexMeta = indexResult.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined;
685
const isGzipped = indexMeta?.encoding === 'gzip';
686
687
let htmlContent: string;
···
709
'Content-Type': 'text/html; charset=utf-8',
710
'Content-Encoding': 'gzip',
711
'Cache-Control': 'public, max-age=300',
712
+
'X-Cache-Tier': indexResult.source,
713
};
714
applyCustomHeaders(headers, indexPath, settings);
715
return new Response(recompressed, { headers });
+46
-35
apps/hosting-service/src/lib/firehose.ts
+46
-35
apps/hosting-service/src/lib/firehose.ts
···
1
-
import { existsSync, rmSync } from 'fs'
2
import {
3
getPdsForDid,
4
downloadAndCacheSite,
···
13
import { invalidateSiteCache, markSiteAsBeingCached, unmarkSiteAsBeingCached } from './cache'
14
import { clearRedirectRulesCache } from './site-cache'
15
16
-
const CACHE_DIR = './cache/sites'
17
18
export class FirehoseWorker {
19
private firehose: Firehose | null = null
20
private idResolver: IdResolver
21
private isShuttingDown = false
22
private lastEventTime = Date.now()
23
private cacheCleanupInterval: NodeJS.Timeout | null = null
24
25
constructor(
26
private logger?: (msg: string, data?: Record<string, unknown>) => void
···
47
48
this.log('IdResolver cache cleared')
49
}, 60 * 60 * 1000) // Every hour
50
}
51
52
start() {
···
61
if (this.cacheCleanupInterval) {
62
clearInterval(this.cacheCleanupInterval)
63
this.cacheCleanupInterval = null
64
}
65
66
if (this.firehose) {
···
80
filterCollections: ['place.wisp.fs', 'place.wisp.settings'],
81
handleEvent: async (evt: any) => {
82
this.lastEventTime = Date.now()
83
84
// Watch for write events
85
if (evt.event === 'create' || evt.event === 'update') {
···
189
}
190
})
191
192
-
this.firehose.start()
193
-
this.log('Firehose started')
194
}
195
196
private async handleCreateOrUpdate(
···
250
}
251
252
// Invalidate in-memory caches before updating
253
-
invalidateSiteCache(did, site)
254
255
// Mark site as being cached to prevent serving stale content during update
256
markSiteAsBeingCached(did, site)
···
340
})
341
}
342
343
-
// Invalidate in-memory caches
344
-
invalidateSiteCache(did, site)
345
-
346
-
// Delete disk cache
347
-
this.deleteCache(did, site)
348
349
this.log('Successfully processed delete', { did, site })
350
}
···
353
this.log('Processing settings change', { did, rkey })
354
355
// Invalidate in-memory caches (includes metadata which stores settings)
356
-
invalidateSiteCache(did, rkey)
357
358
// Check if site is already cached
359
const cacheDir = `${CACHE_DIR}/${did}/${rkey}`
···
413
}
414
415
this.log('Successfully processed settings change', { did, rkey })
416
-
}
417
-
418
-
private deleteCache(did: string, site: string) {
419
-
const cacheDir = `${CACHE_DIR}/${did}/${site}`
420
-
421
-
if (!existsSync(cacheDir)) {
422
-
this.log('Cache directory does not exist, nothing to delete', {
423
-
did,
424
-
site
425
-
})
426
-
return
427
-
}
428
-
429
-
try {
430
-
rmSync(cacheDir, { recursive: true, force: true })
431
-
this.log('Cache deleted', { did, site, path: cacheDir })
432
-
} catch (err) {
433
-
this.log('Failed to delete cache', {
434
-
did,
435
-
site,
436
-
path: cacheDir,
437
-
error: err instanceof Error ? err.message : String(err)
438
-
})
439
-
}
440
}
441
442
getHealth() {
···
1
+
import { existsSync } from 'fs'
2
import {
3
getPdsForDid,
4
downloadAndCacheSite,
···
13
import { invalidateSiteCache, markSiteAsBeingCached, unmarkSiteAsBeingCached } from './cache'
14
import { clearRedirectRulesCache } from './site-cache'
15
16
+
const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'
17
18
export class FirehoseWorker {
19
private firehose: Firehose | null = null
20
private idResolver: IdResolver
21
private isShuttingDown = false
22
private lastEventTime = Date.now()
23
+
private eventCount = 0
24
private cacheCleanupInterval: NodeJS.Timeout | null = null
25
+
private healthCheckInterval: NodeJS.Timeout | null = null
26
27
constructor(
28
private logger?: (msg: string, data?: Record<string, unknown>) => void
···
49
50
this.log('IdResolver cache cleared')
51
}, 60 * 60 * 1000) // Every hour
52
+
53
+
// Health check: log if no events received for 30 seconds
54
+
this.healthCheckInterval = setInterval(() => {
55
+
if (this.isShuttingDown) return
56
+
57
+
const timeSinceLastEvent = Date.now() - this.lastEventTime
58
+
if (timeSinceLastEvent > 30000 && this.eventCount === 0) {
59
+
this.log('Warning: No firehose events received in the last 30 seconds', {
60
+
timeSinceLastEvent,
61
+
eventsReceived: this.eventCount
62
+
})
63
+
} else if (timeSinceLastEvent > 60000) {
64
+
this.log('Firehose status check', {
65
+
timeSinceLastEvent,
66
+
eventsReceived: this.eventCount
67
+
})
68
+
}
69
+
}, 30000) // Every 30 seconds
70
}
71
72
start() {
···
81
if (this.cacheCleanupInterval) {
82
clearInterval(this.cacheCleanupInterval)
83
this.cacheCleanupInterval = null
84
+
}
85
+
86
+
if (this.healthCheckInterval) {
87
+
clearInterval(this.healthCheckInterval)
88
+
this.healthCheckInterval = null
89
}
90
91
if (this.firehose) {
···
105
filterCollections: ['place.wisp.fs', 'place.wisp.settings'],
106
handleEvent: async (evt: any) => {
107
this.lastEventTime = Date.now()
108
+
this.eventCount++
109
+
110
+
if (this.eventCount === 1) {
111
+
this.log('First firehose event received - connection established', {
112
+
eventType: evt.event,
113
+
collection: evt.collection
114
+
})
115
+
}
116
117
// Watch for write events
118
if (evt.event === 'create' || evt.event === 'update') {
···
222
}
223
})
224
225
+
this.firehose.start().catch((err: unknown) => {
226
+
this.log('Fatal firehose error', {
227
+
error: err instanceof Error ? err.message : String(err)
228
+
})
229
+
console.error('Fatal firehose error:', err)
230
+
})
231
+
this.log('Firehose starting')
232
}
233
234
private async handleCreateOrUpdate(
···
288
}
289
290
// Invalidate in-memory caches before updating
291
+
await invalidateSiteCache(did, site)
292
293
// Mark site as being cached to prevent serving stale content during update
294
markSiteAsBeingCached(did, site)
···
378
})
379
}
380
381
+
// Invalidate all caches (tiered storage invalidation is handled by invalidateSiteCache)
382
+
await invalidateSiteCache(did, site)
383
384
this.log('Successfully processed delete', { did, site })
385
}
···
388
this.log('Processing settings change', { did, rkey })
389
390
// Invalidate in-memory caches (includes metadata which stores settings)
391
+
await invalidateSiteCache(did, rkey)
392
393
// Check if site is already cached
394
const cacheDir = `${CACHE_DIR}/${did}/${rkey}`
···
448
}
449
450
this.log('Successfully processed settings change', { did, rkey })
451
}
452
453
getHealth() {
+16
apps/hosting-service/src/lib/html-rewriter.ts
+16
apps/hosting-service/src/lib/html-rewriter.ts
···
189
`\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}'([^']*)'`,
190
'gi'
191
)
192
+
// Unquoted attributes (valid in HTML5 for values without spaces/special chars)
193
+
// Match: attr=value where value starts immediately (no quotes) and continues until space or >
194
+
// Use negative lookahead to ensure we don't match quoted attributes
195
+
const unquotedRegex = new RegExp(
196
+
`\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}(?!["'])([^\\s>]+)`,
197
+
'gi'
198
+
)
199
200
rewritten = rewritten.replace(doubleQuoteRegex, (match, value) => {
201
const rewrittenValue = rewritePath(
···
213
documentPath
214
)
215
return `${attr}='${rewrittenValue}'`
216
+
})
217
+
218
+
rewritten = rewritten.replace(unquotedRegex, (match, value) => {
219
+
const rewrittenValue = rewritePath(
220
+
value,
221
+
normalizedBase,
222
+
documentPath
223
+
)
224
+
return `${attr}=${rewrittenValue}`
225
})
226
}
227
}
+1
-1
apps/hosting-service/src/lib/site-cache.ts
+1
-1
apps/hosting-service/src/lib/site-cache.ts
+270
apps/hosting-service/src/lib/storage.ts
+270
apps/hosting-service/src/lib/storage.ts
···
···
1
+
/**
2
+
* Tiered storage configuration for wisp-hosting-service
3
+
*
4
+
* Implements a three-tier caching strategy:
5
+
* - Hot (Memory): Instant access for frequently used files (index.html, CSS, JS)
6
+
* - Warm (Disk): Local cache with eviction policy
7
+
* - Cold (S3/R2): Object storage as source of truth (optional)
8
+
*
9
+
* When S3 is not configured, falls back to disk-only mode (warm tier acts as source of truth).
10
+
* In cache-only mode (non-master nodes), S3 writes are skipped even if configured.
11
+
*/
12
+
13
+
import {
14
+
TieredStorage,
15
+
MemoryStorageTier,
16
+
DiskStorageTier,
17
+
S3StorageTier,
18
+
type StorageTier,
19
+
type StorageMetadata,
20
+
} from 'tiered-storage';
21
+
22
+
const CACHE_DIR = process.env.CACHE_DIR || './cache/sites';
23
+
const HOT_CACHE_SIZE = parseInt(process.env.HOT_CACHE_SIZE || '104857600', 10); // 100MB default
24
+
const HOT_CACHE_COUNT = parseInt(process.env.HOT_CACHE_COUNT || '500', 10);
25
+
const WARM_CACHE_SIZE = parseInt(process.env.WARM_CACHE_SIZE || '10737418240', 10); // 10GB default
26
+
const WARM_EVICTION_POLICY = (process.env.WARM_EVICTION_POLICY || 'lru') as 'lru' | 'fifo' | 'size';
27
+
28
+
// Cache-only mode: skip S3 writes (non-master nodes)
29
+
// This is the same flag used to skip database writes
30
+
const CACHE_ONLY_MODE = process.env.CACHE_ONLY_MODE === 'true';
31
+
32
+
// S3/Cold tier configuration (optional)
33
+
const S3_BUCKET = process.env.S3_BUCKET || '';
34
+
const S3_METADATA_BUCKET = process.env.S3_METADATA_BUCKET;
35
+
const S3_REGION = process.env.S3_REGION || 'us-east-1';
36
+
const S3_ENDPOINT = process.env.S3_ENDPOINT;
37
+
const S3_FORCE_PATH_STYLE = process.env.S3_FORCE_PATH_STYLE !== 'false';
38
+
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
39
+
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
40
+
const S3_PREFIX = process.env.S3_PREFIX || 'sites/';
41
+
42
+
// Identity serializers for raw binary data (no JSON transformation)
43
+
// Files are stored as-is without any encoding/decoding
44
+
const identitySerialize = async (data: unknown): Promise<Uint8Array> => {
45
+
if (data instanceof Uint8Array) return data;
46
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
47
+
if (Buffer.isBuffer(data)) return new Uint8Array(data);
48
+
// For other types, fall back to JSON (shouldn't happen with file storage)
49
+
return new TextEncoder().encode(JSON.stringify(data));
50
+
};
51
+
52
+
const identityDeserialize = async (data: Uint8Array): Promise<unknown> => {
53
+
// Return as-is for binary file storage
54
+
return data;
55
+
};
56
+
57
+
/**
58
+
* Read-only wrapper for S3 tier in cache-only mode.
59
+
* Allows reads from S3 but skips all writes (for non-master nodes).
60
+
*/
61
+
class ReadOnlyS3Tier implements StorageTier {
62
+
private static hasLoggedWriteSkip = false;
63
+
64
+
constructor(private tier: StorageTier) {}
65
+
66
+
// Read operations - pass through to underlying tier
67
+
async get(key: string) {
68
+
return this.tier.get(key);
69
+
}
70
+
71
+
async getWithMetadata(key: string) {
72
+
return this.tier.getWithMetadata?.(key) ?? null;
73
+
}
74
+
75
+
async getStream(key: string) {
76
+
return this.tier.getStream?.(key) ?? null;
77
+
}
78
+
79
+
async exists(key: string) {
80
+
return this.tier.exists(key);
81
+
}
82
+
83
+
async getMetadata(key: string) {
84
+
return this.tier.getMetadata(key);
85
+
}
86
+
87
+
async *listKeys(prefix?: string) {
88
+
yield* this.tier.listKeys(prefix);
89
+
}
90
+
91
+
async getStats() {
92
+
return this.tier.getStats();
93
+
}
94
+
95
+
// Write operations - no-op in cache-only mode
96
+
async set(key: string, _data: Uint8Array, _metadata: StorageMetadata) {
97
+
this.logWriteSkip('set', key);
98
+
}
99
+
100
+
async setStream(key: string, _stream: NodeJS.ReadableStream, _metadata: StorageMetadata) {
101
+
this.logWriteSkip('setStream', key);
102
+
}
103
+
104
+
async setMetadata(key: string, _metadata: StorageMetadata) {
105
+
this.logWriteSkip('setMetadata', key);
106
+
}
107
+
108
+
async delete(key: string) {
109
+
this.logWriteSkip('delete', key);
110
+
}
111
+
112
+
async deleteMany(keys: string[]) {
113
+
this.logWriteSkip('deleteMany', `${keys.length} keys`);
114
+
}
115
+
116
+
async clear() {
117
+
this.logWriteSkip('clear', 'all keys');
118
+
}
119
+
120
+
private logWriteSkip(operation: string, key: string) {
121
+
// Only log once to avoid spam
122
+
if (!ReadOnlyS3Tier.hasLoggedWriteSkip) {
123
+
console.log(`[Storage] Cache-only mode: skipping S3 writes (operation: ${operation})`);
124
+
ReadOnlyS3Tier.hasLoggedWriteSkip = true;
125
+
}
126
+
}
127
+
}
128
+
129
+
/**
130
+
* Initialize tiered storage
131
+
* Must be called before serving requests
132
+
*/
133
+
function initializeStorage(): TieredStorage<Uint8Array> {
134
+
// Determine cold tier: S3 if configured, otherwise disk acts as cold
135
+
let coldTier: StorageTier;
136
+
let warmTier: StorageTier | undefined;
137
+
138
+
const diskTier = new DiskStorageTier({
139
+
directory: CACHE_DIR,
140
+
maxSizeBytes: WARM_CACHE_SIZE,
141
+
evictionPolicy: WARM_EVICTION_POLICY,
142
+
encodeColons: false, // Preserve colons for readable DID paths on Unix/macOS
143
+
});
144
+
145
+
if (S3_BUCKET) {
146
+
// Full three-tier setup with S3 as cold storage
147
+
const s3Tier = new S3StorageTier({
148
+
bucket: S3_BUCKET,
149
+
metadataBucket: S3_METADATA_BUCKET,
150
+
region: S3_REGION,
151
+
endpoint: S3_ENDPOINT,
152
+
forcePathStyle: S3_FORCE_PATH_STYLE,
153
+
credentials:
154
+
AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY
155
+
? { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY }
156
+
: undefined,
157
+
prefix: S3_PREFIX,
158
+
});
159
+
160
+
// In cache-only mode, wrap S3 tier to make it read-only
161
+
coldTier = CACHE_ONLY_MODE ? new ReadOnlyS3Tier(s3Tier) : s3Tier;
162
+
warmTier = diskTier;
163
+
164
+
if (CACHE_ONLY_MODE) {
165
+
console.log('[Storage] Cache-only mode: S3 as read-only cold tier (no writes), disk as warm tier');
166
+
} else {
167
+
console.log('[Storage] Using S3 as cold tier, disk as warm tier');
168
+
}
169
+
} else {
170
+
// Disk-only mode: disk tier acts as source of truth (cold)
171
+
coldTier = diskTier;
172
+
warmTier = undefined;
173
+
console.log('[Storage] S3 not configured - using disk-only mode (disk as cold tier)');
174
+
}
175
+
176
+
const storage = new TieredStorage<Uint8Array>({
177
+
tiers: {
178
+
// Hot tier: In-memory LRU for instant serving
179
+
hot: new MemoryStorageTier({
180
+
maxSizeBytes: HOT_CACHE_SIZE,
181
+
maxItems: HOT_CACHE_COUNT,
182
+
}),
183
+
184
+
// Warm tier: Disk-based cache (only when S3 is configured)
185
+
warm: warmTier,
186
+
187
+
// Cold tier: S3/R2 as source of truth, or disk in disk-only mode
188
+
cold: coldTier,
189
+
},
190
+
191
+
// Placement rules: determine which tiers each file goes to
192
+
placementRules: [
193
+
// Metadata is critical: frequently accessed for cache validity checks
194
+
{
195
+
pattern: '**/.metadata.json',
196
+
tiers: ['hot', 'warm', 'cold'],
197
+
},
198
+
199
+
// index.html is critical: write to all tiers for instant serving
200
+
{
201
+
pattern: '**/index.html',
202
+
tiers: ['hot', 'warm', 'cold'],
203
+
},
204
+
{
205
+
pattern: 'index.html',
206
+
tiers: ['hot', 'warm', 'cold'],
207
+
},
208
+
209
+
// CSS and JS: eligible for hot tier if accessed frequently
210
+
{
211
+
pattern: '**/*.{css,js}',
212
+
tiers: ['hot', 'warm', 'cold'],
213
+
},
214
+
215
+
// Media files: never needed in memory, skip hot tier
216
+
{
217
+
pattern: '**/*.{jpg,jpeg,png,gif,webp,svg,ico,mp4,webm,mp3,woff,woff2,ttf,eot}',
218
+
tiers: ['warm', 'cold'],
219
+
},
220
+
221
+
// Default: everything else goes to warm and cold
222
+
{
223
+
pattern: '**',
224
+
tiers: ['warm', 'cold'],
225
+
},
226
+
],
227
+
228
+
// IMPORTANT: Compression is disabled at the tiered-storage level
229
+
// Text files (HTML, CSS, JS, JSON) are pre-compressed with gzip at the app level
230
+
// Binary files (images, video) are stored uncompressed as they're already compressed
231
+
// The file's compression state is tracked in customMetadata.encoding
232
+
compression: false,
233
+
234
+
// TTL for cache entries (14 days)
235
+
defaultTTL: 14 * 24 * 60 * 60 * 1000,
236
+
237
+
// Eager promotion: promote data to upper tiers on read
238
+
// This ensures frequently accessed files end up in hot tier
239
+
promotionStrategy: 'eager',
240
+
241
+
// Identity serialization: store raw binary without JSON transformation
242
+
serialization: {
243
+
serialize: identitySerialize,
244
+
deserialize: identityDeserialize,
245
+
},
246
+
});
247
+
248
+
return storage;
249
+
}
250
+
251
+
// Export singleton instance
252
+
export const storage = initializeStorage();
253
+
254
+
/**
255
+
* Get storage configuration summary for logging
256
+
*/
257
+
export function getStorageConfig() {
258
+
return {
259
+
cacheDir: CACHE_DIR,
260
+
hotCacheSize: `${(HOT_CACHE_SIZE / 1024 / 1024).toFixed(0)}MB`,
261
+
hotCacheCount: HOT_CACHE_COUNT,
262
+
warmCacheSize: `${(WARM_CACHE_SIZE / 1024 / 1024 / 1024).toFixed(1)}GB`,
263
+
warmEvictionPolicy: WARM_EVICTION_POLICY,
264
+
s3Bucket: S3_BUCKET,
265
+
s3Region: S3_REGION,
266
+
s3Endpoint: S3_ENDPOINT || '(default AWS S3)',
267
+
s3Prefix: S3_PREFIX,
268
+
metadataBucket: S3_METADATA_BUCKET || '(embedded in data bucket)',
269
+
};
270
+
}
+473
-8
apps/hosting-service/src/lib/utils.test.ts
+473
-8
apps/hosting-service/src/lib/utils.test.ts
···
1
import { describe, test, expect } from 'bun:test'
2
-
import { sanitizePath, extractBlobCid } from './utils'
3
import { CID } from 'multiformats'
4
5
describe('sanitizePath', () => {
6
test('allows normal file paths', () => {
···
31
32
test('blocks directory traversal in middle of path', () => {
33
expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd')
34
-
// Note: sanitizePath only filters out ".." segments, doesn't resolve paths
35
expect(sanitizePath('a/b/../c')).toBe('a/b/c')
36
expect(sanitizePath('a/../b/../c')).toBe('a/b/c')
37
})
···
50
})
51
52
test('blocks null bytes', () => {
53
-
// Null bytes cause the entire segment to be filtered out
54
expect(sanitizePath('index.html\0.txt')).toBe('')
55
expect(sanitizePath('test\0')).toBe('')
56
-
// Null byte in middle segment
57
expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css')
58
})
59
···
89
90
describe('extractBlobCid', () => {
91
const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
92
-
93
test('extracts CID from IPLD link', () => {
94
const blobRef = { $link: TEST_CID }
95
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
···
103
})
104
105
test('extracts CID from typed BlobRef with IPLD link', () => {
106
-
const blobRef = {
107
ref: { $link: TEST_CID }
108
}
109
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
···
129
})
130
131
test('handles nested structures from AT Proto API', () => {
132
-
// Real structure from AT Proto
133
const blobRef = {
134
$type: 'blob',
135
ref: CID.parse(TEST_CID),
···
150
})
151
152
test('prioritizes checking IPLD link first', () => {
153
-
// Direct $link takes precedence
154
const directLink = { $link: TEST_CID }
155
expect(extractBlobCid(directLink)).toBe(TEST_CID)
156
})
···
167
expect(extractBlobCid(blobRef)).toBe(cidV1)
168
})
169
})
···
1
import { describe, test, expect } from 'bun:test'
2
+
import { sanitizePath, extractBlobCid, extractSubfsUris, expandSubfsNodes } from './utils'
3
import { CID } from 'multiformats'
4
+
import { BlobRef } from '@atproto/lexicon'
5
+
import type {
6
+
Record as WispFsRecord,
7
+
Directory as FsDirectory,
8
+
Entry as FsEntry,
9
+
File as FsFile,
10
+
Subfs as FsSubfs,
11
+
} from '@wisp/lexicons/types/place/wisp/fs'
12
+
import type {
13
+
Record as SubfsRecord,
14
+
Directory as SubfsDirectory,
15
+
Entry as SubfsEntry,
16
+
File as SubfsFile,
17
+
Subfs as SubfsSubfs,
18
+
} from '@wisp/lexicons/types/place/wisp/subfs'
19
+
import type { $Typed } from '@wisp/lexicons/util'
20
21
describe('sanitizePath', () => {
22
test('allows normal file paths', () => {
···
47
48
test('blocks directory traversal in middle of path', () => {
49
expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd')
50
expect(sanitizePath('a/b/../c')).toBe('a/b/c')
51
expect(sanitizePath('a/../b/../c')).toBe('a/b/c')
52
})
···
65
})
66
67
test('blocks null bytes', () => {
68
expect(sanitizePath('index.html\0.txt')).toBe('')
69
expect(sanitizePath('test\0')).toBe('')
70
expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css')
71
})
72
···
102
103
describe('extractBlobCid', () => {
104
const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
105
+
106
test('extracts CID from IPLD link', () => {
107
const blobRef = { $link: TEST_CID }
108
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
···
116
})
117
118
test('extracts CID from typed BlobRef with IPLD link', () => {
119
+
const blobRef = {
120
ref: { $link: TEST_CID }
121
}
122
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
···
142
})
143
144
test('handles nested structures from AT Proto API', () => {
145
const blobRef = {
146
$type: 'blob',
147
ref: CID.parse(TEST_CID),
···
162
})
163
164
test('prioritizes checking IPLD link first', () => {
165
const directLink = { $link: TEST_CID }
166
expect(extractBlobCid(directLink)).toBe(TEST_CID)
167
})
···
178
expect(extractBlobCid(blobRef)).toBe(cidV1)
179
})
180
})
181
+
182
+
const TEST_CID_BASE = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
183
+
184
+
function createMockBlobRef(cidSuffix: string = '', size: number = 100, mimeType: string = 'text/plain'): BlobRef {
185
+
const cidString = TEST_CID_BASE
186
+
return new BlobRef(CID.parse(cidString), mimeType, size)
187
+
}
188
+
189
+
function createFsFile(
190
+
name: string,
191
+
options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {}
192
+
): FsEntry {
193
+
const { mimeType = 'text/plain', size = 100, encoding, base64 } = options
194
+
const file: $Typed<FsFile, 'place.wisp.fs#file'> = {
195
+
$type: 'place.wisp.fs#file',
196
+
type: 'file',
197
+
blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType),
198
+
...(encoding && { encoding }),
199
+
...(mimeType && { mimeType }),
200
+
...(base64 && { base64 }),
201
+
}
202
+
return { name, node: file }
203
+
}
204
+
205
+
function createFsDirectory(name: string, entries: FsEntry[]): FsEntry {
206
+
const dir: $Typed<FsDirectory, 'place.wisp.fs#directory'> = {
207
+
$type: 'place.wisp.fs#directory',
208
+
type: 'directory',
209
+
entries,
210
+
}
211
+
return { name, node: dir }
212
+
}
213
+
214
+
function createFsSubfs(name: string, subject: string, flat: boolean = true): FsEntry {
215
+
const subfs: $Typed<FsSubfs, 'place.wisp.fs#subfs'> = {
216
+
$type: 'place.wisp.fs#subfs',
217
+
type: 'subfs',
218
+
subject,
219
+
flat,
220
+
}
221
+
return { name, node: subfs }
222
+
}
223
+
224
+
function createFsRootDirectory(entries: FsEntry[]): FsDirectory {
225
+
return {
226
+
$type: 'place.wisp.fs#directory',
227
+
type: 'directory',
228
+
entries,
229
+
}
230
+
}
231
+
232
+
function createFsRecord(site: string, entries: FsEntry[], fileCount?: number): WispFsRecord {
233
+
return {
234
+
$type: 'place.wisp.fs',
235
+
site,
236
+
root: createFsRootDirectory(entries),
237
+
...(fileCount !== undefined && { fileCount }),
238
+
createdAt: new Date().toISOString(),
239
+
}
240
+
}
241
+
242
+
function createSubfsFile(
243
+
name: string,
244
+
options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {}
245
+
): SubfsEntry {
246
+
const { mimeType = 'text/plain', size = 100, encoding, base64 } = options
247
+
const file: $Typed<SubfsFile, 'place.wisp.subfs#file'> = {
248
+
$type: 'place.wisp.subfs#file',
249
+
type: 'file',
250
+
blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType),
251
+
...(encoding && { encoding }),
252
+
...(mimeType && { mimeType }),
253
+
...(base64 && { base64 }),
254
+
}
255
+
return { name, node: file }
256
+
}
257
+
258
+
function createSubfsDirectory(name: string, entries: SubfsEntry[]): SubfsEntry {
259
+
const dir: $Typed<SubfsDirectory, 'place.wisp.subfs#directory'> = {
260
+
$type: 'place.wisp.subfs#directory',
261
+
type: 'directory',
262
+
entries,
263
+
}
264
+
return { name, node: dir }
265
+
}
266
+
267
+
function createSubfsSubfs(name: string, subject: string): SubfsEntry {
268
+
const subfs: $Typed<SubfsSubfs, 'place.wisp.subfs#subfs'> = {
269
+
$type: 'place.wisp.subfs#subfs',
270
+
type: 'subfs',
271
+
subject,
272
+
}
273
+
return { name, node: subfs }
274
+
}
275
+
276
+
function createSubfsRootDirectory(entries: SubfsEntry[]): SubfsDirectory {
277
+
return {
278
+
$type: 'place.wisp.subfs#directory',
279
+
type: 'directory',
280
+
entries,
281
+
}
282
+
}
283
+
284
+
function createSubfsRecord(entries: SubfsEntry[], fileCount?: number): SubfsRecord {
285
+
return {
286
+
$type: 'place.wisp.subfs',
287
+
root: createSubfsRootDirectory(entries),
288
+
...(fileCount !== undefined && { fileCount }),
289
+
createdAt: new Date().toISOString(),
290
+
}
291
+
}
292
+
293
+
describe('extractSubfsUris', () => {
294
+
test('extracts subfs URIs from flat directory structure', () => {
295
+
const subfsUri = 'at://did:plc:test/place.wisp.subfs/a'
296
+
const dir = createFsRootDirectory([
297
+
createFsSubfs('a', subfsUri),
298
+
createFsFile('file.txt'),
299
+
])
300
+
301
+
const uris = extractSubfsUris(dir)
302
+
303
+
expect(uris).toHaveLength(1)
304
+
expect(uris[0]).toEqual({ uri: subfsUri, path: 'a' })
305
+
})
306
+
307
+
test('extracts subfs URIs from nested directory structure', () => {
308
+
const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a'
309
+
const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b'
310
+
311
+
const dir = createFsRootDirectory([
312
+
createFsSubfs('a', subfsAUri),
313
+
createFsDirectory('nested', [
314
+
createFsSubfs('b', subfsBUri),
315
+
createFsFile('file.txt'),
316
+
]),
317
+
])
318
+
319
+
const uris = extractSubfsUris(dir)
320
+
321
+
expect(uris).toHaveLength(2)
322
+
expect(uris).toContainEqual({ uri: subfsAUri, path: 'a' })
323
+
expect(uris).toContainEqual({ uri: subfsBUri, path: 'nested/b' })
324
+
})
325
+
326
+
test('returns empty array when no subfs nodes exist', () => {
327
+
const dir = createFsRootDirectory([
328
+
createFsFile('file1.txt'),
329
+
createFsDirectory('dir', [createFsFile('file2.txt')]),
330
+
])
331
+
332
+
const uris = extractSubfsUris(dir)
333
+
expect(uris).toHaveLength(0)
334
+
})
335
+
336
+
test('handles deeply nested subfs', () => {
337
+
const subfsUri = 'at://did:plc:test/place.wisp.subfs/deep'
338
+
const dir = createFsRootDirectory([
339
+
createFsDirectory('a', [
340
+
createFsDirectory('b', [
341
+
createFsDirectory('c', [
342
+
createFsSubfs('deep', subfsUri),
343
+
]),
344
+
]),
345
+
]),
346
+
])
347
+
348
+
const uris = extractSubfsUris(dir)
349
+
350
+
expect(uris).toHaveLength(1)
351
+
expect(uris[0]).toEqual({ uri: subfsUri, path: 'a/b/c/deep' })
352
+
})
353
+
})
354
+
355
+
describe('expandSubfsNodes caching', () => {
356
+
test('cache map is populated after expansion', async () => {
357
+
const subfsCache = new Map<string, SubfsRecord | null>()
358
+
const dir = createFsRootDirectory([createFsFile('file.txt')])
359
+
360
+
const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache)
361
+
362
+
expect(subfsCache.size).toBe(0)
363
+
expect(result.entries).toHaveLength(1)
364
+
expect(result.entries[0]?.name).toBe('file.txt')
365
+
})
366
+
367
+
test('cache is passed through recursion depths', async () => {
368
+
const subfsCache = new Map<string, SubfsRecord | null>()
369
+
const mockSubfsUri = 'at://did:plc:test/place.wisp.subfs/cached'
370
+
const mockRecord = createSubfsRecord([createSubfsFile('cached-file.txt')])
371
+
subfsCache.set(mockSubfsUri, mockRecord)
372
+
373
+
const dir = createFsRootDirectory([createFsSubfs('cached', mockSubfsUri)])
374
+
const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache)
375
+
376
+
expect(subfsCache.has(mockSubfsUri)).toBe(true)
377
+
expect(result.entries).toHaveLength(1)
378
+
expect(result.entries[0]?.name).toBe('cached-file.txt')
379
+
})
380
+
381
+
test('pre-populated cache prevents re-fetching', async () => {
382
+
const subfsCache = new Map<string, SubfsRecord | null>()
383
+
const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a'
384
+
const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b'
385
+
386
+
subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('b', subfsBUri)]))
387
+
subfsCache.set(subfsBUri, createSubfsRecord([createSubfsFile('final.txt')]))
388
+
389
+
const dir = createFsRootDirectory([createFsSubfs('a', subfsAUri)])
390
+
const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache)
391
+
392
+
expect(result.entries).toHaveLength(1)
393
+
expect(result.entries[0]?.name).toBe('final.txt')
394
+
})
395
+
396
+
test('diamond dependency uses cache for shared reference', async () => {
397
+
const subfsCache = new Map<string, SubfsRecord | null>()
398
+
const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a'
399
+
const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b'
400
+
const subfsCUri = 'at://did:plc:test/place.wisp.subfs/c'
401
+
402
+
subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)]))
403
+
subfsCache.set(subfsBUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)]))
404
+
subfsCache.set(subfsCUri, createSubfsRecord([createSubfsFile('shared.txt')]))
405
+
406
+
const dir = createFsRootDirectory([
407
+
createFsSubfs('a', subfsAUri),
408
+
createFsSubfs('b', subfsBUri),
409
+
])
410
+
const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache)
411
+
412
+
expect(result.entries.filter(e => e.name === 'shared.txt')).toHaveLength(2)
413
+
})
414
+
415
+
test('handles null records in cache gracefully', async () => {
416
+
const subfsCache = new Map<string, SubfsRecord | null>()
417
+
const subfsUri = 'at://did:plc:test/place.wisp.subfs/missing'
418
+
subfsCache.set(subfsUri, null)
419
+
420
+
const dir = createFsRootDirectory([
421
+
createFsFile('file.txt'),
422
+
createFsSubfs('missing', subfsUri),
423
+
])
424
+
const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache)
425
+
426
+
expect(result.entries.some(e => e.name === 'file.txt')).toBe(true)
427
+
expect(result.entries.some(e => e.name === 'missing')).toBe(true)
428
+
})
429
+
430
+
test('non-flat subfs merge creates directory instead of hoisting', async () => {
431
+
const subfsCache = new Map<string, SubfsRecord | null>()
432
+
const subfsUri = 'at://did:plc:test/place.wisp.subfs/nested'
433
+
subfsCache.set(subfsUri, createSubfsRecord([createSubfsFile('nested-file.txt')]))
434
+
435
+
const dir = createFsRootDirectory([
436
+
createFsFile('root.txt'),
437
+
createFsSubfs('subdir', subfsUri, false),
438
+
])
439
+
const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache)
440
+
441
+
expect(result.entries).toHaveLength(2)
442
+
443
+
const rootFile = result.entries.find(e => e.name === 'root.txt')
444
+
expect(rootFile).toBeDefined()
445
+
446
+
const subdir = result.entries.find(e => e.name === 'subdir')
447
+
expect(subdir).toBeDefined()
448
+
449
+
if (subdir && 'entries' in subdir.node) {
450
+
expect(subdir.node.type).toBe('directory')
451
+
expect(subdir.node.entries).toHaveLength(1)
452
+
expect(subdir.node.entries[0]?.name).toBe('nested-file.txt')
453
+
}
454
+
})
455
+
})
456
+
457
+
describe('WispFsRecord mock builders', () => {
458
+
test('createFsRecord creates valid record structure', () => {
459
+
const record = createFsRecord('my-site', [
460
+
createFsFile('index.html', { mimeType: 'text/html' }),
461
+
createFsDirectory('assets', [
462
+
createFsFile('style.css', { mimeType: 'text/css' }),
463
+
]),
464
+
])
465
+
466
+
expect(record.$type).toBe('place.wisp.fs')
467
+
expect(record.site).toBe('my-site')
468
+
expect(record.root.type).toBe('directory')
469
+
expect(record.root.entries).toHaveLength(2)
470
+
expect(record.createdAt).toBeDefined()
471
+
})
472
+
473
+
test('createFsFile creates valid file entry', () => {
474
+
const entry = createFsFile('test.html', { mimeType: 'text/html', size: 500 })
475
+
476
+
expect(entry.name).toBe('test.html')
477
+
478
+
const file = entry.node
479
+
if ('blob' in file) {
480
+
expect(file.$type).toBe('place.wisp.fs#file')
481
+
expect(file.type).toBe('file')
482
+
expect(file.blob).toBeDefined()
483
+
expect(file.mimeType).toBe('text/html')
484
+
}
485
+
})
486
+
487
+
test('createFsFile with gzip encoding', () => {
488
+
const entry = createFsFile('bundle.js', { mimeType: 'application/javascript', encoding: 'gzip' })
489
+
490
+
const file = entry.node
491
+
if ('encoding' in file) {
492
+
expect(file.encoding).toBe('gzip')
493
+
}
494
+
})
495
+
496
+
test('createFsFile with base64 flag', () => {
497
+
const entry = createFsFile('data.bin', { base64: true })
498
+
499
+
const file = entry.node
500
+
if ('base64' in file) {
501
+
expect(file.base64).toBe(true)
502
+
}
503
+
})
504
+
505
+
test('createFsDirectory creates valid directory entry', () => {
506
+
const entry = createFsDirectory('assets', [
507
+
createFsFile('file1.txt'),
508
+
createFsFile('file2.txt'),
509
+
])
510
+
511
+
expect(entry.name).toBe('assets')
512
+
513
+
const dir = entry.node
514
+
if ('entries' in dir) {
515
+
expect(dir.$type).toBe('place.wisp.fs#directory')
516
+
expect(dir.type).toBe('directory')
517
+
expect(dir.entries).toHaveLength(2)
518
+
}
519
+
})
520
+
521
+
test('createFsSubfs creates valid subfs entry with flat=true', () => {
522
+
const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext')
523
+
524
+
expect(entry.name).toBe('external')
525
+
526
+
const subfs = entry.node
527
+
if ('subject' in subfs) {
528
+
expect(subfs.$type).toBe('place.wisp.fs#subfs')
529
+
expect(subfs.type).toBe('subfs')
530
+
expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/ext')
531
+
expect(subfs.flat).toBe(true)
532
+
}
533
+
})
534
+
535
+
test('createFsSubfs creates valid subfs entry with flat=false', () => {
536
+
const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext', false)
537
+
538
+
const subfs = entry.node
539
+
if ('subject' in subfs) {
540
+
expect(subfs.flat).toBe(false)
541
+
}
542
+
})
543
+
544
+
test('createFsRecord with fileCount', () => {
545
+
const record = createFsRecord('my-site', [createFsFile('index.html')], 1)
546
+
expect(record.fileCount).toBe(1)
547
+
})
548
+
})
549
+
550
+
describe('SubfsRecord mock builders', () => {
551
+
test('createSubfsRecord creates valid record structure', () => {
552
+
const record = createSubfsRecord([
553
+
createSubfsFile('file1.txt'),
554
+
createSubfsDirectory('nested', [
555
+
createSubfsFile('file2.txt'),
556
+
]),
557
+
])
558
+
559
+
expect(record.$type).toBe('place.wisp.subfs')
560
+
expect(record.root.type).toBe('directory')
561
+
expect(record.root.entries).toHaveLength(2)
562
+
expect(record.createdAt).toBeDefined()
563
+
})
564
+
565
+
test('createSubfsFile creates valid file entry', () => {
566
+
const entry = createSubfsFile('data.json', { mimeType: 'application/json', size: 1024 })
567
+
568
+
expect(entry.name).toBe('data.json')
569
+
570
+
const file = entry.node
571
+
if ('blob' in file) {
572
+
expect(file.$type).toBe('place.wisp.subfs#file')
573
+
expect(file.type).toBe('file')
574
+
expect(file.blob).toBeDefined()
575
+
expect(file.mimeType).toBe('application/json')
576
+
}
577
+
})
578
+
579
+
test('createSubfsDirectory creates valid directory entry', () => {
580
+
const entry = createSubfsDirectory('subdir', [createSubfsFile('inner.txt')])
581
+
582
+
expect(entry.name).toBe('subdir')
583
+
584
+
const dir = entry.node
585
+
if ('entries' in dir) {
586
+
expect(dir.$type).toBe('place.wisp.subfs#directory')
587
+
expect(dir.type).toBe('directory')
588
+
expect(dir.entries).toHaveLength(1)
589
+
}
590
+
})
591
+
592
+
test('createSubfsSubfs creates valid nested subfs entry', () => {
593
+
const entry = createSubfsSubfs('deeper', 'at://did:plc:test/place.wisp.subfs/deeper')
594
+
595
+
expect(entry.name).toBe('deeper')
596
+
597
+
const subfs = entry.node
598
+
if ('subject' in subfs) {
599
+
expect(subfs.$type).toBe('place.wisp.subfs#subfs')
600
+
expect(subfs.type).toBe('subfs')
601
+
expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/deeper')
602
+
expect('flat' in subfs).toBe(false)
603
+
}
604
+
})
605
+
606
+
test('createSubfsRecord with fileCount', () => {
607
+
const record = createSubfsRecord([createSubfsFile('file.txt')], 1)
608
+
expect(record.fileCount).toBe(1)
609
+
})
610
+
})
611
+
612
+
describe('extractBlobCid with typed mock data', () => {
613
+
test('extracts CID from FsFile blob', () => {
614
+
const entry = createFsFile('test.txt')
615
+
const file = entry.node
616
+
617
+
if ('blob' in file) {
618
+
const cid = extractBlobCid(file.blob)
619
+
expect(cid).toBeDefined()
620
+
expect(cid).toContain('bafkrei')
621
+
}
622
+
})
623
+
624
+
test('extracts CID from SubfsFile blob', () => {
625
+
const entry = createSubfsFile('test.txt')
626
+
const file = entry.node
627
+
628
+
if ('blob' in file) {
629
+
const cid = extractBlobCid(file.blob)
630
+
expect(cid).toBeDefined()
631
+
expect(cid).toContain('bafkrei')
632
+
}
633
+
})
634
+
})
+177
-170
apps/hosting-service/src/lib/utils.ts
+177
-170
apps/hosting-service/src/lib/utils.ts
···
4
import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings';
5
import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
6
import { writeFile, readFile, rename } from 'fs/promises';
7
import { safeFetchJson, safeFetchBlob } from '@wisp/safe-fetch';
8
import { CID } from 'multiformats';
9
import { extractBlobCid } from '@wisp/atproto-utils';
10
-
import { sanitizePath, collectFileCidsFromEntries } from '@wisp/fs-utils';
11
import { shouldCompressMimeType } from '@wisp/atproto-utils/compression';
12
13
// Re-export shared utilities for local usage and tests
14
export { extractBlobCid, sanitizePath };
···
89
export async function fetchSiteRecord(did: string, rkey: string): Promise<{ record: WispFsRecord; cid: string } | null> {
90
try {
91
const pdsEndpoint = await getPdsForDid(did);
92
-
if (!pdsEndpoint) return null;
93
94
const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`;
95
const data = await safeFetchJson(url);
···
99
cid: data.cid || ''
100
};
101
} catch (err) {
102
-
console.error('Failed to fetch site record', did, rkey, err);
103
return null;
104
}
105
}
···
120
}
121
122
/**
123
* Extract all subfs URIs from a directory tree with their mount paths
124
*/
125
-
function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> {
126
const uris: Array<{ uri: string; path: string }> = [];
127
128
for (const entry of directory.entries) {
···
182
* Replace subfs nodes in a directory tree with their actual content
183
* Subfs entries are "merged" - their root entries are hoisted into the parent directory
184
* This function is recursive - it will keep expanding until no subfs nodes remain
185
*/
186
-
async function expandSubfsNodes(directory: Directory, pdsEndpoint: string, depth: number = 0): Promise<Directory> {
187
const MAX_DEPTH = 10; // Prevent infinite loops
188
189
if (depth >= MAX_DEPTH) {
···
199
return directory;
200
}
201
202
-
console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs records, fetching...`);
203
204
-
// Fetch all subfs records in parallel
205
-
const subfsRecords = await Promise.all(
206
-
subfsUris.map(async ({ uri, path }) => {
207
-
const record = await fetchSubfsRecord(uri, pdsEndpoint);
208
-
return { record, path };
209
-
})
210
-
);
211
212
-
// Build a map of path -> root entries to merge
213
// Note: SubFS entries are compatible with FS entries at runtime
214
const subfsMap = new Map<string, Entry[]>();
215
-
for (const { record, path } of subfsRecords) {
216
if (record && record.root && record.root.entries) {
217
subfsMap.set(path, record.root.entries as unknown as Entry[]);
218
}
···
280
};
281
282
// Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs)
283
-
return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1);
284
}
285
286
···
300
// Expand subfs nodes before caching
301
const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint);
302
303
// Get existing cache metadata to check for incremental updates
304
const existingMetadata = await getCacheMetadata(did, rkey);
305
const existingFileCids = existingMetadata?.fileCids || {};
306
307
-
// Use a temporary directory with timestamp to avoid collisions
308
-
const tempSuffix = `.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
309
-
const tempDir = `${CACHE_DIR}/${did}/${rkey}${tempSuffix}`;
310
-
const finalDir = `${CACHE_DIR}/${did}/${rkey}`;
311
312
-
try {
313
-
// Collect file CIDs from the new record (using expanded root)
314
-
const newFileCids: Record<string, string> = {};
315
-
collectFileCidsFromEntries(expandedRoot.entries, '', newFileCids);
316
317
-
// Fetch site settings (optional)
318
-
const settings = await fetchSiteSettings(did, rkey);
319
-
320
-
// Download/copy files to temporary directory (with incremental logic, using expanded root)
321
-
await cacheFiles(did, rkey, expandedRoot.entries, pdsEndpoint, '', tempSuffix, existingFileCids, finalDir);
322
-
await saveCacheMetadata(did, rkey, recordCid, tempSuffix, newFileCids, settings);
323
324
-
// Atomically replace old cache with new cache
325
-
// On POSIX systems (Linux/macOS), rename is atomic
326
-
if (existsSync(finalDir)) {
327
-
// Rename old directory to backup
328
-
const backupDir = `${finalDir}.old-${Date.now()}`;
329
-
await rename(finalDir, backupDir);
330
-
331
-
try {
332
-
// Rename new directory to final location
333
-
await rename(tempDir, finalDir);
334
-
335
-
// Clean up old backup
336
-
rmSync(backupDir, { recursive: true, force: true });
337
-
} catch (err) {
338
-
// If rename failed, restore backup
339
-
if (existsSync(backupDir) && !existsSync(finalDir)) {
340
-
await rename(backupDir, finalDir);
341
-
}
342
-
throw err;
343
-
}
344
-
} else {
345
-
// No existing cache, just rename temp to final
346
-
await rename(tempDir, finalDir);
347
-
}
348
-
349
-
console.log('Successfully cached site atomically', did, rkey);
350
-
} catch (err) {
351
-
// Clean up temp directory on failure
352
-
if (existsSync(tempDir)) {
353
-
rmSync(tempDir, { recursive: true, force: true });
354
-
}
355
-
throw err;
356
-
}
357
}
358
359
···
363
entries: Entry[],
364
pdsEndpoint: string,
365
pathPrefix: string,
366
-
dirSuffix: string = '',
367
-
existingFileCids: Record<string, string> = {},
368
-
existingCacheDir?: string
369
): Promise<void> {
370
-
// Collect file tasks, separating unchanged files from new/changed files
371
const downloadTasks: Array<() => Promise<void>> = [];
372
-
const copyTasks: Array<() => Promise<void>> = [];
373
374
function collectFileTasks(
375
entries: Entry[],
···
386
const cid = extractBlobCid(fileNode.blob);
387
388
// Check if file is unchanged (same CID as existing cache)
389
-
if (cid && existingFileCids[currentPath] === cid && existingCacheDir) {
390
-
// File unchanged - copy from existing cache instead of downloading
391
-
copyTasks.push(() => copyExistingFile(
392
-
did,
393
-
site,
394
-
currentPath,
395
-
dirSuffix,
396
-
existingCacheDir
397
-
));
398
} else {
399
// File new or changed - download it
400
downloadTasks.push(() => cacheFileBlob(
···
405
pdsEndpoint,
406
fileNode.encoding,
407
fileNode.mimeType,
408
-
fileNode.base64,
409
-
dirSuffix
410
));
411
}
412
}
···
415
416
collectFileTasks(entries, pathPrefix);
417
418
-
console.log(`[Incremental Update] Files to copy: ${copyTasks.length}, Files to download: ${downloadTasks.length}`);
419
-
420
-
// Copy unchanged files in parallel (fast local operations) - increased limit for better performance
421
-
const copyLimit = 50;
422
-
for (let i = 0; i < copyTasks.length; i += copyLimit) {
423
-
const batch = copyTasks.slice(i, i + copyLimit);
424
-
await Promise.all(batch.map(task => task()));
425
-
if (copyTasks.length > copyLimit) {
426
-
console.log(`[Cache Progress] Copied ${Math.min(i + copyLimit, copyTasks.length)}/${copyTasks.length} unchanged files`);
427
-
}
428
-
}
429
430
-
// Download new/changed files concurrently - increased from 3 to 20 for much better performance
431
const downloadLimit = 20;
432
let successCount = 0;
433
let failureCount = 0;
···
456
}
457
}
458
459
-
/**
460
-
* Copy an unchanged file from existing cache to new cache location
461
-
*/
462
-
async function copyExistingFile(
463
-
did: string,
464
-
site: string,
465
-
filePath: string,
466
-
dirSuffix: string,
467
-
existingCacheDir: string
468
-
): Promise<void> {
469
-
const { copyFile } = await import('fs/promises');
470
-
471
-
const sourceFile = `${existingCacheDir}/${filePath}`;
472
-
const destFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`;
473
-
const destDir = destFile.substring(0, destFile.lastIndexOf('/'));
474
-
475
-
// Create destination directory if needed
476
-
if (destDir && !existsSync(destDir)) {
477
-
mkdirSync(destDir, { recursive: true });
478
-
}
479
-
480
-
try {
481
-
// Copy the file
482
-
await copyFile(sourceFile, destFile);
483
-
484
-
// Copy metadata file if it exists
485
-
const sourceMetaFile = `${sourceFile}.meta`;
486
-
const destMetaFile = `${destFile}.meta`;
487
-
if (existsSync(sourceMetaFile)) {
488
-
await copyFile(sourceMetaFile, destMetaFile);
489
-
}
490
-
} catch (err) {
491
-
console.error(`Failed to copy cached file ${filePath}, will attempt download:`, err);
492
-
throw err;
493
-
}
494
-
}
495
-
496
async function cacheFileBlob(
497
did: string,
498
site: string,
···
501
pdsEndpoint: string,
502
encoding?: 'gzip',
503
mimeType?: string,
504
-
base64?: boolean,
505
-
dirSuffix: string = ''
506
): Promise<void> {
507
const cid = extractBlobCid(blobRef);
508
if (!cid) {
···
514
515
console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`);
516
517
-
// Allow up to 500MB per file blob, with 5 minute timeout
518
-
let content = await safeFetchBlob(blobUrl, { maxSize: 500 * 1024 * 1024, timeout: 300000 });
519
520
// If content is base64-encoded, decode it back to raw binary (gzipped or not)
521
if (base64) {
···
526
content = Buffer.from(base64String, 'base64');
527
}
528
529
-
const cacheFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`;
530
-
const fileDir = cacheFile.substring(0, cacheFile.lastIndexOf('/'));
531
-
532
-
if (fileDir && !existsSync(fileDir)) {
533
-
mkdirSync(fileDir, { recursive: true });
534
-
}
535
-
536
// Use the shared function to determine if this should remain compressed
537
const shouldStayCompressed = shouldCompressMimeType(mimeType);
538
···
550
}
551
}
552
553
-
await writeFile(cacheFile, content);
554
555
-
// Store metadata only if file is still compressed
556
if (encoding === 'gzip' && mimeType) {
557
-
const metaFile = `${cacheFile}.meta`;
558
-
await writeFile(metaFile, JSON.stringify({ encoding, mimeType }));
559
console.log('Cached file', filePath, content.length, 'bytes (gzipped,', mimeType + ')');
560
} else {
561
console.log('Cached file', filePath, content.length, 'bytes');
···
568
return `${CACHE_DIR}/${did}/${site}/${sanitizedPath}`;
569
}
570
571
-
export function isCached(did: string, site: string): boolean {
572
-
return existsSync(`${CACHE_DIR}/${did}/${site}`);
573
}
574
575
-
async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = '', fileCids?: Record<string, string>, settings?: WispSettings | null): Promise<void> {
576
const metadata: CacheMetadata = {
577
recordCid,
578
cachedAt: Date.now(),
···
582
settings: settings || undefined
583
};
584
585
-
const metadataPath = `${CACHE_DIR}/${did}/${rkey}${dirSuffix}/.metadata.json`;
586
-
const metadataDir = metadataPath.substring(0, metadataPath.lastIndexOf('/'));
587
-
588
-
if (!existsSync(metadataDir)) {
589
-
mkdirSync(metadataDir, { recursive: true });
590
-
}
591
-
592
-
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
593
}
594
595
async function getCacheMetadata(did: string, rkey: string): Promise<CacheMetadata | null> {
596
try {
597
-
const metadataPath = `${CACHE_DIR}/${did}/${rkey}/.metadata.json`;
598
-
if (!existsSync(metadataPath)) return null;
599
600
-
const content = await readFile(metadataPath, 'utf-8');
601
-
return JSON.parse(content) as CacheMetadata;
602
} catch (err) {
603
console.error('Failed to read cache metadata', err);
604
return null;
···
632
}
633
634
export async function updateCacheMetadataSettings(did: string, rkey: string, settings: WispSettings | null): Promise<void> {
635
-
const metadataPath = `${CACHE_DIR}/${did}/${rkey}/.metadata.json`;
636
637
-
if (!existsSync(metadataPath)) {
638
-
console.warn('Metadata file does not exist, cannot update settings', { did, rkey });
639
-
return;
640
-
}
641
-
642
-
try {
643
-
// Read existing metadata
644
-
const content = await readFile(metadataPath, 'utf-8');
645
-
const metadata = JSON.parse(content) as CacheMetadata;
646
647
// Update settings field
648
// Store null explicitly to cache "no settings" state and avoid repeated fetches
649
metadata.settings = settings ?? null;
650
651
-
// Write back to disk
652
-
await writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
653
console.log('Updated metadata settings', { did, rkey, hasSettings: !!settings });
654
} catch (err) {
655
console.error('Failed to update metadata settings', err);
···
4
import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings';
5
import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
6
import { writeFile, readFile, rename } from 'fs/promises';
7
+
import { Readable } from 'stream';
8
import { safeFetchJson, safeFetchBlob } from '@wisp/safe-fetch';
9
import { CID } from 'multiformats';
10
import { extractBlobCid } from '@wisp/atproto-utils';
11
+
import { sanitizePath, collectFileCidsFromEntries, countFilesInDirectory } from '@wisp/fs-utils';
12
import { shouldCompressMimeType } from '@wisp/atproto-utils/compression';
13
+
import { MAX_BLOB_SIZE, MAX_FILE_COUNT, MAX_SITE_SIZE } from '@wisp/constants';
14
+
import { storage } from './storage';
15
16
// Re-export shared utilities for local usage and tests
17
export { extractBlobCid, sanitizePath };
···
92
export async function fetchSiteRecord(did: string, rkey: string): Promise<{ record: WispFsRecord; cid: string } | null> {
93
try {
94
const pdsEndpoint = await getPdsForDid(did);
95
+
if (!pdsEndpoint) {
96
+
console.error('[hosting-service] Failed to get PDS endpoint for DID', { did, rkey });
97
+
return null;
98
+
}
99
100
const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`;
101
const data = await safeFetchJson(url);
···
105
cid: data.cid || ''
106
};
107
} catch (err) {
108
+
const errorCode = (err as any)?.code;
109
+
const errorMsg = err instanceof Error ? err.message : String(err);
110
+
111
+
// Better error logging to distinguish between network errors and 404s
112
+
if (errorMsg.includes('HTTP 404') || errorMsg.includes('Not Found')) {
113
+
console.log('[hosting-service] Site record not found', { did, rkey });
114
+
} else if (errorCode && ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', 'ETIMEDOUT'].includes(errorCode)) {
115
+
console.error('[hosting-service] Network/SSL error fetching site record (after retries)', {
116
+
did,
117
+
rkey,
118
+
error: errorMsg,
119
+
code: errorCode
120
+
});
121
+
} else {
122
+
console.error('[hosting-service] Failed to fetch site record', {
123
+
did,
124
+
rkey,
125
+
error: errorMsg,
126
+
code: errorCode
127
+
});
128
+
}
129
+
130
return null;
131
}
132
}
···
147
}
148
149
/**
150
+
* Calculate total size of all blobs in a directory tree from manifest metadata
151
+
*/
152
+
function calculateTotalBlobSize(directory: Directory): number {
153
+
let totalSize = 0;
154
+
155
+
function sumBlobSizes(entries: Entry[]) {
156
+
for (const entry of entries) {
157
+
const node = entry.node;
158
+
159
+
if ('type' in node && node.type === 'directory' && 'entries' in node) {
160
+
// Recursively sum subdirectories
161
+
sumBlobSizes(node.entries);
162
+
} else if ('type' in node && node.type === 'file' && 'blob' in node) {
163
+
// Add blob size from manifest
164
+
const fileNode = node as File;
165
+
const blobSize = (fileNode.blob as any)?.size || 0;
166
+
totalSize += blobSize;
167
+
}
168
+
}
169
+
}
170
+
171
+
sumBlobSizes(directory.entries);
172
+
return totalSize;
173
+
}
174
+
175
+
/**
176
* Extract all subfs URIs from a directory tree with their mount paths
177
*/
178
+
export function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> {
179
const uris: Array<{ uri: string; path: string }> = [];
180
181
for (const entry of directory.entries) {
···
235
* Replace subfs nodes in a directory tree with their actual content
236
* Subfs entries are "merged" - their root entries are hoisted into the parent directory
237
* This function is recursive - it will keep expanding until no subfs nodes remain
238
+
* Uses a cache to avoid re-fetching the same subfs records across recursion depths
239
*/
240
+
export async function expandSubfsNodes(
241
+
directory: Directory,
242
+
pdsEndpoint: string,
243
+
depth: number = 0,
244
+
subfsCache: Map<string, SubfsRecord | null> = new Map()
245
+
): Promise<Directory> {
246
const MAX_DEPTH = 10; // Prevent infinite loops
247
248
if (depth >= MAX_DEPTH) {
···
258
return directory;
259
}
260
261
+
// Filter to only URIs we haven't fetched yet
262
+
const uncachedUris = subfsUris.filter(({ uri }) => !subfsCache.has(uri));
263
264
+
if (uncachedUris.length > 0) {
265
+
console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, fetching ${uncachedUris.length} new records (${subfsUris.length - uncachedUris.length} cached)...`);
266
267
+
// Fetch only uncached subfs records in parallel
268
+
const fetchedRecords = await Promise.all(
269
+
uncachedUris.map(async ({ uri }) => {
270
+
const record = await fetchSubfsRecord(uri, pdsEndpoint);
271
+
return { uri, record };
272
+
})
273
+
);
274
+
275
+
// Add fetched records to cache
276
+
for (const { uri, record } of fetchedRecords) {
277
+
subfsCache.set(uri, record);
278
+
}
279
+
} else {
280
+
console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, all cached`);
281
+
}
282
+
283
+
// Build a map of path -> root entries to merge using the cache
284
// Note: SubFS entries are compatible with FS entries at runtime
285
const subfsMap = new Map<string, Entry[]>();
286
+
for (const { uri, path } of subfsUris) {
287
+
const record = subfsCache.get(uri);
288
if (record && record.root && record.root.entries) {
289
subfsMap.set(path, record.root.entries as unknown as Entry[]);
290
}
···
352
};
353
354
// Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs)
355
+
// Pass the cache to avoid re-fetching records
356
+
return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1, subfsCache);
357
}
358
359
···
373
// Expand subfs nodes before caching
374
const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint);
375
376
+
// Verify all subfs nodes were expanded
377
+
const remainingSubfs = extractSubfsUris(expandedRoot);
378
+
if (remainingSubfs.length > 0) {
379
+
console.warn(`[Cache] Warning: ${remainingSubfs.length} subfs nodes remain unexpanded after expansion`, remainingSubfs);
380
+
}
381
+
382
+
// Validate file count limit
383
+
const fileCount = countFilesInDirectory(expandedRoot);
384
+
if (fileCount > MAX_FILE_COUNT) {
385
+
throw new Error(`Site exceeds file count limit: ${fileCount} files (max ${MAX_FILE_COUNT})`);
386
+
}
387
+
console.log(`[Cache] File count validation passed: ${fileCount} files (limit: ${MAX_FILE_COUNT})`);
388
+
389
+
// Validate total size from blob metadata
390
+
const totalBlobSize = calculateTotalBlobSize(expandedRoot);
391
+
if (totalBlobSize > MAX_SITE_SIZE) {
392
+
throw new Error(`Site exceeds size limit: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (max ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`);
393
+
}
394
+
console.log(`[Cache] Size validation passed: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (limit: ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`);
395
+
396
// Get existing cache metadata to check for incremental updates
397
const existingMetadata = await getCacheMetadata(did, rkey);
398
const existingFileCids = existingMetadata?.fileCids || {};
399
400
+
// Collect file CIDs from the new record (using expanded root)
401
+
const newFileCids: Record<string, string> = {};
402
+
collectFileCidsFromEntries(expandedRoot.entries, '', newFileCids);
403
404
+
// Fetch site settings (optional)
405
+
const settings = await fetchSiteSettings(did, rkey);
406
407
+
// Download files directly to tiered storage (with incremental logic)
408
+
await cacheFiles(did, rkey, expandedRoot.entries, pdsEndpoint, '', existingFileCids);
409
+
await saveCacheMetadata(did, rkey, recordCid, newFileCids, settings);
410
411
+
console.log('Successfully cached site', did, rkey);
412
}
413
414
···
418
entries: Entry[],
419
pdsEndpoint: string,
420
pathPrefix: string,
421
+
existingFileCids: Record<string, string> = {}
422
): Promise<void> {
423
+
// Collect file download tasks (skip unchanged files)
424
const downloadTasks: Array<() => Promise<void>> = [];
425
426
function collectFileTasks(
427
entries: Entry[],
···
438
const cid = extractBlobCid(fileNode.blob);
439
440
// Check if file is unchanged (same CID as existing cache)
441
+
if (cid && existingFileCids[currentPath] === cid) {
442
+
// File unchanged - skip download (already in tiered storage)
443
+
console.log(`Skipping unchanged file: ${currentPath}`);
444
} else {
445
// File new or changed - download it
446
downloadTasks.push(() => cacheFileBlob(
···
451
pdsEndpoint,
452
fileNode.encoding,
453
fileNode.mimeType,
454
+
fileNode.base64
455
));
456
}
457
}
···
460
461
collectFileTasks(entries, pathPrefix);
462
463
+
console.log(`[Incremental Update] Files to download: ${downloadTasks.length}`);
464
465
+
// Download new/changed files concurrently
466
const downloadLimit = 20;
467
let successCount = 0;
468
let failureCount = 0;
···
491
}
492
}
493
494
async function cacheFileBlob(
495
did: string,
496
site: string,
···
499
pdsEndpoint: string,
500
encoding?: 'gzip',
501
mimeType?: string,
502
+
base64?: boolean
503
): Promise<void> {
504
const cid = extractBlobCid(blobRef);
505
if (!cid) {
···
511
512
console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`);
513
514
+
let content = await safeFetchBlob(blobUrl, { maxSize: MAX_BLOB_SIZE, timeout: 300000 });
515
516
// If content is base64-encoded, decode it back to raw binary (gzipped or not)
517
if (base64) {
···
522
content = Buffer.from(base64String, 'base64');
523
}
524
525
// Use the shared function to determine if this should remain compressed
526
const shouldStayCompressed = shouldCompressMimeType(mimeType);
527
···
539
}
540
}
541
542
+
// Write to tiered storage with metadata
543
+
const stream = Readable.from([content]);
544
+
const key = `${did}/${site}/${filePath}`;
545
546
+
// Build metadata object, only including defined values
547
+
const customMetadata: Record<string, string> = {};
548
+
if (encoding) customMetadata.encoding = encoding;
549
+
if (mimeType) customMetadata.mimeType = mimeType;
550
+
551
+
await storage.setStream(key, stream, {
552
+
size: content.length,
553
+
skipTiers: ['hot'], // Don't put in memory on ingest, only on access
554
+
metadata: customMetadata,
555
+
});
556
+
557
+
// Log completion
558
if (encoding === 'gzip' && mimeType) {
559
console.log('Cached file', filePath, content.length, 'bytes (gzipped,', mimeType + ')');
560
} else {
561
console.log('Cached file', filePath, content.length, 'bytes');
···
568
return `${CACHE_DIR}/${did}/${site}/${sanitizedPath}`;
569
}
570
571
+
/**
572
+
* Check if a site exists in any tier of the cache (without checking metadata)
573
+
* This is a quick existence check - for actual retrieval, use storage.get()
574
+
*/
575
+
export async function isCached(did: string, site: string): Promise<boolean> {
576
+
// Check if any file exists for this site by checking for the index.html
577
+
// If index.html exists, the site is cached
578
+
const indexKey = `${did}/${site}/index.html`;
579
+
return await storage.exists(indexKey);
580
}
581
582
+
async function saveCacheMetadata(did: string, rkey: string, recordCid: string, fileCids?: Record<string, string>, settings?: WispSettings | null): Promise<void> {
583
const metadata: CacheMetadata = {
584
recordCid,
585
cachedAt: Date.now(),
···
589
settings: settings || undefined
590
};
591
592
+
// Store through tiered storage for persistence to S3/cold tier
593
+
const metadataKey = `${did}/${rkey}/.metadata.json`;
594
+
const metadataBytes = new TextEncoder().encode(JSON.stringify(metadata, null, 2));
595
+
await storage.set(metadataKey, metadataBytes);
596
}
597
598
async function getCacheMetadata(did: string, rkey: string): Promise<CacheMetadata | null> {
599
try {
600
+
// Retrieve metadata from tiered storage
601
+
const metadataKey = `${did}/${rkey}/.metadata.json`;
602
+
const data = await storage.get(metadataKey);
603
+
604
+
if (!data) return null;
605
606
+
// Deserialize from Uint8Array to JSON (storage uses identity serialization)
607
+
const jsonString = new TextDecoder().decode(data as Uint8Array);
608
+
return JSON.parse(jsonString) as CacheMetadata;
609
} catch (err) {
610
console.error('Failed to read cache metadata', err);
611
return null;
···
639
}
640
641
export async function updateCacheMetadataSettings(did: string, rkey: string, settings: WispSettings | null): Promise<void> {
642
+
try {
643
+
// Read existing metadata from tiered storage
644
+
const metadata = await getCacheMetadata(did, rkey);
645
646
+
if (!metadata) {
647
+
console.warn('Metadata does not exist, cannot update settings', { did, rkey });
648
+
return;
649
+
}
650
651
// Update settings field
652
// Store null explicitly to cache "no settings" state and avoid repeated fetches
653
metadata.settings = settings ?? null;
654
655
+
// Write back through tiered storage
656
+
// Convert to Uint8Array since storage is typed for binary data
657
+
const metadataKey = `${did}/${rkey}/.metadata.json`;
658
+
const metadataBytes = new TextEncoder().encode(JSON.stringify(metadata, null, 2));
659
+
await storage.set(metadataKey, metadataBytes);
660
console.log('Updated metadata settings', { did, rkey, hasSettings: !!settings });
661
} catch (err) {
662
console.error('Failed to update metadata settings', err);
+4
-1
apps/hosting-service/src/server.ts
+4
-1
apps/hosting-service/src/server.ts
···
80
return c.text('Invalid identifier', 400);
81
}
82
83
// Check if site is currently being cached - return updating response early
84
if (isSiteBeingCached(did, site)) {
85
return siteUpdatingResponse();
···
93
94
// Serve with HTML path rewriting to handle absolute paths
95
const basePath = `/${identifier}/${site}/`;
96
const headers = extractHeaders(c.req.raw.headers);
97
return serveFromCacheWithRewrite(did, site, filePath, basePath, c.req.url, headers);
98
}
···
227
228
app.get('/__internal__/observability/cache', async (c) => {
229
const { getCacheStats } = await import('./lib/cache');
230
-
const stats = getCacheStats();
231
return c.json({ cache: stats });
232
});
233
···
80
return c.text('Invalid identifier', 400);
81
}
82
83
+
console.log(`[Server] sites.wisp.place request: identifier=${identifier}, site=${site}, filePath=${filePath}`);
84
+
85
// Check if site is currently being cached - return updating response early
86
if (isSiteBeingCached(did, site)) {
87
return siteUpdatingResponse();
···
95
96
// Serve with HTML path rewriting to handle absolute paths
97
const basePath = `/${identifier}/${site}/`;
98
+
console.log(`[Server] Serving with basePath: ${basePath}`);
99
const headers = extractHeaders(c.req.raw.headers);
100
return serveFromCacheWithRewrite(did, site, filePath, basePath, c.req.url, headers);
101
}
···
230
231
app.get('/__internal__/observability/cache', async (c) => {
232
const { getCacheStats } = await import('./lib/cache');
233
+
const stats = await getCacheStats();
234
return c.json({ cache: stats });
235
});
236
+11
-7
apps/main-app/package.json
+11
-7
apps/main-app/package.json
···
7
"dev": "bun run --watch src/index.ts",
8
"start": "bun run src/index.ts",
9
"build": "bun run build.ts",
10
"screenshot": "bun run scripts/screenshot-sites.ts"
11
},
12
"dependencies": {
13
-
"@atproto/api": "^0.17.3",
14
-
"@atproto/common-web": "^0.4.5",
15
"@atproto/jwk-jose": "^0.1.11",
16
-
"@atproto/lex-cli": "^0.9.5",
17
-
"@atproto/oauth-client-node": "^0.3.9",
18
-
"@atproto/xrpc-server": "^0.9.5",
19
"@elysiajs/cors": "^1.4.0",
20
"@elysiajs/eden": "^1.4.3",
21
"@elysiajs/openapi": "^1.4.11",
···
35
"@wisp/lexicons": "workspace:*",
36
"@wisp/observability": "workspace:*",
37
"actor-typeahead": "^0.1.1",
38
-
"atproto-ui": "^0.11.3",
39
"bun-plugin-tailwind": "^0.1.2",
40
"class-variance-authority": "^0.7.1",
41
"clsx": "^2.1.1",
42
-
"elysia": "latest",
43
"ignore": "^7.0.5",
44
"iron-session": "^8.0.4",
45
"lucide-react": "^0.546.0",
···
53
"zlib": "^1.0.5"
54
},
55
"devDependencies": {
56
"@types/react": "^19.2.2",
57
"@types/react-dom": "^19.2.1",
58
"bun-types": "latest",
···
7
"dev": "bun run --watch src/index.ts",
8
"start": "bun run src/index.ts",
9
"build": "bun run build.ts",
10
+
"check": "tsc --noEmit",
11
"screenshot": "bun run scripts/screenshot-sites.ts"
12
},
13
"dependencies": {
14
+
"@atproto-labs/did-resolver": "^0.2.4",
15
+
"@atproto/api": "^0.17.7",
16
+
"@atproto/common-web": "^0.4.6",
17
"@atproto/jwk-jose": "^0.1.11",
18
+
"@atproto/lex-cli": "^0.9.7",
19
+
"@atproto/oauth-client-node": "^0.3.12",
20
+
"@atproto/xrpc-server": "^0.9.6",
21
"@elysiajs/cors": "^1.4.0",
22
"@elysiajs/eden": "^1.4.3",
23
"@elysiajs/openapi": "^1.4.11",
···
37
"@wisp/lexicons": "workspace:*",
38
"@wisp/observability": "workspace:*",
39
"actor-typeahead": "^0.1.1",
40
+
"atproto-ui": "^0.12.0",
41
"bun-plugin-tailwind": "^0.1.2",
42
"class-variance-authority": "^0.7.1",
43
"clsx": "^2.1.1",
44
+
"elysia": "^1.4.18",
45
"ignore": "^7.0.5",
46
"iron-session": "^8.0.4",
47
"lucide-react": "^0.546.0",
···
55
"zlib": "^1.0.5"
56
},
57
"devDependencies": {
58
+
"@atproto-labs/handle-resolver": "^0.3.4",
59
+
"@atproto/did": "^0.2.3",
60
"@types/react": "^19.2.2",
61
"@types/react-dom": "^19.2.1",
62
"bun-types": "latest",
+4
-4
apps/main-app/public/acceptable-use/acceptable-use.tsx
+4
-4
apps/main-app/public/acceptable-use/acceptable-use.tsx
···
6
7
function AcceptableUsePage() {
8
return (
9
-
<div className="min-h-screen bg-background">
10
{/* Header */}
11
-
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
12
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
13
<div className="flex items-center gap-2">
14
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
15
<span className="text-xl font-semibold text-foreground">
···
326
</div>
327
328
{/* Footer */}
329
-
<footer className="border-t border-border/40 bg-muted/20 mt-12">
330
<div className="container mx-auto px-4 py-8">
331
<div className="text-center text-sm text-muted-foreground">
332
<p>
···
6
7
function AcceptableUsePage() {
8
return (
9
+
<div className="w-full min-h-screen bg-background flex flex-col">
10
{/* Header */}
11
+
<header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
12
+
<div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between">
13
<div className="flex items-center gap-2">
14
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
15
<span className="text-xl font-semibold text-foreground">
···
326
</div>
327
328
{/* Footer */}
329
+
<footer className="border-t border-border/40 bg-muted/20 mt-auto">
330
<div className="container mx-auto px-4 py-8">
331
<div className="text-center text-sm text-muted-foreground">
332
<p>
+1
-1
apps/main-app/public/components/ui/checkbox.tsx
+1
-1
apps/main-app/public/components/ui/checkbox.tsx
···
12
<CheckboxPrimitive.Root
13
data-slot="checkbox"
14
className={cn(
15
-
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
className
17
)}
18
{...props}
···
12
<CheckboxPrimitive.Root
13
data-slot="checkbox"
14
className={cn(
15
+
"peer border-border bg-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
className
17
)}
18
{...props}
+6
-6
apps/main-app/public/editor/editor.tsx
+6
-6
apps/main-app/public/editor/editor.tsx
···
302
return (
303
<div className="w-full min-h-screen bg-background">
304
{/* Header Skeleton */}
305
-
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
306
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
307
<div className="flex items-center gap-2">
308
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
309
<span className="text-xl font-semibold text-foreground">
···
366
}
367
368
return (
369
-
<div className="w-full min-h-screen bg-background">
370
{/* Header */}
371
-
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
372
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
373
<div className="flex items-center gap-2">
374
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
375
<span className="text-xl font-semibold text-foreground">
···
454
</div>
455
456
{/* Footer */}
457
-
<footer className="border-t border-border/40 bg-muted/20 mt-12">
458
<div className="container mx-auto px-4 py-8">
459
<div className="text-center text-sm text-muted-foreground">
460
<p>
···
302
return (
303
<div className="w-full min-h-screen bg-background">
304
{/* Header Skeleton */}
305
+
<header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
306
+
<div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between">
307
<div className="flex items-center gap-2">
308
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
309
<span className="text-xl font-semibold text-foreground">
···
366
}
367
368
return (
369
+
<div className="w-full min-h-screen bg-background flex flex-col">
370
{/* Header */}
371
+
<header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
372
+
<div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between">
373
<div className="flex items-center gap-2">
374
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
375
<span className="text-xl font-semibold text-foreground">
···
454
</div>
455
456
{/* Footer */}
457
+
<footer className="border-t border-border/40 bg-muted/20 mt-auto">
458
<div className="container mx-auto px-4 py-8">
459
<div className="text-center text-sm text-muted-foreground">
460
<p>
+74
-66
apps/main-app/public/index.tsx
+74
-66
apps/main-app/public/index.tsx
···
88
89
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
90
const navigationKeys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Enter', 'Escape']
91
-
92
// Mark that we should preserve the index for navigation keys
93
if (navigationKeys.includes(e.key)) {
94
preserveIndexRef.current = true
···
142
setIndex(-1)
143
setIsOpen(false)
144
onSelect?.(handle)
145
-
146
// Auto-submit the form if enabled
147
if (autoSubmit && inputRef.current) {
148
const form = inputRef.current.closest('form')
···
236
height: 'calc(1.5rem + 12px)',
237
borderRadius: '4px',
238
cursor: 'pointer',
239
-
backgroundColor: i === index ? 'hsl(var(--accent) / 0.5)' : 'transparent',
240
transition: 'background-color 0.1s'
241
}}
242
onMouseEnter={() => setIndex(i)}
···
246
width: '1.5rem',
247
height: '1.5rem',
248
borderRadius: '50%',
249
-
backgroundColor: 'hsl(var(--muted))',
250
overflow: 'hidden',
251
flexShrink: 0
252
}}
···
255
<img
256
src={actor.avatar}
257
alt=""
258
style={{
259
display: 'block',
260
width: '100%',
···
359
360
return (
361
<>
362
-
<div className="min-h-screen">
363
{/* Header */}
364
-
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
365
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
366
<div className="flex items-center gap-2">
367
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
368
-
<span className="text-xl font-semibold text-foreground">
369
wisp.place
370
</span>
371
</div>
372
-
<div className="flex items-center gap-3">
373
<Button
374
-
variant="ghost"
375
size="sm"
376
onClick={() => setShowForm(true)}
377
>
378
Sign In
379
</Button>
380
-
<Button
381
-
size="sm"
382
-
className="bg-accent text-accent-foreground hover:bg-accent/90"
383
-
asChild
384
-
>
385
-
<a href="https://docs.wisp.place" target="_blank" rel="noopener noreferrer">
386
-
Read the Docs
387
-
</a>
388
-
</Button>
389
</div>
390
</div>
391
</header>
392
393
{/* Hero Section */}
394
-
<section className="container mx-auto px-4 py-20 md:py-32">
395
<div className="max-w-4xl mx-auto text-center">
396
-
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8">
397
-
<span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span>
398
-
<span className="text-sm text-foreground">
399
-
Built on AT Protocol
400
-
</span>
401
-
</div>
402
-
403
-
<h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight">
404
-
Your Website.Your Control. Lightning Fast.
405
</h1>
406
407
-
<p className="text-xl md:text-2xl text-muted-foreground text-balance mb-10 leading-relaxed max-w-3xl mx-auto">
408
-
Host static sites in your AT Protocol account. You
409
-
keep ownership and control. We just serve them fast
410
-
through our CDN.
411
</p>
412
413
-
<div className="max-w-md mx-auto relative">
414
<div
415
-
className={`transition-all duration-500 ease-in-out ${
416
-
showForm
417
-
? 'opacity-0 -translate-y-5 pointer-events-none'
418
-
: 'opacity-100 translate-y-0'
419
-
}`}
420
>
421
-
<Button
422
-
size="lg"
423
-
className="bg-primary text-primary-foreground hover:bg-primary/90 text-lg px-8 py-6 w-full"
424
-
onClick={() => setShowForm(true)}
425
-
>
426
-
Log in with AT Proto
427
-
<ArrowRight className="ml-2 w-5 h-5" />
428
-
</Button>
429
</div>
430
431
<div
432
-
className={`transition-all duration-500 ease-in-out absolute inset-0 ${
433
-
showForm
434
-
? 'opacity-100 translate-y-0'
435
-
: 'opacity-0 translate-y-5 pointer-events-none'
436
-
}`}
437
>
438
<form
439
onSubmit={async (e) => {
···
494
</ActorTypeahead>
495
<button
496
type="submit"
497
-
className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors"
498
>
499
Continue
500
<ArrowRight className="ml-2 w-5 h-5" />
···
518
</div>
519
<div>
520
<h3 className="text-xl font-semibold mb-2">
521
-
Upload your static site
522
</h3>
523
<p className="text-muted-foreground">
524
-
Your HTML, CSS, and JavaScript files are
525
-
stored in your AT Protocol account as
526
-
gzipped blobs and a manifest record.
527
</p>
528
</div>
529
</div>
···
533
</div>
534
<div>
535
<h3 className="text-xl font-semibold mb-2">
536
-
We serve it globally
537
</h3>
538
<p className="text-muted-foreground">
539
-
Wisp.place reads your site from your
540
-
account and delivers it through our CDN
541
-
for fast loading anywhere.
542
</p>
543
</div>
544
</div>
···
548
</div>
549
<div>
550
<h3 className="text-xl font-semibold mb-2">
551
-
You stay in control
552
</h3>
553
<p className="text-muted-foreground">
554
-
Update or remove your site anytime
555
-
through your AT Protocol account. No
556
-
lock-in, no middleman ownership.
557
</p>
558
</div>
559
</div>
···
686
</section>
687
688
{/* Footer */}
689
-
<footer className="border-t border-border/40 bg-muted/20">
690
<div className="container mx-auto px-4 py-8">
691
<div className="text-center text-sm text-muted-foreground">
692
<p>
···
88
89
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
90
const navigationKeys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Enter', 'Escape']
91
+
92
// Mark that we should preserve the index for navigation keys
93
if (navigationKeys.includes(e.key)) {
94
preserveIndexRef.current = true
···
142
setIndex(-1)
143
setIsOpen(false)
144
onSelect?.(handle)
145
+
146
// Auto-submit the form if enabled
147
if (autoSubmit && inputRef.current) {
148
const form = inputRef.current.closest('form')
···
236
height: 'calc(1.5rem + 12px)',
237
borderRadius: '4px',
238
cursor: 'pointer',
239
+
backgroundColor: i === index ? 'color-mix(in oklch, var(--accent) 50%, transparent)' : 'transparent',
240
transition: 'background-color 0.1s'
241
}}
242
onMouseEnter={() => setIndex(i)}
···
246
width: '1.5rem',
247
height: '1.5rem',
248
borderRadius: '50%',
249
+
backgroundColor: 'var(--muted)',
250
overflow: 'hidden',
251
flexShrink: 0
252
}}
···
255
<img
256
src={actor.avatar}
257
alt=""
258
+
loading="lazy"
259
style={{
260
display: 'block',
261
width: '100%',
···
360
361
return (
362
<>
363
+
<div className="w-full min-h-screen flex flex-col">
364
{/* Header */}
365
+
<header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
366
+
<div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between">
367
<div className="flex items-center gap-2">
368
<img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" />
369
+
<span className="text-lg font-semibold text-foreground">
370
wisp.place
371
</span>
372
</div>
373
+
<div className="flex items-center gap-4">
374
+
<a
375
+
href="https://docs.wisp.place"
376
+
target="_blank"
377
+
rel="noopener noreferrer"
378
+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
379
+
>
380
+
Read the Docs
381
+
</a>
382
<Button
383
+
variant="outline"
384
size="sm"
385
+
className="btn-hover-lift"
386
onClick={() => setShowForm(true)}
387
>
388
Sign In
389
</Button>
390
</div>
391
</div>
392
</header>
393
394
{/* Hero Section */}
395
+
<section className="container mx-auto px-4 py-24 md:py-36">
396
<div className="max-w-4xl mx-auto text-center">
397
+
{/* Main Headline */}
398
+
<h1 className="animate-fade-in-up animate-delay-100 text-5xl md:text-7xl font-bold mb-2 leading-tight tracking-tight">
399
+
Deploy Anywhere.
400
+
</h1>
401
+
<h1 className="animate-fade-in-up animate-delay-200 text-5xl md:text-7xl font-bold mb-8 leading-tight tracking-tight text-gradient-animate">
402
+
For Free. Forever.
403
</h1>
404
405
+
{/* Subheadline */}
406
+
<p className="animate-fade-in-up animate-delay-300 text-lg md:text-xl text-muted-foreground mb-12 leading-relaxed max-w-2xl mx-auto">
407
+
The easiest way to deploy and orchestrate static sites.
408
+
Push updates instantly. Host on our infrastructure or yours.
409
+
All powered by AT Protocol.
410
</p>
411
412
+
{/* CTA Buttons */}
413
+
<div className="animate-fade-in-up animate-delay-400 max-w-lg mx-auto relative">
414
<div
415
+
className={`transition-all duration-500 ease-in-out ${showForm
416
+
? 'opacity-0 -translate-y-5 pointer-events-none absolute inset-0'
417
+
: 'opacity-100 translate-y-0'
418
+
}`}
419
>
420
+
<div className="flex flex-col sm:flex-row gap-3 justify-center">
421
+
<Button
422
+
size="lg"
423
+
className="bg-foreground text-background hover:bg-foreground/90 text-base px-6 py-5 btn-hover-lift"
424
+
onClick={() => setShowForm(true)}
425
+
>
426
+
<span className="mr-2 font-bold">@</span>
427
+
Deploy with AT
428
+
</Button>
429
+
<Button
430
+
variant="outline"
431
+
size="lg"
432
+
className="text-base px-6 py-5 btn-hover-lift"
433
+
asChild
434
+
>
435
+
<a href="https://docs.wisp.place/cli/" target="_blank" rel="noopener noreferrer">
436
+
<span className="font-mono mr-2 text-muted-foreground">>_</span>
437
+
Install wisp-cli
438
+
</a>
439
+
</Button>
440
+
</div>
441
</div>
442
443
<div
444
+
className={`transition-all duration-500 ease-in-out ${showForm
445
+
? 'opacity-100 translate-y-0'
446
+
: 'opacity-0 translate-y-5 pointer-events-none absolute inset-0'
447
+
}`}
448
>
449
<form
450
onSubmit={async (e) => {
···
505
</ActorTypeahead>
506
<button
507
type="submit"
508
+
className="w-full bg-foreground text-background hover:bg-foreground/90 font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors btn-hover-lift"
509
>
510
Continue
511
<ArrowRight className="ml-2 w-5 h-5" />
···
529
</div>
530
<div>
531
<h3 className="text-xl font-semibold mb-2">
532
+
Drop in your files
533
</h3>
534
<p className="text-muted-foreground">
535
+
Upload your site through our dashboard or push with the CLI.
536
+
Everything gets stored directly in your AT Protocol account.
537
</p>
538
</div>
539
</div>
···
543
</div>
544
<div>
545
<h3 className="text-xl font-semibold mb-2">
546
+
We handle the rest
547
</h3>
548
<p className="text-muted-foreground">
549
+
Your site goes live instantly on our global CDN.
550
+
Custom domains, HTTPS, cachingโall automatic.
551
</p>
552
</div>
553
</div>
···
557
</div>
558
<div>
559
<h3 className="text-xl font-semibold mb-2">
560
+
Push updates instantly
561
</h3>
562
<p className="text-muted-foreground">
563
+
Ship changes in seconds. Update through the dashboard,
564
+
run wisp-cli deploy, or wire up your CI/CD pipeline.
565
</p>
566
</div>
567
</div>
···
694
</section>
695
696
{/* Footer */}
697
+
<footer className="border-t border-border/40 bg-muted/20 mt-auto">
698
<div className="container mx-auto px-4 py-8">
699
<div className="text-center text-sm text-muted-foreground">
700
<p>
+16
-19
apps/main-app/public/onboarding/onboarding.tsx
+16
-19
apps/main-app/public/onboarding/onboarding.tsx
···
161
return (
162
<div className="w-full min-h-screen bg-background">
163
{/* Header */}
164
-
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
165
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
166
<div className="flex items-center gap-2">
167
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
168
<Globe className="w-5 h-5 text-primary-foreground" />
···
179
<div className="mb-8">
180
<div className="flex items-center justify-center gap-2 mb-4">
181
<div
182
-
className={`w-8 h-8 rounded-full flex items-center justify-center ${
183
-
step === 'domain'
184
-
? 'bg-primary text-primary-foreground'
185
-
: 'bg-green-500 text-white'
186
-
}`}
187
>
188
{step === 'domain' ? (
189
'1'
···
193
</div>
194
<div className="w-16 h-0.5 bg-border"></div>
195
<div
196
-
className={`w-8 h-8 rounded-full flex items-center justify-center ${
197
-
step === 'upload'
198
-
? 'bg-primary text-primary-foreground'
199
-
: step === 'domain'
200
-
? 'bg-muted text-muted-foreground'
201
-
: 'bg-green-500 text-white'
202
-
}`}
203
>
204
{step === 'complete' ? (
205
<CheckCircle2 className="w-5 h-5" />
···
258
{!isCheckingAvailability &&
259
isAvailable !== null && (
260
<div
261
-
className={`absolute right-3 top-1/2 -translate-y-1/2 ${
262
-
isAvailable
263
-
? 'text-green-500'
264
-
: 'text-red-500'
265
-
}`}
266
>
267
{isAvailable ? 'โ' : 'โ'}
268
</div>
···
161
return (
162
<div className="w-full min-h-screen bg-background">
163
{/* Header */}
164
+
<header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
165
+
<div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between">
166
<div className="flex items-center gap-2">
167
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
168
<Globe className="w-5 h-5 text-primary-foreground" />
···
179
<div className="mb-8">
180
<div className="flex items-center justify-center gap-2 mb-4">
181
<div
182
+
className={`w-8 h-8 rounded-full flex items-center justify-center ${step === 'domain'
183
+
? 'bg-primary text-primary-foreground'
184
+
: 'bg-green-500 text-white'
185
+
}`}
186
>
187
{step === 'domain' ? (
188
'1'
···
192
</div>
193
<div className="w-16 h-0.5 bg-border"></div>
194
<div
195
+
className={`w-8 h-8 rounded-full flex items-center justify-center ${step === 'upload'
196
+
? 'bg-primary text-primary-foreground'
197
+
: step === 'domain'
198
+
? 'bg-muted text-muted-foreground'
199
+
: 'bg-green-500 text-white'
200
+
}`}
201
>
202
{step === 'complete' ? (
203
<CheckCircle2 className="w-5 h-5" />
···
256
{!isCheckingAvailability &&
257
isAvailable !== null && (
258
<div
259
+
className={`absolute right-3 top-1/2 -translate-y-1/2 ${isAvailable
260
+
? 'text-green-500'
261
+
: 'text-red-500'
262
+
}`}
263
>
264
{isAvailable ? 'โ' : 'โ'}
265
</div>
+212
-39
apps/main-app/public/styles/global.css
+212
-39
apps/main-app/public/styles/global.css
···
6
:root {
7
color-scheme: light;
8
9
-
/* Warm beige background inspired by Sunset design #E9DDD8 */
10
-
--background: oklch(0.90 0.012 35);
11
-
/* Very dark brown text for strong contrast #2A2420 */
12
-
--foreground: oklch(0.18 0.01 30);
13
14
-
/* Slightly lighter card background */
15
-
--card: oklch(0.93 0.01 35);
16
-
--card-foreground: oklch(0.18 0.01 30);
17
18
-
--popover: oklch(0.93 0.01 35);
19
-
--popover-foreground: oklch(0.18 0.01 30);
20
21
-
/* Dark brown primary inspired by #645343 */
22
-
--primary: oklch(0.35 0.02 35);
23
-
--primary-foreground: oklch(0.95 0.01 35);
24
25
-
/* Bright pink accent for links #FFAAD2 */
26
-
--accent: oklch(0.78 0.15 345);
27
-
--accent-foreground: oklch(0.18 0.01 30);
28
29
-
/* Medium taupe secondary inspired by #867D76 */
30
-
--secondary: oklch(0.52 0.015 30);
31
-
--secondary-foreground: oklch(0.95 0.01 35);
32
33
-
/* Light warm muted background */
34
--muted: oklch(0.88 0.01 35);
35
-
--muted-foreground: oklch(0.42 0.015 30);
36
37
-
--border: oklch(0.75 0.015 30);
38
-
--input: oklch(0.92 0.01 35);
39
-
--ring: oklch(0.72 0.08 15);
40
41
-
--destructive: oklch(0.577 0.245 27.325);
42
-
--destructive-foreground: oklch(0.985 0 0);
43
44
-
--chart-1: oklch(0.78 0.15 345);
45
--chart-2: oklch(0.32 0.04 285);
46
-
--chart-3: oklch(0.56 0.08 220);
47
-
--chart-4: oklch(0.85 0.02 130);
48
-
--chart-5: oklch(0.93 0.03 85);
49
50
--radius: 0.75rem;
51
-
--sidebar: oklch(0.985 0 0);
52
-
--sidebar-foreground: oklch(0.145 0 0);
53
-
--sidebar-primary: oklch(0.205 0 0);
54
-
--sidebar-primary-foreground: oklch(0.985 0 0);
55
-
--sidebar-accent: oklch(0.97 0 0);
56
-
--sidebar-accent-foreground: oklch(0.205 0 0);
57
-
--sidebar-border: oklch(0.922 0 0);
58
-
--sidebar-ring: oklch(0.708 0 0);
59
}
60
61
.dark {
···
160
* {
161
@apply border-border outline-ring/50;
162
}
163
body {
164
@apply bg-background text-foreground;
165
}
166
}
167
168
@keyframes arrow-bounce {
169
-
0%, 100% {
170
transform: translateX(0);
171
}
172
50% {
173
transform: translateX(4px);
174
}
···
189
border-radius: 0.5rem;
190
padding: 1rem;
191
overflow-x: auto;
192
-
border: 1px solid hsl(var(--border));
193
}
194
195
.shiki-wrapper pre {
196
margin: 0 !important;
197
padding: 0 !important;
198
}
···
6
:root {
7
color-scheme: light;
8
9
+
/* Warm beige background inspired by Sunset design */
10
+
--background: oklch(0.92 0.012 35);
11
+
/* Very dark brown text for strong contrast */
12
+
--foreground: oklch(0.15 0.015 30);
13
14
+
/* Slightly lighter card background for elevation */
15
+
--card: oklch(0.95 0.008 35);
16
+
--card-foreground: oklch(0.15 0.015 30);
17
18
+
--popover: oklch(0.96 0.006 35);
19
+
--popover-foreground: oklch(0.15 0.015 30);
20
21
+
/* Dark brown primary - darker for better contrast */
22
+
--primary: oklch(0.30 0.025 35);
23
+
--primary-foreground: oklch(0.96 0.008 35);
24
25
+
/* Deeper pink accent for better visibility */
26
+
--accent: oklch(0.65 0.18 345);
27
+
--accent-foreground: oklch(0.15 0.015 30);
28
29
+
/* Darker taupe secondary for better contrast */
30
+
--secondary: oklch(0.85 0.012 30);
31
+
--secondary-foreground: oklch(0.25 0.02 30);
32
33
+
/* Muted areas with better distinction */
34
--muted: oklch(0.88 0.01 35);
35
+
--muted-foreground: oklch(0.35 0.02 30);
36
37
+
/* Significantly darker border for visibility */
38
+
--border: oklch(0.65 0.02 30);
39
+
/* Input backgrounds lighter than cards */
40
+
--input: oklch(0.97 0.005 35);
41
+
--ring: oklch(0.55 0.12 345);
42
43
+
--destructive: oklch(0.50 0.20 25);
44
+
--destructive-foreground: oklch(0.98 0 0);
45
46
+
--chart-1: oklch(0.65 0.18 345);
47
--chart-2: oklch(0.32 0.04 285);
48
+
--chart-3: oklch(0.50 0.10 220);
49
+
--chart-4: oklch(0.70 0.08 130);
50
+
--chart-5: oklch(0.75 0.06 85);
51
52
--radius: 0.75rem;
53
+
--sidebar: oklch(0.94 0.008 35);
54
+
--sidebar-foreground: oklch(0.15 0.015 30);
55
+
--sidebar-primary: oklch(0.30 0.025 35);
56
+
--sidebar-primary-foreground: oklch(0.96 0.008 35);
57
+
--sidebar-accent: oklch(0.90 0.01 35);
58
+
--sidebar-accent-foreground: oklch(0.20 0.02 30);
59
+
--sidebar-border: oklch(0.65 0.02 30);
60
+
--sidebar-ring: oklch(0.55 0.12 345);
61
}
62
63
.dark {
···
162
* {
163
@apply border-border outline-ring/50;
164
}
165
+
166
+
html {
167
+
scrollbar-gutter: stable;
168
+
}
169
+
170
body {
171
@apply bg-background text-foreground;
172
}
173
}
174
175
@keyframes arrow-bounce {
176
+
177
+
0%,
178
+
100% {
179
transform: translateX(0);
180
}
181
+
182
50% {
183
transform: translateX(4px);
184
}
···
199
border-radius: 0.5rem;
200
padding: 1rem;
201
overflow-x: auto;
202
+
border: 1px solid var(--border);
203
}
204
205
.shiki-wrapper pre {
206
margin: 0 !important;
207
padding: 0 !important;
208
}
209
+
210
+
/* ========== Landing Page Animations ========== */
211
+
212
+
/* Animated gradient for headline text */
213
+
@keyframes gradient-shift {
214
+
215
+
0%,
216
+
100% {
217
+
background-position: 0% 50%;
218
+
}
219
+
220
+
50% {
221
+
background-position: 100% 50%;
222
+
}
223
+
}
224
+
225
+
.text-gradient-animate {
226
+
background: linear-gradient(90deg,
227
+
oklch(0.55 0.22 350),
228
+
oklch(0.60 0.24 10),
229
+
oklch(0.55 0.22 350));
230
+
background-size: 200% auto;
231
+
-webkit-background-clip: text;
232
+
background-clip: text;
233
+
-webkit-text-fill-color: transparent;
234
+
animation: gradient-shift 4s ease-in-out infinite;
235
+
}
236
+
237
+
.dark .text-gradient-animate {
238
+
background: linear-gradient(90deg,
239
+
oklch(0.75 0.12 295),
240
+
oklch(0.85 0.10 5),
241
+
oklch(0.75 0.12 295));
242
+
background-size: 200% auto;
243
+
-webkit-background-clip: text;
244
+
background-clip: text;
245
+
-webkit-text-fill-color: transparent;
246
+
}
247
+
248
+
/* Floating/breathing animation for hero elements */
249
+
@keyframes float {
250
+
251
+
0%,
252
+
100% {
253
+
transform: translateY(0);
254
+
}
255
+
256
+
50% {
257
+
transform: translateY(-8px);
258
+
}
259
+
}
260
+
261
+
.animate-float {
262
+
animation: float 3s ease-in-out infinite;
263
+
}
264
+
265
+
.animate-float-delayed {
266
+
animation: float 3s ease-in-out infinite;
267
+
animation-delay: 0.5s;
268
+
}
269
+
270
+
/* Staggered fade-in animation */
271
+
@keyframes fade-in-up {
272
+
from {
273
+
opacity: 0;
274
+
transform: translateY(20px);
275
+
}
276
+
277
+
to {
278
+
opacity: 1;
279
+
transform: translateY(0);
280
+
}
281
+
}
282
+
283
+
.animate-fade-in-up {
284
+
animation: fade-in-up 0.6s ease-out forwards;
285
+
opacity: 0;
286
+
}
287
+
288
+
.animate-delay-100 {
289
+
animation-delay: 0.1s;
290
+
}
291
+
292
+
.animate-delay-200 {
293
+
animation-delay: 0.2s;
294
+
}
295
+
296
+
.animate-delay-300 {
297
+
animation-delay: 0.3s;
298
+
}
299
+
300
+
.animate-delay-400 {
301
+
animation-delay: 0.4s;
302
+
}
303
+
304
+
.animate-delay-500 {
305
+
animation-delay: 0.5s;
306
+
}
307
+
308
+
.animate-delay-600 {
309
+
animation-delay: 0.6s;
310
+
}
311
+
312
+
/* Terminal cursor blink */
313
+
@keyframes cursor-blink {
314
+
315
+
0%,
316
+
50% {
317
+
opacity: 1;
318
+
}
319
+
320
+
51%,
321
+
100% {
322
+
opacity: 0;
323
+
}
324
+
}
325
+
326
+
.animate-cursor-blink {
327
+
animation: cursor-blink 1s step-end infinite;
328
+
}
329
+
330
+
/* Button hover scale effect */
331
+
.btn-hover-lift {
332
+
transition: all 0.2s ease-out;
333
+
}
334
+
335
+
.btn-hover-lift:hover {
336
+
transform: translateY(-2px);
337
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
338
+
}
339
+
340
+
.btn-hover-lift:active {
341
+
transform: translateY(0);
342
+
}
343
+
344
+
/* Subtle pulse for feature dots */
345
+
@keyframes dot-pulse {
346
+
347
+
0%,
348
+
100% {
349
+
transform: scale(1);
350
+
opacity: 1;
351
+
}
352
+
353
+
50% {
354
+
transform: scale(1.2);
355
+
opacity: 0.8;
356
+
}
357
+
}
358
+
359
+
.animate-dot-pulse {
360
+
animation: dot-pulse 2s ease-in-out infinite;
361
+
}
362
+
363
+
.animate-dot-pulse-delayed-1 {
364
+
animation: dot-pulse 2s ease-in-out infinite;
365
+
animation-delay: 0.3s;
366
+
}
367
+
368
+
.animate-dot-pulse-delayed-2 {
369
+
animation: dot-pulse 2s ease-in-out infinite;
370
+
animation-delay: 0.6s;
371
+
}
+30
-4
apps/main-app/src/index.ts
+30
-4
apps/main-app/src/index.ts
···
12
cleanupExpiredSessions,
13
rotateKeysIfNeeded
14
} from './lib/oauth-client'
15
-
import { getCookieSecret } from './lib/db'
16
import { authRoutes } from './routes/auth'
17
import { wispRoutes } from './routes/wisp'
18
import { domainRoutes } from './routes/domain'
···
20
import { siteRoutes } from './routes/site'
21
import { csrfProtection } from './lib/csrf'
22
import { DNSVerificationWorker } from './lib/dns-verification-worker'
23
-
import { createLogger, logCollector } from '@wisp/observability'
24
import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
25
import { promptAdminSetup } from './lib/admin-auth'
26
import { adminRoutes } from './routes/admin'
27
28
const logger = createLogger('main-app')
29
···
55
setInterval(runMaintenance, 60 * 60 * 1000)
56
57
// Start DNS verification worker (runs every 10 minutes)
58
const dnsVerifier = new DNSVerificationWorker(
59
10 * 60 * 1000, // 10 minutes
60
(msg, data) => {
···
62
}
63
)
64
65
-
dnsVerifier.start()
66
-
logger.info('DNS Verifier Started - checking custom domains every 10 minutes')
67
68
export const app = new Elysia({
69
serve: {
···
194
console.log(
195
`๐ฆ Elysia is running at ${app.server?.hostname}:${app.server?.port}`
196
)
···
12
cleanupExpiredSessions,
13
rotateKeysIfNeeded
14
} from './lib/oauth-client'
15
+
import { getCookieSecret, closeDatabase } from './lib/db'
16
import { authRoutes } from './routes/auth'
17
import { wispRoutes } from './routes/wisp'
18
import { domainRoutes } from './routes/domain'
···
20
import { siteRoutes } from './routes/site'
21
import { csrfProtection } from './lib/csrf'
22
import { DNSVerificationWorker } from './lib/dns-verification-worker'
23
+
import { createLogger, logCollector, initializeGrafanaExporters } from '@wisp/observability'
24
import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
25
import { promptAdminSetup } from './lib/admin-auth'
26
import { adminRoutes } from './routes/admin'
27
+
28
+
// Initialize Grafana exporters if configured
29
+
initializeGrafanaExporters({
30
+
serviceName: 'main-app',
31
+
serviceVersion: '1.0.50'
32
+
})
33
34
const logger = createLogger('main-app')
35
···
61
setInterval(runMaintenance, 60 * 60 * 1000)
62
63
// Start DNS verification worker (runs every 10 minutes)
64
+
// Can be disabled via DISABLE_DNS_WORKER=true environment variable
65
const dnsVerifier = new DNSVerificationWorker(
66
10 * 60 * 1000, // 10 minutes
67
(msg, data) => {
···
69
}
70
)
71
72
+
if (Bun.env.DISABLE_DNS_WORKER !== 'true') {
73
+
dnsVerifier.start()
74
+
logger.info('DNS Verifier Started - checking custom domains every 10 minutes')
75
+
} else {
76
+
logger.info('DNS Verifier disabled via DISABLE_DNS_WORKER environment variable')
77
+
}
78
79
export const app = new Elysia({
80
serve: {
···
205
console.log(
206
`๐ฆ Elysia is running at ${app.server?.hostname}:${app.server?.port}`
207
)
208
+
209
+
// Graceful shutdown
210
+
process.on('SIGINT', async () => {
211
+
console.log('\n๐ Shutting down...')
212
+
dnsVerifier.stop()
213
+
await closeDatabase()
214
+
process.exit(0)
215
+
})
216
+
217
+
process.on('SIGTERM', async () => {
218
+
console.log('\n๐ Shutting down...')
219
+
dnsVerifier.stop()
220
+
await closeDatabase()
221
+
process.exit(0)
222
+
})
+13
apps/main-app/src/lib/db.ts
+13
apps/main-app/src/lib/db.ts
···
526
console.log('[CookieSecret] Generated new cookie signing secret');
527
return secret;
528
};
529
+
530
+
/**
531
+
* Close database connection
532
+
* Call this during graceful shutdown
533
+
*/
534
+
export const closeDatabase = async (): Promise<void> => {
535
+
try {
536
+
await db.end();
537
+
console.log('[DB] Database connection closed');
538
+
} catch (err) {
539
+
console.error('[DB] Error closing database connection:', err);
540
+
}
541
+
};
+5
-4
apps/main-app/src/lib/oauth-client.ts
+5
-4
apps/main-app/src/lib/oauth-client.ts
···
4
import { logger } from "./logger";
5
import { SlingshotHandleResolver } from "./slingshot-handle-resolver";
6
7
// Session timeout configuration (30 days in seconds)
8
const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds
9
// OAuth state timeout (1 hour in seconds)
···
110
// Loopback client for local development
111
// For loopback, scopes and redirect_uri must be in client_id query string
112
const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
113
-
const scope = 'atproto repo:place.wisp.fs repo:place.wisp.domain repo:place.wisp.subfs repo:place.wisp.settings blob:*/* rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview';
114
const params = new URLSearchParams();
115
params.append('redirect_uri', redirectUri);
116
-
params.append('scope', scope);
117
118
return {
119
client_id: `http://localhost?${params.toString()}`,
···
124
response_types: ['code'],
125
application_type: 'web',
126
token_endpoint_auth_method: 'none',
127
-
scope: scope,
128
dpop_bound_access_tokens: false,
129
subject_type: 'public',
130
authorization_signed_response_alg: 'ES256'
···
145
application_type: 'web',
146
token_endpoint_auth_method: 'private_key_jwt',
147
token_endpoint_auth_signing_alg: "ES256",
148
-
scope: "atproto repo:place.wisp.fs repo:place.wisp.domain repo:place.wisp.subfs repo:place.wisp.settings blob:*/* rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview",
149
dpop_bound_access_tokens: true,
150
jwks_uri: `${config.domain}/jwks.json`,
151
subject_type: 'public',
···
4
import { logger } from "./logger";
5
import { SlingshotHandleResolver } from "./slingshot-handle-resolver";
6
7
+
// OAuth scope for all client types
8
+
const OAUTH_SCOPE = 'atproto repo:place.wisp.fs repo:place.wisp.domain repo:place.wisp.subfs repo:place.wisp.settings blob:*/*';
9
// Session timeout configuration (30 days in seconds)
10
const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds
11
// OAuth state timeout (1 hour in seconds)
···
112
// Loopback client for local development
113
// For loopback, scopes and redirect_uri must be in client_id query string
114
const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
115
const params = new URLSearchParams();
116
params.append('redirect_uri', redirectUri);
117
+
params.append('scope', OAUTH_SCOPE);
118
119
return {
120
client_id: `http://localhost?${params.toString()}`,
···
125
response_types: ['code'],
126
application_type: 'web',
127
token_endpoint_auth_method: 'none',
128
+
scope: OAUTH_SCOPE,
129
dpop_bound_access_tokens: false,
130
subject_type: 'public',
131
authorization_signed_response_alg: 'ES256'
···
146
application_type: 'web',
147
token_endpoint_auth_method: 'private_key_jwt',
148
token_endpoint_auth_signing_alg: "ES256",
149
+
scope: OAUTH_SCOPE,
150
dpop_bound_access_tokens: true,
151
jwks_uri: `${config.domain}/jwks.json`,
152
subject_type: 'public',
+10
-12
apps/main-app/src/routes/user.ts
+10
-12
apps/main-app/src/routes/user.ts
···
1
import { Elysia, t } from 'elysia'
2
import { requireAuth } from '../lib/wisp-auth'
3
import { NodeOAuthClient } from '@atproto/oauth-client-node'
4
-
import { Agent } from '@atproto/api'
5
import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db'
6
import { syncSitesFromPDS } from '../lib/sync-sites'
7
import { createLogger } from '@wisp/observability'
8
9
const logger = createLogger('main-app')
10
11
export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) =>
12
new Elysia({
···
42
})
43
.get('/info', async ({ auth }) => {
44
try {
45
-
// Get user's handle from AT Protocol
46
-
const agent = new Agent(auth.session)
47
-
48
let handle = 'unknown'
49
try {
50
-
console.log('[User] Attempting to fetch profile for DID:', auth.did)
51
-
const profile = await agent.getProfile({ actor: auth.did })
52
-
console.log('[User] Profile fetched successfully:', profile.data.handle)
53
-
handle = profile.data.handle
54
} catch (err) {
55
-
console.error('[User] Failed to fetch profile - Full error:', err)
56
-
console.error('[User] Error message:', err instanceof Error ? err.message : String(err))
57
-
console.error('[User] Error stack:', err instanceof Error ? err.stack : 'No stack')
58
-
logger.error('[User] Failed to fetch profile', err)
59
}
60
61
return {
···
1
import { Elysia, t } from 'elysia'
2
import { requireAuth } from '../lib/wisp-auth'
3
import { NodeOAuthClient } from '@atproto/oauth-client-node'
4
import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db'
5
import { syncSitesFromPDS } from '../lib/sync-sites'
6
import { createLogger } from '@wisp/observability'
7
+
import { createDidResolver, extractAtprotoData } from '@atproto-labs/did-resolver'
8
9
const logger = createLogger('main-app')
10
+
const didResolver = createDidResolver({})
11
12
export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) =>
13
new Elysia({
···
43
})
44
.get('/info', async ({ auth }) => {
45
try {
46
let handle = 'unknown'
47
try {
48
+
const didDoc = await didResolver.resolve(auth.did)
49
+
const atprotoData = extractAtprotoData(didDoc)
50
+
51
+
if (atprotoData.aka) {
52
+
handle = atprotoData.aka
53
+
}
54
} catch (err) {
55
+
56
+
logger.error('[User] Failed to resolve DID', err)
57
}
58
59
return {
+10
-16
apps/main-app/src/routes/wisp.ts
+10
-16
apps/main-app/src/routes/wisp.ts
···
39
40
const logger = createLogger('main-app')
41
42
-
function isValidSiteName(siteName: string): boolean {
43
if (!siteName || typeof siteName !== 'string') return false;
44
45
// Length check (AT Protocol rkey limit)
···
183
continue;
184
}
185
186
-
console.log(`Processing file ${i + 1}/${fileArray.length}:`, file.name, file.size, 'bytes');
187
updateJobProgress(jobId, {
188
filesProcessed: i + 1,
189
-
currentFile: file.name
190
});
191
192
// Skip files that match ignore patterns
193
-
const normalizedPath = file.name.replace(/^[^\/]*\//, '');
194
195
if (shouldIgnore(ignoreMatcher, normalizedPath)) {
196
-
console.log(`Skipping ignored file: ${file.name}`);
197
skippedFiles.push({
198
-
name: file.name,
199
reason: 'matched ignore pattern'
200
});
201
continue;
···
205
const maxSize = MAX_FILE_SIZE;
206
if (file.size > maxSize) {
207
skippedFiles.push({
208
-
name: file.name,
209
reason: `file too large (${(file.size / 1024 / 1024).toFixed(2)}MB, max 100MB)`
210
});
211
continue;
···
238
// Text files: compress AND base64 encode
239
finalContent = Buffer.from(compressedContent.toString('base64'), 'binary');
240
base64Encoded = true;
241
-
const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1);
242
-
console.log(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`);
243
-
logger.info(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`);
244
} else {
245
// Audio files: just compress, no base64
246
finalContent = compressedContent;
247
-
const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1);
248
-
console.log(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`);
249
-
logger.info(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`);
250
}
251
} else {
252
// Binary files: upload directly
253
finalContent = originalContent;
254
-
console.log(`Uploading ${file.name} directly: ${originalContent.length} bytes (no compression)`);
255
-
logger.info(`Uploading ${file.name} directly: ${originalContent.length} bytes (binary)`);
256
}
257
258
uploadedFiles.push({
259
-
name: file.name,
260
content: finalContent,
261
mimeType: originalMimeType,
262
size: finalContent.length,
···
39
40
const logger = createLogger('main-app')
41
42
+
export function isValidSiteName(siteName: string): boolean {
43
if (!siteName || typeof siteName !== 'string') return false;
44
45
// Length check (AT Protocol rkey limit)
···
183
continue;
184
}
185
186
+
// Use webkitRelativePath when available (directory uploads), fallback to name for regular file uploads
187
+
const webkitPath = 'webkitRelativePath' in file ? String(file.webkitRelativePath) : '';
188
+
const filePath = webkitPath || file.name;
189
+
190
updateJobProgress(jobId, {
191
filesProcessed: i + 1,
192
+
currentFile: filePath
193
});
194
195
// Skip files that match ignore patterns
196
+
const normalizedPath = filePath.replace(/^[^\/]*\//, '');
197
198
if (shouldIgnore(ignoreMatcher, normalizedPath)) {
199
skippedFiles.push({
200
+
name: filePath,
201
reason: 'matched ignore pattern'
202
});
203
continue;
···
207
const maxSize = MAX_FILE_SIZE;
208
if (file.size > maxSize) {
209
skippedFiles.push({
210
+
name: filePath,
211
reason: `file too large (${(file.size / 1024 / 1024).toFixed(2)}MB, max 100MB)`
212
});
213
continue;
···
240
// Text files: compress AND base64 encode
241
finalContent = Buffer.from(compressedContent.toString('base64'), 'binary');
242
base64Encoded = true;
243
} else {
244
// Audio files: just compress, no base64
245
finalContent = compressedContent;
246
}
247
} else {
248
// Binary files: upload directly
249
finalContent = originalContent;
250
}
251
252
uploadedFiles.push({
253
+
name: filePath,
254
content: finalContent,
255
mimeType: originalMimeType,
256
size: finalContent.length,
+59
backup.nix
+59
backup.nix
···
···
1
+
{
2
+
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
3
+
inputs.nci.url = "github:90-008/nix-cargo-integration";
4
+
inputs.nci.inputs.nixpkgs.follows = "nixpkgs";
5
+
inputs.parts.url = "github:hercules-ci/flake-parts";
6
+
inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs";
7
+
inputs.fenix = {
8
+
url = "github:nix-community/fenix";
9
+
inputs.nixpkgs.follows = "nixpkgs";
10
+
};
11
+
12
+
outputs = inputs @ {
13
+
parts,
14
+
nci,
15
+
...
16
+
}:
17
+
parts.lib.mkFlake {inherit inputs;} {
18
+
systems = ["x86_64-linux" "aarch64-darwin"];
19
+
imports = [
20
+
nci.flakeModule
21
+
./crates.nix
22
+
];
23
+
perSystem = {
24
+
pkgs,
25
+
config,
26
+
...
27
+
}: let
28
+
crateOutputs = config.nci.outputs."wisp-cli";
29
+
mkRenamedPackage = name: pkg: isWindows: pkgs.runCommand name {} ''
30
+
mkdir -p $out/bin
31
+
if [ -f ${pkg}/bin/wisp-cli.exe ]; then
32
+
cp ${pkg}/bin/wisp-cli.exe $out/bin/${name}
33
+
elif [ -f ${pkg}/bin/wisp-cli ]; then
34
+
cp ${pkg}/bin/wisp-cli $out/bin/${name}
35
+
else
36
+
echo "Error: Could not find wisp-cli binary in ${pkg}/bin/"
37
+
ls -la ${pkg}/bin/ || true
38
+
exit 1
39
+
fi
40
+
'';
41
+
in {
42
+
devShells.default = crateOutputs.devShell;
43
+
packages.default = crateOutputs.packages.release;
44
+
packages.wisp-cli-x86_64-linux = mkRenamedPackage "wisp-cli-x86_64-linux" crateOutputs.packages.release false;
45
+
packages.wisp-cli-aarch64-linux = mkRenamedPackage "wisp-cli-aarch64-linux" crateOutputs.allTargets."aarch64-unknown-linux-gnu".packages.release false;
46
+
packages.wisp-cli-x86_64-windows = mkRenamedPackage "wisp-cli-x86_64-windows.exe" crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release true;
47
+
packages.wisp-cli-aarch64-darwin = mkRenamedPackage "wisp-cli-aarch64-darwin" crateOutputs.allTargets."aarch64-apple-darwin".packages.release false;
48
+
packages.all = pkgs.symlinkJoin {
49
+
name = "wisp-cli-all";
50
+
paths = [
51
+
config.packages.wisp-cli-x86_64-linux
52
+
config.packages.wisp-cli-aarch64-linux
53
+
config.packages.wisp-cli-x86_64-windows
54
+
config.packages.wisp-cli-aarch64-darwin
55
+
];
56
+
};
57
+
};
58
+
};
59
+
}
+9
-2
binaries/index.html
+9
-2
binaries/index.html
···
51
transition: background-color 200ms ease, color 200ms ease;
52
font-family: system-ui, sans-serif;
53
line-height: 1.6;
54
}
55
56
.container {
57
max-width: 860px;
58
margin: 40px auto;
59
padding: 0 20px;
60
-
min-height: 100vh;
61
}
62
63
h1 {
···
224
}
225
226
.footer {
227
-
margin-top: 3rem;
228
padding-top: 2rem;
229
border-top: 1px solid var(--demo-hr);
230
text-align: center;
231
color: var(--demo-text-secondary);
···
51
transition: background-color 200ms ease, color 200ms ease;
52
font-family: system-ui, sans-serif;
53
line-height: 1.6;
54
+
display: flex;
55
+
flex-direction: column;
56
+
min-height: 100vh;
57
}
58
59
.container {
60
max-width: 860px;
61
margin: 40px auto;
62
padding: 0 20px;
63
+
flex: 1;
64
+
display: flex;
65
+
flex-direction: column;
66
+
width: 100%;
67
}
68
69
h1 {
···
230
}
231
232
.footer {
233
+
margin-top: auto;
234
padding-top: 2rem;
235
+
padding-bottom: 2rem;
236
border-top: 1px solid var(--demo-hr);
237
text-align: center;
238
color: var(--demo-text-secondary);
+541
-162
bun.lock
+541
-162
bun.lock
···
1
{
2
"lockfileVersion": 1,
3
-
"configVersion": 0,
4
"workspaces": {
5
"": {
6
-
"name": "elysia-static",
7
"dependencies": {
8
"@tailwindcss/cli": "^4.1.17",
9
"bun-plugin-tailwind": "^0.1.2",
10
"tailwindcss": "^4.1.17",
11
},
12
},
13
"apps/hosting-service": {
14
"name": "wisp-hosting-service",
···
16
"dependencies": {
17
"@atproto/api": "^0.17.4",
18
"@atproto/identity": "^0.4.9",
19
-
"@atproto/lexicon": "^0.5.1",
20
"@atproto/sync": "^0.1.36",
21
"@atproto/xrpc": "^0.7.5",
22
"@hono/node-server": "^1.19.6",
···
31
"mime-types": "^2.1.35",
32
"multiformats": "^13.4.1",
33
"postgres": "^3.4.5",
34
},
35
"devDependencies": {
36
"@types/bun": "^1.3.1",
37
"@types/mime-types": "^2.1.4",
38
"@types/node": "^22.10.5",
39
"tsx": "^4.19.2",
40
},
41
},
42
"apps/main-app": {
43
"name": "@wisp/main-app",
44
"version": "1.0.50",
45
"dependencies": {
46
-
"@atproto/api": "^0.17.3",
47
-
"@atproto/common-web": "^0.4.5",
48
"@atproto/jwk-jose": "^0.1.11",
49
-
"@atproto/lex-cli": "^0.9.5",
50
-
"@atproto/oauth-client-node": "^0.3.9",
51
-
"@atproto/xrpc-server": "^0.9.5",
52
"@elysiajs/cors": "^1.4.0",
53
"@elysiajs/eden": "^1.4.3",
54
"@elysiajs/openapi": "^1.4.11",
···
68
"@wisp/lexicons": "workspace:*",
69
"@wisp/observability": "workspace:*",
70
"actor-typeahead": "^0.1.1",
71
-
"atproto-ui": "^0.11.3",
72
"bun-plugin-tailwind": "^0.1.2",
73
"class-variance-authority": "^0.7.1",
74
"clsx": "^2.1.1",
75
-
"elysia": "latest",
76
"ignore": "^7.0.5",
77
"iron-session": "^8.0.4",
78
"lucide-react": "^0.546.0",
···
86
"zlib": "^1.0.5",
87
},
88
"devDependencies": {
89
"@types/react": "^19.2.2",
90
"@types/react-dom": "^19.2.1",
91
"bun-types": "latest",
···
101
"@atproto/api": "^0.14.1",
102
"@wisp/lexicons": "workspace:*",
103
"multiformats": "^13.3.1",
104
},
105
},
106
"packages/@wisp/constants": {
···
131
},
132
"devDependencies": {
133
"@atproto/lex-cli": "^0.9.5",
134
},
135
},
136
"packages/@wisp/observability": {
137
"name": "@wisp/observability",
138
"version": "1.0.0",
139
"peerDependencies": {
140
-
"hono": "^4.0.0",
141
},
142
"optionalPeers": [
143
"hono",
···
149
},
150
},
151
"trustedDependencies": [
152
-
"core-js",
153
"cbor-extract",
154
"protobufjs",
155
],
156
"packages": {
157
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
158
159
-
"@atcute/bluesky": ["@atcute/bluesky@3.2.10", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.2" } }, "sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg=="],
160
161
-
"@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
162
163
-
"@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="],
164
165
"@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.4", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@atcute/util-fetch": "^1.0.3", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA=="],
166
167
-
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
168
169
-
"@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="],
170
171
-
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="],
172
173
-
"@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.2", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "zod": "^3.23.8" } }, "sha512-ca2B7xR43tVoQ8XxBvha58DXwIH8cIyKQl6lpOKGkPUrJuFoO4iCLlDiSDi2Ueh+yE1rMDPP/qveHdajgDX3WQ=="],
174
175
"@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="],
176
177
"@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="],
178
179
-
"@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.2", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "zod": "^3.23.8" } }, "sha512-KIerCzh3qb+zZoqWbIvTlvBY0XPq0r56kwViaJY/LTe/3oPO2JaqlYKS/F4dByWBhHK6YoUOJ0sWrh6PMJl40A=="],
180
181
-
"@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.21", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.2", "@atproto/did": "0.2.1" } }, "sha512-fuJy5Px5pGF3lJX/ATdurbT8tbmaFWtf+PPxAQDFy7ot2no3t+iaAgymhyxYymrssOuWs6BwOP8tyF3VrfdwtQ=="],
182
183
-
"@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.2", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver": "0.3.2" } }, "sha512-MYxO9pe0WsFyi5HFdKAwqIqHfiF2kBPoVhAIuH/4PYHzGr799ED47xLhNMxR3ZUYrJm5+TQzWXypGZ0Btw1Ffw=="],
184
185
"@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="],
186
···
192
193
"@atproto/common": ["@atproto/common@0.4.12", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ=="],
194
195
-
"@atproto/common-web": ["@atproto/common-web@0.4.5", "", { "dependencies": { "@atproto/lex-data": "0.0.1", "@atproto/lex-json": "0.0.1", "zod": "^3.23.8" } }, "sha512-Tx0xUafLm3vRvOQpbBl5eb9V8xlC7TaRXs6dAulHRkDG3Kb+P9qn3pkDteq+aeMshbVXbVa1rm3Ok4vFyuoyYA=="],
196
197
-
"@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="],
198
199
-
"@atproto/did": ["@atproto/did@0.2.1", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-1i5BTU2GnBaaeYWhxUOnuEKFVq9euT5+dQPFabHpa927BlJ54PmLGyBBaOI7/NbLmN5HWwBa18SBkMpg3jGZRA=="],
200
201
"@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="],
202
···
206
207
"@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="],
208
209
-
"@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.1", "", { "dependencies": { "@atproto/lex-data": "0.0.1", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-GCgowcC041tYmsoIxalIECJq4ZRHgREk6lFa4BzNRUZarMqwz57YF/7eUlo2Q6hoaMUL7Bjr6FvXwcZFaKrhvA=="],
210
211
-
"@atproto/lex-cli": ["@atproto/lex-cli@0.9.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-EedEKmURoSP735YwSDHsFrLOhZ4P2it8goCHv5ApWi/R9DFpOKOpmYfIXJ9MAprK8cw+yBnjDJbzpLJy7UXlTg=="],
212
213
-
"@atproto/lex-data": ["@atproto/lex-data@0.0.1", "", { "dependencies": { "@atproto/syntax": "0.4.1", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-DrS/8cQcQs3s5t9ELAFNtyDZ8/PdiCx47ALtFEP2GnX2uCBHZRkqWG7xmu6ehjc787nsFzZBvlnz3T/gov5fGA=="],
214
215
-
"@atproto/lex-json": ["@atproto/lex-json@0.0.1", "", { "dependencies": { "@atproto/lex-data": "0.0.1", "tslib": "^2.8.1" } }, "sha512-ivcF7+pDRuD/P97IEKQ/9TruunXj0w58Khvwk3M6psaI5eZT6LRsRZ4cWcKaXiFX4SHnjy+x43g0f7pPtIsERg=="],
216
217
"@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="],
218
219
-
"@atproto/oauth-client": ["@atproto/oauth-client@0.5.8", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.2", "@atproto-labs/identity-resolver": "0.3.2", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.0", "@atproto/xrpc": "0.7.5", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-7YEym6d97+Dd73qGdkQTXi5La8xvCQxwRUDzzlR/NVAARa9a4YP7MCmqBJVeP2anT0By+DSAPyPDLTsxcjIcCg=="],
220
221
-
"@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver-node": "0.1.21", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.8", "@atproto/oauth-types": "0.5.0" } }, "sha512-6khKlJqu1Ed5rt3rzcTD5hymB6JUjKdOHWYXwiphw4inkAIo6GxLCighI4eGOqZorYk2j8ueeTNB6KsgH0kcRw=="],
222
223
-
"@atproto/oauth-types": ["@atproto/oauth-types@0.5.0", "", { "dependencies": { "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-33xz7HcXhbl+XRqbIMVu3GE02iK1nKe2oMWENASsfZEYbCz2b9ZOarOFuwi7g4LKqpGowGp0iRKsQHFcq4SDaQ=="],
224
225
"@atproto/repo": ["@atproto/repo@0.8.11", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.2", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-b/WCu5ITws4ILHoXiZz0XXB5U9C08fUVzkBQDwpnme62GXv8gUaAPL/ttG61OusW09ARwMMQm4vxoP0hTFg+zA=="],
226
227
"@atproto/sync": ["@atproto/sync@0.1.38", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/identity": "^0.4.10", "@atproto/lexicon": "^0.5.2", "@atproto/repo": "^0.8.11", "@atproto/syntax": "^0.4.1", "@atproto/xrpc-server": "^0.10.0", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-2rE0SM21Nk4hWw/XcIYFnzlWO6/gBg8mrzuWbOvDhD49sA/wW4zyjaHZ5t1gvk28/SLok2VZiIR8nYBdbf7F5Q=="],
228
229
-
"@atproto/syntax": ["@atproto/syntax@0.4.1", "", {}, "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw=="],
230
231
-
"@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="],
232
233
"@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="],
234
235
-
"@atproto/xrpc-server": ["@atproto/xrpc-server@0.9.5", "", { "dependencies": { "@atproto/common": "^0.4.12", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.1", "@atproto/xrpc": "^0.7.5", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-V0srjUgy6mQ5yf9+MSNBLs457m4qclEaWZsnqIE7RfYywvntexTAbMoo7J7ONfTNwdmA9Gw4oLak2z2cDAET4w=="],
236
237
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
238
···
252
253
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
254
255
-
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],
256
257
"@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="],
258
259
-
"@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.6", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-jR7t4M6ZvMnBqzzHsNTL6y3sNq9jbGi2vKxbkizi/OO5tlvlKl/rnBGyFjZUjQ1Hte7rCz+2kfmgOQMhkjk+Og=="],
260
261
-
"@elysiajs/static": ["@elysiajs/static@1.4.6", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-cd61aY/DHOVhlnBjzTBX8E1XANIrsCH8MwEGHeLMaZzNrz0gD4Q8Qsde2dFMzu81I7ZDaaZ2Rim9blSLtUrYBg=="],
262
263
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="],
264
···
312
313
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="],
314
315
-
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="],
316
317
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
318
···
342
343
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
344
345
-
"@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
346
347
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="],
348
···
352
353
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="],
354
355
-
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
356
357
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="],
358
···
368
369
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
370
371
-
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
372
373
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
374
375
-
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
376
377
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
378
379
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
380
381
-
"@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
382
383
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="],
384
385
-
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
386
387
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
388
···
392
393
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
394
395
-
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="],
396
397
-
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="],
398
399
-
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="],
400
401
-
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="],
402
403
-
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="],
404
405
-
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="],
406
407
-
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="],
408
409
-
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="],
410
411
-
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="],
412
413
-
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="],
414
415
-
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-s00T99MjB+xLOWq+t+wVaVBrry+oBOZNiTJijt+bmkp/MJptYS3FGvs7a+nkjLNzoNDoWQcXgKew6AaHES37Bg=="],
416
417
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
418
···
516
517
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
518
519
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
520
521
"@tailwindcss/cli": ["@tailwindcss/cli@4.1.17", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.17" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg=="],
···
548
549
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
550
551
-
"@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="],
552
553
-
"@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="],
554
555
-
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
556
557
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
558
559
"@ts-morph/common": ["@ts-morph/common@0.25.0", "", { "dependencies": { "minimatch": "^9.0.4", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.9" } }, "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg=="],
560
561
-
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
562
563
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
564
565
"@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
566
567
-
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
568
569
-
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
570
571
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
572
···
594
595
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
596
597
-
"actor-typeahead": ["actor-typeahead@0.1.1", "", {}, "sha512-ilsBwzplKwMSBiO6Tg6RdaZ5xxqgXds5jCQuHV+ib9Aq3ja9g0T7u2Y1PmihotmS7l5RxhpGI/tPm3ljoRDRwg=="],
598
599
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
600
···
606
607
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
608
609
-
"atproto-ui": ["atproto-ui@0.11.3", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-NIBsORuo9lpCpr1SNKcKhNvqOVpsEy9IoHqFe1CM9gNTArpQL1hUcoP1Cou9a1O5qzCul9kaiu5xBHnB81I/WQ=="],
610
611
"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],
612
···
614
615
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
616
617
-
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
618
619
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
620
621
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
622
623
-
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
624
625
-
"bun": ["bun@1.3.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.2", "@oven/bun-darwin-x64": "1.3.2", "@oven/bun-darwin-x64-baseline": "1.3.2", "@oven/bun-linux-aarch64": "1.3.2", "@oven/bun-linux-aarch64-musl": "1.3.2", "@oven/bun-linux-x64": "1.3.2", "@oven/bun-linux-x64-baseline": "1.3.2", "@oven/bun-linux-x64-musl": "1.3.2", "@oven/bun-linux-x64-musl-baseline": "1.3.2", "@oven/bun-windows-x64": "1.3.2", "@oven/bun-windows-x64-baseline": "1.3.2" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-x75mPJiEfhO1j4Tfc65+PtW6ZyrAB6yTZInydnjDZXF9u9PRAnr6OK3v0Q9dpDl0dxRHkXlYvJ8tteJxc8t4Sw=="],
626
627
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
628
629
-
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
630
631
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
632
···
662
663
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
664
665
-
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
666
667
-
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
668
669
-
"core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="],
670
671
-
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
672
673
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
674
···
684
685
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
686
687
-
"elysia": ["elysia@1.4.16", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-KZtKN160/bdWVKg2hEgyoNXY8jRRquc+m6PboyisaLZL891I+Ufb7Ja6lDAD7vMQur8sLEWIcidZOzj5lWw9UA=="],
688
689
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
690
···
714
715
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
716
717
-
"exact-mirror": ["exact-mirror@0.2.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="],
718
719
-
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
720
721
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
722
723
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
724
725
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
726
727
-
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
728
-
729
-
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
730
731
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
732
733
-
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
734
735
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
736
···
754
755
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
756
757
-
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
758
-
759
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
760
761
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
762
763
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
764
765
-
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
766
767
-
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
768
769
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
770
···
898
899
"pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="],
900
901
-
"playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="],
902
903
-
"playwright-core": ["playwright-core@1.56.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="],
904
905
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
906
907
-
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
908
909
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
910
···
916
917
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
918
919
-
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
920
921
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
922
···
924
925
"rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="],
926
927
-
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
928
929
-
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
930
931
-
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
932
933
-
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
934
935
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
936
937
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
938
939
-
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
940
941
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
942
···
956
957
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
958
959
-
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
960
961
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
962
···
978
979
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
980
981
-
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
982
983
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
984
985
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
986
987
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
988
989
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
990
···
992
993
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
994
995
-
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
996
997
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
998
999
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
1000
1001
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="],
1002
1003
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
1004
···
1014
1015
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1016
1017
-
"tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
1018
1019
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
1020
···
1040
1041
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
1042
1043
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
1044
1045
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
···
1064
1065
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
1066
1067
-
"@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="],
1068
-
1069
-
"@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1070
1071
"@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="],
1072
···
1074
1075
"@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1076
1077
-
"@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1078
-
1079
"@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1080
1081
"@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1082
1083
"@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1084
1085
-
"@atproto/lex-cli/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="],
1086
-
1087
"@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1088
1089
"@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1090
-
1091
-
"@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="],
1092
1093
"@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1094
1095
-
"@atproto/repo/@atproto/common": ["@atproto/common@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.5", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-0S57sjzw4r9OLc5srJFi6uAz/aTKYl6btz3x36tSnGriL716m6h0x2IVtgd+FhUfIQfisevrqcqw8SfaGk8VTw=="],
1096
1097
"@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1098
1099
-
"@atproto/sync/@atproto/common": ["@atproto/common@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.5", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-0S57sjzw4r9OLc5srJFi6uAz/aTKYl6btz3x36tSnGriL716m6h0x2IVtgd+FhUfIQfisevrqcqw8SfaGk8VTw=="],
1100
1101
-
"@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.1", "", { "dependencies": { "@atproto/common": "^0.5.1", "@atproto/crypto": "^0.4.4", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "@atproto/lexicon": "^0.5.2", "@atproto/ws-client": "^0.0.3", "@atproto/xrpc": "^0.7.6", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-kHXykL4inBV/49vefn5zR5zv/VM1//+BIRqk9OvB3+mbERw0jkFiHhc6PWyY/81VD4ciu7FZwUCpRy/mtQtIaA=="],
1102
1103
"@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1104
1105
-
"@atproto/ws-client/@atproto/common": ["@atproto/common@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.5", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-0S57sjzw4r9OLc5srJFi6uAz/aTKYl6btz3x36tSnGriL716m6h0x2IVtgd+FhUfIQfisevrqcqw8SfaGk8VTw=="],
1106
1107
-
"@atproto/xrpc-server/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="],
1108
1109
-
"@atproto/xrpc-server/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="],
1110
1111
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1112
1113
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
1114
1115
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
···
1124
1125
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
1126
1127
-
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
1128
1129
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
1130
···
1132
1133
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1134
1135
-
"@wisp/main-app/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="],
1136
1137
-
"bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
1138
1139
-
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
1140
1141
-
"fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1142
1143
"iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1144
···
1146
1147
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
1148
1149
-
"protobufjs/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
1150
1151
"require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1152
1153
-
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
1154
1155
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1156
1157
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1158
1159
-
"tsx/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
1160
1161
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1162
···
1164
1165
"wisp-hosting-service/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="],
1166
1167
-
"@atproto/lex-cli/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1168
1169
-
"@atproto/lex-cli/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1170
1171
-
"@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="],
1172
1173
-
"@atproto/ws-client/@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1174
1175
-
"@atproto/xrpc-server/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1176
1177
-
"@atproto/xrpc-server/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1178
1179
-
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1180
1181
-
"@wisp/main-app/@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1182
1183
-
"@wisp/main-app/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="],
1184
1185
-
"@wisp/main-app/@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="],
1186
1187
"@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1188
1189
-
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
1190
-
1191
-
"protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
1192
1193
"require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1194
1195
-
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
1196
1197
-
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
1198
1199
-
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
1200
1201
-
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
1202
1203
-
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
1204
1205
-
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
1206
1207
-
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
1208
1209
-
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
1210
1211
-
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
1212
1213
-
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
1214
1215
-
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
1216
1217
-
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
1218
1219
-
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
1220
1221
-
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
1222
1223
-
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
1224
1225
-
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
1226
1227
-
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
1228
1229
-
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
1230
1231
-
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
1232
1233
-
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
1234
1235
-
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
1236
1237
-
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
1238
1239
-
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
1240
1241
-
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
1242
1243
-
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
1244
1245
-
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
1246
1247
-
"wisp-hosting-service/@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1248
1249
-
"wisp-hosting-service/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="],
1250
1251
-
"wisp-hosting-service/@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="],
1252
1253
"wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1254
1255
-
"@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
1256
}
1257
}
···
1
{
2
"lockfileVersion": 1,
3
+
"configVersion": 1,
4
"workspaces": {
5
"": {
6
+
"name": "@wisp/monorepo",
7
"dependencies": {
8
"@tailwindcss/cli": "^4.1.17",
9
+
"atproto-ui": "^0.12.0",
10
"bun-plugin-tailwind": "^0.1.2",
11
+
"elysia": "^1.4.18",
12
"tailwindcss": "^4.1.17",
13
},
14
+
"devDependencies": {
15
+
"@types/bun": "^1.3.5",
16
+
},
17
},
18
"apps/hosting-service": {
19
"name": "wisp-hosting-service",
···
21
"dependencies": {
22
"@atproto/api": "^0.17.4",
23
"@atproto/identity": "^0.4.9",
24
+
"@atproto/lexicon": "^0.5.2",
25
"@atproto/sync": "^0.1.36",
26
"@atproto/xrpc": "^0.7.5",
27
"@hono/node-server": "^1.19.6",
···
36
"mime-types": "^2.1.35",
37
"multiformats": "^13.4.1",
38
"postgres": "^3.4.5",
39
+
"tiered-storage": "1.0.3",
40
},
41
"devDependencies": {
42
"@types/bun": "^1.3.1",
43
"@types/mime-types": "^2.1.4",
44
"@types/node": "^22.10.5",
45
"tsx": "^4.19.2",
46
+
"typescript": "^5.9.3",
47
},
48
},
49
"apps/main-app": {
50
"name": "@wisp/main-app",
51
"version": "1.0.50",
52
"dependencies": {
53
+
"@atproto-labs/did-resolver": "^0.2.4",
54
+
"@atproto/api": "^0.17.7",
55
+
"@atproto/common-web": "^0.4.6",
56
"@atproto/jwk-jose": "^0.1.11",
57
+
"@atproto/lex-cli": "^0.9.7",
58
+
"@atproto/oauth-client-node": "^0.3.12",
59
+
"@atproto/xrpc-server": "^0.9.6",
60
"@elysiajs/cors": "^1.4.0",
61
"@elysiajs/eden": "^1.4.3",
62
"@elysiajs/openapi": "^1.4.11",
···
76
"@wisp/lexicons": "workspace:*",
77
"@wisp/observability": "workspace:*",
78
"actor-typeahead": "^0.1.1",
79
+
"atproto-ui": "^0.12.0",
80
"bun-plugin-tailwind": "^0.1.2",
81
"class-variance-authority": "^0.7.1",
82
"clsx": "^2.1.1",
83
+
"elysia": "^1.4.18",
84
"ignore": "^7.0.5",
85
"iron-session": "^8.0.4",
86
"lucide-react": "^0.546.0",
···
94
"zlib": "^1.0.5",
95
},
96
"devDependencies": {
97
+
"@atproto-labs/handle-resolver": "^0.3.4",
98
+
"@atproto/did": "^0.2.3",
99
"@types/react": "^19.2.2",
100
"@types/react-dom": "^19.2.1",
101
"bun-types": "latest",
···
111
"@atproto/api": "^0.14.1",
112
"@wisp/lexicons": "workspace:*",
113
"multiformats": "^13.3.1",
114
+
},
115
+
"devDependencies": {
116
+
"@atproto/lexicon": "^0.5.2",
117
},
118
},
119
"packages/@wisp/constants": {
···
144
},
145
"devDependencies": {
146
"@atproto/lex-cli": "^0.9.5",
147
+
"multiformats": "^13.4.1",
148
},
149
},
150
"packages/@wisp/observability": {
151
"name": "@wisp/observability",
152
"version": "1.0.0",
153
+
"dependencies": {
154
+
"@opentelemetry/api": "^1.9.0",
155
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
156
+
"@opentelemetry/resources": "^1.29.0",
157
+
"@opentelemetry/sdk-metrics": "^1.29.0",
158
+
"@opentelemetry/semantic-conventions": "^1.29.0",
159
+
},
160
+
"devDependencies": {
161
+
"@hono/node-server": "^1.19.6",
162
+
"bun-types": "^1.3.3",
163
+
"typescript": "^5.9.3",
164
+
},
165
"peerDependencies": {
166
+
"hono": "^4.10.7",
167
},
168
"optionalPeers": [
169
"hono",
···
175
},
176
},
177
"trustedDependencies": [
178
+
"esbuild",
179
"cbor-extract",
180
"protobufjs",
181
+
"core-js",
182
+
"bun",
183
+
"@parcel/watcher",
184
],
185
"packages": {
186
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
187
188
+
"@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="],
189
190
+
"@atcute/client": ["@atcute/client@4.1.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5" } }, "sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ=="],
191
192
+
"@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="],
193
194
"@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.4", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@atcute/util-fetch": "^1.0.3", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA=="],
195
196
+
"@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="],
197
198
+
"@atcute/tangled": ["@atcute/tangled@1.0.12", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.3" } }, "sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA=="],
199
200
+
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.4", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg=="],
201
202
+
"@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.4", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "zod": "^3.23.8" } }, "sha512-sbXxBnAJWsKv/FEGG6a/WLz7zQYUr1vA2TXvNnPwwJQJCjPwEJMOh1vM22wBr185Phy7D2GD88PcRokn7eUVyw=="],
203
204
"@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="],
205
206
"@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="],
207
208
+
"@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "zod": "^3.23.8" } }, "sha512-wsNopfzfgO3uPvfnFDgNeXgDufXxSXhjBjp2WEiSzEiLrMy0Jodnqggw4OzD9MJKf0a4Iu2/ydd537qdy91LrQ=="],
209
210
+
"@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.23", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.4", "@atproto/did": "0.2.3" } }, "sha512-tBRr2LCgzn3klk+DL0xrTFv4zg5tEszdeW6vSIFVebBYSb3MLdfhievmSqZdIQ4c9UCC4hN7YXTlZCXj8+2YmQ=="],
211
212
+
"@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.4", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/handle-resolver": "0.3.4" } }, "sha512-HNUEFQIo2ws6iATxmgHd5D5rAsWYupgxZucgwolVHPiMjE1SY+EmxEsfbEN1wDEzM8/u9AKUg/jrxxPEwsgbew=="],
213
214
"@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="],
215
···
221
222
"@atproto/common": ["@atproto/common@0.4.12", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ=="],
223
224
+
"@atproto/common-web": ["@atproto/common-web@0.4.6", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "@atproto/lex-json": "0.0.2", "zod": "^3.23.8" } }, "sha512-+2mG/1oBcB/ZmYIU1ltrFMIiuy9aByKAkb2Fos/0eTdczcLBaH17k0KoxMGvhfsujN2r62XlanOAMzysa7lv1g=="],
225
226
+
"@atproto/crypto": ["@atproto/crypto@0.4.5", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw=="],
227
228
+
"@atproto/did": ["@atproto/did@0.2.3", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg=="],
229
230
"@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="],
231
···
235
236
"@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="],
237
238
+
"@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-sTr3UCL2SgxEoYVpzJGgWTnNl4TpngP5tMcRyaOvi21Se4m3oR4RDsoVDPz8AS6XphiteRwzwPstquN7aWWMbA=="],
239
240
+
"@atproto/lex-cli": ["@atproto/lex-cli@0.9.7", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-UZVf0pK0mB4qiuwbnrxmV0mC9/Vk2v7W3u9pd4wc4GFojzAyGP76MF2TiwWFya5mgzC7723/r5Jb4ADg0rtfng=="],
241
242
+
"@atproto/lex-data": ["@atproto/lex-data@0.0.2", "", { "dependencies": { "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-euV2rDGi+coH8qvZOU+ieUOEbwPwff9ca6IiXIqjZJ76AvlIpj7vtAyIRCxHUW2BoU6h9yqyJgn9MKD2a7oIwg=="],
243
244
+
"@atproto/lex-json": ["@atproto/lex-json@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "tslib": "^2.8.1" } }, "sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g=="],
245
246
"@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="],
247
248
+
"@atproto/oauth-client": ["@atproto/oauth-client@0.5.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.4", "@atproto-labs/identity-resolver": "0.3.4", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.2", "@atproto/xrpc": "0.7.6", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-2mdJFyYbaOw3e/1KMBOQ2/J9p+MfWW8kE6FKdExWrJ7JPJpTJw2ZF2EmdGHCVeXw386dQgXbLkr+w4vbgSqfMQ=="],
249
250
+
"@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.12", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/handle-resolver-node": "0.1.23", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.10", "@atproto/oauth-types": "0.5.2" } }, "sha512-9ejfO1H8qo3EbiAJgxKcdcR5Ay/9hgaC5OdxtTN63bcOrkIhvBN0xpVPGZYLL1iJQyNeK1T5m/LDrv4gUS1B+g=="],
251
252
+
"@atproto/oauth-types": ["@atproto/oauth-types@0.5.2", "", { "dependencies": { "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-9DCDvtvCanTwAaU5UakYDO0hzcOITS3RutK5zfLytE5Y9unj0REmTDdN8Xd8YCfUJl7T/9pYpf04Uyq7bFTASg=="],
253
254
"@atproto/repo": ["@atproto/repo@0.8.11", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.2", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-b/WCu5ITws4ILHoXiZz0XXB5U9C08fUVzkBQDwpnme62GXv8gUaAPL/ttG61OusW09ARwMMQm4vxoP0hTFg+zA=="],
255
256
"@atproto/sync": ["@atproto/sync@0.1.38", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/identity": "^0.4.10", "@atproto/lexicon": "^0.5.2", "@atproto/repo": "^0.8.11", "@atproto/syntax": "^0.4.1", "@atproto/xrpc-server": "^0.10.0", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-2rE0SM21Nk4hWw/XcIYFnzlWO6/gBg8mrzuWbOvDhD49sA/wW4zyjaHZ5t1gvk28/SLok2VZiIR8nYBdbf7F5Q=="],
257
258
+
"@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="],
259
260
+
"@atproto/ws-client": ["@atproto/ws-client@0.0.2", "", { "dependencies": { "@atproto/common": "^0.4.12", "ws": "^8.12.0" } }, "sha512-yb11WtI9cZfx/00MTgZRabB97Quf/TerMmtzIm2H2YirIq2oW++NPoufXYCuXuQGR4ep4fvCyzz0/GX95jCONQ=="],
261
262
"@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="],
263
264
+
"@atproto/xrpc-server": ["@atproto/xrpc-server@0.9.6", "", { "dependencies": { "@atproto/common": "^0.4.12", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.1", "@atproto/ws-client": "^0.0.2", "@atproto/xrpc": "^0.7.5", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-N/wPK0VEk8lZLkVsfG1wlkINQnBLO2fzWT+xclOjYl5lJwDi5xgiiyEQJAyZN49d6cmbsONu0SuOVw9pa5xLCw=="],
265
+
266
+
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
267
+
268
+
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
269
+
270
+
"@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="],
271
+
272
+
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
273
+
274
+
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
275
+
276
+
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
277
+
278
+
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
279
+
280
+
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.962.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-node": "3.962.0", "@aws-sdk/middleware-bucket-endpoint": "3.957.0", "@aws-sdk/middleware-expect-continue": "3.957.0", "@aws-sdk/middleware-flexible-checksums": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-location-constraint": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-sdk-s3": "3.957.0", "@aws-sdk/middleware-ssec": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/signature-v4-multi-region": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/eventstream-serde-browser": "^4.2.7", "@smithy/eventstream-serde-config-resolver": "^4.3.7", "@smithy/eventstream-serde-node": "^4.2.7", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-blob-browser": "^4.2.8", "@smithy/hash-node": "^4.2.7", "@smithy/hash-stream-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/md5-js": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-I2/1McBZCcM3PfM4ck8D6gnZR3K7+yl1fGkwTq/3ThEn9tdLjNwcdgTbPfxfX6LoecLrH9Ekoo+D9nmQ0T261w=="],
281
+
282
+
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg=="],
283
+
284
+
"@aws-sdk/core": ["@aws-sdk/core@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/xml-builder": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw=="],
285
+
286
+
"@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA=="],
287
+
288
+
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog=="],
289
+
290
+
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw=="],
291
+
292
+
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.962.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-login": "3.962.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-h0kVnXLW2d3nxbcrR/Pfg3W/+YoCguasWz7/3nYzVqmdKarGrpJzaFdoZtLgvDSZ8VgWUC4lWOTcsDMV0UNqUQ=="],
293
+
294
+
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.962.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-kHYH6Av2UifG3mPkpPUNRh/PuX6adaAcpmsclJdHdxlixMCRdh8GNeEihq480DC0GmfqdpoSf1w2CLmLLPIS6w=="],
295
+
296
+
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.962.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-ini": "3.962.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-CS78NsWRxLa+nWqeWBEYMZTLacMFIXs1C5WJuM9kD05LLiWL32ksljoPsvNN24Bc7rCSQIIMx/U3KGvkDVZMVg=="],
297
+
298
+
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg=="],
299
+
300
+
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.958.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.958.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/token-providers": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg=="],
301
+
302
+
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA=="],
303
+
304
+
"@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.962.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/smithy-client": "^4.10.2", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.962.0" } }, "sha512-Ai5gWRQkzsUMQ6NPoZZoiLXoQ6/yPRcR4oracIVjyWcu48TfBpsRgbqY/5zNOM55ag1wPX9TtJJGOhK3TNk45g=="],
305
+
306
+
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA=="],
307
+
308
+
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg=="],
309
+
310
+
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.957.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/crc64-nvme": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA=="],
311
+
312
+
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA=="],
313
+
314
+
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ=="],
315
+
316
+
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ=="],
317
+
318
+
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA=="],
319
+
320
+
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg=="],
321
+
322
+
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ=="],
323
+
324
+
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ=="],
325
+
326
+
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw=="],
327
+
328
+
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A=="],
329
+
330
+
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.957.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg=="],
331
+
332
+
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q=="],
333
+
334
+
"@aws-sdk/types": ["@aws-sdk/types@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg=="],
335
+
336
+
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.957.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g=="],
337
+
338
+
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", "tslib": "^2.6.2" } }, "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw=="],
339
+
340
+
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.957.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw=="],
341
+
342
+
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw=="],
343
+
344
+
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.957.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q=="],
345
+
346
+
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA=="],
347
+
348
+
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="],
349
350
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
351
···
365
366
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
367
368
+
"@elysiajs/eden": ["@elysiajs/eden@1.4.5", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g=="],
369
370
"@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="],
371
372
+
"@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.8", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-c9unbcdXfehExCv1GsiTCfos5SyIAyDwP7apcMeXmUMBaJZiAYMfiEH8RFFFIfIHJHC/xlNJzUPodkcUaaoJJQ=="],
373
374
+
"@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="],
375
376
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="],
377
···
425
426
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="],
427
428
+
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.2", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA=="],
429
430
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
431
···
455
456
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
457
458
+
"@opentelemetry/core": ["@opentelemetry/core@1.29.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA=="],
459
460
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="],
461
···
465
466
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="],
467
468
+
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-exporter-base": "0.56.0", "@opentelemetry/otlp-transformer": "0.56.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/sdk-metrics": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GD5QuCT6js+mDpb5OBO6OSyCH+k2Gy3xPHJV9BnjV8W6kpSuY8y2Samzs5vl23UcGMq6sHLAbs+Eq/VYsLMiVw=="],
469
470
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="],
471
···
481
482
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
483
484
+
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="],
485
486
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
487
488
+
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.56.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.56.0", "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/sdk-logs": "0.56.0", "@opentelemetry/sdk-metrics": "1.29.0", "@opentelemetry/sdk-trace-base": "1.29.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ=="],
489
490
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
491
492
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
493
494
+
"@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="],
495
496
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="],
497
498
+
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog=="],
499
500
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
501
···
505
506
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
507
508
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="],
509
510
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow=="],
511
512
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g=="],
513
514
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw=="],
515
516
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw=="],
517
518
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw=="],
519
520
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w=="],
521
522
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w=="],
523
524
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ=="],
525
526
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ=="],
527
528
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA=="],
529
530
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
531
···
629
630
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
631
632
+
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="],
633
+
634
+
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="],
635
+
636
+
"@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="],
637
+
638
+
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg=="],
639
+
640
+
"@smithy/core": ["@smithy/core@3.20.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ=="],
641
+
642
+
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA=="],
643
+
644
+
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
645
+
646
+
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g=="],
647
+
648
+
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ=="],
649
+
650
+
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A=="],
651
+
652
+
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.7", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g=="],
653
+
654
+
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg=="],
655
+
656
+
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.8", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw=="],
657
+
658
+
"@smithy/hash-node": ["@smithy/hash-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw=="],
659
+
660
+
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ=="],
661
+
662
+
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ=="],
663
+
664
+
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
665
+
666
+
"@smithy/md5-js": ["@smithy/md5-js@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw=="],
667
+
668
+
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg=="],
669
+
670
+
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.1", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg=="],
671
+
672
+
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.17", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg=="],
673
+
674
+
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="],
675
+
676
+
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw=="],
677
+
678
+
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.7", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw=="],
679
+
680
+
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ=="],
681
+
682
+
"@smithy/property-provider": ["@smithy/property-provider@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA=="],
683
+
684
+
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="],
685
+
686
+
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="],
687
+
688
+
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w=="],
689
+
690
+
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0" } }, "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA=="],
691
+
692
+
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.2", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg=="],
693
+
694
+
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.7", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg=="],
695
+
696
+
"@smithy/smithy-client": ["@smithy/smithy-client@4.10.2", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g=="],
697
+
698
+
"@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
699
+
700
+
"@smithy/url-parser": ["@smithy/url-parser@4.2.7", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg=="],
701
+
702
+
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
703
+
704
+
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="],
705
+
706
+
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="],
707
+
708
+
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="],
709
+
710
+
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
711
+
712
+
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.16", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ=="],
713
+
714
+
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.19", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA=="],
715
+
716
+
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg=="],
717
+
718
+
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
719
+
720
+
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w=="],
721
+
722
+
"@smithy/util-retry": ["@smithy/util-retry@4.2.7", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg=="],
723
+
724
+
"@smithy/util-stream": ["@smithy/util-stream@4.5.8", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w=="],
725
+
726
+
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
727
+
728
+
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
729
+
730
+
"@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="],
731
+
732
+
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
733
+
734
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
735
736
"@tailwindcss/cli": ["@tailwindcss/cli@4.1.17", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.17" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg=="],
···
763
764
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
765
766
+
"@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
767
768
+
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
769
770
+
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
771
772
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
773
774
"@ts-morph/common": ["@ts-morph/common@0.25.0", "", { "dependencies": { "minimatch": "^9.0.4", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.9" } }, "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg=="],
775
776
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
777
778
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
779
780
"@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
781
782
+
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
783
784
+
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
785
786
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
787
···
809
810
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
811
812
+
"actor-typeahead": ["actor-typeahead@0.1.2", "", {}, "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A=="],
813
814
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
815
···
821
822
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
823
824
+
"atproto-ui": ["atproto-ui@0.12.0", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-vdJmKNyuGWspuIIvySD601dL8wLJafgxfS/6NGBvbBFectoiaZ92Cua2JdDuSD/uRxUnRJ3AvMg7eL0M39DZ3Q=="],
825
826
"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],
827
···
829
830
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
831
832
+
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
833
+
834
+
"bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="],
835
836
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
837
838
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
839
840
+
"buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="],
841
842
+
"bun": ["bun@1.3.3", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.3", "@oven/bun-darwin-x64": "1.3.3", "@oven/bun-darwin-x64-baseline": "1.3.3", "@oven/bun-linux-aarch64": "1.3.3", "@oven/bun-linux-aarch64-musl": "1.3.3", "@oven/bun-linux-x64": "1.3.3", "@oven/bun-linux-x64-baseline": "1.3.3", "@oven/bun-linux-x64-musl": "1.3.3", "@oven/bun-linux-x64-musl-baseline": "1.3.3", "@oven/bun-windows-x64": "1.3.3", "@oven/bun-windows-x64-baseline": "1.3.3" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw=="],
843
844
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
845
846
+
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
847
848
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
849
···
879
880
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
881
882
+
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
883
884
+
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
885
886
+
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
887
888
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
889
890
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
891
···
901
902
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
903
904
+
"elysia": ["elysia@1.4.18", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "0.2.5", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-A6BhlipmSvgCy69SBgWADYZSdDIj3fT2gk8/9iMAC8iD+aGcnCr0fitziX0xr36MFDs/fsvVp8dWqxeq1VCgKg=="],
905
906
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
907
···
931
932
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
933
934
+
"exact-mirror": ["exact-mirror@0.2.5", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ=="],
935
936
+
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
937
938
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
939
940
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
941
+
942
+
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
943
944
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
945
946
+
"file-type": ["file-type@21.1.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="],
947
948
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
949
950
+
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
951
952
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
953
···
971
972
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
973
974
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
975
976
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
977
978
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
979
980
+
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
981
982
+
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
983
984
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
985
···
1113
1114
"pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="],
1115
1116
+
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
1117
1118
+
"playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
1119
1120
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
1121
1122
+
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
1123
1124
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
1125
···
1131
1132
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
1133
1134
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
1135
1136
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
1137
···
1139
1140
"rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="],
1141
1142
+
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
1143
1144
+
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
1145
1146
+
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
1147
1148
+
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
1149
1150
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
1151
1152
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
1153
1154
+
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
1155
1156
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
1157
···
1171
1172
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
1173
1174
+
"send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="],
1175
1176
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
1177
···
1193
1194
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
1195
1196
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1197
+
1198
+
"stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="],
1199
1200
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1201
1202
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
1203
1204
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1205
+
1206
+
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
1207
1208
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
1209
···
1211
1212
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
1213
1214
+
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
1215
1216
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
1217
1218
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
1219
1220
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="],
1221
+
1222
+
"tiered-storage": ["tiered-storage@1.0.3", "", { "dependencies": { "@aws-sdk/client-s3": "^3.500.0", "@aws-sdk/lib-storage": "^3.500.0", "hono": "^4.10.7", "mime-types": "^3.0.2", "tiny-lru": "^11.0.0" } }, "sha512-ntJCsWWYHSQWIDbObMfnJ8xw7cRSJCPzfbrjoY4hu0YIRizCthRWg8kCvJOgsgC+Upt259YHmfNwEr1wLv6Rxw=="],
1223
+
1224
+
"tiny-lru": ["tiny-lru@11.4.5", "", {}, "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw=="],
1225
1226
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
1227
···
1237
1238
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1239
1240
+
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
1241
1242
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
1243
···
1263
1264
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
1265
1266
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
1267
+
1268
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
1269
1270
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
···
1289
1290
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
1291
1292
+
"@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
1293
1294
"@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="],
1295
···
1297
1298
"@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1299
1300
"@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1301
1302
"@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1303
1304
"@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1305
1306
"@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1307
1308
"@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1309
1310
"@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1311
1312
+
"@atproto/repo/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="],
1313
1314
"@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1315
1316
+
"@atproto/sync/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="],
1317
1318
+
"@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.2", "", { "dependencies": { "@atproto/common": "^0.5.2", "@atproto/crypto": "^0.4.5", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "@atproto/lexicon": "^0.5.2", "@atproto/ws-client": "^0.0.3", "@atproto/xrpc": "^0.7.6", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-5AzN8xoV8K1Omn45z6qKH414+B3Z35D536rrScwF3aQGDEdpObAS+vya9UoSg+Gvm2+oOtVEbVri7riLTBW3Vg=="],
1319
1320
"@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1321
1322
+
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
1323
1324
+
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
1325
1326
+
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
1327
1328
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1329
1330
+
"@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1331
+
1332
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1333
+
1334
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1335
+
1336
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1337
+
1338
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1339
+
1340
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1341
+
1342
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1343
+
1344
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1345
+
1346
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1347
+
1348
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1349
+
1350
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1351
+
1352
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1353
+
1354
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
1355
+
1356
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1357
+
1358
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1359
+
1360
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1361
+
1362
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1363
+
1364
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="],
1365
+
1366
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="],
1367
+
1368
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1369
+
1370
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
1371
+
1372
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1373
+
1374
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1375
+
1376
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1377
+
1378
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1379
+
1380
+
"@opentelemetry/exporter-prometheus/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1381
+
1382
+
"@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1383
+
1384
+
"@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1385
+
1386
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1387
+
1388
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1389
+
1390
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1391
+
1392
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1393
+
1394
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1395
+
1396
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1397
+
1398
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1399
+
1400
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1401
+
1402
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1403
+
1404
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1405
+
1406
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1407
+
1408
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1409
+
1410
+
"@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1411
+
1412
+
"@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1413
+
1414
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1415
+
1416
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1417
+
1418
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1419
+
1420
+
"@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g=="],
1421
+
1422
+
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="],
1423
+
1424
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.56.0", "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw=="],
1425
+
1426
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="],
1427
+
1428
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ=="],
1429
+
1430
+
"@opentelemetry/propagator-b3/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1431
+
1432
+
"@opentelemetry/propagator-jaeger/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1433
+
1434
+
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
1435
+
1436
+
"@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1437
+
1438
+
"@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1439
+
1440
+
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1441
+
1442
+
"@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
1443
+
1444
+
"@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1445
+
1446
+
"@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
1447
+
1448
+
"@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1449
+
1450
+
"@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1451
+
1452
+
"@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1453
+
1454
+
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1455
+
1456
+
"@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1457
+
1458
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
1459
1460
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
···
1469
1470
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
1471
1472
+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
1473
1474
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
1475
···
1477
1478
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1479
1480
+
"@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
1481
1482
+
"@wisp/main-app/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="],
1483
1484
+
"@wisp/observability/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
1485
1486
+
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1487
1488
"iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1489
···
1491
1492
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
1493
1494
+
"pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
1495
1496
"require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1497
1498
+
"send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
1499
1500
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1501
1502
+
"send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
1503
+
1504
+
"serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
1505
+
1506
+
"tiered-storage/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
1507
+
1508
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1509
1510
+
"tsx/esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="],
1511
1512
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1513
···
1515
1516
"wisp-hosting-service/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="],
1517
1518
+
"@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="],
1519
1520
+
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
1521
1522
+
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
1523
+
1524
+
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
1525
+
1526
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1527
+
1528
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1529
+
1530
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1531
+
1532
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1533
+
1534
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1535
+
1536
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1537
+
1538
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1539
+
1540
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1541
+
1542
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1543
+
1544
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1545
1546
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1547
1548
+
"@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1549
1550
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1551
1552
+
"@opentelemetry/sdk-metrics/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1553
1554
+
"@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
1555
1556
+
"@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
1557
1558
+
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1559
1560
"@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1561
1562
+
"pino-abstract-transport/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
1563
1564
"require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1565
1566
+
"serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
1567
1568
+
"serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
1569
1570
+
"serve-static/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1571
1572
+
"serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
1573
1574
+
"tiered-storage/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
1575
1576
+
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="],
1577
1578
+
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="],
1579
1580
+
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="],
1581
+
1582
+
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="],
1583
+
1584
+
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="],
1585
1586
+
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="],
1587
1588
+
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="],
1589
1590
+
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="],
1591
1592
+
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="],
1593
1594
+
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="],
1595
1596
+
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="],
1597
1598
+
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="],
1599
1600
+
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="],
1601
1602
+
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="],
1603
1604
+
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="],
1605
1606
+
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="],
1607
1608
+
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="],
1609
1610
+
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="],
1611
1612
+
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="],
1613
1614
+
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="],
1615
1616
+
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="],
1617
1618
+
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="],
1619
1620
+
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="],
1621
1622
+
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="],
1623
1624
+
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="],
1625
1626
+
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="],
1627
1628
"wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1629
1630
+
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
1631
+
1632
+
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
1633
+
1634
+
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
1635
}
1636
}
+116
-401
claude.md
+116
-401
claude.md
···
1
-
# Wisp.place - Codebase Overview
2
3
-
**Project URL**: https://wisp.place
4
5
-
A decentralized static site hosting service built on the AT Protocol (Bluesky). Users can host static websites directly in their AT Protocol accounts, keeping full control and ownership while benefiting from fast CDN distribution.
6
7
-
---
8
9
-
## ๐๏ธ Architecture Overview
10
11
-
### Multi-Part System
12
-
1. **Main Backend** (`/src`) - OAuth, site management, custom domains
13
-
2. **Hosting Service** (`/hosting-service`) - Microservice that serves cached sites
14
-
3. **CLI Tool** (`/cli`) - Rust CLI for direct site uploads to PDS
15
-
4. **Frontend** (`/public`) - React UI for onboarding, editor, admin
16
-
17
-
### Tech Stack
18
-
- **Backend**: Elysia (Bun) + TypeScript + PostgreSQL
19
-
- **Frontend**: React 19 + Tailwind CSS 4 + Radix UI
20
-
- **CLI**: Rust with Jacquard (AT Protocol library)
21
-
- **Database**: PostgreSQL for session/domain/site caching
22
-
- **AT Protocol**: OAuth 2.0 + custom lexicons for storage
23
24
-
---
25
26
-
## ๐ Directory Structure
27
28
-
### `/src` - Main Backend Server
29
-
**Purpose**: Core server handling OAuth, site management, custom domains, admin features
30
31
-
**Key Routes**:
32
-
- `/api/auth/*` - OAuth signin/callback/logout/status
33
-
- `/api/domain/*` - Custom domain management (BYOD)
34
-
- `/wisp/*` - Site upload and management
35
-
- `/api/user/*` - User info and site listing
36
-
- `/api/admin/*` - Admin console (logs, metrics, DNS verification)
37
38
-
**Key Files**:
39
-
- `index.ts` - Express-like Elysia app setup with middleware (CORS, CSP, security headers)
40
-
- `lib/oauth-client.ts` - OAuth client setup with session/state persistence
41
-
- `lib/db.ts` - PostgreSQL schema and queries for all tables
42
-
- `lib/wisp-auth.ts` - Cookie-based authentication middleware
43
-
- `lib/wisp-utils.ts` - File compression (gzip), manifest creation, blob handling
44
-
- `lib/sync-sites.ts` - Syncs user's place.wisp.fs records from PDS to database cache
45
-
- `lib/dns-verify.ts` - DNS verification for custom domains (TXT + CNAME)
46
-
- `lib/dns-verification-worker.ts` - Background worker that checks domain verification every 10 minutes
47
-
- `lib/admin-auth.ts` - Simple username/password admin authentication
48
-
- `lib/observability.ts` - Logging, error tracking, metrics collection
49
-
- `routes/auth.ts` - OAuth flow handlers
50
-
- `routes/wisp.ts` - File upload and site creation (/wisp/upload-files)
51
-
- `routes/domain.ts` - Domain claiming/verification API
52
-
- `routes/user.ts` - User status/info/sites listing
53
-
- `routes/site.ts` - Site metadata and file retrieval
54
-
- `routes/admin.ts` - Admin dashboard API (logs, system health, manual DNS trigger)
55
56
-
### `/lexicons` & `src/lexicons/`
57
-
**Purpose**: AT Protocol Lexicon definitions for custom data types
58
59
-
**Key File**: `fs.json` - Defines `place.wisp.fs` record format
60
-
- **structure**: Virtual filesystem manifest with tree structure
61
-
- **site**: string identifier
62
-
- **root**: directory object containing entries
63
-
- **file**: blob reference + metadata (encoding, mimeType, base64 flag)
64
-
- **directory**: array of entries (recursive)
65
-
- **entry**: name + node (file or directory)
66
-
67
-
**Important**: Files are gzip-compressed and base64-encoded before upload to bypass PDS content sniffing
68
-
69
-
### `/hosting-service`
70
-
**Purpose**: Lightweight microservice that serves cached sites from disk
71
-
72
-
**Architecture**:
73
-
- Routes by domain lookup in PostgreSQL
74
-
- Caches site content locally on first access or firehose event
75
-
- Listens to AT Protocol firehose for new site records
76
-
- Automatically downloads and caches files from PDS
77
-
- SSRF-protected fetch (timeout, size limits, private IP blocking)
78
-
79
-
**Routes**:
80
-
1. Custom domains (`/*`) โ lookup custom_domains table
81
-
2. Wisp subdomains (`/*.wisp.place/*`) โ lookup domains table
82
-
3. DNS hash routing (`/hash.dns.wisp.place/*`) โ lookup custom_domains by hash
83
-
4. Direct serving (`/s.wisp.place/:identifier/:site/*`) โ fetch from PDS if not cached
84
-
85
-
**HTML Path Rewriting**: Absolute paths in HTML (`/style.css`) automatically rewritten to relative (`/:identifier/:site/style.css`)
86
-
87
-
### `/cli`
88
-
**Purpose**: Rust CLI tool for direct site uploads using app password or OAuth
89
-
90
-
**Flow**:
91
-
1. Authenticate with handle + app password or OAuth
92
-
2. Walk directory tree, compress files
93
-
3. Upload blobs to PDS via agent
94
-
4. Create place.wisp.fs record with manifest
95
-
5. Store site in database cache
96
-
97
-
**Auth Methods**:
98
-
- `--password` flag for app password auth
99
-
- OAuth loopback server for browser-based auth
100
-
- Supports both (password preferred if provided)
101
-
102
-
---
103
-
104
-
## ๐ Key Concepts
105
-
106
-
### Custom Domains (BYOD - Bring Your Own Domain)
107
-
**Process**:
108
-
1. User claims custom domain via API
109
-
2. System generates hash (SHA256(domain + secret))
110
-
3. User adds DNS records:
111
-
- TXT at `_wisp.example.com` = their DID
112
-
- CNAME at `example.com` = `{hash}.dns.wisp.place`
113
-
4. Background worker checks verification every 10 minutes
114
-
5. Once verified, custom domain routes to their hosted sites
115
-
116
-
**Tables**: `custom_domains` (id, domain, did, rkey, verified, last_verified_at)
117
-
118
-
### Wisp Subdomains
119
-
**Process**:
120
-
1. Handle claimed on first signup (e.g., alice โ alice.wisp.place)
121
-
2. Stored in `domains` table mapping domain โ DID
122
-
3. Served by hosting service
123
-
124
-
### Site Storage
125
-
**Locations**:
126
-
- **Authoritative**: PDS (AT Protocol repo) as `place.wisp.fs` record
127
-
- **Cache**: PostgreSQL `sites` table (rkey, did, site_name, created_at)
128
-
- **File Cache**: Hosting service caches downloaded files on disk
129
-
130
-
**Limits**:
131
-
- MAX_SITE_SIZE: 300MB total
132
-
- MAX_FILE_SIZE: 100MB per file
133
-
- MAX_FILE_COUNT: 2000 files
134
-
135
-
### File Compression Strategy
136
-
**Why**: Bypass PDS content sniffing issues (was treating HTML as images)
137
-
138
-
**Process**:
139
-
1. All files gzip-compressed (level 9)
140
-
2. Compressed content base64-encoded
141
-
3. Uploaded as `application/octet-stream` MIME type
142
-
4. Blob metadata stores original MIME type + encoding flag
143
-
5. Hosting service decompresses on serve
144
-
145
-
---
146
-
147
-
## ๐ Data Flow
148
-
149
-
### User Registration โ Site Upload
150
-
```
151
-
1. OAuth signin โ state/session stored in DB
152
-
2. Cookie set with DID
153
-
3. Sync sites from PDS to cache DB
154
-
4. If no sites/domain โ redirect to onboarding
155
-
5. User creates site โ POST /wisp/upload-files
156
-
6. Files compressed, uploaded as blobs
157
-
7. place.wisp.fs record created
158
-
8. Site cached in DB
159
-
9. Hosting service notified via firehose
160
```
161
162
-
### Custom Domain Setup
163
-
```
164
-
1. User claims domain (DB check + allocation)
165
-
2. System generates hash
166
-
3. User adds DNS records (_wisp.domain TXT + CNAME)
167
-
4. Background worker verifies every 10 min
168
-
5. Hosting service routes based on verification status
169
-
```
170
171
-
### Site Access
172
-
```
173
-
Hosting Service:
174
-
1. Request arrives at custom domain or *.wisp.place
175
-
2. Domain lookup in PostgreSQL
176
-
3. Check cache for site files
177
-
4. If not cached:
178
-
- Fetch from PDS using DID + rkey
179
-
- Decompress files
180
-
- Save to disk cache
181
-
5. Serve files (with HTML path rewriting)
182
-
```
183
184
-
---
185
186
-
## ๐ ๏ธ Important Implementation Details
187
188
-
### OAuth Implementation
189
-
- **State & Session Storage**: PostgreSQL (with expiration)
190
-
- **Key Rotation**: Periodic rotation + expiration cleanup (hourly)
191
-
- **OAuth Flow**: Redirects to PDS, returns to /api/auth/callback
192
-
- **Session Timeout**: 30 days
193
-
- **State Timeout**: 1 hour
194
195
-
### Security Headers
196
-
- X-Frame-Options: DENY
197
-
- X-Content-Type-Options: nosniff
198
-
- Strict-Transport-Security: max-age=31536000
199
-
- Content-Security-Policy (configured for Elysia + React)
200
-
- X-XSS-Protection: 1; mode=block
201
-
- Referrer-Policy: strict-origin-when-cross-origin
202
-
203
-
### Admin Authentication
204
-
- Simple username/password (hashed with bcrypt)
205
-
- Session-based cookie auth (24hr expiration)
206
-
- Separate `admin_session` cookie
207
-
- Initial setup prompted on startup
208
-
209
-
### Observability
210
-
- **Logging**: Structured logging with service tags + event types
211
-
- **Error Tracking**: Captures error context (message, stack, etc.)
212
-
- **Metrics**: Request counts, latencies, error rates
213
-
- **Log Levels**: debug, info, warn, error
214
-
- **Collection**: Centralized log collector with in-memory buffer
215
-
216
-
---
217
-
218
-
## ๐ Database Schema
219
-
220
-
### oauth_states
221
-
- key (primary key)
222
-
- data (JSON)
223
-
- created_at, expires_at (timestamps)
224
-
225
-
### oauth_sessions
226
-
- sub (primary key - subject/DID)
227
-
- data (JSON with OAuth session)
228
-
- updated_at, expires_at
229
-
230
-
### oauth_keys
231
-
- kid (primary key - key ID)
232
-
- jwk (JSON Web Key)
233
-
- created_at
234
-
235
-
### domains
236
-
- domain (primary key - e.g., alice.wisp.place)
237
-
- did (unique - user's DID)
238
-
- rkey (optional - record key)
239
-
- created_at
240
-
241
-
### custom_domains
242
-
- id (primary key - UUID)
243
-
- domain (unique - e.g., example.com)
244
-
- did (user's DID)
245
-
- rkey (optional)
246
-
- verified (boolean)
247
-
- last_verified_at (timestamp)
248
-
- created_at
249
-
250
-
### sites
251
-
- id, did, rkey, site_name
252
-
- created_at, updated_at
253
-
- Indexes on (did), (did, rkey), (rkey)
254
-
255
-
### admin_users
256
-
- username (primary key)
257
-
- password_hash (bcrypt)
258
-
- created_at
259
-
260
-
---
261
-
262
-
## ๐ Key Workflows
263
-
264
-
### Sign In Flow
265
-
1. POST /api/auth/signin with handle
266
-
2. System generates state token
267
-
3. Redirects to PDS OAuth endpoint
268
-
4. PDS redirects back to /api/auth/callback?code=X&state=Y
269
-
5. Validate state (CSRF protection)
270
-
6. Exchange code for session
271
-
7. Store session in DB, set DID cookie
272
-
8. Sync sites from PDS
273
-
9. Redirect to /editor or /onboarding
274
-
275
-
### File Upload Flow
276
-
1. POST /wisp/upload-files with siteName + files
277
-
2. Validate site name (rkey format rules)
278
-
3. For each file:
279
-
- Check size limits
280
-
- Read as ArrayBuffer
281
-
- Gzip compress
282
-
- Base64 encode
283
-
4. Upload all blobs in parallel via agent.com.atproto.repo.uploadBlob()
284
-
5. Create manifest with all blob refs
285
-
6. putRecord() for place.wisp.fs with manifest
286
-
7. Upsert to sites table
287
-
8. Return URI + CID
288
-
289
-
### Domain Verification Flow
290
-
1. POST /api/custom-domains/claim
291
-
2. Generate hash = SHA256(domain + secret)
292
-
3. Store in custom_domains with verified=false
293
-
4. Return hash for user to configure DNS
294
-
5. Background worker periodically:
295
-
- Query custom_domains where verified=false
296
-
- Verify TXT record at _wisp.domain
297
-
- Verify CNAME points to hash.dns.wisp.place
298
-
- Update verified flag + last_verified_at
299
-
6. Hosting service routes when verified=true
300
-
301
-
---
302
-
303
-
## ๐จ Frontend Structure
304
-
305
-
### `/public`
306
-
- **index.tsx** - Landing page with sign-in form
307
-
- **editor/editor.tsx** - Site editor/management UI
308
-
- **admin/admin.tsx** - Admin dashboard
309
-
- **components/ui/** - Reusable components (Button, Card, Dialog, etc.)
310
-
- **styles/global.css** - Tailwind + custom styles
311
-
312
-
### Page Flow
313
-
1. `/` - Landing page (sign in / get started)
314
-
2. `/editor` - Main app (requires auth)
315
-
3. `/admin` - Admin console (requires admin auth)
316
-
4. `/onboarding` - First-time user setup
317
-
318
-
---
319
320
-
## ๐ Notable Implementation Patterns
321
322
-
### File Handling
323
-
- Files stored as base64-encoded gzip in PDS blobs
324
-
- Metadata preserves original MIME type
325
-
- Hosting service decompresses on serve
326
-
- Workaround for PDS image pipeline issues with HTML
327
328
-
### Error Handling
329
-
- Comprehensive logging with context
330
-
- Graceful degradation (e.g., site sync failure doesn't break auth)
331
-
- Structured error responses with details
332
333
-
### Performance
334
-
- Site sync: Batch fetch up to 100 records per request
335
-
- Blob upload: Parallel promises for all files
336
-
- DNS verification: Batched background worker (10 min intervals)
337
-
- Caching: Two-tier (DB + disk in hosting service)
338
339
-
### Validation
340
-
- Lexicon validation on manifest creation
341
-
- Record type checking
342
-
- Domain format validation
343
-
- Site name format validation (AT Protocol rkey rules)
344
-
- File size limits enforced before upload
345
-
346
-
---
347
-
348
-
## ๐ Known Quirks & Workarounds
349
-
350
-
1. **PDS Content Sniffing**: Files must be uploaded as `application/octet-stream` (even HTML) and base64-encoded to prevent PDS from misinterpreting content
351
-
352
-
2. **Max URL Query Size**: DNS verification worker queries in batch; may need pagination for users with many custom domains
353
-
354
-
3. **File Count Limits**: Max 500 entries per directory (Lexicon constraint); large sites split across multiple directories
355
-
356
-
4. **Blob Size Limits**: Individual blobs limited to 100MB by Lexicon; large files handled differently if needed
357
-
358
-
5. **HTML Path Rewriting**: Only in hosting service for `/s.wisp.place/:identifier/:site/*` routes; custom domains handled differently
359
360
-
---
361
362
-
## ๐ Environment Variables
363
364
-
- `DOMAIN` - Base domain with protocol (default: `https://wisp.place`)
365
-
- `CLIENT_NAME` - OAuth client name (default: `PDS-View`)
366
-
- `DATABASE_URL` - PostgreSQL connection (default: `postgres://postgres:postgres@localhost:5432/wisp`)
367
-
- `NODE_ENV` - production/development
368
-
- `HOSTING_PORT` - Hosting service port (default: 3001)
369
-
- `BASE_DOMAIN` - Domain for URLs (default: wisp.place)
370
-
371
-
---
372
-
373
-
## ๐งโ๐ป Development Notes
374
-
375
-
### Adding New Features
376
-
1. **New routes**: Add to `/src/routes/*.ts`, import in index.ts
377
-
2. **DB changes**: Add migration in db.ts
378
-
3. **New lexicons**: Update `/lexicons/*.json`, regenerate types
379
-
4. **Admin features**: Add to /api/admin endpoints
380
-
381
-
### Testing
382
-
- Run with `bun test`
383
-
- CSRF tests in lib/csrf.test.ts
384
-
- Utility tests in lib/wisp-utils.test.ts
385
-
386
-
### Debugging
387
-
- Check logs via `/api/admin/logs` (requires admin auth)
388
-
- DNS verification manual trigger: POST /api/admin/verify-dns
389
-
- Health check: GET /api/health (includes DNS verifier status)
390
-
391
-
---
392
-
393
-
## ๐ Deployment Considerations
394
-
395
-
1. **Secrets**: Admin password, OAuth keys, database credentials
396
-
2. **HTTPS**: Required (HSTS header enforces it)
397
-
3. **CDN**: Custom domains require DNS configuration
398
-
4. **Scaling**:
399
-
- Main server: Horizontal scaling with session DB
400
-
- Hosting service: Independent scaling, disk cache per instance
401
-
5. **Backups**: PostgreSQL database critical; firehose provides recovery
402
-
403
-
---
404
-
405
-
## ๐ Related Technologies
406
-
407
-
- **AT Protocol**: Decentralized identity, OAuth 2.0
408
-
- **Jacquard**: Rust library for AT Protocol interactions
409
-
- **Elysia**: Bun web framework (similar to Express/Hono)
410
-
- **Lexicon**: AT Protocol's schema definition language
411
-
- **Firehose**: Real-time event stream of repo changes
412
-
- **PDS**: Personal Data Server (where users' data stored)
413
-
414
-
---
415
-
416
-
## ๐ฏ Project Goals
417
-
418
-
โ
Decentralized site hosting (data owned by users)
419
-
โ
Custom domain support with DNS verification
420
-
โ
Fast CDN distribution via hosting service
421
-
โ
Developer tools (CLI + API)
422
-
โ
Admin dashboard for monitoring
423
-
โ
Zero user data retention (sites in PDS, sessions in DB only)
424
-
425
-
---
426
-
427
-
**Last Updated**: November 2025
428
-
**Status**: Active development
···
1
+
The project is wisp.place. It is a static site hoster built on top of the AT Protocol. The overall basis of the project is that users upload site assets to their PDS as blobs, and creates a manifest record listing every blob as well as site name. The hosting service then catches events relating to the site (create, read, upload, delete) and handles them appropriately.
2
3
+
The lexicons look like this:
4
+
```typescript
5
+
//place.wisp.fs
6
+
interface Main {
7
+
$type: 'place.wisp.fs'
8
+
site: string
9
+
root: Directory
10
+
fileCount?: number
11
+
createdAt: string
12
+
}
13
14
+
interface File {
15
+
$type?: 'place.wisp.fs#file'
16
+
type: 'file'
17
+
blob: BlobRef
18
+
encoding?: 'gzip'
19
+
mimeType?: string
20
+
base64?: boolean
21
+
}
22
23
+
interface Directory {
24
+
$type?: 'place.wisp.fs#directory'
25
+
type: 'directory'
26
+
entries: Entry[]
27
+
}
28
29
+
interface Entry {
30
+
$type?: 'place.wisp.fs#entry'
31
+
name: string
32
+
node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string }
33
+
}
34
35
+
interface Subfs {
36
+
$type?: 'place.wisp.fs#subfs'
37
+
type: 'subfs'
38
+
subject: string // AT-URI pointing to a place.wisp.subfs record
39
+
flat?: boolean
40
+
}
41
42
+
//place.wisp.subfs
43
+
interface Main {
44
+
$type: 'place.wisp.subfs'
45
+
root: Directory
46
+
fileCount?: number
47
+
createdAt: string
48
+
}
49
50
+
interface File {
51
+
$type?: 'place.wisp.subfs#file'
52
+
type: 'file'
53
+
blob: BlobRef
54
+
encoding?: 'gzip'
55
+
mimeType?: string
56
+
base64?: boolean
57
+
}
58
59
+
interface Directory {
60
+
$type?: 'place.wisp.subfs#directory'
61
+
type: 'directory'
62
+
entries: Entry[]
63
+
}
64
65
+
interface Entry {
66
+
$type?: 'place.wisp.subfs#entry'
67
+
name: string
68
+
node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string }
69
+
}
70
71
+
interface Subfs {
72
+
$type?: 'place.wisp.subfs#subfs'
73
+
type: 'subfs'
74
+
subject: string // AT-URI pointing to another place.wisp.subfs record
75
+
}
76
77
+
//place.wisp.settings
78
+
interface Main {
79
+
$type: 'place.wisp.settings'
80
+
directoryListing: boolean
81
+
spaMode?: string
82
+
custom404?: string
83
+
indexFiles?: string[]
84
+
cleanUrls: boolean
85
+
headers?: CustomHeader[]
86
+
}
87
88
+
interface CustomHeader {
89
+
$type?: 'place.wisp.settings#customHeader'
90
+
name: string
91
+
value: string
92
+
path?: string // Optional glob pattern
93
+
}
94
```
95
96
+
The main differences between place.wisp.fs and place.wisp.subfs:
97
+
- place.wisp.fs has a required site field
98
+
- place.wisp.fs#subfs has an optional flat field that place.wisp.subfs#subfs doesn't have
99
100
+
The project is a monorepo. The package handler it uses for the typescript side is Bun. For the Rust cli, it is cargo.
101
102
+
### Typescript Bun Workspace Layout
103
104
+
Bun workspaces: `packages/@wisp/*`, `apps/main-app`, `apps/hosting-service`
105
106
+
There are two typescript apps
107
+
**`apps/main-app`** - Main backend (Bun + Elysia)
108
109
+
- OAuth authentication and session management
110
+
- Site CRUD operations via PDS
111
+
- Custom domain management
112
+
- Admin database view in /admin
113
+
- React frontend in public/
114
115
+
**`apps/hosting-service`** - CDN static file server (Node + Hono)
116
117
+
- Watches AT Protocol firehose for `place.wisp.fs` record changes
118
+
- Downloads and caches site files to disk
119
+
- Serves sites at `https://sites.wisp.place/{did}/{site-name}` and custom domains
120
+
- Handles redirects (`_redirects` file support) and routing logic
121
+
- Backfill mode for syncing existing sites
122
123
+
### Shared Packages (`packages/@wisp/*`)
124
125
+
- **`lexicons`** - AT Protocol lexicons (`place.wisp.fs`, `place.wisp.subfs`, `place.wisp.settings`) with
126
+
generated TypeScript types
127
+
- **`fs-utils`** - Filesystem tree building, manifest creation, subfs splitting logic
128
+
- **`atproto-utils`** - AT Protocol helpers (blob upload, record operations, CID handling)
129
+
- **`database`** - PostgreSQL schema and queries
130
+
- **`constants`** - Shared constants (limits, file patterns, default settings)
131
+
- **`observability`** - OpenTelemetry instrumentation
132
+
- **`safe-fetch`** - Wrapped fetch with timeout/retry logic
133
134
+
### CLI
135
136
+
**`cli/`** - Rust CLI using Jacquard (AT Protocol library)
137
+
- Direct PDS uploads without interacting with main-app
138
+
- Can also do the same firehose watching, caching, and serving hosting-service does, just without domain management
139
140
+
### Other Directories
141
142
+
- **`docs/`** - Astro documentation site
143
+
- **`binaries/`** - Compiled CLI binaries for distribution
+417
-504
cli/Cargo.lock
+417
-504
cli/Cargo.lock
···
3
version = 4
4
5
[[package]]
6
-
name = "abnf"
7
-
version = "0.13.0"
8
-
source = "registry+https://github.com/rust-lang/crates.io-index"
9
-
checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a"
10
-
dependencies = [
11
-
"abnf-core",
12
-
"nom",
13
-
]
14
-
15
-
[[package]]
16
-
name = "abnf-core"
17
-
version = "0.5.0"
18
-
source = "registry+https://github.com/rust-lang/crates.io-index"
19
-
checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d"
20
-
dependencies = [
21
-
"nom",
22
-
]
23
-
24
-
[[package]]
25
name = "addr2line"
26
version = "0.25.1"
27
source = "registry+https://github.com/rust-lang/crates.io-index"
···
71
dependencies = [
72
"alloc-no-stdlib",
73
]
74
75
[[package]]
76
name = "android_system_properties"
···
139
140
[[package]]
141
name = "async-compression"
142
-
version = "0.4.34"
143
source = "registry+https://github.com/rust-lang/crates.io-index"
144
-
checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473"
145
dependencies = [
146
"compression-codecs",
147
"compression-core",
···
158
dependencies = [
159
"proc-macro2",
160
"quote",
161
-
"syn 2.0.111",
162
]
163
164
[[package]]
···
175
176
[[package]]
177
name = "axum"
178
-
version = "0.8.7"
179
source = "registry+https://github.com/rust-lang/crates.io-index"
180
-
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
181
dependencies = [
182
"axum-core",
183
"bytes",
···
208
209
[[package]]
210
name = "axum-core"
211
-
version = "0.5.5"
212
source = "registry+https://github.com/rust-lang/crates.io-index"
213
-
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
214
dependencies = [
215
"bytes",
216
"futures-core",
···
237
"miniz_oxide",
238
"object",
239
"rustc-demangle",
240
-
"windows-link 0.2.1",
241
]
242
243
[[package]]
···
285
286
[[package]]
287
name = "base64ct"
288
-
version = "1.8.0"
289
source = "registry+https://github.com/rust-lang/crates.io-index"
290
-
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
291
292
[[package]]
293
name = "bitflags"
···
326
"proc-macro2",
327
"quote",
328
"rustversion",
329
-
"syn 2.0.111",
330
]
331
332
[[package]]
333
name = "borsh"
334
-
version = "1.5.7"
335
source = "registry+https://github.com/rust-lang/crates.io-index"
336
-
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
337
dependencies = [
338
"cfg_aliases",
339
]
···
370
]
371
372
[[package]]
373
-
name = "btree-range-map"
374
-
version = "0.7.2"
375
-
source = "registry+https://github.com/rust-lang/crates.io-index"
376
-
checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33"
377
-
dependencies = [
378
-
"btree-slab",
379
-
"cc-traits",
380
-
"range-traits",
381
-
"serde",
382
-
"slab",
383
-
]
384
-
385
-
[[package]]
386
-
name = "btree-slab"
387
-
version = "0.6.1"
388
-
source = "registry+https://github.com/rust-lang/crates.io-index"
389
-
checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c"
390
-
dependencies = [
391
-
"cc-traits",
392
-
"slab",
393
-
"smallvec",
394
-
]
395
-
396
-
[[package]]
397
name = "buf_redux"
398
version = "0.8.4"
399
source = "registry+https://github.com/rust-lang/crates.io-index"
···
405
406
[[package]]
407
name = "bumpalo"
408
-
version = "3.19.0"
409
source = "registry+https://github.com/rust-lang/crates.io-index"
410
-
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
411
412
[[package]]
413
name = "byteorder"
···
435
436
[[package]]
437
name = "cc"
438
-
version = "1.2.47"
439
source = "registry+https://github.com/rust-lang/crates.io-index"
440
-
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
441
dependencies = [
442
"find-msvc-tools",
443
"shlex",
444
]
445
446
[[package]]
447
-
name = "cc-traits"
448
-
version = "2.0.0"
449
-
source = "registry+https://github.com/rust-lang/crates.io-index"
450
-
checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5"
451
-
dependencies = [
452
-
"slab",
453
-
]
454
-
455
-
[[package]]
456
name = "cesu8"
457
version = "1.1.0"
458
source = "registry+https://github.com/rust-lang/crates.io-index"
···
481
"num-traits",
482
"serde",
483
"wasm-bindgen",
484
-
"windows-link 0.2.1",
485
]
486
487
[[package]]
···
533
534
[[package]]
535
name = "clap"
536
-
version = "4.5.53"
537
source = "registry+https://github.com/rust-lang/crates.io-index"
538
-
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
539
dependencies = [
540
"clap_builder",
541
"clap_derive",
···
543
544
[[package]]
545
name = "clap_builder"
546
-
version = "4.5.53"
547
source = "registry+https://github.com/rust-lang/crates.io-index"
548
-
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
549
dependencies = [
550
"anstream",
551
"anstyle",
···
562
"heck 0.5.0",
563
"proc-macro2",
564
"quote",
565
-
"syn 2.0.111",
566
]
567
568
[[package]]
···
572
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
573
574
[[package]]
575
name = "colorchoice"
576
version = "1.0.4"
577
source = "registry+https://github.com/rust-lang/crates.io-index"
···
589
590
[[package]]
591
name = "compression-codecs"
592
-
version = "0.4.33"
593
source = "registry+https://github.com/rust-lang/crates.io-index"
594
-
checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad"
595
dependencies = [
596
"compression-core",
597
"flate2",
···
603
version = "0.4.31"
604
source = "registry+https://github.com/rust-lang/crates.io-index"
605
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
606
607
[[package]]
608
name = "const-oid"
···
678
dependencies = [
679
"cfg-if",
680
]
681
682
[[package]]
683
name = "crossbeam-channel"
···
762
"proc-macro2",
763
"quote",
764
"strsim",
765
-
"syn 2.0.111",
766
]
767
768
[[package]]
···
773
dependencies = [
774
"darling_core",
775
"quote",
776
-
"syn 2.0.111",
777
]
778
779
[[package]]
···
813
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
814
dependencies = [
815
"data-encoding",
816
-
"syn 2.0.111",
817
]
818
819
[[package]]
···
844
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
845
dependencies = [
846
"powerfmt",
847
-
"serde_core",
848
]
849
850
[[package]]
···
864
dependencies = [
865
"proc-macro2",
866
"quote",
867
-
"syn 2.0.111",
868
"unicode-xid",
869
]
870
···
915
dependencies = [
916
"proc-macro2",
917
"quote",
918
-
"syn 2.0.111",
919
]
920
921
[[package]]
922
-
name = "dyn-clone"
923
-
version = "1.0.20"
924
-
source = "registry+https://github.com/rust-lang/crates.io-index"
925
-
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
926
-
927
-
[[package]]
928
name = "ecdsa"
929
version = "0.16.9"
930
source = "registry+https://github.com/rust-lang/crates.io-index"
···
959
]
960
961
[[package]]
962
name = "encoding_rs"
963
version = "0.8.35"
964
source = "registry+https://github.com/rust-lang/crates.io-index"
···
976
"heck 0.5.0",
977
"proc-macro2",
978
"quote",
979
-
"syn 2.0.111",
980
]
981
982
[[package]]
···
1025
1026
[[package]]
1027
name = "find-msvc-tools"
1028
-
version = "0.1.5"
1029
source = "registry+https://github.com/rust-lang/crates.io-index"
1030
-
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
1031
1032
[[package]]
1033
name = "flate2"
···
1046
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
1047
1048
[[package]]
1049
name = "form_urlencoded"
1050
version = "1.2.2"
1051
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1146
dependencies = [
1147
"proc-macro2",
1148
"quote",
1149
-
"syn 2.0.111",
1150
]
1151
1152
[[package]]
···
1181
1182
[[package]]
1183
name = "generator"
1184
-
version = "0.8.7"
1185
source = "registry+https://github.com/rust-lang/crates.io-index"
1186
-
checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
1187
dependencies = [
1188
"cc",
1189
"cfg-if",
1190
"libc",
1191
"log",
1192
"rustversion",
1193
-
"windows",
1194
]
1195
1196
[[package]]
···
1300
1301
[[package]]
1302
name = "h2"
1303
-
version = "0.4.12"
1304
source = "registry+https://github.com/rust-lang/crates.io-index"
1305
-
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
1306
dependencies = [
1307
"atomic-waker",
1308
"bytes",
···
1310
"futures-core",
1311
"futures-sink",
1312
"http",
1313
-
"indexmap 2.12.1",
1314
"slab",
1315
"tokio",
1316
"tokio-util",
···
1329
]
1330
1331
[[package]]
1332
-
name = "hashbrown"
1333
-
version = "0.12.3"
1334
source = "registry+https://github.com/rust-lang/crates.io-index"
1335
-
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
1336
1337
[[package]]
1338
name = "hashbrown"
···
1342
1343
[[package]]
1344
name = "hashbrown"
1345
version = "0.16.1"
1346
source = "registry+https://github.com/rust-lang/crates.io-index"
1347
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1348
1349
[[package]]
1350
name = "heck"
1351
version = "0.4.1"
1352
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1371
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1372
1373
[[package]]
1374
-
name = "hex_fmt"
1375
-
version = "0.3.0"
1376
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1377
-
checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f"
1378
-
1379
-
[[package]]
1380
name = "hickory-proto"
1381
version = "0.24.4"
1382
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1441
"markup5ever",
1442
"proc-macro2",
1443
"quote",
1444
-
"syn 2.0.111",
1445
]
1446
1447
[[package]]
···
1537
1538
[[package]]
1539
name = "hyper-util"
1540
-
version = "0.1.18"
1541
source = "registry+https://github.com/rust-lang/crates.io-index"
1542
-
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
1543
dependencies = [
1544
"base64 0.22.1",
1545
"bytes",
···
1573
"js-sys",
1574
"log",
1575
"wasm-bindgen",
1576
-
"windows-core 0.62.2",
1577
]
1578
1579
[[package]]
···
1633
1634
[[package]]
1635
name = "icu_properties"
1636
-
version = "2.1.1"
1637
source = "registry+https://github.com/rust-lang/crates.io-index"
1638
-
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
1639
dependencies = [
1640
"icu_collections",
1641
"icu_locale_core",
···
1647
1648
[[package]]
1649
name = "icu_properties_data"
1650
-
version = "2.1.1"
1651
source = "registry+https://github.com/rust-lang/crates.io-index"
1652
-
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
1653
1654
[[package]]
1655
name = "icu_provider"
···
1711
1712
[[package]]
1713
name = "indexmap"
1714
-
version = "1.9.3"
1715
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1716
-
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
1717
-
dependencies = [
1718
-
"autocfg",
1719
-
"hashbrown 0.12.3",
1720
-
"serde",
1721
-
]
1722
-
1723
-
[[package]]
1724
-
name = "indexmap"
1725
version = "2.12.1"
1726
source = "registry+https://github.com/rust-lang/crates.io-index"
1727
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
1728
dependencies = [
1729
"equivalent",
1730
"hashbrown 0.16.1",
1731
-
"serde",
1732
-
"serde_core",
1733
]
1734
1735
[[package]]
1736
-
name = "indoc"
1737
-
version = "2.0.7"
1738
source = "registry+https://github.com/rust-lang/crates.io-index"
1739
-
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
1740
dependencies = [
1741
-
"rustversion",
1742
]
1743
1744
[[package]]
···
1781
1782
[[package]]
1783
name = "iri-string"
1784
-
version = "0.7.9"
1785
source = "registry+https://github.com/rust-lang/crates.io-index"
1786
-
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
1787
dependencies = [
1788
"memchr",
1789
"serde",
···
1803
1804
[[package]]
1805
name = "itoa"
1806
-
version = "1.0.15"
1807
source = "registry+https://github.com/rust-lang/crates.io-index"
1808
-
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1809
1810
[[package]]
1811
name = "jacquard"
1812
-
version = "0.9.3"
1813
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1814
dependencies = [
1815
"bytes",
1816
"getrandom 0.2.16",
···
1839
1840
[[package]]
1841
name = "jacquard-api"
1842
-
version = "0.9.2"
1843
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1844
dependencies = [
1845
"bon",
1846
"bytes",
···
1850
"miette",
1851
"rustversion",
1852
"serde",
1853
"serde_ipld_dagcbor",
1854
"thiserror 2.0.17",
1855
"unicode-segmentation",
···
1857
1858
[[package]]
1859
name = "jacquard-common"
1860
-
version = "0.9.2"
1861
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1862
dependencies = [
1863
"base64 0.22.1",
1864
"bon",
1865
"bytes",
1866
"chrono",
1867
"ciborium",
1868
"cid",
1869
"futures",
1870
"getrandom 0.2.16",
1871
"getrandom 0.3.4",
1872
"http",
1873
"ipld-core",
1874
"k256",
1875
-
"langtag",
1876
"miette",
1877
"multibase",
1878
"multihash",
1879
"n0-future 0.1.3",
1880
"ouroboros",
1881
"p256",
1882
"rand 0.9.2",
1883
"regex",
1884
"regex-lite",
1885
"reqwest",
1886
"serde",
1887
"serde_html_form",
1888
"serde_ipld_dagcbor",
1889
"serde_json",
1890
"signature",
1891
"smol_str",
1892
"thiserror 2.0.17",
1893
"tokio",
1894
"tokio-tungstenite-wasm",
···
1899
1900
[[package]]
1901
name = "jacquard-derive"
1902
-
version = "0.9.3"
1903
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1904
dependencies = [
1905
"heck 0.5.0",
1906
"jacquard-lexicon",
1907
"proc-macro2",
1908
"quote",
1909
-
"syn 2.0.111",
1910
]
1911
1912
[[package]]
1913
name = "jacquard-identity"
1914
-
version = "0.9.2"
1915
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1916
dependencies = [
1917
"bon",
1918
"bytes",
···
1922
"jacquard-common",
1923
"jacquard-lexicon",
1924
"miette",
1925
-
"mini-moka",
1926
"percent-encoding",
1927
"reqwest",
1928
"serde",
···
1937
1938
[[package]]
1939
name = "jacquard-lexicon"
1940
-
version = "0.9.2"
1941
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1942
dependencies = [
1943
"cid",
1944
"dashmap",
···
1956
"serde_repr",
1957
"serde_with",
1958
"sha2",
1959
-
"syn 2.0.111",
1960
"thiserror 2.0.17",
1961
"unicode-segmentation",
1962
]
1963
1964
[[package]]
1965
name = "jacquard-oauth"
1966
-
version = "0.9.2"
1967
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
1968
dependencies = [
1969
"base64 0.22.1",
1970
"bytes",
···
2052
2053
[[package]]
2054
name = "js-sys"
2055
-
version = "0.3.82"
2056
source = "registry+https://github.com/rust-lang/crates.io-index"
2057
-
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
2058
dependencies = [
2059
"once_cell",
2060
"wasm-bindgen",
···
2073
]
2074
2075
[[package]]
2076
-
name = "langtag"
2077
-
version = "0.4.0"
2078
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2079
-
checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600"
2080
-
dependencies = [
2081
-
"serde",
2082
-
"static-regular-grammar",
2083
-
"thiserror 1.0.69",
2084
-
]
2085
-
2086
-
[[package]]
2087
name = "lazy_static"
2088
version = "1.5.0"
2089
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2094
2095
[[package]]
2096
name = "libc"
2097
-
version = "0.2.177"
2098
source = "registry+https://github.com/rust-lang/crates.io-index"
2099
-
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
2100
2101
[[package]]
2102
name = "libm"
···
2106
2107
[[package]]
2108
name = "libredox"
2109
-
version = "0.1.10"
2110
source = "registry+https://github.com/rust-lang/crates.io-index"
2111
-
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
2112
dependencies = [
2113
"bitflags",
2114
"libc",
2115
-
"redox_syscall",
2116
]
2117
2118
[[package]]
···
2144
2145
[[package]]
2146
name = "log"
2147
-
version = "0.4.28"
2148
source = "registry+https://github.com/rust-lang/crates.io-index"
2149
-
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
2150
2151
[[package]]
2152
name = "loom"
···
2183
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
2184
2185
[[package]]
2186
name = "markup5ever"
2187
version = "0.12.1"
2188
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2267
dependencies = [
2268
"proc-macro2",
2269
"quote",
2270
-
"syn 2.0.111",
2271
]
2272
2273
[[package]]
···
2287
]
2288
2289
[[package]]
2290
-
name = "mini-moka"
2291
version = "0.10.99"
2292
-
source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e"
2293
dependencies = [
2294
"crossbeam-channel",
2295
"crossbeam-utils",
···
2301
]
2302
2303
[[package]]
2304
-
name = "minimal-lexical"
2305
-
version = "0.2.1"
2306
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2307
-
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
2308
-
2309
-
[[package]]
2310
name = "miniz_oxide"
2311
version = "0.8.9"
2312
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2318
2319
[[package]]
2320
name = "mio"
2321
-
version = "1.1.0"
2322
source = "registry+https://github.com/rust-lang/crates.io-index"
2323
-
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
2324
dependencies = [
2325
"libc",
2326
"wasi",
···
2369
]
2370
2371
[[package]]
2372
name = "n0-future"
2373
version = "0.1.3"
2374
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2421
version = "1.0.6"
2422
source = "registry+https://github.com/rust-lang/crates.io-index"
2423
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
2424
-
2425
-
[[package]]
2426
-
name = "nom"
2427
-
version = "7.1.3"
2428
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2429
-
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
2430
-
dependencies = [
2431
-
"memchr",
2432
-
"minimal-lexical",
2433
-
]
2434
2435
[[package]]
2436
name = "nu-ansi-term"
···
2513
]
2514
2515
[[package]]
2516
name = "objc2"
2517
version = "0.6.3"
2518
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2560
2561
[[package]]
2562
name = "openssl-probe"
2563
-
version = "0.1.6"
2564
source = "registry+https://github.com/rust-lang/crates.io-index"
2565
-
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
2566
2567
[[package]]
2568
name = "option-ext"
···
2591
"proc-macro2",
2592
"proc-macro2-diagnostics",
2593
"quote",
2594
-
"syn 2.0.111",
2595
]
2596
2597
[[package]]
···
2601
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
2602
2603
[[package]]
2604
name = "p256"
2605
version = "0.13.2"
2606
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2646
dependencies = [
2647
"cfg-if",
2648
"libc",
2649
-
"redox_syscall",
2650
"smallvec",
2651
-
"windows-link 0.2.1",
2652
]
2653
2654
[[package]]
···
2721
dependencies = [
2722
"proc-macro2",
2723
"quote",
2724
-
"syn 2.0.111",
2725
]
2726
2727
[[package]]
···
2755
dependencies = [
2756
"der",
2757
"spki",
2758
]
2759
2760
[[package]]
···
2794
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
2795
dependencies = [
2796
"proc-macro2",
2797
-
"syn 2.0.111",
2798
]
2799
2800
[[package]]
···
2807
]
2808
2809
[[package]]
2810
-
name = "proc-macro-error"
2811
-
version = "1.0.4"
2812
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2813
-
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
2814
-
dependencies = [
2815
-
"proc-macro-error-attr",
2816
-
"proc-macro2",
2817
-
"quote",
2818
-
"syn 1.0.109",
2819
-
"version_check",
2820
-
]
2821
-
2822
-
[[package]]
2823
-
name = "proc-macro-error-attr"
2824
-
version = "1.0.4"
2825
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2826
-
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
2827
-
dependencies = [
2828
-
"proc-macro2",
2829
-
"quote",
2830
-
"version_check",
2831
-
]
2832
-
2833
-
[[package]]
2834
name = "proc-macro2"
2835
-
version = "1.0.103"
2836
source = "registry+https://github.com/rust-lang/crates.io-index"
2837
-
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
2838
dependencies = [
2839
"unicode-ident",
2840
]
···
2847
dependencies = [
2848
"proc-macro2",
2849
"quote",
2850
-
"syn 2.0.111",
2851
"version_check",
2852
"yansi",
2853
]
···
2915
2916
[[package]]
2917
name = "quote"
2918
-
version = "1.0.42"
2919
source = "registry+https://github.com/rust-lang/crates.io-index"
2920
-
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
2921
dependencies = [
2922
"proc-macro2",
2923
]
···
2988
]
2989
2990
[[package]]
2991
-
name = "range-traits"
2992
-
version = "0.3.2"
2993
source = "registry+https://github.com/rust-lang/crates.io-index"
2994
-
checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab"
2995
2996
[[package]]
2997
name = "redox_syscall"
2998
-
version = "0.5.18"
2999
source = "registry+https://github.com/rust-lang/crates.io-index"
3000
-
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
3001
dependencies = [
3002
"bitflags",
3003
]
···
3014
]
3015
3016
[[package]]
3017
-
name = "ref-cast"
3018
-
version = "1.0.25"
3019
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3020
-
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
3021
-
dependencies = [
3022
-
"ref-cast-impl",
3023
-
]
3024
-
3025
-
[[package]]
3026
-
name = "ref-cast-impl"
3027
-
version = "1.0.25"
3028
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3029
-
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
3030
-
dependencies = [
3031
-
"proc-macro2",
3032
-
"quote",
3033
-
"syn 2.0.111",
3034
-
]
3035
-
3036
-
[[package]]
3037
name = "regex"
3038
version = "1.12.2"
3039
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3070
3071
[[package]]
3072
name = "reqwest"
3073
-
version = "0.12.24"
3074
source = "registry+https://github.com/rust-lang/crates.io-index"
3075
-
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
3076
dependencies = [
3077
-
"async-compression",
3078
"base64 0.22.1",
3079
"bytes",
3080
"encoding_rs",
···
3169
3170
[[package]]
3171
name = "rsa"
3172
-
version = "0.9.9"
3173
source = "registry+https://github.com/rust-lang/crates.io-index"
3174
-
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
3175
dependencies = [
3176
"const-oid",
3177
"digest",
···
3198
version = "2.1.1"
3199
source = "registry+https://github.com/rust-lang/crates.io-index"
3200
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
3201
3202
[[package]]
3203
name = "rustix"
3204
-
version = "1.1.2"
3205
source = "registry+https://github.com/rust-lang/crates.io-index"
3206
-
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
3207
dependencies = [
3208
"bitflags",
3209
"errno",
···
3214
3215
[[package]]
3216
name = "rustls"
3217
-
version = "0.23.35"
3218
source = "registry+https://github.com/rust-lang/crates.io-index"
3219
-
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
3220
dependencies = [
3221
"once_cell",
3222
"ring",
···
3228
3229
[[package]]
3230
name = "rustls-native-certs"
3231
-
version = "0.8.2"
3232
source = "registry+https://github.com/rust-lang/crates.io-index"
3233
-
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
3234
dependencies = [
3235
"openssl-probe",
3236
"rustls-pki-types",
···
3240
3241
[[package]]
3242
name = "rustls-pki-types"
3243
-
version = "1.13.0"
3244
source = "registry+https://github.com/rust-lang/crates.io-index"
3245
-
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
3246
dependencies = [
3247
"web-time",
3248
"zeroize",
···
3267
3268
[[package]]
3269
name = "ryu"
3270
-
version = "1.0.20"
3271
source = "registry+https://github.com/rust-lang/crates.io-index"
3272
-
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
3273
3274
[[package]]
3275
name = "safemem"
···
3296
]
3297
3298
[[package]]
3299
-
name = "schemars"
3300
-
version = "0.9.0"
3301
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3302
-
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
3303
-
dependencies = [
3304
-
"dyn-clone",
3305
-
"ref-cast",
3306
-
"serde",
3307
-
"serde_json",
3308
-
]
3309
-
3310
-
[[package]]
3311
-
name = "schemars"
3312
-
version = "1.1.0"
3313
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3314
-
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
3315
-
dependencies = [
3316
-
"dyn-clone",
3317
-
"ref-cast",
3318
-
"serde",
3319
-
"serde_json",
3320
-
]
3321
-
3322
-
[[package]]
3323
name = "scoped-tls"
3324
version = "1.0.1"
3325
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3367
"core-foundation-sys",
3368
"libc",
3369
]
3370
3371
[[package]]
3372
name = "send_wrapper"
···
3411
dependencies = [
3412
"proc-macro2",
3413
"quote",
3414
-
"syn 2.0.111",
3415
]
3416
3417
[[package]]
3418
name = "serde_html_form"
3419
-
version = "0.2.8"
3420
source = "registry+https://github.com/rust-lang/crates.io-index"
3421
-
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
3422
dependencies = [
3423
"form_urlencoded",
3424
-
"indexmap 2.12.1",
3425
"itoa",
3426
-
"ryu",
3427
"serde_core",
3428
]
3429
···
3441
3442
[[package]]
3443
name = "serde_json"
3444
-
version = "1.0.145"
3445
source = "registry+https://github.com/rust-lang/crates.io-index"
3446
-
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
3447
dependencies = [
3448
"itoa",
3449
"memchr",
3450
-
"ryu",
3451
"serde",
3452
"serde_core",
3453
]
3454
3455
[[package]]
···
3471
dependencies = [
3472
"proc-macro2",
3473
"quote",
3474
-
"syn 2.0.111",
3475
]
3476
3477
[[package]]
···
3488
3489
[[package]]
3490
name = "serde_with"
3491
-
version = "3.16.0"
3492
source = "registry+https://github.com/rust-lang/crates.io-index"
3493
-
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
3494
dependencies = [
3495
"base64 0.22.1",
3496
"chrono",
3497
"hex",
3498
-
"indexmap 1.9.3",
3499
-
"indexmap 2.12.1",
3500
-
"schemars 0.9.0",
3501
-
"schemars 1.1.0",
3502
"serde_core",
3503
"serde_json",
3504
"serde_with_macros",
···
3507
3508
[[package]]
3509
name = "serde_with_macros"
3510
-
version = "3.16.0"
3511
source = "registry+https://github.com/rust-lang/crates.io-index"
3512
-
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
3513
dependencies = [
3514
"darling",
3515
"proc-macro2",
3516
"quote",
3517
-
"syn 2.0.111",
3518
]
3519
3520
[[package]]
···
3571
3572
[[package]]
3573
name = "signal-hook-registry"
3574
-
version = "1.4.7"
3575
source = "registry+https://github.com/rust-lang/crates.io-index"
3576
-
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
3577
dependencies = [
3578
"libc",
3579
]
3580
···
3590
3591
[[package]]
3592
name = "simd-adler32"
3593
-
version = "0.3.7"
3594
source = "registry+https://github.com/rust-lang/crates.io-index"
3595
-
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
3596
3597
[[package]]
3598
name = "siphasher"
···
3647
version = "0.9.8"
3648
source = "registry+https://github.com/rust-lang/crates.io-index"
3649
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
3650
3651
[[package]]
3652
name = "spin"
···
3671
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
3672
3673
[[package]]
3674
-
name = "static-regular-grammar"
3675
-
version = "2.0.2"
3676
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3677
-
checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957"
3678
-
dependencies = [
3679
-
"abnf",
3680
-
"btree-range-map",
3681
-
"ciborium",
3682
-
"hex_fmt",
3683
-
"indoc",
3684
-
"proc-macro-error",
3685
-
"proc-macro2",
3686
-
"quote",
3687
-
"serde",
3688
-
"sha2",
3689
-
"syn 2.0.111",
3690
-
"thiserror 1.0.69",
3691
-
]
3692
-
3693
-
[[package]]
3694
name = "static_assertions"
3695
version = "1.1.0"
3696
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3744
3745
[[package]]
3746
name = "supports-hyperlinks"
3747
-
version = "3.1.0"
3748
source = "registry+https://github.com/rust-lang/crates.io-index"
3749
-
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
3750
3751
[[package]]
3752
name = "supports-unicode"
···
3767
3768
[[package]]
3769
name = "syn"
3770
-
version = "2.0.111"
3771
source = "registry+https://github.com/rust-lang/crates.io-index"
3772
-
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
3773
dependencies = [
3774
"proc-macro2",
3775
"quote",
···
3793
dependencies = [
3794
"proc-macro2",
3795
"quote",
3796
-
"syn 2.0.111",
3797
]
3798
3799
[[package]]
···
3825
3826
[[package]]
3827
name = "tempfile"
3828
-
version = "3.23.0"
3829
source = "registry+https://github.com/rust-lang/crates.io-index"
3830
-
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
3831
dependencies = [
3832
"fastrand",
3833
"getrandom 0.3.4",
···
3893
dependencies = [
3894
"proc-macro2",
3895
"quote",
3896
-
"syn 2.0.111",
3897
]
3898
3899
[[package]]
···
3904
dependencies = [
3905
"proc-macro2",
3906
"quote",
3907
-
"syn 2.0.111",
3908
]
3909
3910
[[package]]
···
3932
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
3933
dependencies = [
3934
"deranged",
3935
-
"itoa",
3936
"libc",
3937
"num-conv",
3938
"num_threads",
3939
"powerfmt",
3940
"serde",
3941
"time-core",
3942
-
"time-macros",
3943
]
3944
3945
[[package]]
···
3947
version = "0.1.6"
3948
source = "registry+https://github.com/rust-lang/crates.io-index"
3949
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
3950
-
3951
-
[[package]]
3952
-
name = "time-macros"
3953
-
version = "0.2.24"
3954
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3955
-
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
3956
-
dependencies = [
3957
-
"num-conv",
3958
-
"time-core",
3959
-
]
3960
3961
[[package]]
3962
name = "tiny_http"
···
3997
3998
[[package]]
3999
name = "tokio"
4000
-
version = "1.48.0"
4001
source = "registry+https://github.com/rust-lang/crates.io-index"
4002
-
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
4003
dependencies = [
4004
"bytes",
4005
"libc",
···
4020
dependencies = [
4021
"proc-macro2",
4022
"quote",
4023
-
"syn 2.0.111",
4024
]
4025
4026
[[package]]
···
4070
4071
[[package]]
4072
name = "tokio-util"
4073
-
version = "0.7.17"
4074
source = "registry+https://github.com/rust-lang/crates.io-index"
4075
-
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
4076
dependencies = [
4077
"bytes",
4078
"futures-core",
···
4100
4101
[[package]]
4102
name = "tower-http"
4103
-
version = "0.6.7"
4104
source = "registry+https://github.com/rust-lang/crates.io-index"
4105
-
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
4106
dependencies = [
4107
"async-compression",
4108
"bitflags",
···
4141
4142
[[package]]
4143
name = "tracing"
4144
-
version = "0.1.41"
4145
source = "registry+https://github.com/rust-lang/crates.io-index"
4146
-
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
4147
dependencies = [
4148
"log",
4149
"pin-project-lite",
···
4153
4154
[[package]]
4155
name = "tracing-attributes"
4156
-
version = "0.1.30"
4157
source = "registry+https://github.com/rust-lang/crates.io-index"
4158
-
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
4159
dependencies = [
4160
"proc-macro2",
4161
"quote",
4162
-
"syn 2.0.111",
4163
]
4164
4165
[[package]]
4166
name = "tracing-core"
4167
-
version = "0.1.34"
4168
source = "registry+https://github.com/rust-lang/crates.io-index"
4169
-
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
4170
dependencies = [
4171
"once_cell",
4172
"valuable",
···
4185
4186
[[package]]
4187
name = "tracing-subscriber"
4188
-
version = "0.3.20"
4189
source = "registry+https://github.com/rust-lang/crates.io-index"
4190
-
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
4191
dependencies = [
4192
"matchers",
4193
"nu-ansi-term",
···
4209
dependencies = [
4210
"proc-macro2",
4211
"quote",
4212
-
"syn 2.0.111",
4213
]
4214
4215
[[package]]
···
4261
4262
[[package]]
4263
name = "unicase"
4264
-
version = "2.8.1"
4265
source = "registry+https://github.com/rust-lang/crates.io-index"
4266
-
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
4267
4268
[[package]]
4269
name = "unicode-ident"
···
4315
4316
[[package]]
4317
name = "url"
4318
-
version = "2.5.7"
4319
source = "registry+https://github.com/rust-lang/crates.io-index"
4320
-
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
4321
dependencies = [
4322
"form_urlencoded",
4323
"idna",
4324
"percent-encoding",
4325
"serde",
4326
]
4327
4328
[[package]]
···
4397
4398
[[package]]
4399
name = "wasm-bindgen"
4400
-
version = "0.2.105"
4401
source = "registry+https://github.com/rust-lang/crates.io-index"
4402
-
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
4403
dependencies = [
4404
"cfg-if",
4405
"once_cell",
···
4410
4411
[[package]]
4412
name = "wasm-bindgen-futures"
4413
-
version = "0.4.55"
4414
source = "registry+https://github.com/rust-lang/crates.io-index"
4415
-
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
4416
dependencies = [
4417
"cfg-if",
4418
"js-sys",
···
4423
4424
[[package]]
4425
name = "wasm-bindgen-macro"
4426
-
version = "0.2.105"
4427
source = "registry+https://github.com/rust-lang/crates.io-index"
4428
-
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
4429
dependencies = [
4430
"quote",
4431
"wasm-bindgen-macro-support",
···
4433
4434
[[package]]
4435
name = "wasm-bindgen-macro-support"
4436
-
version = "0.2.105"
4437
source = "registry+https://github.com/rust-lang/crates.io-index"
4438
-
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
4439
dependencies = [
4440
"bumpalo",
4441
"proc-macro2",
4442
"quote",
4443
-
"syn 2.0.111",
4444
"wasm-bindgen-shared",
4445
]
4446
4447
[[package]]
4448
name = "wasm-bindgen-shared"
4449
-
version = "0.2.105"
4450
source = "registry+https://github.com/rust-lang/crates.io-index"
4451
-
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
4452
dependencies = [
4453
"unicode-ident",
4454
]
···
4468
4469
[[package]]
4470
name = "web-sys"
4471
-
version = "0.3.82"
4472
source = "registry+https://github.com/rust-lang/crates.io-index"
4473
-
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
4474
dependencies = [
4475
"js-sys",
4476
"wasm-bindgen",
···
4516
4517
[[package]]
4518
name = "webpki-roots"
4519
-
version = "1.0.4"
4520
source = "registry+https://github.com/rust-lang/crates.io-index"
4521
-
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
4522
dependencies = [
4523
"rustls-pki-types",
4524
]
···
4539
]
4540
4541
[[package]]
4542
-
name = "windows"
4543
-
version = "0.61.3"
4544
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4545
-
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
4546
-
dependencies = [
4547
-
"windows-collections",
4548
-
"windows-core 0.61.2",
4549
-
"windows-future",
4550
-
"windows-link 0.1.3",
4551
-
"windows-numerics",
4552
-
]
4553
-
4554
-
[[package]]
4555
-
name = "windows-collections"
4556
-
version = "0.2.0"
4557
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4558
-
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
4559
-
dependencies = [
4560
-
"windows-core 0.61.2",
4561
-
]
4562
-
4563
-
[[package]]
4564
-
name = "windows-core"
4565
-
version = "0.61.2"
4566
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4567
-
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
4568
-
dependencies = [
4569
-
"windows-implement",
4570
-
"windows-interface",
4571
-
"windows-link 0.1.3",
4572
-
"windows-result 0.3.4",
4573
-
"windows-strings 0.4.2",
4574
-
]
4575
-
4576
-
[[package]]
4577
name = "windows-core"
4578
version = "0.62.2"
4579
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4581
dependencies = [
4582
"windows-implement",
4583
"windows-interface",
4584
-
"windows-link 0.2.1",
4585
-
"windows-result 0.4.1",
4586
-
"windows-strings 0.5.1",
4587
-
]
4588
-
4589
-
[[package]]
4590
-
name = "windows-future"
4591
-
version = "0.2.1"
4592
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4593
-
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
4594
-
dependencies = [
4595
-
"windows-core 0.61.2",
4596
-
"windows-link 0.1.3",
4597
-
"windows-threading",
4598
]
4599
4600
[[package]]
···
4605
dependencies = [
4606
"proc-macro2",
4607
"quote",
4608
-
"syn 2.0.111",
4609
]
4610
4611
[[package]]
···
4616
dependencies = [
4617
"proc-macro2",
4618
"quote",
4619
-
"syn 2.0.111",
4620
]
4621
4622
[[package]]
4623
name = "windows-link"
4624
-
version = "0.1.3"
4625
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4626
-
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
4627
-
4628
-
[[package]]
4629
-
name = "windows-link"
4630
version = "0.2.1"
4631
source = "registry+https://github.com/rust-lang/crates.io-index"
4632
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
4633
4634
[[package]]
4635
-
name = "windows-numerics"
4636
-
version = "0.2.0"
4637
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4638
-
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
4639
-
dependencies = [
4640
-
"windows-core 0.61.2",
4641
-
"windows-link 0.1.3",
4642
-
]
4643
-
4644
-
[[package]]
4645
name = "windows-registry"
4646
version = "0.6.1"
4647
source = "registry+https://github.com/rust-lang/crates.io-index"
4648
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
4649
dependencies = [
4650
-
"windows-link 0.2.1",
4651
-
"windows-result 0.4.1",
4652
-
"windows-strings 0.5.1",
4653
-
]
4654
-
4655
-
[[package]]
4656
-
name = "windows-result"
4657
-
version = "0.3.4"
4658
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4659
-
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
4660
-
dependencies = [
4661
-
"windows-link 0.1.3",
4662
]
4663
4664
[[package]]
···
4667
source = "registry+https://github.com/rust-lang/crates.io-index"
4668
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
4669
dependencies = [
4670
-
"windows-link 0.2.1",
4671
-
]
4672
-
4673
-
[[package]]
4674
-
name = "windows-strings"
4675
-
version = "0.4.2"
4676
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4677
-
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
4678
-
dependencies = [
4679
-
"windows-link 0.1.3",
4680
]
4681
4682
[[package]]
···
4685
source = "registry+https://github.com/rust-lang/crates.io-index"
4686
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
4687
dependencies = [
4688
-
"windows-link 0.2.1",
4689
]
4690
4691
[[package]]
···
4717
4718
[[package]]
4719
name = "windows-sys"
4720
version = "0.60.2"
4721
source = "registry+https://github.com/rust-lang/crates.io-index"
4722
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
···
4730
source = "registry+https://github.com/rust-lang/crates.io-index"
4731
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
4732
dependencies = [
4733
-
"windows-link 0.2.1",
4734
]
4735
4736
[[package]]
···
4785
source = "registry+https://github.com/rust-lang/crates.io-index"
4786
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
4787
dependencies = [
4788
-
"windows-link 0.2.1",
4789
"windows_aarch64_gnullvm 0.53.1",
4790
"windows_aarch64_msvc 0.53.1",
4791
"windows_i686_gnu 0.53.1",
···
4794
"windows_x86_64_gnu 0.53.1",
4795
"windows_x86_64_gnullvm 0.53.1",
4796
"windows_x86_64_msvc 0.53.1",
4797
-
]
4798
-
4799
-
[[package]]
4800
-
name = "windows-threading"
4801
-
version = "0.1.0"
4802
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4803
-
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
4804
-
dependencies = [
4805
-
"windows-link 0.1.3",
4806
]
4807
4808
[[package]]
···
5008
"futures",
5009
"globset",
5010
"ignore",
5011
"jacquard",
5012
"jacquard-api",
5013
"jacquard-common",
···
5032
"tower-http",
5033
"url",
5034
"walkdir",
5035
]
5036
5037
[[package]]
···
5082
dependencies = [
5083
"proc-macro2",
5084
"quote",
5085
-
"syn 2.0.111",
5086
"synstructure",
5087
]
5088
5089
[[package]]
5090
name = "zerocopy"
5091
-
version = "0.8.30"
5092
source = "registry+https://github.com/rust-lang/crates.io-index"
5093
-
checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c"
5094
dependencies = [
5095
"zerocopy-derive",
5096
]
5097
5098
[[package]]
5099
name = "zerocopy-derive"
5100
-
version = "0.8.30"
5101
source = "registry+https://github.com/rust-lang/crates.io-index"
5102
-
checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5"
5103
dependencies = [
5104
"proc-macro2",
5105
"quote",
5106
-
"syn 2.0.111",
5107
]
5108
5109
[[package]]
···
5123
dependencies = [
5124
"proc-macro2",
5125
"quote",
5126
-
"syn 2.0.111",
5127
"synstructure",
5128
]
5129
···
5166
dependencies = [
5167
"proc-macro2",
5168
"quote",
5169
-
"syn 2.0.111",
5170
]
···
3
version = 4
4
5
[[package]]
6
name = "addr2line"
7
version = "0.25.1"
8
source = "registry+https://github.com/rust-lang/crates.io-index"
···
52
dependencies = [
53
"alloc-no-stdlib",
54
]
55
+
56
+
[[package]]
57
+
name = "allocator-api2"
58
+
version = "0.2.21"
59
+
source = "registry+https://github.com/rust-lang/crates.io-index"
60
+
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
61
62
[[package]]
63
name = "android_system_properties"
···
126
127
[[package]]
128
name = "async-compression"
129
+
version = "0.4.36"
130
source = "registry+https://github.com/rust-lang/crates.io-index"
131
+
checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37"
132
dependencies = [
133
"compression-codecs",
134
"compression-core",
···
145
dependencies = [
146
"proc-macro2",
147
"quote",
148
+
"syn 2.0.113",
149
+
]
150
+
151
+
[[package]]
152
+
name = "atomic-polyfill"
153
+
version = "1.0.3"
154
+
source = "registry+https://github.com/rust-lang/crates.io-index"
155
+
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
156
+
dependencies = [
157
+
"critical-section",
158
]
159
160
[[package]]
···
171
172
[[package]]
173
name = "axum"
174
+
version = "0.8.8"
175
source = "registry+https://github.com/rust-lang/crates.io-index"
176
+
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
177
dependencies = [
178
"axum-core",
179
"bytes",
···
204
205
[[package]]
206
name = "axum-core"
207
+
version = "0.5.6"
208
source = "registry+https://github.com/rust-lang/crates.io-index"
209
+
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
210
dependencies = [
211
"bytes",
212
"futures-core",
···
233
"miniz_oxide",
234
"object",
235
"rustc-demangle",
236
+
"windows-link",
237
]
238
239
[[package]]
···
281
282
[[package]]
283
name = "base64ct"
284
+
version = "1.8.2"
285
source = "registry+https://github.com/rust-lang/crates.io-index"
286
+
checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82"
287
288
[[package]]
289
name = "bitflags"
···
322
"proc-macro2",
323
"quote",
324
"rustversion",
325
+
"syn 2.0.113",
326
]
327
328
[[package]]
329
name = "borsh"
330
+
version = "1.6.0"
331
source = "registry+https://github.com/rust-lang/crates.io-index"
332
+
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
333
dependencies = [
334
"cfg_aliases",
335
]
···
366
]
367
368
[[package]]
369
name = "buf_redux"
370
version = "0.8.4"
371
source = "registry+https://github.com/rust-lang/crates.io-index"
···
377
378
[[package]]
379
name = "bumpalo"
380
+
version = "3.19.1"
381
source = "registry+https://github.com/rust-lang/crates.io-index"
382
+
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
383
384
[[package]]
385
name = "byteorder"
···
407
408
[[package]]
409
name = "cc"
410
+
version = "1.2.51"
411
source = "registry+https://github.com/rust-lang/crates.io-index"
412
+
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
413
dependencies = [
414
"find-msvc-tools",
415
"shlex",
416
]
417
418
[[package]]
419
name = "cesu8"
420
version = "1.1.0"
421
source = "registry+https://github.com/rust-lang/crates.io-index"
···
444
"num-traits",
445
"serde",
446
"wasm-bindgen",
447
+
"windows-link",
448
]
449
450
[[package]]
···
496
497
[[package]]
498
name = "clap"
499
+
version = "4.5.54"
500
source = "registry+https://github.com/rust-lang/crates.io-index"
501
+
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
502
dependencies = [
503
"clap_builder",
504
"clap_derive",
···
506
507
[[package]]
508
name = "clap_builder"
509
+
version = "4.5.54"
510
source = "registry+https://github.com/rust-lang/crates.io-index"
511
+
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
512
dependencies = [
513
"anstream",
514
"anstyle",
···
525
"heck 0.5.0",
526
"proc-macro2",
527
"quote",
528
+
"syn 2.0.113",
529
]
530
531
[[package]]
···
535
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
536
537
[[package]]
538
+
name = "cobs"
539
+
version = "0.3.0"
540
+
source = "registry+https://github.com/rust-lang/crates.io-index"
541
+
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
542
+
dependencies = [
543
+
"thiserror 2.0.17",
544
+
]
545
+
546
+
[[package]]
547
name = "colorchoice"
548
version = "1.0.4"
549
source = "registry+https://github.com/rust-lang/crates.io-index"
···
561
562
[[package]]
563
name = "compression-codecs"
564
+
version = "0.4.35"
565
source = "registry+https://github.com/rust-lang/crates.io-index"
566
+
checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2"
567
dependencies = [
568
"compression-core",
569
"flate2",
···
575
version = "0.4.31"
576
source = "registry+https://github.com/rust-lang/crates.io-index"
577
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
578
+
579
+
[[package]]
580
+
name = "console"
581
+
version = "0.15.11"
582
+
source = "registry+https://github.com/rust-lang/crates.io-index"
583
+
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
584
+
dependencies = [
585
+
"encode_unicode",
586
+
"libc",
587
+
"once_cell",
588
+
"unicode-width 0.2.2",
589
+
"windows-sys 0.59.0",
590
+
]
591
592
[[package]]
593
name = "const-oid"
···
663
dependencies = [
664
"cfg-if",
665
]
666
+
667
+
[[package]]
668
+
name = "critical-section"
669
+
version = "1.2.0"
670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
671
+
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
672
673
[[package]]
674
name = "crossbeam-channel"
···
753
"proc-macro2",
754
"quote",
755
"strsim",
756
+
"syn 2.0.113",
757
]
758
759
[[package]]
···
764
dependencies = [
765
"darling_core",
766
"quote",
767
+
"syn 2.0.113",
768
]
769
770
[[package]]
···
804
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
805
dependencies = [
806
"data-encoding",
807
+
"syn 2.0.113",
808
]
809
810
[[package]]
···
835
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
836
dependencies = [
837
"powerfmt",
838
]
839
840
[[package]]
···
854
dependencies = [
855
"proc-macro2",
856
"quote",
857
+
"syn 2.0.113",
858
"unicode-xid",
859
]
860
···
905
dependencies = [
906
"proc-macro2",
907
"quote",
908
+
"syn 2.0.113",
909
]
910
911
[[package]]
912
name = "ecdsa"
913
version = "0.16.9"
914
source = "registry+https://github.com/rust-lang/crates.io-index"
···
943
]
944
945
[[package]]
946
+
name = "embedded-io"
947
+
version = "0.4.0"
948
+
source = "registry+https://github.com/rust-lang/crates.io-index"
949
+
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
950
+
951
+
[[package]]
952
+
name = "embedded-io"
953
+
version = "0.6.1"
954
+
source = "registry+https://github.com/rust-lang/crates.io-index"
955
+
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
956
+
957
+
[[package]]
958
+
name = "encode_unicode"
959
+
version = "1.0.0"
960
+
source = "registry+https://github.com/rust-lang/crates.io-index"
961
+
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
962
+
963
+
[[package]]
964
name = "encoding_rs"
965
version = "0.8.35"
966
source = "registry+https://github.com/rust-lang/crates.io-index"
···
978
"heck 0.5.0",
979
"proc-macro2",
980
"quote",
981
+
"syn 2.0.113",
982
]
983
984
[[package]]
···
1027
1028
[[package]]
1029
name = "find-msvc-tools"
1030
+
version = "0.1.6"
1031
source = "registry+https://github.com/rust-lang/crates.io-index"
1032
+
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
1033
1034
[[package]]
1035
name = "flate2"
···
1048
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
1049
1050
[[package]]
1051
+
name = "foldhash"
1052
+
version = "0.1.5"
1053
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1054
+
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
1055
+
1056
+
[[package]]
1057
name = "form_urlencoded"
1058
version = "1.2.2"
1059
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1154
dependencies = [
1155
"proc-macro2",
1156
"quote",
1157
+
"syn 2.0.113",
1158
]
1159
1160
[[package]]
···
1189
1190
[[package]]
1191
name = "generator"
1192
+
version = "0.8.8"
1193
source = "registry+https://github.com/rust-lang/crates.io-index"
1194
+
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
1195
dependencies = [
1196
"cc",
1197
"cfg-if",
1198
"libc",
1199
"log",
1200
"rustversion",
1201
+
"windows-link",
1202
+
"windows-result",
1203
]
1204
1205
[[package]]
···
1309
1310
[[package]]
1311
name = "h2"
1312
+
version = "0.4.13"
1313
source = "registry+https://github.com/rust-lang/crates.io-index"
1314
+
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
1315
dependencies = [
1316
"atomic-waker",
1317
"bytes",
···
1319
"futures-core",
1320
"futures-sink",
1321
"http",
1322
+
"indexmap",
1323
"slab",
1324
"tokio",
1325
"tokio-util",
···
1338
]
1339
1340
[[package]]
1341
+
name = "hash32"
1342
+
version = "0.2.1"
1343
source = "registry+https://github.com/rust-lang/crates.io-index"
1344
+
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
1345
+
dependencies = [
1346
+
"byteorder",
1347
+
]
1348
1349
[[package]]
1350
name = "hashbrown"
···
1354
1355
[[package]]
1356
name = "hashbrown"
1357
+
version = "0.15.5"
1358
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1359
+
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
1360
+
dependencies = [
1361
+
"allocator-api2",
1362
+
"equivalent",
1363
+
"foldhash",
1364
+
]
1365
+
1366
+
[[package]]
1367
+
name = "hashbrown"
1368
version = "0.16.1"
1369
source = "registry+https://github.com/rust-lang/crates.io-index"
1370
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1371
1372
[[package]]
1373
+
name = "heapless"
1374
+
version = "0.7.17"
1375
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1376
+
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
1377
+
dependencies = [
1378
+
"atomic-polyfill",
1379
+
"hash32",
1380
+
"rustc_version",
1381
+
"serde",
1382
+
"spin 0.9.8",
1383
+
"stable_deref_trait",
1384
+
]
1385
+
1386
+
[[package]]
1387
name = "heck"
1388
version = "0.4.1"
1389
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1408
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1409
1410
[[package]]
1411
name = "hickory-proto"
1412
version = "0.24.4"
1413
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1472
"markup5ever",
1473
"proc-macro2",
1474
"quote",
1475
+
"syn 2.0.113",
1476
]
1477
1478
[[package]]
···
1568
1569
[[package]]
1570
name = "hyper-util"
1571
+
version = "0.1.19"
1572
source = "registry+https://github.com/rust-lang/crates.io-index"
1573
+
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
1574
dependencies = [
1575
"base64 0.22.1",
1576
"bytes",
···
1604
"js-sys",
1605
"log",
1606
"wasm-bindgen",
1607
+
"windows-core",
1608
]
1609
1610
[[package]]
···
1664
1665
[[package]]
1666
name = "icu_properties"
1667
+
version = "2.1.2"
1668
source = "registry+https://github.com/rust-lang/crates.io-index"
1669
+
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
1670
dependencies = [
1671
"icu_collections",
1672
"icu_locale_core",
···
1678
1679
[[package]]
1680
name = "icu_properties_data"
1681
+
version = "2.1.2"
1682
source = "registry+https://github.com/rust-lang/crates.io-index"
1683
+
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
1684
1685
[[package]]
1686
name = "icu_provider"
···
1742
1743
[[package]]
1744
name = "indexmap"
1745
version = "2.12.1"
1746
source = "registry+https://github.com/rust-lang/crates.io-index"
1747
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
1748
dependencies = [
1749
"equivalent",
1750
"hashbrown 0.16.1",
1751
]
1752
1753
[[package]]
1754
+
name = "indicatif"
1755
+
version = "0.17.11"
1756
source = "registry+https://github.com/rust-lang/crates.io-index"
1757
+
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
1758
dependencies = [
1759
+
"console",
1760
+
"number_prefix",
1761
+
"portable-atomic",
1762
+
"unicode-width 0.2.2",
1763
+
"web-time",
1764
]
1765
1766
[[package]]
···
1803
1804
[[package]]
1805
name = "iri-string"
1806
+
version = "0.7.10"
1807
source = "registry+https://github.com/rust-lang/crates.io-index"
1808
+
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
1809
dependencies = [
1810
"memchr",
1811
"serde",
···
1825
1826
[[package]]
1827
name = "itoa"
1828
+
version = "1.0.17"
1829
source = "registry+https://github.com/rust-lang/crates.io-index"
1830
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
1831
1832
[[package]]
1833
name = "jacquard"
1834
+
version = "0.9.5"
1835
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1836
dependencies = [
1837
"bytes",
1838
"getrandom 0.2.16",
···
1861
1862
[[package]]
1863
name = "jacquard-api"
1864
+
version = "0.9.5"
1865
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1866
dependencies = [
1867
"bon",
1868
"bytes",
···
1872
"miette",
1873
"rustversion",
1874
"serde",
1875
+
"serde_bytes",
1876
"serde_ipld_dagcbor",
1877
"thiserror 2.0.17",
1878
"unicode-segmentation",
···
1880
1881
[[package]]
1882
name = "jacquard-common"
1883
+
version = "0.9.5"
1884
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1885
dependencies = [
1886
"base64 0.22.1",
1887
"bon",
1888
"bytes",
1889
"chrono",
1890
"ciborium",
1891
+
"ciborium-io",
1892
"cid",
1893
"futures",
1894
"getrandom 0.2.16",
1895
"getrandom 0.3.4",
1896
+
"hashbrown 0.15.5",
1897
"http",
1898
"ipld-core",
1899
"k256",
1900
+
"maitake-sync",
1901
"miette",
1902
"multibase",
1903
"multihash",
1904
"n0-future 0.1.3",
1905
"ouroboros",
1906
+
"oxilangtag",
1907
"p256",
1908
+
"postcard",
1909
"rand 0.9.2",
1910
"regex",
1911
+
"regex-automata",
1912
"regex-lite",
1913
"reqwest",
1914
"serde",
1915
+
"serde_bytes",
1916
"serde_html_form",
1917
"serde_ipld_dagcbor",
1918
"serde_json",
1919
"signature",
1920
"smol_str",
1921
+
"spin 0.10.0",
1922
"thiserror 2.0.17",
1923
"tokio",
1924
"tokio-tungstenite-wasm",
···
1929
1930
[[package]]
1931
name = "jacquard-derive"
1932
+
version = "0.9.5"
1933
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1934
dependencies = [
1935
"heck 0.5.0",
1936
"jacquard-lexicon",
1937
"proc-macro2",
1938
"quote",
1939
+
"syn 2.0.113",
1940
]
1941
1942
[[package]]
1943
name = "jacquard-identity"
1944
+
version = "0.9.5"
1945
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1946
dependencies = [
1947
"bon",
1948
"bytes",
···
1952
"jacquard-common",
1953
"jacquard-lexicon",
1954
"miette",
1955
+
"mini-moka-wasm",
1956
+
"n0-future 0.1.3",
1957
"percent-encoding",
1958
"reqwest",
1959
"serde",
···
1968
1969
[[package]]
1970
name = "jacquard-lexicon"
1971
+
version = "0.9.5"
1972
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1973
dependencies = [
1974
"cid",
1975
"dashmap",
···
1987
"serde_repr",
1988
"serde_with",
1989
"sha2",
1990
+
"syn 2.0.113",
1991
"thiserror 2.0.17",
1992
"unicode-segmentation",
1993
]
1994
1995
[[package]]
1996
name = "jacquard-oauth"
1997
+
version = "0.9.6"
1998
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
1999
dependencies = [
2000
"base64 0.22.1",
2001
"bytes",
···
2083
2084
[[package]]
2085
name = "js-sys"
2086
+
version = "0.3.83"
2087
source = "registry+https://github.com/rust-lang/crates.io-index"
2088
+
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
2089
dependencies = [
2090
"once_cell",
2091
"wasm-bindgen",
···
2104
]
2105
2106
[[package]]
2107
name = "lazy_static"
2108
version = "1.5.0"
2109
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2114
2115
[[package]]
2116
name = "libc"
2117
+
version = "0.2.179"
2118
source = "registry+https://github.com/rust-lang/crates.io-index"
2119
+
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
2120
2121
[[package]]
2122
name = "libm"
···
2126
2127
[[package]]
2128
name = "libredox"
2129
+
version = "0.1.12"
2130
source = "registry+https://github.com/rust-lang/crates.io-index"
2131
+
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
2132
dependencies = [
2133
"bitflags",
2134
"libc",
2135
+
"redox_syscall 0.7.0",
2136
]
2137
2138
[[package]]
···
2164
2165
[[package]]
2166
name = "log"
2167
+
version = "0.4.29"
2168
source = "registry+https://github.com/rust-lang/crates.io-index"
2169
+
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
2170
2171
[[package]]
2172
name = "loom"
···
2203
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
2204
2205
[[package]]
2206
+
name = "maitake-sync"
2207
+
version = "0.1.2"
2208
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2209
+
checksum = "6816ab14147f80234c675b80ed6dc4f440d8a1cefc158e766067aedb84c0bcd5"
2210
+
dependencies = [
2211
+
"cordyceps",
2212
+
"loom",
2213
+
"mycelium-bitfield",
2214
+
"pin-project",
2215
+
"portable-atomic",
2216
+
]
2217
+
2218
+
[[package]]
2219
name = "markup5ever"
2220
version = "0.12.1"
2221
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2300
dependencies = [
2301
"proc-macro2",
2302
"quote",
2303
+
"syn 2.0.113",
2304
]
2305
2306
[[package]]
···
2320
]
2321
2322
[[package]]
2323
+
name = "mini-moka-wasm"
2324
version = "0.10.99"
2325
+
source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b"
2326
dependencies = [
2327
"crossbeam-channel",
2328
"crossbeam-utils",
···
2334
]
2335
2336
[[package]]
2337
name = "miniz_oxide"
2338
version = "0.8.9"
2339
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2345
2346
[[package]]
2347
name = "mio"
2348
+
version = "1.1.1"
2349
source = "registry+https://github.com/rust-lang/crates.io-index"
2350
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
2351
dependencies = [
2352
"libc",
2353
"wasi",
···
2396
]
2397
2398
[[package]]
2399
+
name = "mycelium-bitfield"
2400
+
version = "0.1.5"
2401
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2402
+
checksum = "24e0cc5e2c585acbd15c5ce911dff71e1f4d5313f43345873311c4f5efd741cc"
2403
+
2404
+
[[package]]
2405
name = "n0-future"
2406
version = "0.1.3"
2407
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2454
version = "1.0.6"
2455
source = "registry+https://github.com/rust-lang/crates.io-index"
2456
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
2457
2458
[[package]]
2459
name = "nu-ansi-term"
···
2536
]
2537
2538
[[package]]
2539
+
name = "number_prefix"
2540
+
version = "0.4.0"
2541
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2542
+
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
2543
+
2544
+
[[package]]
2545
name = "objc2"
2546
version = "0.6.3"
2547
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2589
2590
[[package]]
2591
name = "openssl-probe"
2592
+
version = "0.2.0"
2593
source = "registry+https://github.com/rust-lang/crates.io-index"
2594
+
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
2595
2596
[[package]]
2597
name = "option-ext"
···
2620
"proc-macro2",
2621
"proc-macro2-diagnostics",
2622
"quote",
2623
+
"syn 2.0.113",
2624
]
2625
2626
[[package]]
···
2630
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
2631
2632
[[package]]
2633
+
name = "oxilangtag"
2634
+
version = "0.1.5"
2635
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2636
+
checksum = "23f3f87617a86af77fa3691e6350483e7154c2ead9f1261b75130e21ca0f8acb"
2637
+
dependencies = [
2638
+
"serde",
2639
+
]
2640
+
2641
+
[[package]]
2642
name = "p256"
2643
version = "0.13.2"
2644
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2684
dependencies = [
2685
"cfg-if",
2686
"libc",
2687
+
"redox_syscall 0.5.18",
2688
"smallvec",
2689
+
"windows-link",
2690
]
2691
2692
[[package]]
···
2759
dependencies = [
2760
"proc-macro2",
2761
"quote",
2762
+
"syn 2.0.113",
2763
]
2764
2765
[[package]]
···
2793
dependencies = [
2794
"der",
2795
"spki",
2796
+
]
2797
+
2798
+
[[package]]
2799
+
name = "portable-atomic"
2800
+
version = "1.13.0"
2801
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2802
+
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
2803
+
2804
+
[[package]]
2805
+
name = "postcard"
2806
+
version = "1.1.3"
2807
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2808
+
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
2809
+
dependencies = [
2810
+
"cobs",
2811
+
"embedded-io 0.4.0",
2812
+
"embedded-io 0.6.1",
2813
+
"heapless",
2814
+
"serde",
2815
]
2816
2817
[[package]]
···
2851
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
2852
dependencies = [
2853
"proc-macro2",
2854
+
"syn 2.0.113",
2855
]
2856
2857
[[package]]
···
2864
]
2865
2866
[[package]]
2867
name = "proc-macro2"
2868
+
version = "1.0.105"
2869
source = "registry+https://github.com/rust-lang/crates.io-index"
2870
+
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
2871
dependencies = [
2872
"unicode-ident",
2873
]
···
2880
dependencies = [
2881
"proc-macro2",
2882
"quote",
2883
+
"syn 2.0.113",
2884
"version_check",
2885
"yansi",
2886
]
···
2948
2949
[[package]]
2950
name = "quote"
2951
+
version = "1.0.43"
2952
source = "registry+https://github.com/rust-lang/crates.io-index"
2953
+
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
2954
dependencies = [
2955
"proc-macro2",
2956
]
···
3021
]
3022
3023
[[package]]
3024
+
name = "redox_syscall"
3025
+
version = "0.5.18"
3026
source = "registry+https://github.com/rust-lang/crates.io-index"
3027
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
3028
+
dependencies = [
3029
+
"bitflags",
3030
+
]
3031
3032
[[package]]
3033
name = "redox_syscall"
3034
+
version = "0.7.0"
3035
source = "registry+https://github.com/rust-lang/crates.io-index"
3036
+
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
3037
dependencies = [
3038
"bitflags",
3039
]
···
3050
]
3051
3052
[[package]]
3053
name = "regex"
3054
version = "1.12.2"
3055
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3086
3087
[[package]]
3088
name = "reqwest"
3089
+
version = "0.12.28"
3090
source = "registry+https://github.com/rust-lang/crates.io-index"
3091
+
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
3092
dependencies = [
3093
"base64 0.22.1",
3094
"bytes",
3095
"encoding_rs",
···
3184
3185
[[package]]
3186
name = "rsa"
3187
+
version = "0.9.10"
3188
source = "registry+https://github.com/rust-lang/crates.io-index"
3189
+
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
3190
dependencies = [
3191
"const-oid",
3192
"digest",
···
3213
version = "2.1.1"
3214
source = "registry+https://github.com/rust-lang/crates.io-index"
3215
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
3216
+
3217
+
[[package]]
3218
+
name = "rustc_version"
3219
+
version = "0.4.1"
3220
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3221
+
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
3222
+
dependencies = [
3223
+
"semver",
3224
+
]
3225
3226
[[package]]
3227
name = "rustix"
3228
+
version = "1.1.3"
3229
source = "registry+https://github.com/rust-lang/crates.io-index"
3230
+
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
3231
dependencies = [
3232
"bitflags",
3233
"errno",
···
3238
3239
[[package]]
3240
name = "rustls"
3241
+
version = "0.23.36"
3242
source = "registry+https://github.com/rust-lang/crates.io-index"
3243
+
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
3244
dependencies = [
3245
"once_cell",
3246
"ring",
···
3252
3253
[[package]]
3254
name = "rustls-native-certs"
3255
+
version = "0.8.3"
3256
source = "registry+https://github.com/rust-lang/crates.io-index"
3257
+
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
3258
dependencies = [
3259
"openssl-probe",
3260
"rustls-pki-types",
···
3264
3265
[[package]]
3266
name = "rustls-pki-types"
3267
+
version = "1.13.2"
3268
source = "registry+https://github.com/rust-lang/crates.io-index"
3269
+
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
3270
dependencies = [
3271
"web-time",
3272
"zeroize",
···
3291
3292
[[package]]
3293
name = "ryu"
3294
+
version = "1.0.22"
3295
source = "registry+https://github.com/rust-lang/crates.io-index"
3296
+
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
3297
3298
[[package]]
3299
name = "safemem"
···
3320
]
3321
3322
[[package]]
3323
name = "scoped-tls"
3324
version = "1.0.1"
3325
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3367
"core-foundation-sys",
3368
"libc",
3369
]
3370
+
3371
+
[[package]]
3372
+
name = "semver"
3373
+
version = "1.0.27"
3374
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3375
+
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
3376
3377
[[package]]
3378
name = "send_wrapper"
···
3417
dependencies = [
3418
"proc-macro2",
3419
"quote",
3420
+
"syn 2.0.113",
3421
]
3422
3423
[[package]]
3424
name = "serde_html_form"
3425
+
version = "0.3.2"
3426
source = "registry+https://github.com/rust-lang/crates.io-index"
3427
+
checksum = "2acf96b1d9364968fce46ebb548f1c0e1d7eceae27bdff73865d42e6c7369d94"
3428
dependencies = [
3429
"form_urlencoded",
3430
+
"indexmap",
3431
"itoa",
3432
"serde_core",
3433
]
3434
···
3446
3447
[[package]]
3448
name = "serde_json"
3449
+
version = "1.0.149"
3450
source = "registry+https://github.com/rust-lang/crates.io-index"
3451
+
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
3452
dependencies = [
3453
"itoa",
3454
"memchr",
3455
"serde",
3456
"serde_core",
3457
+
"zmij",
3458
]
3459
3460
[[package]]
···
3476
dependencies = [
3477
"proc-macro2",
3478
"quote",
3479
+
"syn 2.0.113",
3480
]
3481
3482
[[package]]
···
3493
3494
[[package]]
3495
name = "serde_with"
3496
+
version = "3.16.1"
3497
source = "registry+https://github.com/rust-lang/crates.io-index"
3498
+
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
3499
dependencies = [
3500
"base64 0.22.1",
3501
"chrono",
3502
"hex",
3503
"serde_core",
3504
"serde_json",
3505
"serde_with_macros",
···
3508
3509
[[package]]
3510
name = "serde_with_macros"
3511
+
version = "3.16.1"
3512
source = "registry+https://github.com/rust-lang/crates.io-index"
3513
+
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
3514
dependencies = [
3515
"darling",
3516
"proc-macro2",
3517
"quote",
3518
+
"syn 2.0.113",
3519
]
3520
3521
[[package]]
···
3572
3573
[[package]]
3574
name = "signal-hook-registry"
3575
+
version = "1.4.8"
3576
source = "registry+https://github.com/rust-lang/crates.io-index"
3577
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
3578
dependencies = [
3579
+
"errno",
3580
"libc",
3581
]
3582
···
3592
3593
[[package]]
3594
name = "simd-adler32"
3595
+
version = "0.3.8"
3596
source = "registry+https://github.com/rust-lang/crates.io-index"
3597
+
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
3598
3599
[[package]]
3600
name = "siphasher"
···
3649
version = "0.9.8"
3650
source = "registry+https://github.com/rust-lang/crates.io-index"
3651
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
3652
+
dependencies = [
3653
+
"lock_api",
3654
+
]
3655
3656
[[package]]
3657
name = "spin"
···
3676
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
3677
3678
[[package]]
3679
name = "static_assertions"
3680
version = "1.1.0"
3681
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3729
3730
[[package]]
3731
name = "supports-hyperlinks"
3732
+
version = "3.2.0"
3733
source = "registry+https://github.com/rust-lang/crates.io-index"
3734
+
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
3735
3736
[[package]]
3737
name = "supports-unicode"
···
3752
3753
[[package]]
3754
name = "syn"
3755
+
version = "2.0.113"
3756
source = "registry+https://github.com/rust-lang/crates.io-index"
3757
+
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
3758
dependencies = [
3759
"proc-macro2",
3760
"quote",
···
3778
dependencies = [
3779
"proc-macro2",
3780
"quote",
3781
+
"syn 2.0.113",
3782
]
3783
3784
[[package]]
···
3810
3811
[[package]]
3812
name = "tempfile"
3813
+
version = "3.24.0"
3814
source = "registry+https://github.com/rust-lang/crates.io-index"
3815
+
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
3816
dependencies = [
3817
"fastrand",
3818
"getrandom 0.3.4",
···
3878
dependencies = [
3879
"proc-macro2",
3880
"quote",
3881
+
"syn 2.0.113",
3882
]
3883
3884
[[package]]
···
3889
dependencies = [
3890
"proc-macro2",
3891
"quote",
3892
+
"syn 2.0.113",
3893
]
3894
3895
[[package]]
···
3917
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
3918
dependencies = [
3919
"deranged",
3920
"libc",
3921
"num-conv",
3922
"num_threads",
3923
"powerfmt",
3924
"serde",
3925
"time-core",
3926
]
3927
3928
[[package]]
···
3930
version = "0.1.6"
3931
source = "registry+https://github.com/rust-lang/crates.io-index"
3932
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
3933
3934
[[package]]
3935
name = "tiny_http"
···
3970
3971
[[package]]
3972
name = "tokio"
3973
+
version = "1.49.0"
3974
source = "registry+https://github.com/rust-lang/crates.io-index"
3975
+
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
3976
dependencies = [
3977
"bytes",
3978
"libc",
···
3993
dependencies = [
3994
"proc-macro2",
3995
"quote",
3996
+
"syn 2.0.113",
3997
]
3998
3999
[[package]]
···
4043
4044
[[package]]
4045
name = "tokio-util"
4046
+
version = "0.7.18"
4047
source = "registry+https://github.com/rust-lang/crates.io-index"
4048
+
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
4049
dependencies = [
4050
"bytes",
4051
"futures-core",
···
4073
4074
[[package]]
4075
name = "tower-http"
4076
+
version = "0.6.8"
4077
source = "registry+https://github.com/rust-lang/crates.io-index"
4078
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
4079
dependencies = [
4080
"async-compression",
4081
"bitflags",
···
4114
4115
[[package]]
4116
name = "tracing"
4117
+
version = "0.1.44"
4118
source = "registry+https://github.com/rust-lang/crates.io-index"
4119
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
4120
dependencies = [
4121
"log",
4122
"pin-project-lite",
···
4126
4127
[[package]]
4128
name = "tracing-attributes"
4129
+
version = "0.1.31"
4130
source = "registry+https://github.com/rust-lang/crates.io-index"
4131
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
4132
dependencies = [
4133
"proc-macro2",
4134
"quote",
4135
+
"syn 2.0.113",
4136
]
4137
4138
[[package]]
4139
name = "tracing-core"
4140
+
version = "0.1.36"
4141
source = "registry+https://github.com/rust-lang/crates.io-index"
4142
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
4143
dependencies = [
4144
"once_cell",
4145
"valuable",
···
4158
4159
[[package]]
4160
name = "tracing-subscriber"
4161
+
version = "0.3.22"
4162
source = "registry+https://github.com/rust-lang/crates.io-index"
4163
+
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
4164
dependencies = [
4165
"matchers",
4166
"nu-ansi-term",
···
4182
dependencies = [
4183
"proc-macro2",
4184
"quote",
4185
+
"syn 2.0.113",
4186
]
4187
4188
[[package]]
···
4234
4235
[[package]]
4236
name = "unicase"
4237
+
version = "2.9.0"
4238
source = "registry+https://github.com/rust-lang/crates.io-index"
4239
+
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
4240
4241
[[package]]
4242
name = "unicode-ident"
···
4288
4289
[[package]]
4290
name = "url"
4291
+
version = "2.5.8"
4292
source = "registry+https://github.com/rust-lang/crates.io-index"
4293
+
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
4294
dependencies = [
4295
"form_urlencoded",
4296
"idna",
4297
"percent-encoding",
4298
"serde",
4299
+
"serde_derive",
4300
]
4301
4302
[[package]]
···
4371
4372
[[package]]
4373
name = "wasm-bindgen"
4374
+
version = "0.2.106"
4375
source = "registry+https://github.com/rust-lang/crates.io-index"
4376
+
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
4377
dependencies = [
4378
"cfg-if",
4379
"once_cell",
···
4384
4385
[[package]]
4386
name = "wasm-bindgen-futures"
4387
+
version = "0.4.56"
4388
source = "registry+https://github.com/rust-lang/crates.io-index"
4389
+
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
4390
dependencies = [
4391
"cfg-if",
4392
"js-sys",
···
4397
4398
[[package]]
4399
name = "wasm-bindgen-macro"
4400
+
version = "0.2.106"
4401
source = "registry+https://github.com/rust-lang/crates.io-index"
4402
+
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
4403
dependencies = [
4404
"quote",
4405
"wasm-bindgen-macro-support",
···
4407
4408
[[package]]
4409
name = "wasm-bindgen-macro-support"
4410
+
version = "0.2.106"
4411
source = "registry+https://github.com/rust-lang/crates.io-index"
4412
+
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
4413
dependencies = [
4414
"bumpalo",
4415
"proc-macro2",
4416
"quote",
4417
+
"syn 2.0.113",
4418
"wasm-bindgen-shared",
4419
]
4420
4421
[[package]]
4422
name = "wasm-bindgen-shared"
4423
+
version = "0.2.106"
4424
source = "registry+https://github.com/rust-lang/crates.io-index"
4425
+
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
4426
dependencies = [
4427
"unicode-ident",
4428
]
···
4442
4443
[[package]]
4444
name = "web-sys"
4445
+
version = "0.3.83"
4446
source = "registry+https://github.com/rust-lang/crates.io-index"
4447
+
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
4448
dependencies = [
4449
"js-sys",
4450
"wasm-bindgen",
···
4490
4491
[[package]]
4492
name = "webpki-roots"
4493
+
version = "1.0.5"
4494
source = "registry+https://github.com/rust-lang/crates.io-index"
4495
+
checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c"
4496
dependencies = [
4497
"rustls-pki-types",
4498
]
···
4513
]
4514
4515
[[package]]
4516
name = "windows-core"
4517
version = "0.62.2"
4518
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4520
dependencies = [
4521
"windows-implement",
4522
"windows-interface",
4523
+
"windows-link",
4524
+
"windows-result",
4525
+
"windows-strings",
4526
]
4527
4528
[[package]]
···
4533
dependencies = [
4534
"proc-macro2",
4535
"quote",
4536
+
"syn 2.0.113",
4537
]
4538
4539
[[package]]
···
4544
dependencies = [
4545
"proc-macro2",
4546
"quote",
4547
+
"syn 2.0.113",
4548
]
4549
4550
[[package]]
4551
name = "windows-link"
4552
version = "0.2.1"
4553
source = "registry+https://github.com/rust-lang/crates.io-index"
4554
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
4555
4556
[[package]]
4557
name = "windows-registry"
4558
version = "0.6.1"
4559
source = "registry+https://github.com/rust-lang/crates.io-index"
4560
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
4561
dependencies = [
4562
+
"windows-link",
4563
+
"windows-result",
4564
+
"windows-strings",
4565
]
4566
4567
[[package]]
···
4570
source = "registry+https://github.com/rust-lang/crates.io-index"
4571
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
4572
dependencies = [
4573
+
"windows-link",
4574
]
4575
4576
[[package]]
···
4579
source = "registry+https://github.com/rust-lang/crates.io-index"
4580
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
4581
dependencies = [
4582
+
"windows-link",
4583
]
4584
4585
[[package]]
···
4611
4612
[[package]]
4613
name = "windows-sys"
4614
+
version = "0.59.0"
4615
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4616
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
4617
+
dependencies = [
4618
+
"windows-targets 0.52.6",
4619
+
]
4620
+
4621
+
[[package]]
4622
+
name = "windows-sys"
4623
version = "0.60.2"
4624
source = "registry+https://github.com/rust-lang/crates.io-index"
4625
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
···
4633
source = "registry+https://github.com/rust-lang/crates.io-index"
4634
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
4635
dependencies = [
4636
+
"windows-link",
4637
]
4638
4639
[[package]]
···
4688
source = "registry+https://github.com/rust-lang/crates.io-index"
4689
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
4690
dependencies = [
4691
+
"windows-link",
4692
"windows_aarch64_gnullvm 0.53.1",
4693
"windows_aarch64_msvc 0.53.1",
4694
"windows_i686_gnu 0.53.1",
···
4697
"windows_x86_64_gnu 0.53.1",
4698
"windows_x86_64_gnullvm 0.53.1",
4699
"windows_x86_64_msvc 0.53.1",
4700
]
4701
4702
[[package]]
···
4902
"futures",
4903
"globset",
4904
"ignore",
4905
+
"indicatif",
4906
"jacquard",
4907
"jacquard-api",
4908
"jacquard-common",
···
4927
"tower-http",
4928
"url",
4929
"walkdir",
4930
+
"wisp-lexicons",
4931
+
]
4932
+
4933
+
[[package]]
4934
+
name = "wisp-lexicons"
4935
+
version = "0.1.0"
4936
+
dependencies = [
4937
+
"jacquard-common",
4938
+
"jacquard-derive",
4939
+
"jacquard-lexicon",
4940
+
"rustversion",
4941
+
"serde",
4942
]
4943
4944
[[package]]
···
4989
dependencies = [
4990
"proc-macro2",
4991
"quote",
4992
+
"syn 2.0.113",
4993
"synstructure",
4994
]
4995
4996
[[package]]
4997
name = "zerocopy"
4998
+
version = "0.8.32"
4999
source = "registry+https://github.com/rust-lang/crates.io-index"
5000
+
checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56"
5001
dependencies = [
5002
"zerocopy-derive",
5003
]
5004
5005
[[package]]
5006
name = "zerocopy-derive"
5007
+
version = "0.8.32"
5008
source = "registry+https://github.com/rust-lang/crates.io-index"
5009
+
checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90"
5010
dependencies = [
5011
"proc-macro2",
5012
"quote",
5013
+
"syn 2.0.113",
5014
]
5015
5016
[[package]]
···
5030
dependencies = [
5031
"proc-macro2",
5032
"quote",
5033
+
"syn 2.0.113",
5034
"synstructure",
5035
]
5036
···
5073
dependencies = [
5074
"proc-macro2",
5075
"quote",
5076
+
"syn 2.0.113",
5077
]
5078
+
5079
+
[[package]]
5080
+
name = "zmij"
5081
+
version = "1.0.12"
5082
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5083
+
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
+16
-7
cli/Cargo.toml
+16
-7
cli/Cargo.toml
···
8
place_wisp = []
9
10
[dependencies]
11
-
jacquard = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["loopback"] }
12
-
jacquard-oauth = { git = "https://tangled.org/nekomimi.pet/jacquard" }
13
-
jacquard-api = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["streaming"] }
14
-
jacquard-common = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["websocket"] }
15
-
jacquard-identity = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["dns"] }
16
-
jacquard-derive = { git = "https://tangled.org/nekomimi.pet/jacquard" }
17
-
jacquard-lexicon = { git = "https://tangled.org/nekomimi.pet/jacquard" }
18
clap = { version = "4.5.51", features = ["derive"] }
19
tokio = { version = "1.48", features = ["full"] }
20
miette = { version = "7.6.0", features = ["fancy"] }
···
42
regex = "1.11"
43
ignore = "0.4"
44
globset = "0.4"
···
8
place_wisp = []
9
10
[dependencies]
11
+
jacquard = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["loopback"] }
12
+
jacquard-oauth = { git = "https://tangled.org/nonbinary.computer/jacquard" }
13
+
jacquard-api = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["streaming"] }
14
+
jacquard-common = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["websocket"] }
15
+
jacquard-identity = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["dns"] }
16
+
jacquard-derive = { git = "https://tangled.org/nonbinary.computer/jacquard" }
17
+
jacquard-lexicon = { git = "https://tangled.org/nonbinary.computer/jacquard" }
18
+
wisp-lexicons = { path = "crates/lexicons" }
19
+
#jacquard = { path = "../../jacquard/crates/jacquard", features = ["loopback"] }
20
+
#jacquard-oauth = { path = "../../jacquard/crates/jacquard-oauth" }
21
+
#jacquard-api = { path = "../../jacquard/crates/jacquard-api", features = ["streaming"] }
22
+
#jacquard-common = { path = "../../jacquard/crates/jacquard-common", features = ["websocket"] }
23
+
#jacquard-identity = { path = "../../jacquard/crates/jacquard-identity", features = ["dns"] }
24
+
#jacquard-derive = { path = "../../jacquard/crates/jacquard-derive" }
25
+
#jacquard-lexicon = { path = "../../jacquard/crates/jacquard-lexicon" }
26
clap = { version = "4.5.51", features = ["derive"] }
27
tokio = { version = "1.48", features = ["full"] }
28
miette = { version = "7.6.0", features = ["fancy"] }
···
50
regex = "1.11"
51
ignore = "0.4"
52
globset = "0.4"
53
+
indicatif = "0.17"
+72
-2
cli/README.md
+72
-2
cli/README.md
···
32
33
## Usage
34
35
### Basic Deployment
36
37
Deploy the current directory:
38
39
```bash
40
-
wisp-cli nekomimi.ppet --path . --site my-site
41
```
42
43
Deploy a specific directory:
···
46
wisp-cli alice.bsky.social --path ./dist/ --site my-site
47
```
48
49
### Authentication Methods
50
51
#### OAuth (Recommended)
···
79
80
## Command-Line Options
81
82
```
83
-
wisp-cli [OPTIONS] <INPUT>
84
85
Arguments:
86
<INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL
···
90
-s, --site <SITE> Site name (defaults to directory name)
91
--store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json]
92
--password <PASSWORD> App Password for authentication (alternative to OAuth)
93
-h, --help Print help
94
-V, --version Print version
95
```
96
97
## How It Works
···
32
33
## Usage
34
35
+
### Commands
36
+
37
+
The CLI supports three main commands:
38
+
- **deploy**: Upload a site to your PDS (default command)
39
+
- **pull**: Download a site from a PDS to a local directory
40
+
- **serve**: Serve a site locally with real-time firehose updates
41
+
42
### Basic Deployment
43
44
Deploy the current directory:
45
46
```bash
47
+
wisp-cli nekomimi.pet --path . --site my-site
48
```
49
50
Deploy a specific directory:
···
53
wisp-cli alice.bsky.social --path ./dist/ --site my-site
54
```
55
56
+
Or use the explicit `deploy` subcommand:
57
+
58
+
```bash
59
+
wisp-cli deploy alice.bsky.social --path ./dist/ --site my-site
60
+
```
61
+
62
+
### Pull a Site
63
+
64
+
Download a site from a PDS to a local directory:
65
+
66
+
```bash
67
+
wisp-cli pull alice.bsky.social --site my-site --path ./downloaded-site
68
+
```
69
+
70
+
This will download all files from the site to the specified directory.
71
+
72
+
### Serve a Site Locally
73
+
74
+
Serve a site locally with real-time updates from the firehose:
75
+
76
+
```bash
77
+
wisp-cli serve alice.bsky.social --site my-site --path ./site --port 8080
78
+
```
79
+
80
+
This will:
81
+
1. Download the site to the specified path
82
+
2. Start a local server on the specified port (default: 8080)
83
+
3. Watch the firehose for updates and automatically reload files when changed
84
+
85
### Authentication Methods
86
87
#### OAuth (Recommended)
···
115
116
## Command-Line Options
117
118
+
### Deploy Command
119
+
120
```
121
+
wisp-cli [deploy] [OPTIONS] <INPUT>
122
123
Arguments:
124
<INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL
···
128
-s, --site <SITE> Site name (defaults to directory name)
129
--store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json]
130
--password <PASSWORD> App Password for authentication (alternative to OAuth)
131
+
--directory Enable directory listing mode for paths without index files
132
+
--spa Enable SPA mode (serve index.html for all routes)
133
+
-y, --yes Skip confirmation prompts (automatically accept warnings)
134
-h, --help Print help
135
-V, --version Print version
136
+
```
137
+
138
+
### Pull Command
139
+
140
+
```
141
+
wisp-cli pull [OPTIONS] --site <SITE> <INPUT>
142
+
143
+
Arguments:
144
+
<INPUT> Handle (e.g., alice.bsky.social) or DID
145
+
146
+
Options:
147
+
-s, --site <SITE> Site name (record key)
148
+
-p, --path <PATH> Output directory for the downloaded site [default: .]
149
+
-h, --help Print help
150
+
```
151
+
152
+
### Serve Command
153
+
154
+
```
155
+
wisp-cli serve [OPTIONS] --site <SITE> <INPUT>
156
+
157
+
Arguments:
158
+
<INPUT> Handle (e.g., alice.bsky.social) or DID
159
+
160
+
Options:
161
+
-s, --site <SITE> Site name (record key)
162
+
-p, --path <PATH> Output directory for the site files [default: .]
163
+
-P, --port <PORT> Port to serve on [default: 8080]
164
+
-h, --help Print help
165
```
166
167
## How It Works
+15
cli/crates/lexicons/Cargo.toml
+15
cli/crates/lexicons/Cargo.toml
···
···
1
+
[package]
2
+
name = "wisp-lexicons"
3
+
version = "0.1.0"
4
+
edition = "2024"
5
+
6
+
[features]
7
+
default = ["place_wisp"]
8
+
place_wisp = []
9
+
10
+
[dependencies]
11
+
jacquard-common = { git = "https://tangled.org/nonbinary.computer/jacquard" }
12
+
jacquard-derive = { git = "https://tangled.org/nonbinary.computer/jacquard" }
13
+
jacquard-lexicon = { git = "https://tangled.org/nonbinary.computer/jacquard" }
14
+
serde = { version = "1.0", features = ["derive"] }
15
+
rustversion = "1.0"
+43
cli/crates/lexicons/src/builder_types.rs
+43
cli/crates/lexicons/src/builder_types.rs
···
···
1
+
// @generated by jacquard-lexicon. DO NOT EDIT.
2
+
//
3
+
// This file was automatically generated from Lexicon schemas.
4
+
// Any manual changes will be overwritten on the next regeneration.
5
+
6
+
/// Marker type indicating a builder field has been set
7
+
pub struct Set<T>(pub T);
8
+
impl<T> Set<T> {
9
+
/// Extract the inner value
10
+
#[inline]
11
+
pub fn into_inner(self) -> T {
12
+
self.0
13
+
}
14
+
}
15
+
16
+
/// Marker type indicating a builder field has not been set
17
+
pub struct Unset;
18
+
/// Trait indicating a builder field is set (has a value)
19
+
#[rustversion::attr(
20
+
since(1.78.0),
21
+
diagnostic::on_unimplemented(
22
+
message = "the field `{Self}` was not set, but this method requires it to be set",
23
+
label = "the field `{Self}` was not set"
24
+
)
25
+
)]
26
+
pub trait IsSet: private::Sealed {}
27
+
/// Trait indicating a builder field is unset (no value yet)
28
+
#[rustversion::attr(
29
+
since(1.78.0),
30
+
diagnostic::on_unimplemented(
31
+
message = "the field `{Self}` was already set, but this method requires it to be unset",
32
+
label = "the field `{Self}` was already set"
33
+
)
34
+
)]
35
+
pub trait IsUnset: private::Sealed {}
36
+
impl<T> IsSet for Set<T> {}
37
+
impl IsUnset for Unset {}
38
+
mod private {
39
+
/// Sealed trait to prevent external implementations
40
+
pub trait Sealed {}
41
+
impl<T> Sealed for super::Set<T> {}
42
+
impl Sealed for super::Unset {}
43
+
}
+11
cli/crates/lexicons/src/lib.rs
+11
cli/crates/lexicons/src/lib.rs
···
···
1
+
extern crate alloc;
2
+
3
+
// @generated by jacquard-lexicon. DO NOT EDIT.
4
+
//
5
+
// This file was automatically generated from Lexicon schemas.
6
+
// Any manual changes will be overwritten on the next regeneration.
7
+
8
+
pub mod builder_types;
9
+
10
+
#[cfg(feature = "place_wisp")]
11
+
pub mod place_wisp;
+1490
cli/crates/lexicons/src/place_wisp/fs.rs
+1490
cli/crates/lexicons/src/place_wisp/fs.rs
···
···
1
+
// @generated by jacquard-lexicon. DO NOT EDIT.
2
+
//
3
+
// Lexicon: place.wisp.fs
4
+
//
5
+
// This file was automatically generated from Lexicon schemas.
6
+
// Any manual changes will be overwritten on the next regeneration.
7
+
8
+
#[jacquard_derive::lexicon]
9
+
#[derive(
10
+
serde::Serialize,
11
+
serde::Deserialize,
12
+
Debug,
13
+
Clone,
14
+
PartialEq,
15
+
Eq,
16
+
jacquard_derive::IntoStatic
17
+
)]
18
+
#[serde(rename_all = "camelCase")]
19
+
pub struct Directory<'a> {
20
+
#[serde(borrow)]
21
+
pub entries: Vec<crate::place_wisp::fs::Entry<'a>>,
22
+
#[serde(borrow)]
23
+
pub r#type: jacquard_common::CowStr<'a>,
24
+
}
25
+
26
+
pub mod directory_state {
27
+
28
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
29
+
#[allow(unused)]
30
+
use ::core::marker::PhantomData;
31
+
mod sealed {
32
+
pub trait Sealed {}
33
+
}
34
+
/// State trait tracking which required fields have been set
35
+
pub trait State: sealed::Sealed {
36
+
type Type;
37
+
type Entries;
38
+
}
39
+
/// Empty state - all required fields are unset
40
+
pub struct Empty(());
41
+
impl sealed::Sealed for Empty {}
42
+
impl State for Empty {
43
+
type Type = Unset;
44
+
type Entries = Unset;
45
+
}
46
+
///State transition - sets the `type` field to Set
47
+
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
48
+
impl<S: State> sealed::Sealed for SetType<S> {}
49
+
impl<S: State> State for SetType<S> {
50
+
type Type = Set<members::r#type>;
51
+
type Entries = S::Entries;
52
+
}
53
+
///State transition - sets the `entries` field to Set
54
+
pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>);
55
+
impl<S: State> sealed::Sealed for SetEntries<S> {}
56
+
impl<S: State> State for SetEntries<S> {
57
+
type Type = S::Type;
58
+
type Entries = Set<members::entries>;
59
+
}
60
+
/// Marker types for field names
61
+
#[allow(non_camel_case_types)]
62
+
pub mod members {
63
+
///Marker type for the `type` field
64
+
pub struct r#type(());
65
+
///Marker type for the `entries` field
66
+
pub struct entries(());
67
+
}
68
+
}
69
+
70
+
/// Builder for constructing an instance of this type
71
+
pub struct DirectoryBuilder<'a, S: directory_state::State> {
72
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
73
+
__unsafe_private_named: (
74
+
::core::option::Option<Vec<crate::place_wisp::fs::Entry<'a>>>,
75
+
::core::option::Option<jacquard_common::CowStr<'a>>,
76
+
),
77
+
_phantom: ::core::marker::PhantomData<&'a ()>,
78
+
}
79
+
80
+
impl<'a> Directory<'a> {
81
+
/// Create a new builder for this type
82
+
pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> {
83
+
DirectoryBuilder::new()
84
+
}
85
+
}
86
+
87
+
impl<'a> DirectoryBuilder<'a, directory_state::Empty> {
88
+
/// Create a new builder with all fields unset
89
+
pub fn new() -> Self {
90
+
DirectoryBuilder {
91
+
_phantom_state: ::core::marker::PhantomData,
92
+
__unsafe_private_named: (None, None),
93
+
_phantom: ::core::marker::PhantomData,
94
+
}
95
+
}
96
+
}
97
+
98
+
impl<'a, S> DirectoryBuilder<'a, S>
99
+
where
100
+
S: directory_state::State,
101
+
S::Entries: directory_state::IsUnset,
102
+
{
103
+
/// Set the `entries` field (required)
104
+
pub fn entries(
105
+
mut self,
106
+
value: impl Into<Vec<crate::place_wisp::fs::Entry<'a>>>,
107
+
) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> {
108
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
109
+
DirectoryBuilder {
110
+
_phantom_state: ::core::marker::PhantomData,
111
+
__unsafe_private_named: self.__unsafe_private_named,
112
+
_phantom: ::core::marker::PhantomData,
113
+
}
114
+
}
115
+
}
116
+
117
+
impl<'a, S> DirectoryBuilder<'a, S>
118
+
where
119
+
S: directory_state::State,
120
+
S::Type: directory_state::IsUnset,
121
+
{
122
+
/// Set the `type` field (required)
123
+
pub fn r#type(
124
+
mut self,
125
+
value: impl Into<jacquard_common::CowStr<'a>>,
126
+
) -> DirectoryBuilder<'a, directory_state::SetType<S>> {
127
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
128
+
DirectoryBuilder {
129
+
_phantom_state: ::core::marker::PhantomData,
130
+
__unsafe_private_named: self.__unsafe_private_named,
131
+
_phantom: ::core::marker::PhantomData,
132
+
}
133
+
}
134
+
}
135
+
136
+
impl<'a, S> DirectoryBuilder<'a, S>
137
+
where
138
+
S: directory_state::State,
139
+
S::Type: directory_state::IsSet,
140
+
S::Entries: directory_state::IsSet,
141
+
{
142
+
/// Build the final struct
143
+
pub fn build(self) -> Directory<'a> {
144
+
Directory {
145
+
entries: self.__unsafe_private_named.0.unwrap(),
146
+
r#type: self.__unsafe_private_named.1.unwrap(),
147
+
extra_data: Default::default(),
148
+
}
149
+
}
150
+
/// Build the final struct with custom extra_data
151
+
pub fn build_with_data(
152
+
self,
153
+
extra_data: std::collections::BTreeMap<
154
+
jacquard_common::smol_str::SmolStr,
155
+
jacquard_common::types::value::Data<'a>,
156
+
>,
157
+
) -> Directory<'a> {
158
+
Directory {
159
+
entries: self.__unsafe_private_named.0.unwrap(),
160
+
r#type: self.__unsafe_private_named.1.unwrap(),
161
+
extra_data: Some(extra_data),
162
+
}
163
+
}
164
+
}
165
+
166
+
fn lexicon_doc_place_wisp_fs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
167
+
::jacquard_lexicon::lexicon::LexiconDoc {
168
+
lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
169
+
id: ::jacquard_common::CowStr::new_static("place.wisp.fs"),
170
+
revision: None,
171
+
description: None,
172
+
defs: {
173
+
let mut map = ::std::collections::BTreeMap::new();
174
+
map.insert(
175
+
::jacquard_common::smol_str::SmolStr::new_static("directory"),
176
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
177
+
description: None,
178
+
required: Some(
179
+
vec![
180
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
181
+
::jacquard_common::smol_str::SmolStr::new_static("entries")
182
+
],
183
+
),
184
+
nullable: None,
185
+
properties: {
186
+
#[allow(unused_mut)]
187
+
let mut map = ::std::collections::BTreeMap::new();
188
+
map.insert(
189
+
::jacquard_common::smol_str::SmolStr::new_static("entries"),
190
+
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
191
+
description: None,
192
+
items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef {
193
+
description: None,
194
+
r#ref: ::jacquard_common::CowStr::new_static("#entry"),
195
+
}),
196
+
min_length: None,
197
+
max_length: Some(500usize),
198
+
}),
199
+
);
200
+
map.insert(
201
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
202
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
203
+
description: None,
204
+
format: None,
205
+
default: None,
206
+
min_length: None,
207
+
max_length: None,
208
+
min_graphemes: None,
209
+
max_graphemes: None,
210
+
r#enum: None,
211
+
r#const: None,
212
+
known_values: None,
213
+
}),
214
+
);
215
+
map
216
+
},
217
+
}),
218
+
);
219
+
map.insert(
220
+
::jacquard_common::smol_str::SmolStr::new_static("entry"),
221
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
222
+
description: None,
223
+
required: Some(
224
+
vec![
225
+
::jacquard_common::smol_str::SmolStr::new_static("name"),
226
+
::jacquard_common::smol_str::SmolStr::new_static("node")
227
+
],
228
+
),
229
+
nullable: None,
230
+
properties: {
231
+
#[allow(unused_mut)]
232
+
let mut map = ::std::collections::BTreeMap::new();
233
+
map.insert(
234
+
::jacquard_common::smol_str::SmolStr::new_static("name"),
235
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
236
+
description: None,
237
+
format: None,
238
+
default: None,
239
+
min_length: None,
240
+
max_length: Some(255usize),
241
+
min_graphemes: None,
242
+
max_graphemes: None,
243
+
r#enum: None,
244
+
r#const: None,
245
+
known_values: None,
246
+
}),
247
+
);
248
+
map.insert(
249
+
::jacquard_common::smol_str::SmolStr::new_static("node"),
250
+
::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion {
251
+
description: None,
252
+
refs: vec![
253
+
::jacquard_common::CowStr::new_static("#file"),
254
+
::jacquard_common::CowStr::new_static("#directory"),
255
+
::jacquard_common::CowStr::new_static("#subfs")
256
+
],
257
+
closed: None,
258
+
}),
259
+
);
260
+
map
261
+
},
262
+
}),
263
+
);
264
+
map.insert(
265
+
::jacquard_common::smol_str::SmolStr::new_static("file"),
266
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
267
+
description: None,
268
+
required: Some(
269
+
vec![
270
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
271
+
::jacquard_common::smol_str::SmolStr::new_static("blob")
272
+
],
273
+
),
274
+
nullable: None,
275
+
properties: {
276
+
#[allow(unused_mut)]
277
+
let mut map = ::std::collections::BTreeMap::new();
278
+
map.insert(
279
+
::jacquard_common::smol_str::SmolStr::new_static("base64"),
280
+
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
281
+
description: None,
282
+
default: None,
283
+
r#const: None,
284
+
}),
285
+
);
286
+
map.insert(
287
+
::jacquard_common::smol_str::SmolStr::new_static("blob"),
288
+
::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob {
289
+
description: None,
290
+
accept: None,
291
+
max_size: None,
292
+
}),
293
+
);
294
+
map.insert(
295
+
::jacquard_common::smol_str::SmolStr::new_static("encoding"),
296
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
297
+
description: Some(
298
+
::jacquard_common::CowStr::new_static(
299
+
"Content encoding (e.g., gzip for compressed files)",
300
+
),
301
+
),
302
+
format: None,
303
+
default: None,
304
+
min_length: None,
305
+
max_length: None,
306
+
min_graphemes: None,
307
+
max_graphemes: None,
308
+
r#enum: None,
309
+
r#const: None,
310
+
known_values: None,
311
+
}),
312
+
);
313
+
map.insert(
314
+
::jacquard_common::smol_str::SmolStr::new_static("mimeType"),
315
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
316
+
description: Some(
317
+
::jacquard_common::CowStr::new_static(
318
+
"Original MIME type before compression",
319
+
),
320
+
),
321
+
format: None,
322
+
default: None,
323
+
min_length: None,
324
+
max_length: None,
325
+
min_graphemes: None,
326
+
max_graphemes: None,
327
+
r#enum: None,
328
+
r#const: None,
329
+
known_values: None,
330
+
}),
331
+
);
332
+
map.insert(
333
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
334
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
335
+
description: None,
336
+
format: None,
337
+
default: None,
338
+
min_length: None,
339
+
max_length: None,
340
+
min_graphemes: None,
341
+
max_graphemes: None,
342
+
r#enum: None,
343
+
r#const: None,
344
+
known_values: None,
345
+
}),
346
+
);
347
+
map
348
+
},
349
+
}),
350
+
);
351
+
map.insert(
352
+
::jacquard_common::smol_str::SmolStr::new_static("main"),
353
+
::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
354
+
description: Some(
355
+
::jacquard_common::CowStr::new_static(
356
+
"Virtual filesystem manifest for a Wisp site",
357
+
),
358
+
),
359
+
key: None,
360
+
record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
361
+
description: None,
362
+
required: Some(
363
+
vec![
364
+
::jacquard_common::smol_str::SmolStr::new_static("site"),
365
+
::jacquard_common::smol_str::SmolStr::new_static("root"),
366
+
::jacquard_common::smol_str::SmolStr::new_static("createdAt")
367
+
],
368
+
),
369
+
nullable: None,
370
+
properties: {
371
+
#[allow(unused_mut)]
372
+
let mut map = ::std::collections::BTreeMap::new();
373
+
map.insert(
374
+
::jacquard_common::smol_str::SmolStr::new_static(
375
+
"createdAt",
376
+
),
377
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
378
+
description: None,
379
+
format: Some(
380
+
::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
381
+
),
382
+
default: None,
383
+
min_length: None,
384
+
max_length: None,
385
+
min_graphemes: None,
386
+
max_graphemes: None,
387
+
r#enum: None,
388
+
r#const: None,
389
+
known_values: None,
390
+
}),
391
+
);
392
+
map.insert(
393
+
::jacquard_common::smol_str::SmolStr::new_static(
394
+
"fileCount",
395
+
),
396
+
::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger {
397
+
description: None,
398
+
default: None,
399
+
minimum: Some(0i64),
400
+
maximum: Some(1000i64),
401
+
r#enum: None,
402
+
r#const: None,
403
+
}),
404
+
);
405
+
map.insert(
406
+
::jacquard_common::smol_str::SmolStr::new_static("root"),
407
+
::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
408
+
description: None,
409
+
r#ref: ::jacquard_common::CowStr::new_static("#directory"),
410
+
}),
411
+
);
412
+
map.insert(
413
+
::jacquard_common::smol_str::SmolStr::new_static("site"),
414
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
415
+
description: None,
416
+
format: None,
417
+
default: None,
418
+
min_length: None,
419
+
max_length: None,
420
+
min_graphemes: None,
421
+
max_graphemes: None,
422
+
r#enum: None,
423
+
r#const: None,
424
+
known_values: None,
425
+
}),
426
+
);
427
+
map
428
+
},
429
+
}),
430
+
}),
431
+
);
432
+
map.insert(
433
+
::jacquard_common::smol_str::SmolStr::new_static("subfs"),
434
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
435
+
description: None,
436
+
required: Some(
437
+
vec![
438
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
439
+
::jacquard_common::smol_str::SmolStr::new_static("subject")
440
+
],
441
+
),
442
+
nullable: None,
443
+
properties: {
444
+
#[allow(unused_mut)]
445
+
let mut map = ::std::collections::BTreeMap::new();
446
+
map.insert(
447
+
::jacquard_common::smol_str::SmolStr::new_static("flat"),
448
+
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
449
+
description: None,
450
+
default: None,
451
+
r#const: None,
452
+
}),
453
+
);
454
+
map.insert(
455
+
::jacquard_common::smol_str::SmolStr::new_static("subject"),
456
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
457
+
description: Some(
458
+
::jacquard_common::CowStr::new_static(
459
+
"AT-URI pointing to a place.wisp.subfs record containing this subtree.",
460
+
),
461
+
),
462
+
format: Some(
463
+
::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
464
+
),
465
+
default: None,
466
+
min_length: None,
467
+
max_length: None,
468
+
min_graphemes: None,
469
+
max_graphemes: None,
470
+
r#enum: None,
471
+
r#const: None,
472
+
known_values: None,
473
+
}),
474
+
);
475
+
map.insert(
476
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
477
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
478
+
description: None,
479
+
format: None,
480
+
default: None,
481
+
min_length: None,
482
+
max_length: None,
483
+
min_graphemes: None,
484
+
max_graphemes: None,
485
+
r#enum: None,
486
+
r#const: None,
487
+
known_values: None,
488
+
}),
489
+
);
490
+
map
491
+
},
492
+
}),
493
+
);
494
+
map
495
+
},
496
+
}
497
+
}
498
+
499
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> {
500
+
fn nsid() -> &'static str {
501
+
"place.wisp.fs"
502
+
}
503
+
fn def_name() -> &'static str {
504
+
"directory"
505
+
}
506
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
507
+
lexicon_doc_place_wisp_fs()
508
+
}
509
+
fn validate(
510
+
&self,
511
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
512
+
{
513
+
let value = &self.entries;
514
+
#[allow(unused_comparisons)]
515
+
if value.len() > 500usize {
516
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
517
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
518
+
"entries",
519
+
),
520
+
max: 500usize,
521
+
actual: value.len(),
522
+
});
523
+
}
524
+
}
525
+
Ok(())
526
+
}
527
+
}
528
+
529
+
#[jacquard_derive::lexicon]
530
+
#[derive(
531
+
serde::Serialize,
532
+
serde::Deserialize,
533
+
Debug,
534
+
Clone,
535
+
PartialEq,
536
+
Eq,
537
+
jacquard_derive::IntoStatic
538
+
)]
539
+
#[serde(rename_all = "camelCase")]
540
+
pub struct Entry<'a> {
541
+
#[serde(borrow)]
542
+
pub name: jacquard_common::CowStr<'a>,
543
+
#[serde(borrow)]
544
+
pub node: EntryNode<'a>,
545
+
}
546
+
547
+
pub mod entry_state {
548
+
549
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
550
+
#[allow(unused)]
551
+
use ::core::marker::PhantomData;
552
+
mod sealed {
553
+
pub trait Sealed {}
554
+
}
555
+
/// State trait tracking which required fields have been set
556
+
pub trait State: sealed::Sealed {
557
+
type Node;
558
+
type Name;
559
+
}
560
+
/// Empty state - all required fields are unset
561
+
pub struct Empty(());
562
+
impl sealed::Sealed for Empty {}
563
+
impl State for Empty {
564
+
type Node = Unset;
565
+
type Name = Unset;
566
+
}
567
+
///State transition - sets the `node` field to Set
568
+
pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>);
569
+
impl<S: State> sealed::Sealed for SetNode<S> {}
570
+
impl<S: State> State for SetNode<S> {
571
+
type Node = Set<members::node>;
572
+
type Name = S::Name;
573
+
}
574
+
///State transition - sets the `name` field to Set
575
+
pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>);
576
+
impl<S: State> sealed::Sealed for SetName<S> {}
577
+
impl<S: State> State for SetName<S> {
578
+
type Node = S::Node;
579
+
type Name = Set<members::name>;
580
+
}
581
+
/// Marker types for field names
582
+
#[allow(non_camel_case_types)]
583
+
pub mod members {
584
+
///Marker type for the `node` field
585
+
pub struct node(());
586
+
///Marker type for the `name` field
587
+
pub struct name(());
588
+
}
589
+
}
590
+
591
+
/// Builder for constructing an instance of this type
592
+
pub struct EntryBuilder<'a, S: entry_state::State> {
593
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
594
+
__unsafe_private_named: (
595
+
::core::option::Option<jacquard_common::CowStr<'a>>,
596
+
::core::option::Option<EntryNode<'a>>,
597
+
),
598
+
_phantom: ::core::marker::PhantomData<&'a ()>,
599
+
}
600
+
601
+
impl<'a> Entry<'a> {
602
+
/// Create a new builder for this type
603
+
pub fn new() -> EntryBuilder<'a, entry_state::Empty> {
604
+
EntryBuilder::new()
605
+
}
606
+
}
607
+
608
+
impl<'a> EntryBuilder<'a, entry_state::Empty> {
609
+
/// Create a new builder with all fields unset
610
+
pub fn new() -> Self {
611
+
EntryBuilder {
612
+
_phantom_state: ::core::marker::PhantomData,
613
+
__unsafe_private_named: (None, None),
614
+
_phantom: ::core::marker::PhantomData,
615
+
}
616
+
}
617
+
}
618
+
619
+
impl<'a, S> EntryBuilder<'a, S>
620
+
where
621
+
S: entry_state::State,
622
+
S::Name: entry_state::IsUnset,
623
+
{
624
+
/// Set the `name` field (required)
625
+
pub fn name(
626
+
mut self,
627
+
value: impl Into<jacquard_common::CowStr<'a>>,
628
+
) -> EntryBuilder<'a, entry_state::SetName<S>> {
629
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
630
+
EntryBuilder {
631
+
_phantom_state: ::core::marker::PhantomData,
632
+
__unsafe_private_named: self.__unsafe_private_named,
633
+
_phantom: ::core::marker::PhantomData,
634
+
}
635
+
}
636
+
}
637
+
638
+
impl<'a, S> EntryBuilder<'a, S>
639
+
where
640
+
S: entry_state::State,
641
+
S::Node: entry_state::IsUnset,
642
+
{
643
+
/// Set the `node` field (required)
644
+
pub fn node(
645
+
mut self,
646
+
value: impl Into<EntryNode<'a>>,
647
+
) -> EntryBuilder<'a, entry_state::SetNode<S>> {
648
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
649
+
EntryBuilder {
650
+
_phantom_state: ::core::marker::PhantomData,
651
+
__unsafe_private_named: self.__unsafe_private_named,
652
+
_phantom: ::core::marker::PhantomData,
653
+
}
654
+
}
655
+
}
656
+
657
+
impl<'a, S> EntryBuilder<'a, S>
658
+
where
659
+
S: entry_state::State,
660
+
S::Node: entry_state::IsSet,
661
+
S::Name: entry_state::IsSet,
662
+
{
663
+
/// Build the final struct
664
+
pub fn build(self) -> Entry<'a> {
665
+
Entry {
666
+
name: self.__unsafe_private_named.0.unwrap(),
667
+
node: self.__unsafe_private_named.1.unwrap(),
668
+
extra_data: Default::default(),
669
+
}
670
+
}
671
+
/// Build the final struct with custom extra_data
672
+
pub fn build_with_data(
673
+
self,
674
+
extra_data: std::collections::BTreeMap<
675
+
jacquard_common::smol_str::SmolStr,
676
+
jacquard_common::types::value::Data<'a>,
677
+
>,
678
+
) -> Entry<'a> {
679
+
Entry {
680
+
name: self.__unsafe_private_named.0.unwrap(),
681
+
node: self.__unsafe_private_named.1.unwrap(),
682
+
extra_data: Some(extra_data),
683
+
}
684
+
}
685
+
}
686
+
687
+
#[jacquard_derive::open_union]
688
+
#[derive(
689
+
serde::Serialize,
690
+
serde::Deserialize,
691
+
Debug,
692
+
Clone,
693
+
PartialEq,
694
+
Eq,
695
+
jacquard_derive::IntoStatic
696
+
)]
697
+
#[serde(tag = "$type")]
698
+
#[serde(bound(deserialize = "'de: 'a"))]
699
+
pub enum EntryNode<'a> {
700
+
#[serde(rename = "place.wisp.fs#file")]
701
+
File(Box<crate::place_wisp::fs::File<'a>>),
702
+
#[serde(rename = "place.wisp.fs#directory")]
703
+
Directory(Box<crate::place_wisp::fs::Directory<'a>>),
704
+
#[serde(rename = "place.wisp.fs#subfs")]
705
+
Subfs(Box<crate::place_wisp::fs::Subfs<'a>>),
706
+
}
707
+
708
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> {
709
+
fn nsid() -> &'static str {
710
+
"place.wisp.fs"
711
+
}
712
+
fn def_name() -> &'static str {
713
+
"entry"
714
+
}
715
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
716
+
lexicon_doc_place_wisp_fs()
717
+
}
718
+
fn validate(
719
+
&self,
720
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
721
+
{
722
+
let value = &self.name;
723
+
#[allow(unused_comparisons)]
724
+
if <str>::len(value.as_ref()) > 255usize {
725
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
726
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
727
+
"name",
728
+
),
729
+
max: 255usize,
730
+
actual: <str>::len(value.as_ref()),
731
+
});
732
+
}
733
+
}
734
+
Ok(())
735
+
}
736
+
}
737
+
738
+
#[jacquard_derive::lexicon]
739
+
#[derive(
740
+
serde::Serialize,
741
+
serde::Deserialize,
742
+
Debug,
743
+
Clone,
744
+
PartialEq,
745
+
Eq,
746
+
jacquard_derive::IntoStatic
747
+
)]
748
+
#[serde(rename_all = "camelCase")]
749
+
pub struct File<'a> {
750
+
/// True if blob content is base64-encoded (used to bypass PDS content sniffing)
751
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
752
+
pub base64: std::option::Option<bool>,
753
+
/// Content blob ref
754
+
#[serde(borrow)]
755
+
pub blob: jacquard_common::types::blob::BlobRef<'a>,
756
+
/// Content encoding (e.g., gzip for compressed files)
757
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
758
+
#[serde(borrow)]
759
+
pub encoding: std::option::Option<jacquard_common::CowStr<'a>>,
760
+
/// Original MIME type before compression
761
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
762
+
#[serde(borrow)]
763
+
pub mime_type: std::option::Option<jacquard_common::CowStr<'a>>,
764
+
#[serde(borrow)]
765
+
pub r#type: jacquard_common::CowStr<'a>,
766
+
}
767
+
768
+
pub mod file_state {
769
+
770
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
771
+
#[allow(unused)]
772
+
use ::core::marker::PhantomData;
773
+
mod sealed {
774
+
pub trait Sealed {}
775
+
}
776
+
/// State trait tracking which required fields have been set
777
+
pub trait State: sealed::Sealed {
778
+
type Type;
779
+
type Blob;
780
+
}
781
+
/// Empty state - all required fields are unset
782
+
pub struct Empty(());
783
+
impl sealed::Sealed for Empty {}
784
+
impl State for Empty {
785
+
type Type = Unset;
786
+
type Blob = Unset;
787
+
}
788
+
///State transition - sets the `type` field to Set
789
+
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
790
+
impl<S: State> sealed::Sealed for SetType<S> {}
791
+
impl<S: State> State for SetType<S> {
792
+
type Type = Set<members::r#type>;
793
+
type Blob = S::Blob;
794
+
}
795
+
///State transition - sets the `blob` field to Set
796
+
pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>);
797
+
impl<S: State> sealed::Sealed for SetBlob<S> {}
798
+
impl<S: State> State for SetBlob<S> {
799
+
type Type = S::Type;
800
+
type Blob = Set<members::blob>;
801
+
}
802
+
/// Marker types for field names
803
+
#[allow(non_camel_case_types)]
804
+
pub mod members {
805
+
///Marker type for the `type` field
806
+
pub struct r#type(());
807
+
///Marker type for the `blob` field
808
+
pub struct blob(());
809
+
}
810
+
}
811
+
812
+
/// Builder for constructing an instance of this type
813
+
pub struct FileBuilder<'a, S: file_state::State> {
814
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
815
+
__unsafe_private_named: (
816
+
::core::option::Option<bool>,
817
+
::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>,
818
+
::core::option::Option<jacquard_common::CowStr<'a>>,
819
+
::core::option::Option<jacquard_common::CowStr<'a>>,
820
+
::core::option::Option<jacquard_common::CowStr<'a>>,
821
+
),
822
+
_phantom: ::core::marker::PhantomData<&'a ()>,
823
+
}
824
+
825
+
impl<'a> File<'a> {
826
+
/// Create a new builder for this type
827
+
pub fn new() -> FileBuilder<'a, file_state::Empty> {
828
+
FileBuilder::new()
829
+
}
830
+
}
831
+
832
+
impl<'a> FileBuilder<'a, file_state::Empty> {
833
+
/// Create a new builder with all fields unset
834
+
pub fn new() -> Self {
835
+
FileBuilder {
836
+
_phantom_state: ::core::marker::PhantomData,
837
+
__unsafe_private_named: (None, None, None, None, None),
838
+
_phantom: ::core::marker::PhantomData,
839
+
}
840
+
}
841
+
}
842
+
843
+
impl<'a, S: file_state::State> FileBuilder<'a, S> {
844
+
/// Set the `base64` field (optional)
845
+
pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self {
846
+
self.__unsafe_private_named.0 = value.into();
847
+
self
848
+
}
849
+
/// Set the `base64` field to an Option value (optional)
850
+
pub fn maybe_base64(mut self, value: Option<bool>) -> Self {
851
+
self.__unsafe_private_named.0 = value;
852
+
self
853
+
}
854
+
}
855
+
856
+
impl<'a, S> FileBuilder<'a, S>
857
+
where
858
+
S: file_state::State,
859
+
S::Blob: file_state::IsUnset,
860
+
{
861
+
/// Set the `blob` field (required)
862
+
pub fn blob(
863
+
mut self,
864
+
value: impl Into<jacquard_common::types::blob::BlobRef<'a>>,
865
+
) -> FileBuilder<'a, file_state::SetBlob<S>> {
866
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
867
+
FileBuilder {
868
+
_phantom_state: ::core::marker::PhantomData,
869
+
__unsafe_private_named: self.__unsafe_private_named,
870
+
_phantom: ::core::marker::PhantomData,
871
+
}
872
+
}
873
+
}
874
+
875
+
impl<'a, S: file_state::State> FileBuilder<'a, S> {
876
+
/// Set the `encoding` field (optional)
877
+
pub fn encoding(
878
+
mut self,
879
+
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
880
+
) -> Self {
881
+
self.__unsafe_private_named.2 = value.into();
882
+
self
883
+
}
884
+
/// Set the `encoding` field to an Option value (optional)
885
+
pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
886
+
self.__unsafe_private_named.2 = value;
887
+
self
888
+
}
889
+
}
890
+
891
+
impl<'a, S: file_state::State> FileBuilder<'a, S> {
892
+
/// Set the `mimeType` field (optional)
893
+
pub fn mime_type(
894
+
mut self,
895
+
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
896
+
) -> Self {
897
+
self.__unsafe_private_named.3 = value.into();
898
+
self
899
+
}
900
+
/// Set the `mimeType` field to an Option value (optional)
901
+
pub fn maybe_mime_type(
902
+
mut self,
903
+
value: Option<jacquard_common::CowStr<'a>>,
904
+
) -> Self {
905
+
self.__unsafe_private_named.3 = value;
906
+
self
907
+
}
908
+
}
909
+
910
+
impl<'a, S> FileBuilder<'a, S>
911
+
where
912
+
S: file_state::State,
913
+
S::Type: file_state::IsUnset,
914
+
{
915
+
/// Set the `type` field (required)
916
+
pub fn r#type(
917
+
mut self,
918
+
value: impl Into<jacquard_common::CowStr<'a>>,
919
+
) -> FileBuilder<'a, file_state::SetType<S>> {
920
+
self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into());
921
+
FileBuilder {
922
+
_phantom_state: ::core::marker::PhantomData,
923
+
__unsafe_private_named: self.__unsafe_private_named,
924
+
_phantom: ::core::marker::PhantomData,
925
+
}
926
+
}
927
+
}
928
+
929
+
impl<'a, S> FileBuilder<'a, S>
930
+
where
931
+
S: file_state::State,
932
+
S::Type: file_state::IsSet,
933
+
S::Blob: file_state::IsSet,
934
+
{
935
+
/// Build the final struct
936
+
pub fn build(self) -> File<'a> {
937
+
File {
938
+
base64: self.__unsafe_private_named.0,
939
+
blob: self.__unsafe_private_named.1.unwrap(),
940
+
encoding: self.__unsafe_private_named.2,
941
+
mime_type: self.__unsafe_private_named.3,
942
+
r#type: self.__unsafe_private_named.4.unwrap(),
943
+
extra_data: Default::default(),
944
+
}
945
+
}
946
+
/// Build the final struct with custom extra_data
947
+
pub fn build_with_data(
948
+
self,
949
+
extra_data: std::collections::BTreeMap<
950
+
jacquard_common::smol_str::SmolStr,
951
+
jacquard_common::types::value::Data<'a>,
952
+
>,
953
+
) -> File<'a> {
954
+
File {
955
+
base64: self.__unsafe_private_named.0,
956
+
blob: self.__unsafe_private_named.1.unwrap(),
957
+
encoding: self.__unsafe_private_named.2,
958
+
mime_type: self.__unsafe_private_named.3,
959
+
r#type: self.__unsafe_private_named.4.unwrap(),
960
+
extra_data: Some(extra_data),
961
+
}
962
+
}
963
+
}
964
+
965
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> {
966
+
fn nsid() -> &'static str {
967
+
"place.wisp.fs"
968
+
}
969
+
fn def_name() -> &'static str {
970
+
"file"
971
+
}
972
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
973
+
lexicon_doc_place_wisp_fs()
974
+
}
975
+
fn validate(
976
+
&self,
977
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
978
+
Ok(())
979
+
}
980
+
}
981
+
982
+
/// Virtual filesystem manifest for a Wisp site
983
+
#[jacquard_derive::lexicon]
984
+
#[derive(
985
+
serde::Serialize,
986
+
serde::Deserialize,
987
+
Debug,
988
+
Clone,
989
+
PartialEq,
990
+
Eq,
991
+
jacquard_derive::IntoStatic
992
+
)]
993
+
#[serde(rename_all = "camelCase")]
994
+
pub struct Fs<'a> {
995
+
pub created_at: jacquard_common::types::string::Datetime,
996
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
997
+
pub file_count: std::option::Option<i64>,
998
+
#[serde(borrow)]
999
+
pub root: crate::place_wisp::fs::Directory<'a>,
1000
+
#[serde(borrow)]
1001
+
pub site: jacquard_common::CowStr<'a>,
1002
+
}
1003
+
1004
+
pub mod fs_state {
1005
+
1006
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
1007
+
#[allow(unused)]
1008
+
use ::core::marker::PhantomData;
1009
+
mod sealed {
1010
+
pub trait Sealed {}
1011
+
}
1012
+
/// State trait tracking which required fields have been set
1013
+
pub trait State: sealed::Sealed {
1014
+
type CreatedAt;
1015
+
type Site;
1016
+
type Root;
1017
+
}
1018
+
/// Empty state - all required fields are unset
1019
+
pub struct Empty(());
1020
+
impl sealed::Sealed for Empty {}
1021
+
impl State for Empty {
1022
+
type CreatedAt = Unset;
1023
+
type Site = Unset;
1024
+
type Root = Unset;
1025
+
}
1026
+
///State transition - sets the `created_at` field to Set
1027
+
pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
1028
+
impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
1029
+
impl<S: State> State for SetCreatedAt<S> {
1030
+
type CreatedAt = Set<members::created_at>;
1031
+
type Site = S::Site;
1032
+
type Root = S::Root;
1033
+
}
1034
+
///State transition - sets the `site` field to Set
1035
+
pub struct SetSite<S: State = Empty>(PhantomData<fn() -> S>);
1036
+
impl<S: State> sealed::Sealed for SetSite<S> {}
1037
+
impl<S: State> State for SetSite<S> {
1038
+
type CreatedAt = S::CreatedAt;
1039
+
type Site = Set<members::site>;
1040
+
type Root = S::Root;
1041
+
}
1042
+
///State transition - sets the `root` field to Set
1043
+
pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>);
1044
+
impl<S: State> sealed::Sealed for SetRoot<S> {}
1045
+
impl<S: State> State for SetRoot<S> {
1046
+
type CreatedAt = S::CreatedAt;
1047
+
type Site = S::Site;
1048
+
type Root = Set<members::root>;
1049
+
}
1050
+
/// Marker types for field names
1051
+
#[allow(non_camel_case_types)]
1052
+
pub mod members {
1053
+
///Marker type for the `created_at` field
1054
+
pub struct created_at(());
1055
+
///Marker type for the `site` field
1056
+
pub struct site(());
1057
+
///Marker type for the `root` field
1058
+
pub struct root(());
1059
+
}
1060
+
}
1061
+
1062
+
/// Builder for constructing an instance of this type
1063
+
pub struct FsBuilder<'a, S: fs_state::State> {
1064
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1065
+
__unsafe_private_named: (
1066
+
::core::option::Option<jacquard_common::types::string::Datetime>,
1067
+
::core::option::Option<i64>,
1068
+
::core::option::Option<crate::place_wisp::fs::Directory<'a>>,
1069
+
::core::option::Option<jacquard_common::CowStr<'a>>,
1070
+
),
1071
+
_phantom: ::core::marker::PhantomData<&'a ()>,
1072
+
}
1073
+
1074
+
impl<'a> Fs<'a> {
1075
+
/// Create a new builder for this type
1076
+
pub fn new() -> FsBuilder<'a, fs_state::Empty> {
1077
+
FsBuilder::new()
1078
+
}
1079
+
}
1080
+
1081
+
impl<'a> FsBuilder<'a, fs_state::Empty> {
1082
+
/// Create a new builder with all fields unset
1083
+
pub fn new() -> Self {
1084
+
FsBuilder {
1085
+
_phantom_state: ::core::marker::PhantomData,
1086
+
__unsafe_private_named: (None, None, None, None),
1087
+
_phantom: ::core::marker::PhantomData,
1088
+
}
1089
+
}
1090
+
}
1091
+
1092
+
impl<'a, S> FsBuilder<'a, S>
1093
+
where
1094
+
S: fs_state::State,
1095
+
S::CreatedAt: fs_state::IsUnset,
1096
+
{
1097
+
/// Set the `createdAt` field (required)
1098
+
pub fn created_at(
1099
+
mut self,
1100
+
value: impl Into<jacquard_common::types::string::Datetime>,
1101
+
) -> FsBuilder<'a, fs_state::SetCreatedAt<S>> {
1102
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
1103
+
FsBuilder {
1104
+
_phantom_state: ::core::marker::PhantomData,
1105
+
__unsafe_private_named: self.__unsafe_private_named,
1106
+
_phantom: ::core::marker::PhantomData,
1107
+
}
1108
+
}
1109
+
}
1110
+
1111
+
impl<'a, S: fs_state::State> FsBuilder<'a, S> {
1112
+
/// Set the `fileCount` field (optional)
1113
+
pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self {
1114
+
self.__unsafe_private_named.1 = value.into();
1115
+
self
1116
+
}
1117
+
/// Set the `fileCount` field to an Option value (optional)
1118
+
pub fn maybe_file_count(mut self, value: Option<i64>) -> Self {
1119
+
self.__unsafe_private_named.1 = value;
1120
+
self
1121
+
}
1122
+
}
1123
+
1124
+
impl<'a, S> FsBuilder<'a, S>
1125
+
where
1126
+
S: fs_state::State,
1127
+
S::Root: fs_state::IsUnset,
1128
+
{
1129
+
/// Set the `root` field (required)
1130
+
pub fn root(
1131
+
mut self,
1132
+
value: impl Into<crate::place_wisp::fs::Directory<'a>>,
1133
+
) -> FsBuilder<'a, fs_state::SetRoot<S>> {
1134
+
self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
1135
+
FsBuilder {
1136
+
_phantom_state: ::core::marker::PhantomData,
1137
+
__unsafe_private_named: self.__unsafe_private_named,
1138
+
_phantom: ::core::marker::PhantomData,
1139
+
}
1140
+
}
1141
+
}
1142
+
1143
+
impl<'a, S> FsBuilder<'a, S>
1144
+
where
1145
+
S: fs_state::State,
1146
+
S::Site: fs_state::IsUnset,
1147
+
{
1148
+
/// Set the `site` field (required)
1149
+
pub fn site(
1150
+
mut self,
1151
+
value: impl Into<jacquard_common::CowStr<'a>>,
1152
+
) -> FsBuilder<'a, fs_state::SetSite<S>> {
1153
+
self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into());
1154
+
FsBuilder {
1155
+
_phantom_state: ::core::marker::PhantomData,
1156
+
__unsafe_private_named: self.__unsafe_private_named,
1157
+
_phantom: ::core::marker::PhantomData,
1158
+
}
1159
+
}
1160
+
}
1161
+
1162
+
impl<'a, S> FsBuilder<'a, S>
1163
+
where
1164
+
S: fs_state::State,
1165
+
S::CreatedAt: fs_state::IsSet,
1166
+
S::Site: fs_state::IsSet,
1167
+
S::Root: fs_state::IsSet,
1168
+
{
1169
+
/// Build the final struct
1170
+
pub fn build(self) -> Fs<'a> {
1171
+
Fs {
1172
+
created_at: self.__unsafe_private_named.0.unwrap(),
1173
+
file_count: self.__unsafe_private_named.1,
1174
+
root: self.__unsafe_private_named.2.unwrap(),
1175
+
site: self.__unsafe_private_named.3.unwrap(),
1176
+
extra_data: Default::default(),
1177
+
}
1178
+
}
1179
+
/// Build the final struct with custom extra_data
1180
+
pub fn build_with_data(
1181
+
self,
1182
+
extra_data: std::collections::BTreeMap<
1183
+
jacquard_common::smol_str::SmolStr,
1184
+
jacquard_common::types::value::Data<'a>,
1185
+
>,
1186
+
) -> Fs<'a> {
1187
+
Fs {
1188
+
created_at: self.__unsafe_private_named.0.unwrap(),
1189
+
file_count: self.__unsafe_private_named.1,
1190
+
root: self.__unsafe_private_named.2.unwrap(),
1191
+
site: self.__unsafe_private_named.3.unwrap(),
1192
+
extra_data: Some(extra_data),
1193
+
}
1194
+
}
1195
+
}
1196
+
1197
+
impl<'a> Fs<'a> {
1198
+
pub fn uri(
1199
+
uri: impl Into<jacquard_common::CowStr<'a>>,
1200
+
) -> Result<
1201
+
jacquard_common::types::uri::RecordUri<'a, FsRecord>,
1202
+
jacquard_common::types::uri::UriError,
1203
+
> {
1204
+
jacquard_common::types::uri::RecordUri::try_from_uri(
1205
+
jacquard_common::types::string::AtUri::new_cow(uri.into())?,
1206
+
)
1207
+
}
1208
+
}
1209
+
1210
+
/// Typed wrapper for GetRecord response with this collection's record type.
1211
+
#[derive(
1212
+
serde::Serialize,
1213
+
serde::Deserialize,
1214
+
Debug,
1215
+
Clone,
1216
+
PartialEq,
1217
+
Eq,
1218
+
jacquard_derive::IntoStatic
1219
+
)]
1220
+
#[serde(rename_all = "camelCase")]
1221
+
pub struct FsGetRecordOutput<'a> {
1222
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
1223
+
#[serde(borrow)]
1224
+
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
1225
+
#[serde(borrow)]
1226
+
pub uri: jacquard_common::types::string::AtUri<'a>,
1227
+
#[serde(borrow)]
1228
+
pub value: Fs<'a>,
1229
+
}
1230
+
1231
+
impl From<FsGetRecordOutput<'_>> for Fs<'_> {
1232
+
fn from(output: FsGetRecordOutput<'_>) -> Self {
1233
+
use jacquard_common::IntoStatic;
1234
+
output.value.into_static()
1235
+
}
1236
+
}
1237
+
1238
+
impl jacquard_common::types::collection::Collection for Fs<'_> {
1239
+
const NSID: &'static str = "place.wisp.fs";
1240
+
type Record = FsRecord;
1241
+
}
1242
+
1243
+
/// Marker type for deserializing records from this collection.
1244
+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
1245
+
pub struct FsRecord;
1246
+
impl jacquard_common::xrpc::XrpcResp for FsRecord {
1247
+
const NSID: &'static str = "place.wisp.fs";
1248
+
const ENCODING: &'static str = "application/json";
1249
+
type Output<'de> = FsGetRecordOutput<'de>;
1250
+
type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
1251
+
}
1252
+
1253
+
impl jacquard_common::types::collection::Collection for FsRecord {
1254
+
const NSID: &'static str = "place.wisp.fs";
1255
+
type Record = FsRecord;
1256
+
}
1257
+
1258
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Fs<'a> {
1259
+
fn nsid() -> &'static str {
1260
+
"place.wisp.fs"
1261
+
}
1262
+
fn def_name() -> &'static str {
1263
+
"main"
1264
+
}
1265
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1266
+
lexicon_doc_place_wisp_fs()
1267
+
}
1268
+
fn validate(
1269
+
&self,
1270
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1271
+
if let Some(ref value) = self.file_count {
1272
+
if *value > 1000i64 {
1273
+
return Err(::jacquard_lexicon::validation::ConstraintError::Maximum {
1274
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1275
+
"file_count",
1276
+
),
1277
+
max: 1000i64,
1278
+
actual: *value,
1279
+
});
1280
+
}
1281
+
}
1282
+
if let Some(ref value) = self.file_count {
1283
+
if *value < 0i64 {
1284
+
return Err(::jacquard_lexicon::validation::ConstraintError::Minimum {
1285
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1286
+
"file_count",
1287
+
),
1288
+
min: 0i64,
1289
+
actual: *value,
1290
+
});
1291
+
}
1292
+
}
1293
+
Ok(())
1294
+
}
1295
+
}
1296
+
1297
+
#[jacquard_derive::lexicon]
1298
+
#[derive(
1299
+
serde::Serialize,
1300
+
serde::Deserialize,
1301
+
Debug,
1302
+
Clone,
1303
+
PartialEq,
1304
+
Eq,
1305
+
jacquard_derive::IntoStatic
1306
+
)]
1307
+
#[serde(rename_all = "camelCase")]
1308
+
pub struct Subfs<'a> {
1309
+
/// If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure.
1310
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
1311
+
pub flat: std::option::Option<bool>,
1312
+
/// AT-URI pointing to a place.wisp.subfs record containing this subtree.
1313
+
#[serde(borrow)]
1314
+
pub subject: jacquard_common::types::string::AtUri<'a>,
1315
+
#[serde(borrow)]
1316
+
pub r#type: jacquard_common::CowStr<'a>,
1317
+
}
1318
+
1319
+
pub mod subfs_state {
1320
+
1321
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
1322
+
#[allow(unused)]
1323
+
use ::core::marker::PhantomData;
1324
+
mod sealed {
1325
+
pub trait Sealed {}
1326
+
}
1327
+
/// State trait tracking which required fields have been set
1328
+
pub trait State: sealed::Sealed {
1329
+
type Type;
1330
+
type Subject;
1331
+
}
1332
+
/// Empty state - all required fields are unset
1333
+
pub struct Empty(());
1334
+
impl sealed::Sealed for Empty {}
1335
+
impl State for Empty {
1336
+
type Type = Unset;
1337
+
type Subject = Unset;
1338
+
}
1339
+
///State transition - sets the `type` field to Set
1340
+
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
1341
+
impl<S: State> sealed::Sealed for SetType<S> {}
1342
+
impl<S: State> State for SetType<S> {
1343
+
type Type = Set<members::r#type>;
1344
+
type Subject = S::Subject;
1345
+
}
1346
+
///State transition - sets the `subject` field to Set
1347
+
pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>);
1348
+
impl<S: State> sealed::Sealed for SetSubject<S> {}
1349
+
impl<S: State> State for SetSubject<S> {
1350
+
type Type = S::Type;
1351
+
type Subject = Set<members::subject>;
1352
+
}
1353
+
/// Marker types for field names
1354
+
#[allow(non_camel_case_types)]
1355
+
pub mod members {
1356
+
///Marker type for the `type` field
1357
+
pub struct r#type(());
1358
+
///Marker type for the `subject` field
1359
+
pub struct subject(());
1360
+
}
1361
+
}
1362
+
1363
+
/// Builder for constructing an instance of this type
1364
+
pub struct SubfsBuilder<'a, S: subfs_state::State> {
1365
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1366
+
__unsafe_private_named: (
1367
+
::core::option::Option<bool>,
1368
+
::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
1369
+
::core::option::Option<jacquard_common::CowStr<'a>>,
1370
+
),
1371
+
_phantom: ::core::marker::PhantomData<&'a ()>,
1372
+
}
1373
+
1374
+
impl<'a> Subfs<'a> {
1375
+
/// Create a new builder for this type
1376
+
pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> {
1377
+
SubfsBuilder::new()
1378
+
}
1379
+
}
1380
+
1381
+
impl<'a> SubfsBuilder<'a, subfs_state::Empty> {
1382
+
/// Create a new builder with all fields unset
1383
+
pub fn new() -> Self {
1384
+
SubfsBuilder {
1385
+
_phantom_state: ::core::marker::PhantomData,
1386
+
__unsafe_private_named: (None, None, None),
1387
+
_phantom: ::core::marker::PhantomData,
1388
+
}
1389
+
}
1390
+
}
1391
+
1392
+
impl<'a, S: subfs_state::State> SubfsBuilder<'a, S> {
1393
+
/// Set the `flat` field (optional)
1394
+
pub fn flat(mut self, value: impl Into<Option<bool>>) -> Self {
1395
+
self.__unsafe_private_named.0 = value.into();
1396
+
self
1397
+
}
1398
+
/// Set the `flat` field to an Option value (optional)
1399
+
pub fn maybe_flat(mut self, value: Option<bool>) -> Self {
1400
+
self.__unsafe_private_named.0 = value;
1401
+
self
1402
+
}
1403
+
}
1404
+
1405
+
impl<'a, S> SubfsBuilder<'a, S>
1406
+
where
1407
+
S: subfs_state::State,
1408
+
S::Subject: subfs_state::IsUnset,
1409
+
{
1410
+
/// Set the `subject` field (required)
1411
+
pub fn subject(
1412
+
mut self,
1413
+
value: impl Into<jacquard_common::types::string::AtUri<'a>>,
1414
+
) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> {
1415
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
1416
+
SubfsBuilder {
1417
+
_phantom_state: ::core::marker::PhantomData,
1418
+
__unsafe_private_named: self.__unsafe_private_named,
1419
+
_phantom: ::core::marker::PhantomData,
1420
+
}
1421
+
}
1422
+
}
1423
+
1424
+
impl<'a, S> SubfsBuilder<'a, S>
1425
+
where
1426
+
S: subfs_state::State,
1427
+
S::Type: subfs_state::IsUnset,
1428
+
{
1429
+
/// Set the `type` field (required)
1430
+
pub fn r#type(
1431
+
mut self,
1432
+
value: impl Into<jacquard_common::CowStr<'a>>,
1433
+
) -> SubfsBuilder<'a, subfs_state::SetType<S>> {
1434
+
self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
1435
+
SubfsBuilder {
1436
+
_phantom_state: ::core::marker::PhantomData,
1437
+
__unsafe_private_named: self.__unsafe_private_named,
1438
+
_phantom: ::core::marker::PhantomData,
1439
+
}
1440
+
}
1441
+
}
1442
+
1443
+
impl<'a, S> SubfsBuilder<'a, S>
1444
+
where
1445
+
S: subfs_state::State,
1446
+
S::Type: subfs_state::IsSet,
1447
+
S::Subject: subfs_state::IsSet,
1448
+
{
1449
+
/// Build the final struct
1450
+
pub fn build(self) -> Subfs<'a> {
1451
+
Subfs {
1452
+
flat: self.__unsafe_private_named.0,
1453
+
subject: self.__unsafe_private_named.1.unwrap(),
1454
+
r#type: self.__unsafe_private_named.2.unwrap(),
1455
+
extra_data: Default::default(),
1456
+
}
1457
+
}
1458
+
/// Build the final struct with custom extra_data
1459
+
pub fn build_with_data(
1460
+
self,
1461
+
extra_data: std::collections::BTreeMap<
1462
+
jacquard_common::smol_str::SmolStr,
1463
+
jacquard_common::types::value::Data<'a>,
1464
+
>,
1465
+
) -> Subfs<'a> {
1466
+
Subfs {
1467
+
flat: self.__unsafe_private_named.0,
1468
+
subject: self.__unsafe_private_named.1.unwrap(),
1469
+
r#type: self.__unsafe_private_named.2.unwrap(),
1470
+
extra_data: Some(extra_data),
1471
+
}
1472
+
}
1473
+
}
1474
+
1475
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> {
1476
+
fn nsid() -> &'static str {
1477
+
"place.wisp.fs"
1478
+
}
1479
+
fn def_name() -> &'static str {
1480
+
"subfs"
1481
+
}
1482
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1483
+
lexicon_doc_place_wisp_fs()
1484
+
}
1485
+
fn validate(
1486
+
&self,
1487
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1488
+
Ok(())
1489
+
}
1490
+
}
+653
cli/crates/lexicons/src/place_wisp/settings.rs
+653
cli/crates/lexicons/src/place_wisp/settings.rs
···
···
1
+
// @generated by jacquard-lexicon. DO NOT EDIT.
2
+
//
3
+
// Lexicon: place.wisp.settings
4
+
//
5
+
// This file was automatically generated from Lexicon schemas.
6
+
// Any manual changes will be overwritten on the next regeneration.
7
+
8
+
/// Custom HTTP header configuration
9
+
#[jacquard_derive::lexicon]
10
+
#[derive(
11
+
serde::Serialize,
12
+
serde::Deserialize,
13
+
Debug,
14
+
Clone,
15
+
PartialEq,
16
+
Eq,
17
+
jacquard_derive::IntoStatic,
18
+
Default
19
+
)]
20
+
#[serde(rename_all = "camelCase")]
21
+
pub struct CustomHeader<'a> {
22
+
/// HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')
23
+
#[serde(borrow)]
24
+
pub name: jacquard_common::CowStr<'a>,
25
+
/// Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.
26
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
27
+
#[serde(borrow)]
28
+
pub path: std::option::Option<jacquard_common::CowStr<'a>>,
29
+
/// HTTP header value
30
+
#[serde(borrow)]
31
+
pub value: jacquard_common::CowStr<'a>,
32
+
}
33
+
34
+
fn lexicon_doc_place_wisp_settings() -> ::jacquard_lexicon::lexicon::LexiconDoc<
35
+
'static,
36
+
> {
37
+
::jacquard_lexicon::lexicon::LexiconDoc {
38
+
lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
39
+
id: ::jacquard_common::CowStr::new_static("place.wisp.settings"),
40
+
revision: None,
41
+
description: None,
42
+
defs: {
43
+
let mut map = ::std::collections::BTreeMap::new();
44
+
map.insert(
45
+
::jacquard_common::smol_str::SmolStr::new_static("customHeader"),
46
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
47
+
description: Some(
48
+
::jacquard_common::CowStr::new_static(
49
+
"Custom HTTP header configuration",
50
+
),
51
+
),
52
+
required: Some(
53
+
vec![
54
+
::jacquard_common::smol_str::SmolStr::new_static("name"),
55
+
::jacquard_common::smol_str::SmolStr::new_static("value")
56
+
],
57
+
),
58
+
nullable: None,
59
+
properties: {
60
+
#[allow(unused_mut)]
61
+
let mut map = ::std::collections::BTreeMap::new();
62
+
map.insert(
63
+
::jacquard_common::smol_str::SmolStr::new_static("name"),
64
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
65
+
description: Some(
66
+
::jacquard_common::CowStr::new_static(
67
+
"HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')",
68
+
),
69
+
),
70
+
format: None,
71
+
default: None,
72
+
min_length: None,
73
+
max_length: Some(100usize),
74
+
min_graphemes: None,
75
+
max_graphemes: None,
76
+
r#enum: None,
77
+
r#const: None,
78
+
known_values: None,
79
+
}),
80
+
);
81
+
map.insert(
82
+
::jacquard_common::smol_str::SmolStr::new_static("path"),
83
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
84
+
description: Some(
85
+
::jacquard_common::CowStr::new_static(
86
+
"Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.",
87
+
),
88
+
),
89
+
format: None,
90
+
default: None,
91
+
min_length: None,
92
+
max_length: Some(500usize),
93
+
min_graphemes: None,
94
+
max_graphemes: None,
95
+
r#enum: None,
96
+
r#const: None,
97
+
known_values: None,
98
+
}),
99
+
);
100
+
map.insert(
101
+
::jacquard_common::smol_str::SmolStr::new_static("value"),
102
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
103
+
description: Some(
104
+
::jacquard_common::CowStr::new_static("HTTP header value"),
105
+
),
106
+
format: None,
107
+
default: None,
108
+
min_length: None,
109
+
max_length: Some(1000usize),
110
+
min_graphemes: None,
111
+
max_graphemes: None,
112
+
r#enum: None,
113
+
r#const: None,
114
+
known_values: None,
115
+
}),
116
+
);
117
+
map
118
+
},
119
+
}),
120
+
);
121
+
map.insert(
122
+
::jacquard_common::smol_str::SmolStr::new_static("main"),
123
+
::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
124
+
description: Some(
125
+
::jacquard_common::CowStr::new_static(
126
+
"Configuration settings for a static site hosted on wisp.place",
127
+
),
128
+
),
129
+
key: Some(::jacquard_common::CowStr::new_static("any")),
130
+
record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
131
+
description: None,
132
+
required: None,
133
+
nullable: None,
134
+
properties: {
135
+
#[allow(unused_mut)]
136
+
let mut map = ::std::collections::BTreeMap::new();
137
+
map.insert(
138
+
::jacquard_common::smol_str::SmolStr::new_static(
139
+
"cleanUrls",
140
+
),
141
+
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
142
+
description: None,
143
+
default: None,
144
+
r#const: None,
145
+
}),
146
+
);
147
+
map.insert(
148
+
::jacquard_common::smol_str::SmolStr::new_static(
149
+
"custom404",
150
+
),
151
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
152
+
description: Some(
153
+
::jacquard_common::CowStr::new_static(
154
+
"Custom 404 error page file path. Incompatible with directoryListing and spaMode.",
155
+
),
156
+
),
157
+
format: None,
158
+
default: None,
159
+
min_length: None,
160
+
max_length: Some(500usize),
161
+
min_graphemes: None,
162
+
max_graphemes: None,
163
+
r#enum: None,
164
+
r#const: None,
165
+
known_values: None,
166
+
}),
167
+
);
168
+
map.insert(
169
+
::jacquard_common::smol_str::SmolStr::new_static(
170
+
"directoryListing",
171
+
),
172
+
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
173
+
description: None,
174
+
default: None,
175
+
r#const: None,
176
+
}),
177
+
);
178
+
map.insert(
179
+
::jacquard_common::smol_str::SmolStr::new_static("headers"),
180
+
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
181
+
description: Some(
182
+
::jacquard_common::CowStr::new_static(
183
+
"Custom HTTP headers to set on responses",
184
+
),
185
+
),
186
+
items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef {
187
+
description: None,
188
+
r#ref: ::jacquard_common::CowStr::new_static(
189
+
"#customHeader",
190
+
),
191
+
}),
192
+
min_length: None,
193
+
max_length: Some(50usize),
194
+
}),
195
+
);
196
+
map.insert(
197
+
::jacquard_common::smol_str::SmolStr::new_static(
198
+
"indexFiles",
199
+
),
200
+
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
201
+
description: Some(
202
+
::jacquard_common::CowStr::new_static(
203
+
"Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.",
204
+
),
205
+
),
206
+
items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString {
207
+
description: None,
208
+
format: None,
209
+
default: None,
210
+
min_length: None,
211
+
max_length: Some(255usize),
212
+
min_graphemes: None,
213
+
max_graphemes: None,
214
+
r#enum: None,
215
+
r#const: None,
216
+
known_values: None,
217
+
}),
218
+
min_length: None,
219
+
max_length: Some(10usize),
220
+
}),
221
+
);
222
+
map.insert(
223
+
::jacquard_common::smol_str::SmolStr::new_static("spaMode"),
224
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
225
+
description: Some(
226
+
::jacquard_common::CowStr::new_static(
227
+
"File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.",
228
+
),
229
+
),
230
+
format: None,
231
+
default: None,
232
+
min_length: None,
233
+
max_length: Some(500usize),
234
+
min_graphemes: None,
235
+
max_graphemes: None,
236
+
r#enum: None,
237
+
r#const: None,
238
+
known_values: None,
239
+
}),
240
+
);
241
+
map
242
+
},
243
+
}),
244
+
}),
245
+
);
246
+
map
247
+
},
248
+
}
249
+
}
250
+
251
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for CustomHeader<'a> {
252
+
fn nsid() -> &'static str {
253
+
"place.wisp.settings"
254
+
}
255
+
fn def_name() -> &'static str {
256
+
"customHeader"
257
+
}
258
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
259
+
lexicon_doc_place_wisp_settings()
260
+
}
261
+
fn validate(
262
+
&self,
263
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
264
+
{
265
+
let value = &self.name;
266
+
#[allow(unused_comparisons)]
267
+
if <str>::len(value.as_ref()) > 100usize {
268
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
269
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
270
+
"name",
271
+
),
272
+
max: 100usize,
273
+
actual: <str>::len(value.as_ref()),
274
+
});
275
+
}
276
+
}
277
+
if let Some(ref value) = self.path {
278
+
#[allow(unused_comparisons)]
279
+
if <str>::len(value.as_ref()) > 500usize {
280
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
281
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
282
+
"path",
283
+
),
284
+
max: 500usize,
285
+
actual: <str>::len(value.as_ref()),
286
+
});
287
+
}
288
+
}
289
+
{
290
+
let value = &self.value;
291
+
#[allow(unused_comparisons)]
292
+
if <str>::len(value.as_ref()) > 1000usize {
293
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
294
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
295
+
"value",
296
+
),
297
+
max: 1000usize,
298
+
actual: <str>::len(value.as_ref()),
299
+
});
300
+
}
301
+
}
302
+
Ok(())
303
+
}
304
+
}
305
+
306
+
/// Configuration settings for a static site hosted on wisp.place
307
+
#[jacquard_derive::lexicon]
308
+
#[derive(
309
+
serde::Serialize,
310
+
serde::Deserialize,
311
+
Debug,
312
+
Clone,
313
+
PartialEq,
314
+
Eq,
315
+
jacquard_derive::IntoStatic
316
+
)]
317
+
#[serde(rename_all = "camelCase")]
318
+
pub struct Settings<'a> {
319
+
/// Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically.
320
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
321
+
pub clean_urls: std::option::Option<bool>,
322
+
/// Custom 404 error page file path. Incompatible with directoryListing and spaMode.
323
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
324
+
#[serde(borrow)]
325
+
pub custom404: std::option::Option<jacquard_common::CowStr<'a>>,
326
+
/// Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode.
327
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
328
+
pub directory_listing: std::option::Option<bool>,
329
+
/// Custom HTTP headers to set on responses
330
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
331
+
#[serde(borrow)]
332
+
pub headers: std::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>,
333
+
/// Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.
334
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
335
+
#[serde(borrow)]
336
+
pub index_files: std::option::Option<Vec<jacquard_common::CowStr<'a>>>,
337
+
/// File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.
338
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
339
+
#[serde(borrow)]
340
+
pub spa_mode: std::option::Option<jacquard_common::CowStr<'a>>,
341
+
}
342
+
343
+
pub mod settings_state {
344
+
345
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
346
+
#[allow(unused)]
347
+
use ::core::marker::PhantomData;
348
+
mod sealed {
349
+
pub trait Sealed {}
350
+
}
351
+
/// State trait tracking which required fields have been set
352
+
pub trait State: sealed::Sealed {}
353
+
/// Empty state - all required fields are unset
354
+
pub struct Empty(());
355
+
impl sealed::Sealed for Empty {}
356
+
impl State for Empty {}
357
+
/// Marker types for field names
358
+
#[allow(non_camel_case_types)]
359
+
pub mod members {}
360
+
}
361
+
362
+
/// Builder for constructing an instance of this type
363
+
pub struct SettingsBuilder<'a, S: settings_state::State> {
364
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
365
+
__unsafe_private_named: (
366
+
::core::option::Option<bool>,
367
+
::core::option::Option<jacquard_common::CowStr<'a>>,
368
+
::core::option::Option<bool>,
369
+
::core::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>,
370
+
::core::option::Option<Vec<jacquard_common::CowStr<'a>>>,
371
+
::core::option::Option<jacquard_common::CowStr<'a>>,
372
+
),
373
+
_phantom: ::core::marker::PhantomData<&'a ()>,
374
+
}
375
+
376
+
impl<'a> Settings<'a> {
377
+
/// Create a new builder for this type
378
+
pub fn new() -> SettingsBuilder<'a, settings_state::Empty> {
379
+
SettingsBuilder::new()
380
+
}
381
+
}
382
+
383
+
impl<'a> SettingsBuilder<'a, settings_state::Empty> {
384
+
/// Create a new builder with all fields unset
385
+
pub fn new() -> Self {
386
+
SettingsBuilder {
387
+
_phantom_state: ::core::marker::PhantomData,
388
+
__unsafe_private_named: (None, None, None, None, None, None),
389
+
_phantom: ::core::marker::PhantomData,
390
+
}
391
+
}
392
+
}
393
+
394
+
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
395
+
/// Set the `cleanUrls` field (optional)
396
+
pub fn clean_urls(mut self, value: impl Into<Option<bool>>) -> Self {
397
+
self.__unsafe_private_named.0 = value.into();
398
+
self
399
+
}
400
+
/// Set the `cleanUrls` field to an Option value (optional)
401
+
pub fn maybe_clean_urls(mut self, value: Option<bool>) -> Self {
402
+
self.__unsafe_private_named.0 = value;
403
+
self
404
+
}
405
+
}
406
+
407
+
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
408
+
/// Set the `custom404` field (optional)
409
+
pub fn custom404(
410
+
mut self,
411
+
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
412
+
) -> Self {
413
+
self.__unsafe_private_named.1 = value.into();
414
+
self
415
+
}
416
+
/// Set the `custom404` field to an Option value (optional)
417
+
pub fn maybe_custom404(
418
+
mut self,
419
+
value: Option<jacquard_common::CowStr<'a>>,
420
+
) -> Self {
421
+
self.__unsafe_private_named.1 = value;
422
+
self
423
+
}
424
+
}
425
+
426
+
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
427
+
/// Set the `directoryListing` field (optional)
428
+
pub fn directory_listing(mut self, value: impl Into<Option<bool>>) -> Self {
429
+
self.__unsafe_private_named.2 = value.into();
430
+
self
431
+
}
432
+
/// Set the `directoryListing` field to an Option value (optional)
433
+
pub fn maybe_directory_listing(mut self, value: Option<bool>) -> Self {
434
+
self.__unsafe_private_named.2 = value;
435
+
self
436
+
}
437
+
}
438
+
439
+
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
440
+
/// Set the `headers` field (optional)
441
+
pub fn headers(
442
+
mut self,
443
+
value: impl Into<Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>>,
444
+
) -> Self {
445
+
self.__unsafe_private_named.3 = value.into();
446
+
self
447
+
}
448
+
/// Set the `headers` field to an Option value (optional)
449
+
pub fn maybe_headers(
450
+
mut self,
451
+
value: Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>,
452
+
) -> Self {
453
+
self.__unsafe_private_named.3 = value;
454
+
self
455
+
}
456
+
}
457
+
458
+
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
459
+
/// Set the `indexFiles` field (optional)
460
+
pub fn index_files(
461
+
mut self,
462
+
value: impl Into<Option<Vec<jacquard_common::CowStr<'a>>>>,
463
+
) -> Self {
464
+
self.__unsafe_private_named.4 = value.into();
465
+
self
466
+
}
467
+
/// Set the `indexFiles` field to an Option value (optional)
468
+
pub fn maybe_index_files(
469
+
mut self,
470
+
value: Option<Vec<jacquard_common::CowStr<'a>>>,
471
+
) -> Self {
472
+
self.__unsafe_private_named.4 = value;
473
+
self
474
+
}
475
+
}
476
+
477
+
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
478
+
/// Set the `spaMode` field (optional)
479
+
pub fn spa_mode(
480
+
mut self,
481
+
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
482
+
) -> Self {
483
+
self.__unsafe_private_named.5 = value.into();
484
+
self
485
+
}
486
+
/// Set the `spaMode` field to an Option value (optional)
487
+
pub fn maybe_spa_mode(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
488
+
self.__unsafe_private_named.5 = value;
489
+
self
490
+
}
491
+
}
492
+
493
+
impl<'a, S> SettingsBuilder<'a, S>
494
+
where
495
+
S: settings_state::State,
496
+
{
497
+
/// Build the final struct
498
+
pub fn build(self) -> Settings<'a> {
499
+
Settings {
500
+
clean_urls: self.__unsafe_private_named.0,
501
+
custom404: self.__unsafe_private_named.1,
502
+
directory_listing: self.__unsafe_private_named.2,
503
+
headers: self.__unsafe_private_named.3,
504
+
index_files: self.__unsafe_private_named.4,
505
+
spa_mode: self.__unsafe_private_named.5,
506
+
extra_data: Default::default(),
507
+
}
508
+
}
509
+
/// Build the final struct with custom extra_data
510
+
pub fn build_with_data(
511
+
self,
512
+
extra_data: std::collections::BTreeMap<
513
+
jacquard_common::smol_str::SmolStr,
514
+
jacquard_common::types::value::Data<'a>,
515
+
>,
516
+
) -> Settings<'a> {
517
+
Settings {
518
+
clean_urls: self.__unsafe_private_named.0,
519
+
custom404: self.__unsafe_private_named.1,
520
+
directory_listing: self.__unsafe_private_named.2,
521
+
headers: self.__unsafe_private_named.3,
522
+
index_files: self.__unsafe_private_named.4,
523
+
spa_mode: self.__unsafe_private_named.5,
524
+
extra_data: Some(extra_data),
525
+
}
526
+
}
527
+
}
528
+
529
+
impl<'a> Settings<'a> {
530
+
pub fn uri(
531
+
uri: impl Into<jacquard_common::CowStr<'a>>,
532
+
) -> Result<
533
+
jacquard_common::types::uri::RecordUri<'a, SettingsRecord>,
534
+
jacquard_common::types::uri::UriError,
535
+
> {
536
+
jacquard_common::types::uri::RecordUri::try_from_uri(
537
+
jacquard_common::types::string::AtUri::new_cow(uri.into())?,
538
+
)
539
+
}
540
+
}
541
+
542
+
/// Typed wrapper for GetRecord response with this collection's record type.
543
+
#[derive(
544
+
serde::Serialize,
545
+
serde::Deserialize,
546
+
Debug,
547
+
Clone,
548
+
PartialEq,
549
+
Eq,
550
+
jacquard_derive::IntoStatic
551
+
)]
552
+
#[serde(rename_all = "camelCase")]
553
+
pub struct SettingsGetRecordOutput<'a> {
554
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
555
+
#[serde(borrow)]
556
+
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
557
+
#[serde(borrow)]
558
+
pub uri: jacquard_common::types::string::AtUri<'a>,
559
+
#[serde(borrow)]
560
+
pub value: Settings<'a>,
561
+
}
562
+
563
+
impl From<SettingsGetRecordOutput<'_>> for Settings<'_> {
564
+
fn from(output: SettingsGetRecordOutput<'_>) -> Self {
565
+
use jacquard_common::IntoStatic;
566
+
output.value.into_static()
567
+
}
568
+
}
569
+
570
+
impl jacquard_common::types::collection::Collection for Settings<'_> {
571
+
const NSID: &'static str = "place.wisp.settings";
572
+
type Record = SettingsRecord;
573
+
}
574
+
575
+
/// Marker type for deserializing records from this collection.
576
+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
577
+
pub struct SettingsRecord;
578
+
impl jacquard_common::xrpc::XrpcResp for SettingsRecord {
579
+
const NSID: &'static str = "place.wisp.settings";
580
+
const ENCODING: &'static str = "application/json";
581
+
type Output<'de> = SettingsGetRecordOutput<'de>;
582
+
type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
583
+
}
584
+
585
+
impl jacquard_common::types::collection::Collection for SettingsRecord {
586
+
const NSID: &'static str = "place.wisp.settings";
587
+
type Record = SettingsRecord;
588
+
}
589
+
590
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Settings<'a> {
591
+
fn nsid() -> &'static str {
592
+
"place.wisp.settings"
593
+
}
594
+
fn def_name() -> &'static str {
595
+
"main"
596
+
}
597
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
598
+
lexicon_doc_place_wisp_settings()
599
+
}
600
+
fn validate(
601
+
&self,
602
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
603
+
if let Some(ref value) = self.custom404 {
604
+
#[allow(unused_comparisons)]
605
+
if <str>::len(value.as_ref()) > 500usize {
606
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
607
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
608
+
"custom404",
609
+
),
610
+
max: 500usize,
611
+
actual: <str>::len(value.as_ref()),
612
+
});
613
+
}
614
+
}
615
+
if let Some(ref value) = self.headers {
616
+
#[allow(unused_comparisons)]
617
+
if value.len() > 50usize {
618
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
619
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
620
+
"headers",
621
+
),
622
+
max: 50usize,
623
+
actual: value.len(),
624
+
});
625
+
}
626
+
}
627
+
if let Some(ref value) = self.index_files {
628
+
#[allow(unused_comparisons)]
629
+
if value.len() > 10usize {
630
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
631
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
632
+
"index_files",
633
+
),
634
+
max: 10usize,
635
+
actual: value.len(),
636
+
});
637
+
}
638
+
}
639
+
if let Some(ref value) = self.spa_mode {
640
+
#[allow(unused_comparisons)]
641
+
if <str>::len(value.as_ref()) > 500usize {
642
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
643
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
644
+
"spa_mode",
645
+
),
646
+
max: 500usize,
647
+
actual: <str>::len(value.as_ref()),
648
+
});
649
+
}
650
+
}
651
+
Ok(())
652
+
}
653
+
}
+1408
cli/crates/lexicons/src/place_wisp/subfs.rs
+1408
cli/crates/lexicons/src/place_wisp/subfs.rs
···
···
1
+
// @generated by jacquard-lexicon. DO NOT EDIT.
2
+
//
3
+
// Lexicon: place.wisp.subfs
4
+
//
5
+
// This file was automatically generated from Lexicon schemas.
6
+
// Any manual changes will be overwritten on the next regeneration.
7
+
8
+
#[jacquard_derive::lexicon]
9
+
#[derive(
10
+
serde::Serialize,
11
+
serde::Deserialize,
12
+
Debug,
13
+
Clone,
14
+
PartialEq,
15
+
Eq,
16
+
jacquard_derive::IntoStatic
17
+
)]
18
+
#[serde(rename_all = "camelCase")]
19
+
pub struct Directory<'a> {
20
+
#[serde(borrow)]
21
+
pub entries: Vec<crate::place_wisp::subfs::Entry<'a>>,
22
+
#[serde(borrow)]
23
+
pub r#type: jacquard_common::CowStr<'a>,
24
+
}
25
+
26
+
pub mod directory_state {
27
+
28
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
29
+
#[allow(unused)]
30
+
use ::core::marker::PhantomData;
31
+
mod sealed {
32
+
pub trait Sealed {}
33
+
}
34
+
/// State trait tracking which required fields have been set
35
+
pub trait State: sealed::Sealed {
36
+
type Type;
37
+
type Entries;
38
+
}
39
+
/// Empty state - all required fields are unset
40
+
pub struct Empty(());
41
+
impl sealed::Sealed for Empty {}
42
+
impl State for Empty {
43
+
type Type = Unset;
44
+
type Entries = Unset;
45
+
}
46
+
///State transition - sets the `type` field to Set
47
+
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
48
+
impl<S: State> sealed::Sealed for SetType<S> {}
49
+
impl<S: State> State for SetType<S> {
50
+
type Type = Set<members::r#type>;
51
+
type Entries = S::Entries;
52
+
}
53
+
///State transition - sets the `entries` field to Set
54
+
pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>);
55
+
impl<S: State> sealed::Sealed for SetEntries<S> {}
56
+
impl<S: State> State for SetEntries<S> {
57
+
type Type = S::Type;
58
+
type Entries = Set<members::entries>;
59
+
}
60
+
/// Marker types for field names
61
+
#[allow(non_camel_case_types)]
62
+
pub mod members {
63
+
///Marker type for the `type` field
64
+
pub struct r#type(());
65
+
///Marker type for the `entries` field
66
+
pub struct entries(());
67
+
}
68
+
}
69
+
70
+
/// Builder for constructing an instance of this type
71
+
pub struct DirectoryBuilder<'a, S: directory_state::State> {
72
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
73
+
__unsafe_private_named: (
74
+
::core::option::Option<Vec<crate::place_wisp::subfs::Entry<'a>>>,
75
+
::core::option::Option<jacquard_common::CowStr<'a>>,
76
+
),
77
+
_phantom: ::core::marker::PhantomData<&'a ()>,
78
+
}
79
+
80
+
impl<'a> Directory<'a> {
81
+
/// Create a new builder for this type
82
+
pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> {
83
+
DirectoryBuilder::new()
84
+
}
85
+
}
86
+
87
+
impl<'a> DirectoryBuilder<'a, directory_state::Empty> {
88
+
/// Create a new builder with all fields unset
89
+
pub fn new() -> Self {
90
+
DirectoryBuilder {
91
+
_phantom_state: ::core::marker::PhantomData,
92
+
__unsafe_private_named: (None, None),
93
+
_phantom: ::core::marker::PhantomData,
94
+
}
95
+
}
96
+
}
97
+
98
+
impl<'a, S> DirectoryBuilder<'a, S>
99
+
where
100
+
S: directory_state::State,
101
+
S::Entries: directory_state::IsUnset,
102
+
{
103
+
/// Set the `entries` field (required)
104
+
pub fn entries(
105
+
mut self,
106
+
value: impl Into<Vec<crate::place_wisp::subfs::Entry<'a>>>,
107
+
) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> {
108
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
109
+
DirectoryBuilder {
110
+
_phantom_state: ::core::marker::PhantomData,
111
+
__unsafe_private_named: self.__unsafe_private_named,
112
+
_phantom: ::core::marker::PhantomData,
113
+
}
114
+
}
115
+
}
116
+
117
+
impl<'a, S> DirectoryBuilder<'a, S>
118
+
where
119
+
S: directory_state::State,
120
+
S::Type: directory_state::IsUnset,
121
+
{
122
+
/// Set the `type` field (required)
123
+
pub fn r#type(
124
+
mut self,
125
+
value: impl Into<jacquard_common::CowStr<'a>>,
126
+
) -> DirectoryBuilder<'a, directory_state::SetType<S>> {
127
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
128
+
DirectoryBuilder {
129
+
_phantom_state: ::core::marker::PhantomData,
130
+
__unsafe_private_named: self.__unsafe_private_named,
131
+
_phantom: ::core::marker::PhantomData,
132
+
}
133
+
}
134
+
}
135
+
136
+
impl<'a, S> DirectoryBuilder<'a, S>
137
+
where
138
+
S: directory_state::State,
139
+
S::Type: directory_state::IsSet,
140
+
S::Entries: directory_state::IsSet,
141
+
{
142
+
/// Build the final struct
143
+
pub fn build(self) -> Directory<'a> {
144
+
Directory {
145
+
entries: self.__unsafe_private_named.0.unwrap(),
146
+
r#type: self.__unsafe_private_named.1.unwrap(),
147
+
extra_data: Default::default(),
148
+
}
149
+
}
150
+
/// Build the final struct with custom extra_data
151
+
pub fn build_with_data(
152
+
self,
153
+
extra_data: std::collections::BTreeMap<
154
+
jacquard_common::smol_str::SmolStr,
155
+
jacquard_common::types::value::Data<'a>,
156
+
>,
157
+
) -> Directory<'a> {
158
+
Directory {
159
+
entries: self.__unsafe_private_named.0.unwrap(),
160
+
r#type: self.__unsafe_private_named.1.unwrap(),
161
+
extra_data: Some(extra_data),
162
+
}
163
+
}
164
+
}
165
+
166
+
fn lexicon_doc_place_wisp_subfs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
167
+
::jacquard_lexicon::lexicon::LexiconDoc {
168
+
lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
169
+
id: ::jacquard_common::CowStr::new_static("place.wisp.subfs"),
170
+
revision: None,
171
+
description: None,
172
+
defs: {
173
+
let mut map = ::std::collections::BTreeMap::new();
174
+
map.insert(
175
+
::jacquard_common::smol_str::SmolStr::new_static("directory"),
176
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
177
+
description: None,
178
+
required: Some(
179
+
vec![
180
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
181
+
::jacquard_common::smol_str::SmolStr::new_static("entries")
182
+
],
183
+
),
184
+
nullable: None,
185
+
properties: {
186
+
#[allow(unused_mut)]
187
+
let mut map = ::std::collections::BTreeMap::new();
188
+
map.insert(
189
+
::jacquard_common::smol_str::SmolStr::new_static("entries"),
190
+
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
191
+
description: None,
192
+
items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef {
193
+
description: None,
194
+
r#ref: ::jacquard_common::CowStr::new_static("#entry"),
195
+
}),
196
+
min_length: None,
197
+
max_length: Some(500usize),
198
+
}),
199
+
);
200
+
map.insert(
201
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
202
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
203
+
description: None,
204
+
format: None,
205
+
default: None,
206
+
min_length: None,
207
+
max_length: None,
208
+
min_graphemes: None,
209
+
max_graphemes: None,
210
+
r#enum: None,
211
+
r#const: None,
212
+
known_values: None,
213
+
}),
214
+
);
215
+
map
216
+
},
217
+
}),
218
+
);
219
+
map.insert(
220
+
::jacquard_common::smol_str::SmolStr::new_static("entry"),
221
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
222
+
description: None,
223
+
required: Some(
224
+
vec![
225
+
::jacquard_common::smol_str::SmolStr::new_static("name"),
226
+
::jacquard_common::smol_str::SmolStr::new_static("node")
227
+
],
228
+
),
229
+
nullable: None,
230
+
properties: {
231
+
#[allow(unused_mut)]
232
+
let mut map = ::std::collections::BTreeMap::new();
233
+
map.insert(
234
+
::jacquard_common::smol_str::SmolStr::new_static("name"),
235
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
236
+
description: None,
237
+
format: None,
238
+
default: None,
239
+
min_length: None,
240
+
max_length: Some(255usize),
241
+
min_graphemes: None,
242
+
max_graphemes: None,
243
+
r#enum: None,
244
+
r#const: None,
245
+
known_values: None,
246
+
}),
247
+
);
248
+
map.insert(
249
+
::jacquard_common::smol_str::SmolStr::new_static("node"),
250
+
::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion {
251
+
description: None,
252
+
refs: vec![
253
+
::jacquard_common::CowStr::new_static("#file"),
254
+
::jacquard_common::CowStr::new_static("#directory"),
255
+
::jacquard_common::CowStr::new_static("#subfs")
256
+
],
257
+
closed: None,
258
+
}),
259
+
);
260
+
map
261
+
},
262
+
}),
263
+
);
264
+
map.insert(
265
+
::jacquard_common::smol_str::SmolStr::new_static("file"),
266
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
267
+
description: None,
268
+
required: Some(
269
+
vec![
270
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
271
+
::jacquard_common::smol_str::SmolStr::new_static("blob")
272
+
],
273
+
),
274
+
nullable: None,
275
+
properties: {
276
+
#[allow(unused_mut)]
277
+
let mut map = ::std::collections::BTreeMap::new();
278
+
map.insert(
279
+
::jacquard_common::smol_str::SmolStr::new_static("base64"),
280
+
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
281
+
description: None,
282
+
default: None,
283
+
r#const: None,
284
+
}),
285
+
);
286
+
map.insert(
287
+
::jacquard_common::smol_str::SmolStr::new_static("blob"),
288
+
::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob {
289
+
description: None,
290
+
accept: None,
291
+
max_size: None,
292
+
}),
293
+
);
294
+
map.insert(
295
+
::jacquard_common::smol_str::SmolStr::new_static("encoding"),
296
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
297
+
description: Some(
298
+
::jacquard_common::CowStr::new_static(
299
+
"Content encoding (e.g., gzip for compressed files)",
300
+
),
301
+
),
302
+
format: None,
303
+
default: None,
304
+
min_length: None,
305
+
max_length: None,
306
+
min_graphemes: None,
307
+
max_graphemes: None,
308
+
r#enum: None,
309
+
r#const: None,
310
+
known_values: None,
311
+
}),
312
+
);
313
+
map.insert(
314
+
::jacquard_common::smol_str::SmolStr::new_static("mimeType"),
315
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
316
+
description: Some(
317
+
::jacquard_common::CowStr::new_static(
318
+
"Original MIME type before compression",
319
+
),
320
+
),
321
+
format: None,
322
+
default: None,
323
+
min_length: None,
324
+
max_length: None,
325
+
min_graphemes: None,
326
+
max_graphemes: None,
327
+
r#enum: None,
328
+
r#const: None,
329
+
known_values: None,
330
+
}),
331
+
);
332
+
map.insert(
333
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
334
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
335
+
description: None,
336
+
format: None,
337
+
default: None,
338
+
min_length: None,
339
+
max_length: None,
340
+
min_graphemes: None,
341
+
max_graphemes: None,
342
+
r#enum: None,
343
+
r#const: None,
344
+
known_values: None,
345
+
}),
346
+
);
347
+
map
348
+
},
349
+
}),
350
+
);
351
+
map.insert(
352
+
::jacquard_common::smol_str::SmolStr::new_static("main"),
353
+
::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
354
+
description: Some(
355
+
::jacquard_common::CowStr::new_static(
356
+
"Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.",
357
+
),
358
+
),
359
+
key: None,
360
+
record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
361
+
description: None,
362
+
required: Some(
363
+
vec![
364
+
::jacquard_common::smol_str::SmolStr::new_static("root"),
365
+
::jacquard_common::smol_str::SmolStr::new_static("createdAt")
366
+
],
367
+
),
368
+
nullable: None,
369
+
properties: {
370
+
#[allow(unused_mut)]
371
+
let mut map = ::std::collections::BTreeMap::new();
372
+
map.insert(
373
+
::jacquard_common::smol_str::SmolStr::new_static(
374
+
"createdAt",
375
+
),
376
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
377
+
description: None,
378
+
format: Some(
379
+
::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
380
+
),
381
+
default: None,
382
+
min_length: None,
383
+
max_length: None,
384
+
min_graphemes: None,
385
+
max_graphemes: None,
386
+
r#enum: None,
387
+
r#const: None,
388
+
known_values: None,
389
+
}),
390
+
);
391
+
map.insert(
392
+
::jacquard_common::smol_str::SmolStr::new_static(
393
+
"fileCount",
394
+
),
395
+
::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger {
396
+
description: None,
397
+
default: None,
398
+
minimum: Some(0i64),
399
+
maximum: Some(1000i64),
400
+
r#enum: None,
401
+
r#const: None,
402
+
}),
403
+
);
404
+
map.insert(
405
+
::jacquard_common::smol_str::SmolStr::new_static("root"),
406
+
::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
407
+
description: None,
408
+
r#ref: ::jacquard_common::CowStr::new_static("#directory"),
409
+
}),
410
+
);
411
+
map
412
+
},
413
+
}),
414
+
}),
415
+
);
416
+
map.insert(
417
+
::jacquard_common::smol_str::SmolStr::new_static("subfs"),
418
+
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
419
+
description: None,
420
+
required: Some(
421
+
vec![
422
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
423
+
::jacquard_common::smol_str::SmolStr::new_static("subject")
424
+
],
425
+
),
426
+
nullable: None,
427
+
properties: {
428
+
#[allow(unused_mut)]
429
+
let mut map = ::std::collections::BTreeMap::new();
430
+
map.insert(
431
+
::jacquard_common::smol_str::SmolStr::new_static("subject"),
432
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
433
+
description: Some(
434
+
::jacquard_common::CowStr::new_static(
435
+
"AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.",
436
+
),
437
+
),
438
+
format: Some(
439
+
::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
440
+
),
441
+
default: None,
442
+
min_length: None,
443
+
max_length: None,
444
+
min_graphemes: None,
445
+
max_graphemes: None,
446
+
r#enum: None,
447
+
r#const: None,
448
+
known_values: None,
449
+
}),
450
+
);
451
+
map.insert(
452
+
::jacquard_common::smol_str::SmolStr::new_static("type"),
453
+
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
454
+
description: None,
455
+
format: None,
456
+
default: None,
457
+
min_length: None,
458
+
max_length: None,
459
+
min_graphemes: None,
460
+
max_graphemes: None,
461
+
r#enum: None,
462
+
r#const: None,
463
+
known_values: None,
464
+
}),
465
+
);
466
+
map
467
+
},
468
+
}),
469
+
);
470
+
map
471
+
},
472
+
}
473
+
}
474
+
475
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> {
476
+
fn nsid() -> &'static str {
477
+
"place.wisp.subfs"
478
+
}
479
+
fn def_name() -> &'static str {
480
+
"directory"
481
+
}
482
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
483
+
lexicon_doc_place_wisp_subfs()
484
+
}
485
+
fn validate(
486
+
&self,
487
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
488
+
{
489
+
let value = &self.entries;
490
+
#[allow(unused_comparisons)]
491
+
if value.len() > 500usize {
492
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
493
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
494
+
"entries",
495
+
),
496
+
max: 500usize,
497
+
actual: value.len(),
498
+
});
499
+
}
500
+
}
501
+
Ok(())
502
+
}
503
+
}
504
+
505
+
#[jacquard_derive::lexicon]
506
+
#[derive(
507
+
serde::Serialize,
508
+
serde::Deserialize,
509
+
Debug,
510
+
Clone,
511
+
PartialEq,
512
+
Eq,
513
+
jacquard_derive::IntoStatic
514
+
)]
515
+
#[serde(rename_all = "camelCase")]
516
+
pub struct Entry<'a> {
517
+
#[serde(borrow)]
518
+
pub name: jacquard_common::CowStr<'a>,
519
+
#[serde(borrow)]
520
+
pub node: EntryNode<'a>,
521
+
}
522
+
523
+
pub mod entry_state {
524
+
525
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
526
+
#[allow(unused)]
527
+
use ::core::marker::PhantomData;
528
+
mod sealed {
529
+
pub trait Sealed {}
530
+
}
531
+
/// State trait tracking which required fields have been set
532
+
pub trait State: sealed::Sealed {
533
+
type Name;
534
+
type Node;
535
+
}
536
+
/// Empty state - all required fields are unset
537
+
pub struct Empty(());
538
+
impl sealed::Sealed for Empty {}
539
+
impl State for Empty {
540
+
type Name = Unset;
541
+
type Node = Unset;
542
+
}
543
+
///State transition - sets the `name` field to Set
544
+
pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>);
545
+
impl<S: State> sealed::Sealed for SetName<S> {}
546
+
impl<S: State> State for SetName<S> {
547
+
type Name = Set<members::name>;
548
+
type Node = S::Node;
549
+
}
550
+
///State transition - sets the `node` field to Set
551
+
pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>);
552
+
impl<S: State> sealed::Sealed for SetNode<S> {}
553
+
impl<S: State> State for SetNode<S> {
554
+
type Name = S::Name;
555
+
type Node = Set<members::node>;
556
+
}
557
+
/// Marker types for field names
558
+
#[allow(non_camel_case_types)]
559
+
pub mod members {
560
+
///Marker type for the `name` field
561
+
pub struct name(());
562
+
///Marker type for the `node` field
563
+
pub struct node(());
564
+
}
565
+
}
566
+
567
+
/// Builder for constructing an instance of this type
568
+
pub struct EntryBuilder<'a, S: entry_state::State> {
569
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
570
+
__unsafe_private_named: (
571
+
::core::option::Option<jacquard_common::CowStr<'a>>,
572
+
::core::option::Option<EntryNode<'a>>,
573
+
),
574
+
_phantom: ::core::marker::PhantomData<&'a ()>,
575
+
}
576
+
577
+
impl<'a> Entry<'a> {
578
+
/// Create a new builder for this type
579
+
pub fn new() -> EntryBuilder<'a, entry_state::Empty> {
580
+
EntryBuilder::new()
581
+
}
582
+
}
583
+
584
+
impl<'a> EntryBuilder<'a, entry_state::Empty> {
585
+
/// Create a new builder with all fields unset
586
+
pub fn new() -> Self {
587
+
EntryBuilder {
588
+
_phantom_state: ::core::marker::PhantomData,
589
+
__unsafe_private_named: (None, None),
590
+
_phantom: ::core::marker::PhantomData,
591
+
}
592
+
}
593
+
}
594
+
595
+
impl<'a, S> EntryBuilder<'a, S>
596
+
where
597
+
S: entry_state::State,
598
+
S::Name: entry_state::IsUnset,
599
+
{
600
+
/// Set the `name` field (required)
601
+
pub fn name(
602
+
mut self,
603
+
value: impl Into<jacquard_common::CowStr<'a>>,
604
+
) -> EntryBuilder<'a, entry_state::SetName<S>> {
605
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
606
+
EntryBuilder {
607
+
_phantom_state: ::core::marker::PhantomData,
608
+
__unsafe_private_named: self.__unsafe_private_named,
609
+
_phantom: ::core::marker::PhantomData,
610
+
}
611
+
}
612
+
}
613
+
614
+
impl<'a, S> EntryBuilder<'a, S>
615
+
where
616
+
S: entry_state::State,
617
+
S::Node: entry_state::IsUnset,
618
+
{
619
+
/// Set the `node` field (required)
620
+
pub fn node(
621
+
mut self,
622
+
value: impl Into<EntryNode<'a>>,
623
+
) -> EntryBuilder<'a, entry_state::SetNode<S>> {
624
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
625
+
EntryBuilder {
626
+
_phantom_state: ::core::marker::PhantomData,
627
+
__unsafe_private_named: self.__unsafe_private_named,
628
+
_phantom: ::core::marker::PhantomData,
629
+
}
630
+
}
631
+
}
632
+
633
+
impl<'a, S> EntryBuilder<'a, S>
634
+
where
635
+
S: entry_state::State,
636
+
S::Name: entry_state::IsSet,
637
+
S::Node: entry_state::IsSet,
638
+
{
639
+
/// Build the final struct
640
+
pub fn build(self) -> Entry<'a> {
641
+
Entry {
642
+
name: self.__unsafe_private_named.0.unwrap(),
643
+
node: self.__unsafe_private_named.1.unwrap(),
644
+
extra_data: Default::default(),
645
+
}
646
+
}
647
+
/// Build the final struct with custom extra_data
648
+
pub fn build_with_data(
649
+
self,
650
+
extra_data: std::collections::BTreeMap<
651
+
jacquard_common::smol_str::SmolStr,
652
+
jacquard_common::types::value::Data<'a>,
653
+
>,
654
+
) -> Entry<'a> {
655
+
Entry {
656
+
name: self.__unsafe_private_named.0.unwrap(),
657
+
node: self.__unsafe_private_named.1.unwrap(),
658
+
extra_data: Some(extra_data),
659
+
}
660
+
}
661
+
}
662
+
663
+
#[jacquard_derive::open_union]
664
+
#[derive(
665
+
serde::Serialize,
666
+
serde::Deserialize,
667
+
Debug,
668
+
Clone,
669
+
PartialEq,
670
+
Eq,
671
+
jacquard_derive::IntoStatic
672
+
)]
673
+
#[serde(tag = "$type")]
674
+
#[serde(bound(deserialize = "'de: 'a"))]
675
+
pub enum EntryNode<'a> {
676
+
#[serde(rename = "place.wisp.subfs#file")]
677
+
File(Box<crate::place_wisp::subfs::File<'a>>),
678
+
#[serde(rename = "place.wisp.subfs#directory")]
679
+
Directory(Box<crate::place_wisp::subfs::Directory<'a>>),
680
+
#[serde(rename = "place.wisp.subfs#subfs")]
681
+
Subfs(Box<crate::place_wisp::subfs::Subfs<'a>>),
682
+
}
683
+
684
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> {
685
+
fn nsid() -> &'static str {
686
+
"place.wisp.subfs"
687
+
}
688
+
fn def_name() -> &'static str {
689
+
"entry"
690
+
}
691
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
692
+
lexicon_doc_place_wisp_subfs()
693
+
}
694
+
fn validate(
695
+
&self,
696
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
697
+
{
698
+
let value = &self.name;
699
+
#[allow(unused_comparisons)]
700
+
if <str>::len(value.as_ref()) > 255usize {
701
+
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
702
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
703
+
"name",
704
+
),
705
+
max: 255usize,
706
+
actual: <str>::len(value.as_ref()),
707
+
});
708
+
}
709
+
}
710
+
Ok(())
711
+
}
712
+
}
713
+
714
+
#[jacquard_derive::lexicon]
715
+
#[derive(
716
+
serde::Serialize,
717
+
serde::Deserialize,
718
+
Debug,
719
+
Clone,
720
+
PartialEq,
721
+
Eq,
722
+
jacquard_derive::IntoStatic
723
+
)]
724
+
#[serde(rename_all = "camelCase")]
725
+
pub struct File<'a> {
726
+
/// True if blob content is base64-encoded (used to bypass PDS content sniffing)
727
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
728
+
pub base64: std::option::Option<bool>,
729
+
/// Content blob ref
730
+
#[serde(borrow)]
731
+
pub blob: jacquard_common::types::blob::BlobRef<'a>,
732
+
/// Content encoding (e.g., gzip for compressed files)
733
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
734
+
#[serde(borrow)]
735
+
pub encoding: std::option::Option<jacquard_common::CowStr<'a>>,
736
+
/// Original MIME type before compression
737
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
738
+
#[serde(borrow)]
739
+
pub mime_type: std::option::Option<jacquard_common::CowStr<'a>>,
740
+
#[serde(borrow)]
741
+
pub r#type: jacquard_common::CowStr<'a>,
742
+
}
743
+
744
+
pub mod file_state {
745
+
746
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
747
+
#[allow(unused)]
748
+
use ::core::marker::PhantomData;
749
+
mod sealed {
750
+
pub trait Sealed {}
751
+
}
752
+
/// State trait tracking which required fields have been set
753
+
pub trait State: sealed::Sealed {
754
+
type Blob;
755
+
type Type;
756
+
}
757
+
/// Empty state - all required fields are unset
758
+
pub struct Empty(());
759
+
impl sealed::Sealed for Empty {}
760
+
impl State for Empty {
761
+
type Blob = Unset;
762
+
type Type = Unset;
763
+
}
764
+
///State transition - sets the `blob` field to Set
765
+
pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>);
766
+
impl<S: State> sealed::Sealed for SetBlob<S> {}
767
+
impl<S: State> State for SetBlob<S> {
768
+
type Blob = Set<members::blob>;
769
+
type Type = S::Type;
770
+
}
771
+
///State transition - sets the `type` field to Set
772
+
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
773
+
impl<S: State> sealed::Sealed for SetType<S> {}
774
+
impl<S: State> State for SetType<S> {
775
+
type Blob = S::Blob;
776
+
type Type = Set<members::r#type>;
777
+
}
778
+
/// Marker types for field names
779
+
#[allow(non_camel_case_types)]
780
+
pub mod members {
781
+
///Marker type for the `blob` field
782
+
pub struct blob(());
783
+
///Marker type for the `type` field
784
+
pub struct r#type(());
785
+
}
786
+
}
787
+
788
+
/// Builder for constructing an instance of this type
789
+
pub struct FileBuilder<'a, S: file_state::State> {
790
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
791
+
__unsafe_private_named: (
792
+
::core::option::Option<bool>,
793
+
::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>,
794
+
::core::option::Option<jacquard_common::CowStr<'a>>,
795
+
::core::option::Option<jacquard_common::CowStr<'a>>,
796
+
::core::option::Option<jacquard_common::CowStr<'a>>,
797
+
),
798
+
_phantom: ::core::marker::PhantomData<&'a ()>,
799
+
}
800
+
801
+
impl<'a> File<'a> {
802
+
/// Create a new builder for this type
803
+
pub fn new() -> FileBuilder<'a, file_state::Empty> {
804
+
FileBuilder::new()
805
+
}
806
+
}
807
+
808
+
impl<'a> FileBuilder<'a, file_state::Empty> {
809
+
/// Create a new builder with all fields unset
810
+
pub fn new() -> Self {
811
+
FileBuilder {
812
+
_phantom_state: ::core::marker::PhantomData,
813
+
__unsafe_private_named: (None, None, None, None, None),
814
+
_phantom: ::core::marker::PhantomData,
815
+
}
816
+
}
817
+
}
818
+
819
+
impl<'a, S: file_state::State> FileBuilder<'a, S> {
820
+
/// Set the `base64` field (optional)
821
+
pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self {
822
+
self.__unsafe_private_named.0 = value.into();
823
+
self
824
+
}
825
+
/// Set the `base64` field to an Option value (optional)
826
+
pub fn maybe_base64(mut self, value: Option<bool>) -> Self {
827
+
self.__unsafe_private_named.0 = value;
828
+
self
829
+
}
830
+
}
831
+
832
+
impl<'a, S> FileBuilder<'a, S>
833
+
where
834
+
S: file_state::State,
835
+
S::Blob: file_state::IsUnset,
836
+
{
837
+
/// Set the `blob` field (required)
838
+
pub fn blob(
839
+
mut self,
840
+
value: impl Into<jacquard_common::types::blob::BlobRef<'a>>,
841
+
) -> FileBuilder<'a, file_state::SetBlob<S>> {
842
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
843
+
FileBuilder {
844
+
_phantom_state: ::core::marker::PhantomData,
845
+
__unsafe_private_named: self.__unsafe_private_named,
846
+
_phantom: ::core::marker::PhantomData,
847
+
}
848
+
}
849
+
}
850
+
851
+
impl<'a, S: file_state::State> FileBuilder<'a, S> {
852
+
/// Set the `encoding` field (optional)
853
+
pub fn encoding(
854
+
mut self,
855
+
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
856
+
) -> Self {
857
+
self.__unsafe_private_named.2 = value.into();
858
+
self
859
+
}
860
+
/// Set the `encoding` field to an Option value (optional)
861
+
pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
862
+
self.__unsafe_private_named.2 = value;
863
+
self
864
+
}
865
+
}
866
+
867
+
impl<'a, S: file_state::State> FileBuilder<'a, S> {
868
+
/// Set the `mimeType` field (optional)
869
+
pub fn mime_type(
870
+
mut self,
871
+
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
872
+
) -> Self {
873
+
self.__unsafe_private_named.3 = value.into();
874
+
self
875
+
}
876
+
/// Set the `mimeType` field to an Option value (optional)
877
+
pub fn maybe_mime_type(
878
+
mut self,
879
+
value: Option<jacquard_common::CowStr<'a>>,
880
+
) -> Self {
881
+
self.__unsafe_private_named.3 = value;
882
+
self
883
+
}
884
+
}
885
+
886
+
impl<'a, S> FileBuilder<'a, S>
887
+
where
888
+
S: file_state::State,
889
+
S::Type: file_state::IsUnset,
890
+
{
891
+
/// Set the `type` field (required)
892
+
pub fn r#type(
893
+
mut self,
894
+
value: impl Into<jacquard_common::CowStr<'a>>,
895
+
) -> FileBuilder<'a, file_state::SetType<S>> {
896
+
self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into());
897
+
FileBuilder {
898
+
_phantom_state: ::core::marker::PhantomData,
899
+
__unsafe_private_named: self.__unsafe_private_named,
900
+
_phantom: ::core::marker::PhantomData,
901
+
}
902
+
}
903
+
}
904
+
905
+
impl<'a, S> FileBuilder<'a, S>
906
+
where
907
+
S: file_state::State,
908
+
S::Blob: file_state::IsSet,
909
+
S::Type: file_state::IsSet,
910
+
{
911
+
/// Build the final struct
912
+
pub fn build(self) -> File<'a> {
913
+
File {
914
+
base64: self.__unsafe_private_named.0,
915
+
blob: self.__unsafe_private_named.1.unwrap(),
916
+
encoding: self.__unsafe_private_named.2,
917
+
mime_type: self.__unsafe_private_named.3,
918
+
r#type: self.__unsafe_private_named.4.unwrap(),
919
+
extra_data: Default::default(),
920
+
}
921
+
}
922
+
/// Build the final struct with custom extra_data
923
+
pub fn build_with_data(
924
+
self,
925
+
extra_data: std::collections::BTreeMap<
926
+
jacquard_common::smol_str::SmolStr,
927
+
jacquard_common::types::value::Data<'a>,
928
+
>,
929
+
) -> File<'a> {
930
+
File {
931
+
base64: self.__unsafe_private_named.0,
932
+
blob: self.__unsafe_private_named.1.unwrap(),
933
+
encoding: self.__unsafe_private_named.2,
934
+
mime_type: self.__unsafe_private_named.3,
935
+
r#type: self.__unsafe_private_named.4.unwrap(),
936
+
extra_data: Some(extra_data),
937
+
}
938
+
}
939
+
}
940
+
941
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> {
942
+
fn nsid() -> &'static str {
943
+
"place.wisp.subfs"
944
+
}
945
+
fn def_name() -> &'static str {
946
+
"file"
947
+
}
948
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
949
+
lexicon_doc_place_wisp_subfs()
950
+
}
951
+
fn validate(
952
+
&self,
953
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
954
+
Ok(())
955
+
}
956
+
}
957
+
958
+
/// Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.
959
+
#[jacquard_derive::lexicon]
960
+
#[derive(
961
+
serde::Serialize,
962
+
serde::Deserialize,
963
+
Debug,
964
+
Clone,
965
+
PartialEq,
966
+
Eq,
967
+
jacquard_derive::IntoStatic
968
+
)]
969
+
#[serde(rename_all = "camelCase")]
970
+
pub struct SubfsRecord<'a> {
971
+
pub created_at: jacquard_common::types::string::Datetime,
972
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
973
+
pub file_count: std::option::Option<i64>,
974
+
#[serde(borrow)]
975
+
pub root: crate::place_wisp::subfs::Directory<'a>,
976
+
}
977
+
978
+
pub mod subfs_record_state {
979
+
980
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
981
+
#[allow(unused)]
982
+
use ::core::marker::PhantomData;
983
+
mod sealed {
984
+
pub trait Sealed {}
985
+
}
986
+
/// State trait tracking which required fields have been set
987
+
pub trait State: sealed::Sealed {
988
+
type Root;
989
+
type CreatedAt;
990
+
}
991
+
/// Empty state - all required fields are unset
992
+
pub struct Empty(());
993
+
impl sealed::Sealed for Empty {}
994
+
impl State for Empty {
995
+
type Root = Unset;
996
+
type CreatedAt = Unset;
997
+
}
998
+
///State transition - sets the `root` field to Set
999
+
pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>);
1000
+
impl<S: State> sealed::Sealed for SetRoot<S> {}
1001
+
impl<S: State> State for SetRoot<S> {
1002
+
type Root = Set<members::root>;
1003
+
type CreatedAt = S::CreatedAt;
1004
+
}
1005
+
///State transition - sets the `created_at` field to Set
1006
+
pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
1007
+
impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
1008
+
impl<S: State> State for SetCreatedAt<S> {
1009
+
type Root = S::Root;
1010
+
type CreatedAt = Set<members::created_at>;
1011
+
}
1012
+
/// Marker types for field names
1013
+
#[allow(non_camel_case_types)]
1014
+
pub mod members {
1015
+
///Marker type for the `root` field
1016
+
pub struct root(());
1017
+
///Marker type for the `created_at` field
1018
+
pub struct created_at(());
1019
+
}
1020
+
}
1021
+
1022
+
/// Builder for constructing an instance of this type
1023
+
pub struct SubfsRecordBuilder<'a, S: subfs_record_state::State> {
1024
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1025
+
__unsafe_private_named: (
1026
+
::core::option::Option<jacquard_common::types::string::Datetime>,
1027
+
::core::option::Option<i64>,
1028
+
::core::option::Option<crate::place_wisp::subfs::Directory<'a>>,
1029
+
),
1030
+
_phantom: ::core::marker::PhantomData<&'a ()>,
1031
+
}
1032
+
1033
+
impl<'a> SubfsRecord<'a> {
1034
+
/// Create a new builder for this type
1035
+
pub fn new() -> SubfsRecordBuilder<'a, subfs_record_state::Empty> {
1036
+
SubfsRecordBuilder::new()
1037
+
}
1038
+
}
1039
+
1040
+
impl<'a> SubfsRecordBuilder<'a, subfs_record_state::Empty> {
1041
+
/// Create a new builder with all fields unset
1042
+
pub fn new() -> Self {
1043
+
SubfsRecordBuilder {
1044
+
_phantom_state: ::core::marker::PhantomData,
1045
+
__unsafe_private_named: (None, None, None),
1046
+
_phantom: ::core::marker::PhantomData,
1047
+
}
1048
+
}
1049
+
}
1050
+
1051
+
impl<'a, S> SubfsRecordBuilder<'a, S>
1052
+
where
1053
+
S: subfs_record_state::State,
1054
+
S::CreatedAt: subfs_record_state::IsUnset,
1055
+
{
1056
+
/// Set the `createdAt` field (required)
1057
+
pub fn created_at(
1058
+
mut self,
1059
+
value: impl Into<jacquard_common::types::string::Datetime>,
1060
+
) -> SubfsRecordBuilder<'a, subfs_record_state::SetCreatedAt<S>> {
1061
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
1062
+
SubfsRecordBuilder {
1063
+
_phantom_state: ::core::marker::PhantomData,
1064
+
__unsafe_private_named: self.__unsafe_private_named,
1065
+
_phantom: ::core::marker::PhantomData,
1066
+
}
1067
+
}
1068
+
}
1069
+
1070
+
impl<'a, S: subfs_record_state::State> SubfsRecordBuilder<'a, S> {
1071
+
/// Set the `fileCount` field (optional)
1072
+
pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self {
1073
+
self.__unsafe_private_named.1 = value.into();
1074
+
self
1075
+
}
1076
+
/// Set the `fileCount` field to an Option value (optional)
1077
+
pub fn maybe_file_count(mut self, value: Option<i64>) -> Self {
1078
+
self.__unsafe_private_named.1 = value;
1079
+
self
1080
+
}
1081
+
}
1082
+
1083
+
impl<'a, S> SubfsRecordBuilder<'a, S>
1084
+
where
1085
+
S: subfs_record_state::State,
1086
+
S::Root: subfs_record_state::IsUnset,
1087
+
{
1088
+
/// Set the `root` field (required)
1089
+
pub fn root(
1090
+
mut self,
1091
+
value: impl Into<crate::place_wisp::subfs::Directory<'a>>,
1092
+
) -> SubfsRecordBuilder<'a, subfs_record_state::SetRoot<S>> {
1093
+
self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
1094
+
SubfsRecordBuilder {
1095
+
_phantom_state: ::core::marker::PhantomData,
1096
+
__unsafe_private_named: self.__unsafe_private_named,
1097
+
_phantom: ::core::marker::PhantomData,
1098
+
}
1099
+
}
1100
+
}
1101
+
1102
+
impl<'a, S> SubfsRecordBuilder<'a, S>
1103
+
where
1104
+
S: subfs_record_state::State,
1105
+
S::Root: subfs_record_state::IsSet,
1106
+
S::CreatedAt: subfs_record_state::IsSet,
1107
+
{
1108
+
/// Build the final struct
1109
+
pub fn build(self) -> SubfsRecord<'a> {
1110
+
SubfsRecord {
1111
+
created_at: self.__unsafe_private_named.0.unwrap(),
1112
+
file_count: self.__unsafe_private_named.1,
1113
+
root: self.__unsafe_private_named.2.unwrap(),
1114
+
extra_data: Default::default(),
1115
+
}
1116
+
}
1117
+
/// Build the final struct with custom extra_data
1118
+
pub fn build_with_data(
1119
+
self,
1120
+
extra_data: std::collections::BTreeMap<
1121
+
jacquard_common::smol_str::SmolStr,
1122
+
jacquard_common::types::value::Data<'a>,
1123
+
>,
1124
+
) -> SubfsRecord<'a> {
1125
+
SubfsRecord {
1126
+
created_at: self.__unsafe_private_named.0.unwrap(),
1127
+
file_count: self.__unsafe_private_named.1,
1128
+
root: self.__unsafe_private_named.2.unwrap(),
1129
+
extra_data: Some(extra_data),
1130
+
}
1131
+
}
1132
+
}
1133
+
1134
+
impl<'a> SubfsRecord<'a> {
1135
+
pub fn uri(
1136
+
uri: impl Into<jacquard_common::CowStr<'a>>,
1137
+
) -> Result<
1138
+
jacquard_common::types::uri::RecordUri<'a, SubfsRecordRecord>,
1139
+
jacquard_common::types::uri::UriError,
1140
+
> {
1141
+
jacquard_common::types::uri::RecordUri::try_from_uri(
1142
+
jacquard_common::types::string::AtUri::new_cow(uri.into())?,
1143
+
)
1144
+
}
1145
+
}
1146
+
1147
+
/// Typed wrapper for GetRecord response with this collection's record type.
1148
+
#[derive(
1149
+
serde::Serialize,
1150
+
serde::Deserialize,
1151
+
Debug,
1152
+
Clone,
1153
+
PartialEq,
1154
+
Eq,
1155
+
jacquard_derive::IntoStatic
1156
+
)]
1157
+
#[serde(rename_all = "camelCase")]
1158
+
pub struct SubfsRecordGetRecordOutput<'a> {
1159
+
#[serde(skip_serializing_if = "std::option::Option::is_none")]
1160
+
#[serde(borrow)]
1161
+
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
1162
+
#[serde(borrow)]
1163
+
pub uri: jacquard_common::types::string::AtUri<'a>,
1164
+
#[serde(borrow)]
1165
+
pub value: SubfsRecord<'a>,
1166
+
}
1167
+
1168
+
impl From<SubfsRecordGetRecordOutput<'_>> for SubfsRecord<'_> {
1169
+
fn from(output: SubfsRecordGetRecordOutput<'_>) -> Self {
1170
+
use jacquard_common::IntoStatic;
1171
+
output.value.into_static()
1172
+
}
1173
+
}
1174
+
1175
+
impl jacquard_common::types::collection::Collection for SubfsRecord<'_> {
1176
+
const NSID: &'static str = "place.wisp.subfs";
1177
+
type Record = SubfsRecordRecord;
1178
+
}
1179
+
1180
+
/// Marker type for deserializing records from this collection.
1181
+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
1182
+
pub struct SubfsRecordRecord;
1183
+
impl jacquard_common::xrpc::XrpcResp for SubfsRecordRecord {
1184
+
const NSID: &'static str = "place.wisp.subfs";
1185
+
const ENCODING: &'static str = "application/json";
1186
+
type Output<'de> = SubfsRecordGetRecordOutput<'de>;
1187
+
type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
1188
+
}
1189
+
1190
+
impl jacquard_common::types::collection::Collection for SubfsRecordRecord {
1191
+
const NSID: &'static str = "place.wisp.subfs";
1192
+
type Record = SubfsRecordRecord;
1193
+
}
1194
+
1195
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubfsRecord<'a> {
1196
+
fn nsid() -> &'static str {
1197
+
"place.wisp.subfs"
1198
+
}
1199
+
fn def_name() -> &'static str {
1200
+
"main"
1201
+
}
1202
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1203
+
lexicon_doc_place_wisp_subfs()
1204
+
}
1205
+
fn validate(
1206
+
&self,
1207
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1208
+
if let Some(ref value) = self.file_count {
1209
+
if *value > 1000i64 {
1210
+
return Err(::jacquard_lexicon::validation::ConstraintError::Maximum {
1211
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1212
+
"file_count",
1213
+
),
1214
+
max: 1000i64,
1215
+
actual: *value,
1216
+
});
1217
+
}
1218
+
}
1219
+
if let Some(ref value) = self.file_count {
1220
+
if *value < 0i64 {
1221
+
return Err(::jacquard_lexicon::validation::ConstraintError::Minimum {
1222
+
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1223
+
"file_count",
1224
+
),
1225
+
min: 0i64,
1226
+
actual: *value,
1227
+
});
1228
+
}
1229
+
}
1230
+
Ok(())
1231
+
}
1232
+
}
1233
+
1234
+
#[jacquard_derive::lexicon]
1235
+
#[derive(
1236
+
serde::Serialize,
1237
+
serde::Deserialize,
1238
+
Debug,
1239
+
Clone,
1240
+
PartialEq,
1241
+
Eq,
1242
+
jacquard_derive::IntoStatic
1243
+
)]
1244
+
#[serde(rename_all = "camelCase")]
1245
+
pub struct Subfs<'a> {
1246
+
/// AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.
1247
+
#[serde(borrow)]
1248
+
pub subject: jacquard_common::types::string::AtUri<'a>,
1249
+
#[serde(borrow)]
1250
+
pub r#type: jacquard_common::CowStr<'a>,
1251
+
}
1252
+
1253
+
pub mod subfs_state {
1254
+
1255
+
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
1256
+
#[allow(unused)]
1257
+
use ::core::marker::PhantomData;
1258
+
mod sealed {
1259
+
pub trait Sealed {}
1260
+
}
1261
+
/// State trait tracking which required fields have been set
1262
+
pub trait State: sealed::Sealed {
1263
+
type Subject;
1264
+
type Type;
1265
+
}
1266
+
/// Empty state - all required fields are unset
1267
+
pub struct Empty(());
1268
+
impl sealed::Sealed for Empty {}
1269
+
impl State for Empty {
1270
+
type Subject = Unset;
1271
+
type Type = Unset;
1272
+
}
1273
+
///State transition - sets the `subject` field to Set
1274
+
pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>);
1275
+
impl<S: State> sealed::Sealed for SetSubject<S> {}
1276
+
impl<S: State> State for SetSubject<S> {
1277
+
type Subject = Set<members::subject>;
1278
+
type Type = S::Type;
1279
+
}
1280
+
///State transition - sets the `type` field to Set
1281
+
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
1282
+
impl<S: State> sealed::Sealed for SetType<S> {}
1283
+
impl<S: State> State for SetType<S> {
1284
+
type Subject = S::Subject;
1285
+
type Type = Set<members::r#type>;
1286
+
}
1287
+
/// Marker types for field names
1288
+
#[allow(non_camel_case_types)]
1289
+
pub mod members {
1290
+
///Marker type for the `subject` field
1291
+
pub struct subject(());
1292
+
///Marker type for the `type` field
1293
+
pub struct r#type(());
1294
+
}
1295
+
}
1296
+
1297
+
/// Builder for constructing an instance of this type
1298
+
pub struct SubfsBuilder<'a, S: subfs_state::State> {
1299
+
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1300
+
__unsafe_private_named: (
1301
+
::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
1302
+
::core::option::Option<jacquard_common::CowStr<'a>>,
1303
+
),
1304
+
_phantom: ::core::marker::PhantomData<&'a ()>,
1305
+
}
1306
+
1307
+
impl<'a> Subfs<'a> {
1308
+
/// Create a new builder for this type
1309
+
pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> {
1310
+
SubfsBuilder::new()
1311
+
}
1312
+
}
1313
+
1314
+
impl<'a> SubfsBuilder<'a, subfs_state::Empty> {
1315
+
/// Create a new builder with all fields unset
1316
+
pub fn new() -> Self {
1317
+
SubfsBuilder {
1318
+
_phantom_state: ::core::marker::PhantomData,
1319
+
__unsafe_private_named: (None, None),
1320
+
_phantom: ::core::marker::PhantomData,
1321
+
}
1322
+
}
1323
+
}
1324
+
1325
+
impl<'a, S> SubfsBuilder<'a, S>
1326
+
where
1327
+
S: subfs_state::State,
1328
+
S::Subject: subfs_state::IsUnset,
1329
+
{
1330
+
/// Set the `subject` field (required)
1331
+
pub fn subject(
1332
+
mut self,
1333
+
value: impl Into<jacquard_common::types::string::AtUri<'a>>,
1334
+
) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> {
1335
+
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
1336
+
SubfsBuilder {
1337
+
_phantom_state: ::core::marker::PhantomData,
1338
+
__unsafe_private_named: self.__unsafe_private_named,
1339
+
_phantom: ::core::marker::PhantomData,
1340
+
}
1341
+
}
1342
+
}
1343
+
1344
+
impl<'a, S> SubfsBuilder<'a, S>
1345
+
where
1346
+
S: subfs_state::State,
1347
+
S::Type: subfs_state::IsUnset,
1348
+
{
1349
+
/// Set the `type` field (required)
1350
+
pub fn r#type(
1351
+
mut self,
1352
+
value: impl Into<jacquard_common::CowStr<'a>>,
1353
+
) -> SubfsBuilder<'a, subfs_state::SetType<S>> {
1354
+
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
1355
+
SubfsBuilder {
1356
+
_phantom_state: ::core::marker::PhantomData,
1357
+
__unsafe_private_named: self.__unsafe_private_named,
1358
+
_phantom: ::core::marker::PhantomData,
1359
+
}
1360
+
}
1361
+
}
1362
+
1363
+
impl<'a, S> SubfsBuilder<'a, S>
1364
+
where
1365
+
S: subfs_state::State,
1366
+
S::Subject: subfs_state::IsSet,
1367
+
S::Type: subfs_state::IsSet,
1368
+
{
1369
+
/// Build the final struct
1370
+
pub fn build(self) -> Subfs<'a> {
1371
+
Subfs {
1372
+
subject: self.__unsafe_private_named.0.unwrap(),
1373
+
r#type: self.__unsafe_private_named.1.unwrap(),
1374
+
extra_data: Default::default(),
1375
+
}
1376
+
}
1377
+
/// Build the final struct with custom extra_data
1378
+
pub fn build_with_data(
1379
+
self,
1380
+
extra_data: std::collections::BTreeMap<
1381
+
jacquard_common::smol_str::SmolStr,
1382
+
jacquard_common::types::value::Data<'a>,
1383
+
>,
1384
+
) -> Subfs<'a> {
1385
+
Subfs {
1386
+
subject: self.__unsafe_private_named.0.unwrap(),
1387
+
r#type: self.__unsafe_private_named.1.unwrap(),
1388
+
extra_data: Some(extra_data),
1389
+
}
1390
+
}
1391
+
}
1392
+
1393
+
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> {
1394
+
fn nsid() -> &'static str {
1395
+
"place.wisp.subfs"
1396
+
}
1397
+
fn def_name() -> &'static str {
1398
+
"subfs"
1399
+
}
1400
+
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1401
+
lexicon_doc_place_wisp_subfs()
1402
+
}
1403
+
fn validate(
1404
+
&self,
1405
+
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1406
+
Ok(())
1407
+
}
1408
+
}
+8
cli/crates/lexicons/src/place_wisp.rs
+8
cli/crates/lexicons/src/place_wisp.rs
+16
cli/default.nix
+16
cli/default.nix
···
···
1
+
{
2
+
rustPlatform,
3
+
glibc,
4
+
}:
5
+
rustPlatform.buildRustPackage {
6
+
name = "wisp-cli";
7
+
src = ./.;
8
+
cargoLock = {
9
+
lockFile = ./Cargo.lock;
10
+
outputHashes = {
11
+
"jacquard-0.9.5" = "sha256-75bas4VAYFcZAcBspSqS4vlJe8nmFn9ncTgeoT/OvnA=";
12
+
};
13
+
};
14
+
buildInputs = [glibc.static];
15
+
RUSTFLAGS = ["-C" "target-feature=+crt-static"];
16
+
}
+96
cli/flake.lock
+96
cli/flake.lock
···
···
1
+
{
2
+
"nodes": {
3
+
"flake-utils": {
4
+
"inputs": {
5
+
"systems": "systems"
6
+
},
7
+
"locked": {
8
+
"lastModified": 1731533236,
9
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10
+
"owner": "numtide",
11
+
"repo": "flake-utils",
12
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13
+
"type": "github"
14
+
},
15
+
"original": {
16
+
"owner": "numtide",
17
+
"repo": "flake-utils",
18
+
"type": "github"
19
+
}
20
+
},
21
+
"nixpkgs": {
22
+
"locked": {
23
+
"lastModified": 1767640445,
24
+
"narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=",
25
+
"owner": "NixOS",
26
+
"repo": "nixpkgs",
27
+
"rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5",
28
+
"type": "github"
29
+
},
30
+
"original": {
31
+
"owner": "NixOS",
32
+
"ref": "nixos-unstable",
33
+
"repo": "nixpkgs",
34
+
"type": "github"
35
+
}
36
+
},
37
+
"nixpkgs_2": {
38
+
"locked": {
39
+
"lastModified": 1744536153,
40
+
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
41
+
"owner": "NixOS",
42
+
"repo": "nixpkgs",
43
+
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
44
+
"type": "github"
45
+
},
46
+
"original": {
47
+
"owner": "NixOS",
48
+
"ref": "nixpkgs-unstable",
49
+
"repo": "nixpkgs",
50
+
"type": "github"
51
+
}
52
+
},
53
+
"root": {
54
+
"inputs": {
55
+
"flake-utils": "flake-utils",
56
+
"nixpkgs": "nixpkgs",
57
+
"rust-overlay": "rust-overlay"
58
+
}
59
+
},
60
+
"rust-overlay": {
61
+
"inputs": {
62
+
"nixpkgs": "nixpkgs_2"
63
+
},
64
+
"locked": {
65
+
"lastModified": 1767667566,
66
+
"narHash": "sha256-COy+yxZGuhQRVD1r4bWVgeFt1GB+IB1k5WRpDKbLfI8=",
67
+
"owner": "oxalica",
68
+
"repo": "rust-overlay",
69
+
"rev": "056ce5b125ab32ffe78c7d3e394d9da44733c95e",
70
+
"type": "github"
71
+
},
72
+
"original": {
73
+
"owner": "oxalica",
74
+
"repo": "rust-overlay",
75
+
"type": "github"
76
+
}
77
+
},
78
+
"systems": {
79
+
"locked": {
80
+
"lastModified": 1681028828,
81
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
82
+
"owner": "nix-systems",
83
+
"repo": "default",
84
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
85
+
"type": "github"
86
+
},
87
+
"original": {
88
+
"owner": "nix-systems",
89
+
"repo": "default",
90
+
"type": "github"
91
+
}
92
+
}
93
+
},
94
+
"root": "root",
95
+
"version": 7
96
+
}
+136
cli/flake.nix
+136
cli/flake.nix
···
···
1
+
{
2
+
inputs = {
3
+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
4
+
rust-overlay.url = "github:oxalica/rust-overlay";
5
+
flake-utils.url = "github:numtide/flake-utils";
6
+
};
7
+
8
+
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
9
+
flake-utils.lib.eachDefaultSystem (system:
10
+
let
11
+
overlays = [ (import rust-overlay) ];
12
+
pkgs = import nixpkgs { inherit system overlays; };
13
+
14
+
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
15
+
targets = [
16
+
"x86_64-unknown-linux-musl"
17
+
"aarch64-unknown-linux-musl"
18
+
"x86_64-apple-darwin"
19
+
"aarch64-apple-darwin"
20
+
];
21
+
};
22
+
23
+
cargoLockConfig = {
24
+
lockFile = ./Cargo.lock;
25
+
outputHashes = {
26
+
"jacquard-0.9.5" = "sha256-75bas4VAYFcZAcBspSqS4vlJe8nmFn9ncTgeoT/OvnA=";
27
+
};
28
+
};
29
+
30
+
# Native build for current system
31
+
native = pkgs.rustPlatform.buildRustPackage {
32
+
pname = "wisp-cli";
33
+
version = "0.4.2";
34
+
src = ./.;
35
+
cargoLock = cargoLockConfig;
36
+
nativeBuildInputs = [ rustToolchain ];
37
+
};
38
+
39
+
# Cross-compilation targets (Linux from macOS needs zigbuild)
40
+
linuxTargets = let
41
+
zigbuildPkgs = pkgs;
42
+
in {
43
+
x86_64-linux = pkgs.stdenv.mkDerivation {
44
+
pname = "wisp-cli-x86_64-linux";
45
+
version = "0.4.2";
46
+
src = ./.;
47
+
48
+
nativeBuildInputs = [
49
+
rustToolchain
50
+
pkgs.cargo-zigbuild
51
+
pkgs.zig
52
+
];
53
+
54
+
buildPhase = ''
55
+
export HOME=$(mktemp -d)
56
+
cargo zigbuild --release --target x86_64-unknown-linux-musl
57
+
'';
58
+
59
+
installPhase = ''
60
+
mkdir -p $out/bin
61
+
cp target/x86_64-unknown-linux-musl/release/wisp-cli $out/bin/
62
+
'';
63
+
64
+
# Skip Nix's cargo vendor - we use network
65
+
dontConfigure = true;
66
+
dontFixup = true;
67
+
};
68
+
69
+
aarch64-linux = pkgs.stdenv.mkDerivation {
70
+
pname = "wisp-cli-aarch64-linux";
71
+
version = "0.4.2";
72
+
src = ./.;
73
+
74
+
nativeBuildInputs = [
75
+
rustToolchain
76
+
pkgs.cargo-zigbuild
77
+
pkgs.zig
78
+
];
79
+
80
+
buildPhase = ''
81
+
export HOME=$(mktemp -d)
82
+
cargo zigbuild --release --target aarch64-unknown-linux-musl
83
+
'';
84
+
85
+
installPhase = ''
86
+
mkdir -p $out/bin
87
+
cp target/aarch64-unknown-linux-musl/release/wisp-cli $out/bin/
88
+
'';
89
+
90
+
dontConfigure = true;
91
+
dontFixup = true;
92
+
};
93
+
};
94
+
95
+
in {
96
+
packages = {
97
+
default = native;
98
+
inherit native;
99
+
100
+
# macOS universal binary
101
+
macos-universal = pkgs.stdenv.mkDerivation {
102
+
pname = "wisp-cli-macos-universal";
103
+
version = "0.4.2";
104
+
src = ./.;
105
+
106
+
nativeBuildInputs = [ rustToolchain pkgs.darwin.lipo ];
107
+
108
+
buildPhase = ''
109
+
export HOME=$(mktemp -d)
110
+
cargo build --release --target aarch64-apple-darwin
111
+
cargo build --release --target x86_64-apple-darwin
112
+
'';
113
+
114
+
installPhase = ''
115
+
mkdir -p $out/bin
116
+
lipo -create \
117
+
target/aarch64-apple-darwin/release/wisp-cli \
118
+
target/x86_64-apple-darwin/release/wisp-cli \
119
+
-output $out/bin/wisp-cli
120
+
'';
121
+
122
+
dontConfigure = true;
123
+
dontFixup = true;
124
+
};
125
+
} // (if pkgs.stdenv.isDarwin then linuxTargets else {});
126
+
127
+
devShells.default = pkgs.mkShell {
128
+
buildInputs = [
129
+
rustToolchain
130
+
pkgs.cargo-zigbuild
131
+
pkgs.zig
132
+
];
133
+
};
134
+
}
135
+
);
136
+
}
+1
-1
cli/src/blob_map.rs
+1
-1
cli/src/blob_map.rs
-43
cli/src/builder_types.rs
-43
cli/src/builder_types.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// This file was automatically generated from Lexicon schemas.
4
-
// Any manual changes will be overwritten on the next regeneration.
5
-
6
-
/// Marker type indicating a builder field has been set
7
-
pub struct Set<T>(pub T);
8
-
impl<T> Set<T> {
9
-
/// Extract the inner value
10
-
#[inline]
11
-
pub fn into_inner(self) -> T {
12
-
self.0
13
-
}
14
-
}
15
-
16
-
/// Marker type indicating a builder field has not been set
17
-
pub struct Unset;
18
-
/// Trait indicating a builder field is set (has a value)
19
-
#[rustversion::attr(
20
-
since(1.78.0),
21
-
diagnostic::on_unimplemented(
22
-
message = "the field `{Self}` was not set, but this method requires it to be set",
23
-
label = "the field `{Self}` was not set"
24
-
)
25
-
)]
26
-
pub trait IsSet: private::Sealed {}
27
-
/// Trait indicating a builder field is unset (no value yet)
28
-
#[rustversion::attr(
29
-
since(1.78.0),
30
-
diagnostic::on_unimplemented(
31
-
message = "the field `{Self}` was already set, but this method requires it to be unset",
32
-
label = "the field `{Self}` was already set"
33
-
)
34
-
)]
35
-
pub trait IsUnset: private::Sealed {}
36
-
impl<T> IsSet for Set<T> {}
37
-
impl IsUnset for Unset {}
38
-
mod private {
39
-
/// Sealed trait to prevent external implementations
40
-
pub trait Sealed {}
41
-
impl<T> Sealed for super::Set<T> {}
42
-
impl Sealed for super::Unset {}
43
-
}
···
-9
cli/src/lib.rs
-9
cli/src/lib.rs
+190
-54
cli/src/main.rs
+190
-54
cli/src/main.rs
···
1
-
mod builder_types;
2
-
mod place_wisp;
3
mod cid;
4
mod blob_map;
5
mod metadata;
···
26
use std::io::Write;
27
use base64::Engine;
28
use futures::stream::{self, StreamExt};
29
30
-
use place_wisp::fs::*;
31
-
use place_wisp::settings::*;
32
33
#[derive(Parser, Debug)]
34
#[command(author, version, about = "wisp.place CLI tool")]
35
struct Args {
36
#[command(subcommand)]
37
command: Option<Commands>,
38
-
39
// Deploy arguments (when no subcommand is specified)
40
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
41
-
#[arg(global = true, conflicts_with = "command")]
42
input: Option<CowStr<'static>>,
43
44
/// Path to the directory containing your static site
45
-
#[arg(short, long, global = true, conflicts_with = "command")]
46
path: Option<PathBuf>,
47
48
/// Site name (defaults to directory name)
49
-
#[arg(short, long, global = true, conflicts_with = "command")]
50
site: Option<String>,
51
52
/// Path to auth store file
53
-
#[arg(long, global = true, conflicts_with = "command")]
54
store: Option<String>,
55
56
/// App Password for authentication
57
-
#[arg(long, global = true, conflicts_with = "command")]
58
password: Option<CowStr<'static>>,
59
60
/// Enable directory listing mode for paths without index files
61
-
#[arg(long, global = true, conflicts_with = "command")]
62
directory: bool,
63
64
/// Enable SPA mode (serve index.html for all routes)
65
-
#[arg(long, global = true, conflicts_with = "command")]
66
spa: bool,
67
}
68
69
#[derive(Subcommand, Debug)]
···
96
/// Enable SPA mode (serve index.html for all routes)
97
#[arg(long)]
98
spa: bool,
99
},
100
/// Pull a site from the PDS to a local directory
101
Pull {
···
108
109
/// Output directory for the downloaded site
110
#[arg(short, long, default_value = ".")]
111
-
output: PathBuf,
112
},
113
/// Serve a site locally with real-time firehose updates
114
Serve {
···
121
122
/// Output directory for the site files
123
#[arg(short, long, default_value = ".")]
124
-
output: PathBuf,
125
126
/// Port to serve on
127
-
#[arg(short, long, default_value = "8080")]
128
port: u16,
129
},
130
}
···
134
let args = Args::parse();
135
136
let result = match args.command {
137
-
Some(Commands::Deploy { input, path, site, store, password, directory, spa }) => {
138
// Dispatch to appropriate authentication method
139
if let Some(password) = password {
140
-
run_with_app_password(input, password, path, site, directory, spa).await
141
} else {
142
-
run_with_oauth(input, store, path, site, directory, spa).await
143
}
144
}
145
-
Some(Commands::Pull { input, site, output }) => {
146
-
pull::pull_site(input, CowStr::from(site), output).await
147
}
148
-
Some(Commands::Serve { input, site, output, port }) => {
149
-
serve::serve_site(input, CowStr::from(site), output, port).await
150
}
151
None => {
152
// Legacy mode: if input is provided, assume deploy command
···
156
157
// Dispatch to appropriate authentication method
158
if let Some(password) = args.password {
159
-
run_with_app_password(input, password, path, args.site, args.directory, args.spa).await
160
} else {
161
-
run_with_oauth(input, store, path, args.site, args.directory, args.spa).await
162
}
163
} else {
164
// No command and no input, show help
···
187
site: Option<String>,
188
directory: bool,
189
spa: bool,
190
) -> miette::Result<()> {
191
let (session, auth) =
192
MemoryCredentialSession::authenticated(input, password, None, None).await?;
193
println!("Signed in as {}", auth.handle);
194
195
let agent: Agent<_> = Agent::from(session);
196
-
deploy_site(&agent, path, site, directory, spa).await
197
}
198
199
/// Run deployment with OAuth authentication
···
204
site: Option<String>,
205
directory: bool,
206
spa: bool,
207
) -> miette::Result<()> {
208
use jacquard::oauth::scopes::Scope;
209
use jacquard::oauth::atproto::AtprotoClientMetadata;
···
236
.await?;
237
238
let agent: Agent<_> = Agent::from(session);
239
-
deploy_site(&agent, path, site, directory, spa).await
240
}
241
242
/// Deploy the site using the provided agent
···
246
site: Option<String>,
247
directory_listing: bool,
248
spa_mode: bool,
249
) -> miette::Result<()> {
250
// Verify the path exists
251
if !path.exists() {
···
263
264
println!("Deploying site '{}'...", site_name);
265
266
// Try to fetch existing manifest for incremental updates
267
let (existing_blob_map, old_subfs_uris): (HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, Vec<(String, String)>) = {
268
use jacquard_common::types::string::AtUri;
···
324
}
325
};
326
327
-
// Build directory tree with ignore patterns
328
-
let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?;
329
-
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher).await?;
330
let uploaded_count = total_files - reused_count;
331
332
// Check if we need to split into subfs records
333
const MAX_MANIFEST_SIZE: usize = 140 * 1024; // 140KB (PDS limit is 150KB)
···
378
let chunk_file_count = subfs_utils::count_files_in_directory(chunk);
379
let chunk_size = subfs_utils::estimate_directory_size(chunk);
380
381
-
let chunk_manifest = crate::place_wisp::subfs::SubfsRecord::new()
382
.root(convert_fs_dir_to_subfs_dir(chunk.clone()))
383
.file_count(Some(chunk_file_count as i64))
384
.created_at(Datetime::now())
···
401
// Each chunk reference MUST have flat: true to merge chunk contents
402
println!(" โ Creating parent subfs with {} chunk references...", chunk_uris.len());
403
use jacquard_common::CowStr;
404
-
use crate::place_wisp::fs::{Subfs};
405
406
// Convert to fs::Subfs (which has the 'flat' field) instead of subfs::Subfs
407
let parent_entries_fs: Vec<Entry> = chunk_uris.iter().enumerate().map(|(i, (uri, _))| {
···
431
let parent_tid = Tid::now_0();
432
let parent_rkey = parent_tid.to_string();
433
434
-
let parent_manifest = crate::place_wisp::subfs::SubfsRecord::new()
435
.root(parent_root_subfs)
436
.file_count(Some(largest_dir.file_count as i64))
437
.created_at(Datetime::now())
···
450
let subfs_tid = Tid::now_0();
451
let subfs_rkey = subfs_tid.to_string();
452
453
-
let subfs_manifest = crate::place_wisp::subfs::SubfsRecord::new()
454
.root(convert_fs_dir_to_subfs_dir(largest_dir.directory.clone()))
455
.file_count(Some(largest_dir.file_count as i64))
456
.created_at(Datetime::now())
···
606
existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
607
current_path: String,
608
ignore_matcher: &'a ignore_patterns::IgnoreMatcher,
609
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>>
610
{
611
Box::pin(async move {
···
653
}
654
}
655
656
-
// Process files concurrently with a limit of 5
657
let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks)
658
.map(|(name, path, full_path)| async move {
659
-
let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs).await?;
660
let entry = Entry::new()
661
.name(CowStr::from(name))
662
.node(EntryNode::File(Box::new(file_node)))
663
.build();
664
Ok::<_, miette::Report>((entry, reused))
665
})
666
-
.buffer_unordered(5)
667
.collect::<Vec<_>>()
668
.await
669
.into_iter()
···
690
} else {
691
format!("{}/{}", current_path, name)
692
};
693
-
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher).await?;
694
dir_entries.push(Entry::new()
695
.name(CowStr::from(name))
696
.node(EntryNode::Directory(Box::new(subdir)))
···
722
file_path: &Path,
723
file_path_key: &str,
724
existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
725
) -> miette::Result<(File<'static>, bool)>
726
{
727
// Read file
···
761
if let Some((existing_blob_ref, existing_cid)) = existing_blob {
762
if existing_cid == &file_cid {
763
// CIDs match - reuse existing blob
764
-
println!(" โ Reusing blob for {} (CID: {})", file_path_key, file_cid);
765
let mut file_builder = File::new()
766
.r#type(CowStr::from("file"))
767
.blob(existing_blob_ref.clone())
···
775
}
776
777
return Ok((file_builder.build(), true));
778
-
} else {
779
-
// CID mismatch - file changed
780
-
println!(" โ File changed: {} (old CID: {}, new CID: {})", file_path_key, existing_cid, file_cid);
781
-
}
782
-
} else {
783
-
// File not in existing blob map
784
-
if file_path_key.starts_with("imgs/") {
785
-
println!(" โ New file (not in blob map): {}", file_path_key);
786
}
787
}
788
···
793
MimeType::new_static("application/octet-stream")
794
};
795
796
-
println!(" โ Uploading {} ({} bytes, CID: {})", file_path_key, upload_bytes.len(), file_cid);
797
let blob = agent.upload_blob(upload_bytes, mime_type).await?;
798
799
let mut file_builder = File::new()
800
.r#type(CowStr::from("file"))
···
813
814
/// Convert fs::Directory to subfs::Directory
815
/// They have the same structure, but different types
816
-
fn convert_fs_dir_to_subfs_dir(fs_dir: place_wisp::fs::Directory<'static>) -> place_wisp::subfs::Directory<'static> {
817
-
use place_wisp::subfs::{Directory as SubfsDirectory, Entry as SubfsEntry, EntryNode as SubfsEntryNode, File as SubfsFile};
818
819
let subfs_entries: Vec<SubfsEntry> = fs_dir.entries.into_iter().map(|entry| {
820
let node = match entry.node {
821
-
place_wisp::fs::EntryNode::File(file) => {
822
SubfsEntryNode::File(Box::new(SubfsFile::new()
823
.r#type(file.r#type)
824
.blob(file.blob)
···
827
.base64(file.base64)
828
.build()))
829
}
830
-
place_wisp::fs::EntryNode::Directory(dir) => {
831
SubfsEntryNode::Directory(Box::new(convert_fs_dir_to_subfs_dir(*dir)))
832
}
833
-
place_wisp::fs::EntryNode::Subfs(subfs) => {
834
// Nested subfs in the directory we're converting
835
// Note: subfs::Subfs doesn't have the 'flat' field - that's only in fs::Subfs
836
-
SubfsEntryNode::Subfs(Box::new(place_wisp::subfs::Subfs::new()
837
.r#type(subfs.r#type)
838
.subject(subfs.subject)
839
.build()))
840
}
841
-
place_wisp::fs::EntryNode::Unknown(unknown) => {
842
SubfsEntryNode::Unknown(unknown)
843
}
844
};
···
1
mod cid;
2
mod blob_map;
3
mod metadata;
···
24
use std::io::Write;
25
use base64::Engine;
26
use futures::stream::{self, StreamExt};
27
+
use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
28
29
+
use wisp_lexicons::place_wisp::fs::*;
30
+
use wisp_lexicons::place_wisp::settings::*;
31
+
32
+
/// Maximum number of concurrent file uploads to the PDS
33
+
const MAX_CONCURRENT_UPLOADS: usize = 2;
34
+
35
+
/// Limits for caching on wisp.place (from @wisp/constants)
36
+
const MAX_FILE_COUNT: usize = 1000;
37
+
const MAX_SITE_SIZE: usize = 300 * 1024 * 1024; // 300MB
38
39
#[derive(Parser, Debug)]
40
#[command(author, version, about = "wisp.place CLI tool")]
41
struct Args {
42
#[command(subcommand)]
43
command: Option<Commands>,
44
+
45
// Deploy arguments (when no subcommand is specified)
46
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
47
input: Option<CowStr<'static>>,
48
49
/// Path to the directory containing your static site
50
+
#[arg(short, long)]
51
path: Option<PathBuf>,
52
53
/// Site name (defaults to directory name)
54
+
#[arg(short, long)]
55
site: Option<String>,
56
57
/// Path to auth store file
58
+
#[arg(long)]
59
store: Option<String>,
60
61
/// App Password for authentication
62
+
#[arg(long)]
63
password: Option<CowStr<'static>>,
64
65
/// Enable directory listing mode for paths without index files
66
+
#[arg(long)]
67
directory: bool,
68
69
/// Enable SPA mode (serve index.html for all routes)
70
+
#[arg(long)]
71
spa: bool,
72
+
73
+
/// Skip confirmation prompts (automatically accept warnings)
74
+
#[arg(short = 'y', long)]
75
+
yes: bool,
76
}
77
78
#[derive(Subcommand, Debug)]
···
105
/// Enable SPA mode (serve index.html for all routes)
106
#[arg(long)]
107
spa: bool,
108
+
109
+
/// Skip confirmation prompts (automatically accept warnings)
110
+
#[arg(short = 'y', long)]
111
+
yes: bool,
112
},
113
/// Pull a site from the PDS to a local directory
114
Pull {
···
121
122
/// Output directory for the downloaded site
123
#[arg(short, long, default_value = ".")]
124
+
path: PathBuf,
125
},
126
/// Serve a site locally with real-time firehose updates
127
Serve {
···
134
135
/// Output directory for the site files
136
#[arg(short, long, default_value = ".")]
137
+
path: PathBuf,
138
139
/// Port to serve on
140
+
#[arg(short = 'P', long, default_value = "8080")]
141
port: u16,
142
},
143
}
···
147
let args = Args::parse();
148
149
let result = match args.command {
150
+
Some(Commands::Deploy { input, path, site, store, password, directory, spa, yes }) => {
151
// Dispatch to appropriate authentication method
152
if let Some(password) = password {
153
+
run_with_app_password(input, password, path, site, directory, spa, yes).await
154
} else {
155
+
run_with_oauth(input, store, path, site, directory, spa, yes).await
156
}
157
}
158
+
Some(Commands::Pull { input, site, path }) => {
159
+
pull::pull_site(input, CowStr::from(site), path).await
160
}
161
+
Some(Commands::Serve { input, site, path, port }) => {
162
+
serve::serve_site(input, CowStr::from(site), path, port).await
163
}
164
None => {
165
// Legacy mode: if input is provided, assume deploy command
···
169
170
// Dispatch to appropriate authentication method
171
if let Some(password) = args.password {
172
+
run_with_app_password(input, password, path, args.site, args.directory, args.spa, args.yes).await
173
} else {
174
+
run_with_oauth(input, store, path, args.site, args.directory, args.spa, args.yes).await
175
}
176
} else {
177
// No command and no input, show help
···
200
site: Option<String>,
201
directory: bool,
202
spa: bool,
203
+
yes: bool,
204
) -> miette::Result<()> {
205
let (session, auth) =
206
MemoryCredentialSession::authenticated(input, password, None, None).await?;
207
println!("Signed in as {}", auth.handle);
208
209
let agent: Agent<_> = Agent::from(session);
210
+
deploy_site(&agent, path, site, directory, spa, yes).await
211
}
212
213
/// Run deployment with OAuth authentication
···
218
site: Option<String>,
219
directory: bool,
220
spa: bool,
221
+
yes: bool,
222
) -> miette::Result<()> {
223
use jacquard::oauth::scopes::Scope;
224
use jacquard::oauth::atproto::AtprotoClientMetadata;
···
251
.await?;
252
253
let agent: Agent<_> = Agent::from(session);
254
+
deploy_site(&agent, path, site, directory, spa, yes).await
255
+
}
256
+
257
+
/// Scan directory to count files and calculate total size
258
+
/// Returns (file_count, total_size_bytes)
259
+
fn scan_directory_stats(
260
+
dir_path: &Path,
261
+
ignore_matcher: &ignore_patterns::IgnoreMatcher,
262
+
current_path: String,
263
+
) -> miette::Result<(usize, u64)> {
264
+
let mut file_count = 0;
265
+
let mut total_size = 0u64;
266
+
267
+
let dir_entries: Vec<_> = std::fs::read_dir(dir_path)
268
+
.into_diagnostic()?
269
+
.collect::<Result<Vec<_>, _>>()
270
+
.into_diagnostic()?;
271
+
272
+
for entry in dir_entries {
273
+
let path = entry.path();
274
+
let name = entry.file_name();
275
+
let name_str = name.to_str()
276
+
.ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?
277
+
.to_string();
278
+
279
+
let full_path = if current_path.is_empty() {
280
+
name_str.clone()
281
+
} else {
282
+
format!("{}/{}", current_path, name_str)
283
+
};
284
+
285
+
// Skip files/directories that match ignore patterns
286
+
if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) {
287
+
continue;
288
+
}
289
+
290
+
let metadata = entry.metadata().into_diagnostic()?;
291
+
292
+
if metadata.is_file() {
293
+
file_count += 1;
294
+
total_size += metadata.len();
295
+
} else if metadata.is_dir() {
296
+
let subdir_path = if current_path.is_empty() {
297
+
name_str
298
+
} else {
299
+
format!("{}/{}", current_path, name_str)
300
+
};
301
+
let (sub_count, sub_size) = scan_directory_stats(&path, ignore_matcher, subdir_path)?;
302
+
file_count += sub_count;
303
+
total_size += sub_size;
304
+
}
305
+
}
306
+
307
+
Ok((file_count, total_size))
308
}
309
310
/// Deploy the site using the provided agent
···
314
site: Option<String>,
315
directory_listing: bool,
316
spa_mode: bool,
317
+
skip_prompts: bool,
318
) -> miette::Result<()> {
319
// Verify the path exists
320
if !path.exists() {
···
332
333
println!("Deploying site '{}'...", site_name);
334
335
+
// Scan directory to check file count and size
336
+
let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?;
337
+
let (file_count, total_size) = scan_directory_stats(&path, &ignore_matcher, String::new())?;
338
+
339
+
let size_mb = total_size as f64 / (1024.0 * 1024.0);
340
+
println!("Scanned: {} files, {:.1} MB total", file_count, size_mb);
341
+
342
+
// Check if limits are exceeded
343
+
let exceeds_file_count = file_count > MAX_FILE_COUNT;
344
+
let exceeds_size = total_size > MAX_SITE_SIZE as u64;
345
+
346
+
if exceeds_file_count || exceeds_size {
347
+
println!("\nโ ๏ธ Warning: Your site exceeds wisp.place caching limits:");
348
+
349
+
if exceeds_file_count {
350
+
println!(" โข File count: {} (limit: {})", file_count, MAX_FILE_COUNT);
351
+
}
352
+
353
+
if exceeds_size {
354
+
let size_mb = total_size as f64 / (1024.0 * 1024.0);
355
+
let limit_mb = MAX_SITE_SIZE as f64 / (1024.0 * 1024.0);
356
+
println!(" โข Total size: {:.1} MB (limit: {:.0} MB)", size_mb, limit_mb);
357
+
}
358
+
359
+
println!("\nwisp.place will NOT serve your site if you proceed.");
360
+
println!("Your site will be uploaded to your PDS, but will only be accessible via:");
361
+
println!(" โข wisp-cli serve (local hosting)");
362
+
println!(" โข Other hosting services with more generous limits");
363
+
364
+
if !skip_prompts {
365
+
// Prompt for confirmation
366
+
use std::io::{self, Write};
367
+
print!("\nDo you want to upload anyway? (y/N): ");
368
+
io::stdout().flush().into_diagnostic()?;
369
+
370
+
let mut input = String::new();
371
+
io::stdin().read_line(&mut input).into_diagnostic()?;
372
+
let input = input.trim().to_lowercase();
373
+
374
+
if input != "y" && input != "yes" {
375
+
println!("Upload cancelled.");
376
+
return Ok(());
377
+
}
378
+
} else {
379
+
println!("\nSkipping confirmation (--yes flag set).");
380
+
}
381
+
382
+
println!("\nProceeding with upload...\n");
383
+
}
384
+
385
// Try to fetch existing manifest for incremental updates
386
let (existing_blob_map, old_subfs_uris): (HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, Vec<(String, String)>) = {
387
use jacquard_common::types::string::AtUri;
···
443
}
444
};
445
446
+
// Create progress tracking (spinner style since we don't know total count upfront)
447
+
let multi_progress = MultiProgress::new();
448
+
let progress = multi_progress.add(ProgressBar::new_spinner());
449
+
progress.set_style(
450
+
ProgressStyle::default_spinner()
451
+
.template("[{elapsed_precise}] {spinner:.cyan} {pos} files {msg}")
452
+
.into_diagnostic()?
453
+
.tick_chars("โ โ โ โกโขโ โ โ ")
454
+
);
455
+
progress.set_message("Scanning files...");
456
+
progress.enable_steady_tick(std::time::Duration::from_millis(100));
457
+
458
+
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher, &progress).await?;
459
let uploaded_count = total_files - reused_count;
460
+
461
+
progress.finish_with_message(format!("โ {} files ({} uploaded, {} reused)", total_files, uploaded_count, reused_count));
462
463
// Check if we need to split into subfs records
464
const MAX_MANIFEST_SIZE: usize = 140 * 1024; // 140KB (PDS limit is 150KB)
···
509
let chunk_file_count = subfs_utils::count_files_in_directory(chunk);
510
let chunk_size = subfs_utils::estimate_directory_size(chunk);
511
512
+
let chunk_manifest = wisp_lexicons::place_wisp::subfs::SubfsRecord::new()
513
.root(convert_fs_dir_to_subfs_dir(chunk.clone()))
514
.file_count(Some(chunk_file_count as i64))
515
.created_at(Datetime::now())
···
532
// Each chunk reference MUST have flat: true to merge chunk contents
533
println!(" โ Creating parent subfs with {} chunk references...", chunk_uris.len());
534
use jacquard_common::CowStr;
535
+
use wisp_lexicons::place_wisp::fs::{Subfs};
536
537
// Convert to fs::Subfs (which has the 'flat' field) instead of subfs::Subfs
538
let parent_entries_fs: Vec<Entry> = chunk_uris.iter().enumerate().map(|(i, (uri, _))| {
···
562
let parent_tid = Tid::now_0();
563
let parent_rkey = parent_tid.to_string();
564
565
+
let parent_manifest = wisp_lexicons::place_wisp::subfs::SubfsRecord::new()
566
.root(parent_root_subfs)
567
.file_count(Some(largest_dir.file_count as i64))
568
.created_at(Datetime::now())
···
581
let subfs_tid = Tid::now_0();
582
let subfs_rkey = subfs_tid.to_string();
583
584
+
let subfs_manifest = wisp_lexicons::place_wisp::subfs::SubfsRecord::new()
585
.root(convert_fs_dir_to_subfs_dir(largest_dir.directory.clone()))
586
.file_count(Some(largest_dir.file_count as i64))
587
.created_at(Datetime::now())
···
737
existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
738
current_path: String,
739
ignore_matcher: &'a ignore_patterns::IgnoreMatcher,
740
+
progress: &'a ProgressBar,
741
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>>
742
{
743
Box::pin(async move {
···
785
}
786
}
787
788
+
// Process files concurrently with a limit of 2
789
let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks)
790
.map(|(name, path, full_path)| async move {
791
+
let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs, progress).await?;
792
+
progress.inc(1);
793
let entry = Entry::new()
794
.name(CowStr::from(name))
795
.node(EntryNode::File(Box::new(file_node)))
796
.build();
797
Ok::<_, miette::Report>((entry, reused))
798
})
799
+
.buffer_unordered(MAX_CONCURRENT_UPLOADS)
800
.collect::<Vec<_>>()
801
.await
802
.into_iter()
···
823
} else {
824
format!("{}/{}", current_path, name)
825
};
826
+
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher, progress).await?;
827
dir_entries.push(Entry::new()
828
.name(CowStr::from(name))
829
.node(EntryNode::Directory(Box::new(subdir)))
···
855
file_path: &Path,
856
file_path_key: &str,
857
existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
858
+
progress: &ProgressBar,
859
) -> miette::Result<(File<'static>, bool)>
860
{
861
// Read file
···
895
if let Some((existing_blob_ref, existing_cid)) = existing_blob {
896
if existing_cid == &file_cid {
897
// CIDs match - reuse existing blob
898
+
progress.set_message(format!("โ Reused {}", file_path_key));
899
let mut file_builder = File::new()
900
.r#type(CowStr::from("file"))
901
.blob(existing_blob_ref.clone())
···
909
}
910
911
return Ok((file_builder.build(), true));
912
}
913
}
914
···
919
MimeType::new_static("application/octet-stream")
920
};
921
922
+
// Format file size nicely
923
+
let size_str = if upload_bytes.len() < 1024 {
924
+
format!("{} B", upload_bytes.len())
925
+
} else if upload_bytes.len() < 1024 * 1024 {
926
+
format!("{:.1} KB", upload_bytes.len() as f64 / 1024.0)
927
+
} else {
928
+
format!("{:.1} MB", upload_bytes.len() as f64 / (1024.0 * 1024.0))
929
+
};
930
+
931
+
progress.set_message(format!("โ Uploading {} ({})", file_path_key, size_str));
932
let blob = agent.upload_blob(upload_bytes, mime_type).await?;
933
+
progress.set_message(format!("โ Uploaded {}", file_path_key));
934
935
let mut file_builder = File::new()
936
.r#type(CowStr::from("file"))
···
949
950
/// Convert fs::Directory to subfs::Directory
951
/// They have the same structure, but different types
952
+
fn convert_fs_dir_to_subfs_dir(fs_dir: wisp_lexicons::place_wisp::fs::Directory<'static>) -> wisp_lexicons::place_wisp::subfs::Directory<'static> {
953
+
use wisp_lexicons::place_wisp::subfs::{Directory as SubfsDirectory, Entry as SubfsEntry, EntryNode as SubfsEntryNode, File as SubfsFile};
954
955
let subfs_entries: Vec<SubfsEntry> = fs_dir.entries.into_iter().map(|entry| {
956
let node = match entry.node {
957
+
wisp_lexicons::place_wisp::fs::EntryNode::File(file) => {
958
SubfsEntryNode::File(Box::new(SubfsFile::new()
959
.r#type(file.r#type)
960
.blob(file.blob)
···
963
.base64(file.base64)
964
.build()))
965
}
966
+
wisp_lexicons::place_wisp::fs::EntryNode::Directory(dir) => {
967
SubfsEntryNode::Directory(Box::new(convert_fs_dir_to_subfs_dir(*dir)))
968
}
969
+
wisp_lexicons::place_wisp::fs::EntryNode::Subfs(subfs) => {
970
// Nested subfs in the directory we're converting
971
// Note: subfs::Subfs doesn't have the 'flat' field - that's only in fs::Subfs
972
+
SubfsEntryNode::Subfs(Box::new(wisp_lexicons::place_wisp::subfs::Subfs::new()
973
.r#type(subfs.r#type)
974
.subject(subfs.subject)
975
.build()))
976
}
977
+
wisp_lexicons::place_wisp::fs::EntryNode::Unknown(unknown) => {
978
SubfsEntryNode::Unknown(unknown)
979
}
980
};
-9
cli/src/mod.rs
-9
cli/src/mod.rs
-1490
cli/src/place_wisp/fs.rs
-1490
cli/src/place_wisp/fs.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: place.wisp.fs
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(
10
-
serde::Serialize,
11
-
serde::Deserialize,
12
-
Debug,
13
-
Clone,
14
-
PartialEq,
15
-
Eq,
16
-
jacquard_derive::IntoStatic
17
-
)]
18
-
#[serde(rename_all = "camelCase")]
19
-
pub struct Directory<'a> {
20
-
#[serde(borrow)]
21
-
pub entries: Vec<crate::place_wisp::fs::Entry<'a>>,
22
-
#[serde(borrow)]
23
-
pub r#type: jacquard_common::CowStr<'a>,
24
-
}
25
-
26
-
pub mod directory_state {
27
-
28
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
29
-
#[allow(unused)]
30
-
use ::core::marker::PhantomData;
31
-
mod sealed {
32
-
pub trait Sealed {}
33
-
}
34
-
/// State trait tracking which required fields have been set
35
-
pub trait State: sealed::Sealed {
36
-
type Type;
37
-
type Entries;
38
-
}
39
-
/// Empty state - all required fields are unset
40
-
pub struct Empty(());
41
-
impl sealed::Sealed for Empty {}
42
-
impl State for Empty {
43
-
type Type = Unset;
44
-
type Entries = Unset;
45
-
}
46
-
///State transition - sets the `type` field to Set
47
-
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
48
-
impl<S: State> sealed::Sealed for SetType<S> {}
49
-
impl<S: State> State for SetType<S> {
50
-
type Type = Set<members::r#type>;
51
-
type Entries = S::Entries;
52
-
}
53
-
///State transition - sets the `entries` field to Set
54
-
pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>);
55
-
impl<S: State> sealed::Sealed for SetEntries<S> {}
56
-
impl<S: State> State for SetEntries<S> {
57
-
type Type = S::Type;
58
-
type Entries = Set<members::entries>;
59
-
}
60
-
/// Marker types for field names
61
-
#[allow(non_camel_case_types)]
62
-
pub mod members {
63
-
///Marker type for the `type` field
64
-
pub struct r#type(());
65
-
///Marker type for the `entries` field
66
-
pub struct entries(());
67
-
}
68
-
}
69
-
70
-
/// Builder for constructing an instance of this type
71
-
pub struct DirectoryBuilder<'a, S: directory_state::State> {
72
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
73
-
__unsafe_private_named: (
74
-
::core::option::Option<Vec<crate::place_wisp::fs::Entry<'a>>>,
75
-
::core::option::Option<jacquard_common::CowStr<'a>>,
76
-
),
77
-
_phantom: ::core::marker::PhantomData<&'a ()>,
78
-
}
79
-
80
-
impl<'a> Directory<'a> {
81
-
/// Create a new builder for this type
82
-
pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> {
83
-
DirectoryBuilder::new()
84
-
}
85
-
}
86
-
87
-
impl<'a> DirectoryBuilder<'a, directory_state::Empty> {
88
-
/// Create a new builder with all fields unset
89
-
pub fn new() -> Self {
90
-
DirectoryBuilder {
91
-
_phantom_state: ::core::marker::PhantomData,
92
-
__unsafe_private_named: (None, None),
93
-
_phantom: ::core::marker::PhantomData,
94
-
}
95
-
}
96
-
}
97
-
98
-
impl<'a, S> DirectoryBuilder<'a, S>
99
-
where
100
-
S: directory_state::State,
101
-
S::Entries: directory_state::IsUnset,
102
-
{
103
-
/// Set the `entries` field (required)
104
-
pub fn entries(
105
-
mut self,
106
-
value: impl Into<Vec<crate::place_wisp::fs::Entry<'a>>>,
107
-
) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> {
108
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
109
-
DirectoryBuilder {
110
-
_phantom_state: ::core::marker::PhantomData,
111
-
__unsafe_private_named: self.__unsafe_private_named,
112
-
_phantom: ::core::marker::PhantomData,
113
-
}
114
-
}
115
-
}
116
-
117
-
impl<'a, S> DirectoryBuilder<'a, S>
118
-
where
119
-
S: directory_state::State,
120
-
S::Type: directory_state::IsUnset,
121
-
{
122
-
/// Set the `type` field (required)
123
-
pub fn r#type(
124
-
mut self,
125
-
value: impl Into<jacquard_common::CowStr<'a>>,
126
-
) -> DirectoryBuilder<'a, directory_state::SetType<S>> {
127
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
128
-
DirectoryBuilder {
129
-
_phantom_state: ::core::marker::PhantomData,
130
-
__unsafe_private_named: self.__unsafe_private_named,
131
-
_phantom: ::core::marker::PhantomData,
132
-
}
133
-
}
134
-
}
135
-
136
-
impl<'a, S> DirectoryBuilder<'a, S>
137
-
where
138
-
S: directory_state::State,
139
-
S::Type: directory_state::IsSet,
140
-
S::Entries: directory_state::IsSet,
141
-
{
142
-
/// Build the final struct
143
-
pub fn build(self) -> Directory<'a> {
144
-
Directory {
145
-
entries: self.__unsafe_private_named.0.unwrap(),
146
-
r#type: self.__unsafe_private_named.1.unwrap(),
147
-
extra_data: Default::default(),
148
-
}
149
-
}
150
-
/// Build the final struct with custom extra_data
151
-
pub fn build_with_data(
152
-
self,
153
-
extra_data: std::collections::BTreeMap<
154
-
jacquard_common::smol_str::SmolStr,
155
-
jacquard_common::types::value::Data<'a>,
156
-
>,
157
-
) -> Directory<'a> {
158
-
Directory {
159
-
entries: self.__unsafe_private_named.0.unwrap(),
160
-
r#type: self.__unsafe_private_named.1.unwrap(),
161
-
extra_data: Some(extra_data),
162
-
}
163
-
}
164
-
}
165
-
166
-
fn lexicon_doc_place_wisp_fs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
167
-
::jacquard_lexicon::lexicon::LexiconDoc {
168
-
lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
169
-
id: ::jacquard_common::CowStr::new_static("place.wisp.fs"),
170
-
revision: None,
171
-
description: None,
172
-
defs: {
173
-
let mut map = ::std::collections::BTreeMap::new();
174
-
map.insert(
175
-
::jacquard_common::smol_str::SmolStr::new_static("directory"),
176
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
177
-
description: None,
178
-
required: Some(
179
-
vec![
180
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
181
-
::jacquard_common::smol_str::SmolStr::new_static("entries")
182
-
],
183
-
),
184
-
nullable: None,
185
-
properties: {
186
-
#[allow(unused_mut)]
187
-
let mut map = ::std::collections::BTreeMap::new();
188
-
map.insert(
189
-
::jacquard_common::smol_str::SmolStr::new_static("entries"),
190
-
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
191
-
description: None,
192
-
items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef {
193
-
description: None,
194
-
r#ref: ::jacquard_common::CowStr::new_static("#entry"),
195
-
}),
196
-
min_length: None,
197
-
max_length: Some(500usize),
198
-
}),
199
-
);
200
-
map.insert(
201
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
202
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
203
-
description: None,
204
-
format: None,
205
-
default: None,
206
-
min_length: None,
207
-
max_length: None,
208
-
min_graphemes: None,
209
-
max_graphemes: None,
210
-
r#enum: None,
211
-
r#const: None,
212
-
known_values: None,
213
-
}),
214
-
);
215
-
map
216
-
},
217
-
}),
218
-
);
219
-
map.insert(
220
-
::jacquard_common::smol_str::SmolStr::new_static("entry"),
221
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
222
-
description: None,
223
-
required: Some(
224
-
vec![
225
-
::jacquard_common::smol_str::SmolStr::new_static("name"),
226
-
::jacquard_common::smol_str::SmolStr::new_static("node")
227
-
],
228
-
),
229
-
nullable: None,
230
-
properties: {
231
-
#[allow(unused_mut)]
232
-
let mut map = ::std::collections::BTreeMap::new();
233
-
map.insert(
234
-
::jacquard_common::smol_str::SmolStr::new_static("name"),
235
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
236
-
description: None,
237
-
format: None,
238
-
default: None,
239
-
min_length: None,
240
-
max_length: Some(255usize),
241
-
min_graphemes: None,
242
-
max_graphemes: None,
243
-
r#enum: None,
244
-
r#const: None,
245
-
known_values: None,
246
-
}),
247
-
);
248
-
map.insert(
249
-
::jacquard_common::smol_str::SmolStr::new_static("node"),
250
-
::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion {
251
-
description: None,
252
-
refs: vec![
253
-
::jacquard_common::CowStr::new_static("#file"),
254
-
::jacquard_common::CowStr::new_static("#directory"),
255
-
::jacquard_common::CowStr::new_static("#subfs")
256
-
],
257
-
closed: None,
258
-
}),
259
-
);
260
-
map
261
-
},
262
-
}),
263
-
);
264
-
map.insert(
265
-
::jacquard_common::smol_str::SmolStr::new_static("file"),
266
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
267
-
description: None,
268
-
required: Some(
269
-
vec![
270
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
271
-
::jacquard_common::smol_str::SmolStr::new_static("blob")
272
-
],
273
-
),
274
-
nullable: None,
275
-
properties: {
276
-
#[allow(unused_mut)]
277
-
let mut map = ::std::collections::BTreeMap::new();
278
-
map.insert(
279
-
::jacquard_common::smol_str::SmolStr::new_static("base64"),
280
-
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
281
-
description: None,
282
-
default: None,
283
-
r#const: None,
284
-
}),
285
-
);
286
-
map.insert(
287
-
::jacquard_common::smol_str::SmolStr::new_static("blob"),
288
-
::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob {
289
-
description: None,
290
-
accept: None,
291
-
max_size: None,
292
-
}),
293
-
);
294
-
map.insert(
295
-
::jacquard_common::smol_str::SmolStr::new_static("encoding"),
296
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
297
-
description: Some(
298
-
::jacquard_common::CowStr::new_static(
299
-
"Content encoding (e.g., gzip for compressed files)",
300
-
),
301
-
),
302
-
format: None,
303
-
default: None,
304
-
min_length: None,
305
-
max_length: None,
306
-
min_graphemes: None,
307
-
max_graphemes: None,
308
-
r#enum: None,
309
-
r#const: None,
310
-
known_values: None,
311
-
}),
312
-
);
313
-
map.insert(
314
-
::jacquard_common::smol_str::SmolStr::new_static("mimeType"),
315
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
316
-
description: Some(
317
-
::jacquard_common::CowStr::new_static(
318
-
"Original MIME type before compression",
319
-
),
320
-
),
321
-
format: None,
322
-
default: None,
323
-
min_length: None,
324
-
max_length: None,
325
-
min_graphemes: None,
326
-
max_graphemes: None,
327
-
r#enum: None,
328
-
r#const: None,
329
-
known_values: None,
330
-
}),
331
-
);
332
-
map.insert(
333
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
334
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
335
-
description: None,
336
-
format: None,
337
-
default: None,
338
-
min_length: None,
339
-
max_length: None,
340
-
min_graphemes: None,
341
-
max_graphemes: None,
342
-
r#enum: None,
343
-
r#const: None,
344
-
known_values: None,
345
-
}),
346
-
);
347
-
map
348
-
},
349
-
}),
350
-
);
351
-
map.insert(
352
-
::jacquard_common::smol_str::SmolStr::new_static("main"),
353
-
::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
354
-
description: Some(
355
-
::jacquard_common::CowStr::new_static(
356
-
"Virtual filesystem manifest for a Wisp site",
357
-
),
358
-
),
359
-
key: None,
360
-
record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
361
-
description: None,
362
-
required: Some(
363
-
vec![
364
-
::jacquard_common::smol_str::SmolStr::new_static("site"),
365
-
::jacquard_common::smol_str::SmolStr::new_static("root"),
366
-
::jacquard_common::smol_str::SmolStr::new_static("createdAt")
367
-
],
368
-
),
369
-
nullable: None,
370
-
properties: {
371
-
#[allow(unused_mut)]
372
-
let mut map = ::std::collections::BTreeMap::new();
373
-
map.insert(
374
-
::jacquard_common::smol_str::SmolStr::new_static(
375
-
"createdAt",
376
-
),
377
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
378
-
description: None,
379
-
format: Some(
380
-
::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
381
-
),
382
-
default: None,
383
-
min_length: None,
384
-
max_length: None,
385
-
min_graphemes: None,
386
-
max_graphemes: None,
387
-
r#enum: None,
388
-
r#const: None,
389
-
known_values: None,
390
-
}),
391
-
);
392
-
map.insert(
393
-
::jacquard_common::smol_str::SmolStr::new_static(
394
-
"fileCount",
395
-
),
396
-
::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger {
397
-
description: None,
398
-
default: None,
399
-
minimum: Some(0i64),
400
-
maximum: Some(1000i64),
401
-
r#enum: None,
402
-
r#const: None,
403
-
}),
404
-
);
405
-
map.insert(
406
-
::jacquard_common::smol_str::SmolStr::new_static("root"),
407
-
::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
408
-
description: None,
409
-
r#ref: ::jacquard_common::CowStr::new_static("#directory"),
410
-
}),
411
-
);
412
-
map.insert(
413
-
::jacquard_common::smol_str::SmolStr::new_static("site"),
414
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
415
-
description: None,
416
-
format: None,
417
-
default: None,
418
-
min_length: None,
419
-
max_length: None,
420
-
min_graphemes: None,
421
-
max_graphemes: None,
422
-
r#enum: None,
423
-
r#const: None,
424
-
known_values: None,
425
-
}),
426
-
);
427
-
map
428
-
},
429
-
}),
430
-
}),
431
-
);
432
-
map.insert(
433
-
::jacquard_common::smol_str::SmolStr::new_static("subfs"),
434
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
435
-
description: None,
436
-
required: Some(
437
-
vec![
438
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
439
-
::jacquard_common::smol_str::SmolStr::new_static("subject")
440
-
],
441
-
),
442
-
nullable: None,
443
-
properties: {
444
-
#[allow(unused_mut)]
445
-
let mut map = ::std::collections::BTreeMap::new();
446
-
map.insert(
447
-
::jacquard_common::smol_str::SmolStr::new_static("flat"),
448
-
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
449
-
description: None,
450
-
default: None,
451
-
r#const: None,
452
-
}),
453
-
);
454
-
map.insert(
455
-
::jacquard_common::smol_str::SmolStr::new_static("subject"),
456
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
457
-
description: Some(
458
-
::jacquard_common::CowStr::new_static(
459
-
"AT-URI pointing to a place.wisp.subfs record containing this subtree.",
460
-
),
461
-
),
462
-
format: Some(
463
-
::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
464
-
),
465
-
default: None,
466
-
min_length: None,
467
-
max_length: None,
468
-
min_graphemes: None,
469
-
max_graphemes: None,
470
-
r#enum: None,
471
-
r#const: None,
472
-
known_values: None,
473
-
}),
474
-
);
475
-
map.insert(
476
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
477
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
478
-
description: None,
479
-
format: None,
480
-
default: None,
481
-
min_length: None,
482
-
max_length: None,
483
-
min_graphemes: None,
484
-
max_graphemes: None,
485
-
r#enum: None,
486
-
r#const: None,
487
-
known_values: None,
488
-
}),
489
-
);
490
-
map
491
-
},
492
-
}),
493
-
);
494
-
map
495
-
},
496
-
}
497
-
}
498
-
499
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> {
500
-
fn nsid() -> &'static str {
501
-
"place.wisp.fs"
502
-
}
503
-
fn def_name() -> &'static str {
504
-
"directory"
505
-
}
506
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
507
-
lexicon_doc_place_wisp_fs()
508
-
}
509
-
fn validate(
510
-
&self,
511
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
512
-
{
513
-
let value = &self.entries;
514
-
#[allow(unused_comparisons)]
515
-
if value.len() > 500usize {
516
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
517
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
518
-
"entries",
519
-
),
520
-
max: 500usize,
521
-
actual: value.len(),
522
-
});
523
-
}
524
-
}
525
-
Ok(())
526
-
}
527
-
}
528
-
529
-
#[jacquard_derive::lexicon]
530
-
#[derive(
531
-
serde::Serialize,
532
-
serde::Deserialize,
533
-
Debug,
534
-
Clone,
535
-
PartialEq,
536
-
Eq,
537
-
jacquard_derive::IntoStatic
538
-
)]
539
-
#[serde(rename_all = "camelCase")]
540
-
pub struct Entry<'a> {
541
-
#[serde(borrow)]
542
-
pub name: jacquard_common::CowStr<'a>,
543
-
#[serde(borrow)]
544
-
pub node: EntryNode<'a>,
545
-
}
546
-
547
-
pub mod entry_state {
548
-
549
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
550
-
#[allow(unused)]
551
-
use ::core::marker::PhantomData;
552
-
mod sealed {
553
-
pub trait Sealed {}
554
-
}
555
-
/// State trait tracking which required fields have been set
556
-
pub trait State: sealed::Sealed {
557
-
type Name;
558
-
type Node;
559
-
}
560
-
/// Empty state - all required fields are unset
561
-
pub struct Empty(());
562
-
impl sealed::Sealed for Empty {}
563
-
impl State for Empty {
564
-
type Name = Unset;
565
-
type Node = Unset;
566
-
}
567
-
///State transition - sets the `name` field to Set
568
-
pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>);
569
-
impl<S: State> sealed::Sealed for SetName<S> {}
570
-
impl<S: State> State for SetName<S> {
571
-
type Name = Set<members::name>;
572
-
type Node = S::Node;
573
-
}
574
-
///State transition - sets the `node` field to Set
575
-
pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>);
576
-
impl<S: State> sealed::Sealed for SetNode<S> {}
577
-
impl<S: State> State for SetNode<S> {
578
-
type Name = S::Name;
579
-
type Node = Set<members::node>;
580
-
}
581
-
/// Marker types for field names
582
-
#[allow(non_camel_case_types)]
583
-
pub mod members {
584
-
///Marker type for the `name` field
585
-
pub struct name(());
586
-
///Marker type for the `node` field
587
-
pub struct node(());
588
-
}
589
-
}
590
-
591
-
/// Builder for constructing an instance of this type
592
-
pub struct EntryBuilder<'a, S: entry_state::State> {
593
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
594
-
__unsafe_private_named: (
595
-
::core::option::Option<jacquard_common::CowStr<'a>>,
596
-
::core::option::Option<EntryNode<'a>>,
597
-
),
598
-
_phantom: ::core::marker::PhantomData<&'a ()>,
599
-
}
600
-
601
-
impl<'a> Entry<'a> {
602
-
/// Create a new builder for this type
603
-
pub fn new() -> EntryBuilder<'a, entry_state::Empty> {
604
-
EntryBuilder::new()
605
-
}
606
-
}
607
-
608
-
impl<'a> EntryBuilder<'a, entry_state::Empty> {
609
-
/// Create a new builder with all fields unset
610
-
pub fn new() -> Self {
611
-
EntryBuilder {
612
-
_phantom_state: ::core::marker::PhantomData,
613
-
__unsafe_private_named: (None, None),
614
-
_phantom: ::core::marker::PhantomData,
615
-
}
616
-
}
617
-
}
618
-
619
-
impl<'a, S> EntryBuilder<'a, S>
620
-
where
621
-
S: entry_state::State,
622
-
S::Name: entry_state::IsUnset,
623
-
{
624
-
/// Set the `name` field (required)
625
-
pub fn name(
626
-
mut self,
627
-
value: impl Into<jacquard_common::CowStr<'a>>,
628
-
) -> EntryBuilder<'a, entry_state::SetName<S>> {
629
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
630
-
EntryBuilder {
631
-
_phantom_state: ::core::marker::PhantomData,
632
-
__unsafe_private_named: self.__unsafe_private_named,
633
-
_phantom: ::core::marker::PhantomData,
634
-
}
635
-
}
636
-
}
637
-
638
-
impl<'a, S> EntryBuilder<'a, S>
639
-
where
640
-
S: entry_state::State,
641
-
S::Node: entry_state::IsUnset,
642
-
{
643
-
/// Set the `node` field (required)
644
-
pub fn node(
645
-
mut self,
646
-
value: impl Into<EntryNode<'a>>,
647
-
) -> EntryBuilder<'a, entry_state::SetNode<S>> {
648
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
649
-
EntryBuilder {
650
-
_phantom_state: ::core::marker::PhantomData,
651
-
__unsafe_private_named: self.__unsafe_private_named,
652
-
_phantom: ::core::marker::PhantomData,
653
-
}
654
-
}
655
-
}
656
-
657
-
impl<'a, S> EntryBuilder<'a, S>
658
-
where
659
-
S: entry_state::State,
660
-
S::Name: entry_state::IsSet,
661
-
S::Node: entry_state::IsSet,
662
-
{
663
-
/// Build the final struct
664
-
pub fn build(self) -> Entry<'a> {
665
-
Entry {
666
-
name: self.__unsafe_private_named.0.unwrap(),
667
-
node: self.__unsafe_private_named.1.unwrap(),
668
-
extra_data: Default::default(),
669
-
}
670
-
}
671
-
/// Build the final struct with custom extra_data
672
-
pub fn build_with_data(
673
-
self,
674
-
extra_data: std::collections::BTreeMap<
675
-
jacquard_common::smol_str::SmolStr,
676
-
jacquard_common::types::value::Data<'a>,
677
-
>,
678
-
) -> Entry<'a> {
679
-
Entry {
680
-
name: self.__unsafe_private_named.0.unwrap(),
681
-
node: self.__unsafe_private_named.1.unwrap(),
682
-
extra_data: Some(extra_data),
683
-
}
684
-
}
685
-
}
686
-
687
-
#[jacquard_derive::open_union]
688
-
#[derive(
689
-
serde::Serialize,
690
-
serde::Deserialize,
691
-
Debug,
692
-
Clone,
693
-
PartialEq,
694
-
Eq,
695
-
jacquard_derive::IntoStatic
696
-
)]
697
-
#[serde(tag = "$type")]
698
-
#[serde(bound(deserialize = "'de: 'a"))]
699
-
pub enum EntryNode<'a> {
700
-
#[serde(rename = "place.wisp.fs#file")]
701
-
File(Box<crate::place_wisp::fs::File<'a>>),
702
-
#[serde(rename = "place.wisp.fs#directory")]
703
-
Directory(Box<crate::place_wisp::fs::Directory<'a>>),
704
-
#[serde(rename = "place.wisp.fs#subfs")]
705
-
Subfs(Box<crate::place_wisp::fs::Subfs<'a>>),
706
-
}
707
-
708
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> {
709
-
fn nsid() -> &'static str {
710
-
"place.wisp.fs"
711
-
}
712
-
fn def_name() -> &'static str {
713
-
"entry"
714
-
}
715
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
716
-
lexicon_doc_place_wisp_fs()
717
-
}
718
-
fn validate(
719
-
&self,
720
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
721
-
{
722
-
let value = &self.name;
723
-
#[allow(unused_comparisons)]
724
-
if <str>::len(value.as_ref()) > 255usize {
725
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
726
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
727
-
"name",
728
-
),
729
-
max: 255usize,
730
-
actual: <str>::len(value.as_ref()),
731
-
});
732
-
}
733
-
}
734
-
Ok(())
735
-
}
736
-
}
737
-
738
-
#[jacquard_derive::lexicon]
739
-
#[derive(
740
-
serde::Serialize,
741
-
serde::Deserialize,
742
-
Debug,
743
-
Clone,
744
-
PartialEq,
745
-
Eq,
746
-
jacquard_derive::IntoStatic
747
-
)]
748
-
#[serde(rename_all = "camelCase")]
749
-
pub struct File<'a> {
750
-
/// True if blob content is base64-encoded (used to bypass PDS content sniffing)
751
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
752
-
pub base64: Option<bool>,
753
-
/// Content blob ref
754
-
#[serde(borrow)]
755
-
pub blob: jacquard_common::types::blob::BlobRef<'a>,
756
-
/// Content encoding (e.g., gzip for compressed files)
757
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
758
-
#[serde(borrow)]
759
-
pub encoding: Option<jacquard_common::CowStr<'a>>,
760
-
/// Original MIME type before compression
761
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
762
-
#[serde(borrow)]
763
-
pub mime_type: Option<jacquard_common::CowStr<'a>>,
764
-
#[serde(borrow)]
765
-
pub r#type: jacquard_common::CowStr<'a>,
766
-
}
767
-
768
-
pub mod file_state {
769
-
770
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
771
-
#[allow(unused)]
772
-
use ::core::marker::PhantomData;
773
-
mod sealed {
774
-
pub trait Sealed {}
775
-
}
776
-
/// State trait tracking which required fields have been set
777
-
pub trait State: sealed::Sealed {
778
-
type Type;
779
-
type Blob;
780
-
}
781
-
/// Empty state - all required fields are unset
782
-
pub struct Empty(());
783
-
impl sealed::Sealed for Empty {}
784
-
impl State for Empty {
785
-
type Type = Unset;
786
-
type Blob = Unset;
787
-
}
788
-
///State transition - sets the `type` field to Set
789
-
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
790
-
impl<S: State> sealed::Sealed for SetType<S> {}
791
-
impl<S: State> State for SetType<S> {
792
-
type Type = Set<members::r#type>;
793
-
type Blob = S::Blob;
794
-
}
795
-
///State transition - sets the `blob` field to Set
796
-
pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>);
797
-
impl<S: State> sealed::Sealed for SetBlob<S> {}
798
-
impl<S: State> State for SetBlob<S> {
799
-
type Type = S::Type;
800
-
type Blob = Set<members::blob>;
801
-
}
802
-
/// Marker types for field names
803
-
#[allow(non_camel_case_types)]
804
-
pub mod members {
805
-
///Marker type for the `type` field
806
-
pub struct r#type(());
807
-
///Marker type for the `blob` field
808
-
pub struct blob(());
809
-
}
810
-
}
811
-
812
-
/// Builder for constructing an instance of this type
813
-
pub struct FileBuilder<'a, S: file_state::State> {
814
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
815
-
__unsafe_private_named: (
816
-
::core::option::Option<bool>,
817
-
::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>,
818
-
::core::option::Option<jacquard_common::CowStr<'a>>,
819
-
::core::option::Option<jacquard_common::CowStr<'a>>,
820
-
::core::option::Option<jacquard_common::CowStr<'a>>,
821
-
),
822
-
_phantom: ::core::marker::PhantomData<&'a ()>,
823
-
}
824
-
825
-
impl<'a> File<'a> {
826
-
/// Create a new builder for this type
827
-
pub fn new() -> FileBuilder<'a, file_state::Empty> {
828
-
FileBuilder::new()
829
-
}
830
-
}
831
-
832
-
impl<'a> FileBuilder<'a, file_state::Empty> {
833
-
/// Create a new builder with all fields unset
834
-
pub fn new() -> Self {
835
-
FileBuilder {
836
-
_phantom_state: ::core::marker::PhantomData,
837
-
__unsafe_private_named: (None, None, None, None, None),
838
-
_phantom: ::core::marker::PhantomData,
839
-
}
840
-
}
841
-
}
842
-
843
-
impl<'a, S: file_state::State> FileBuilder<'a, S> {
844
-
/// Set the `base64` field (optional)
845
-
pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self {
846
-
self.__unsafe_private_named.0 = value.into();
847
-
self
848
-
}
849
-
/// Set the `base64` field to an Option value (optional)
850
-
pub fn maybe_base64(mut self, value: Option<bool>) -> Self {
851
-
self.__unsafe_private_named.0 = value;
852
-
self
853
-
}
854
-
}
855
-
856
-
impl<'a, S> FileBuilder<'a, S>
857
-
where
858
-
S: file_state::State,
859
-
S::Blob: file_state::IsUnset,
860
-
{
861
-
/// Set the `blob` field (required)
862
-
pub fn blob(
863
-
mut self,
864
-
value: impl Into<jacquard_common::types::blob::BlobRef<'a>>,
865
-
) -> FileBuilder<'a, file_state::SetBlob<S>> {
866
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
867
-
FileBuilder {
868
-
_phantom_state: ::core::marker::PhantomData,
869
-
__unsafe_private_named: self.__unsafe_private_named,
870
-
_phantom: ::core::marker::PhantomData,
871
-
}
872
-
}
873
-
}
874
-
875
-
impl<'a, S: file_state::State> FileBuilder<'a, S> {
876
-
/// Set the `encoding` field (optional)
877
-
pub fn encoding(
878
-
mut self,
879
-
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
880
-
) -> Self {
881
-
self.__unsafe_private_named.2 = value.into();
882
-
self
883
-
}
884
-
/// Set the `encoding` field to an Option value (optional)
885
-
pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
886
-
self.__unsafe_private_named.2 = value;
887
-
self
888
-
}
889
-
}
890
-
891
-
impl<'a, S: file_state::State> FileBuilder<'a, S> {
892
-
/// Set the `mimeType` field (optional)
893
-
pub fn mime_type(
894
-
mut self,
895
-
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
896
-
) -> Self {
897
-
self.__unsafe_private_named.3 = value.into();
898
-
self
899
-
}
900
-
/// Set the `mimeType` field to an Option value (optional)
901
-
pub fn maybe_mime_type(
902
-
mut self,
903
-
value: Option<jacquard_common::CowStr<'a>>,
904
-
) -> Self {
905
-
self.__unsafe_private_named.3 = value;
906
-
self
907
-
}
908
-
}
909
-
910
-
impl<'a, S> FileBuilder<'a, S>
911
-
where
912
-
S: file_state::State,
913
-
S::Type: file_state::IsUnset,
914
-
{
915
-
/// Set the `type` field (required)
916
-
pub fn r#type(
917
-
mut self,
918
-
value: impl Into<jacquard_common::CowStr<'a>>,
919
-
) -> FileBuilder<'a, file_state::SetType<S>> {
920
-
self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into());
921
-
FileBuilder {
922
-
_phantom_state: ::core::marker::PhantomData,
923
-
__unsafe_private_named: self.__unsafe_private_named,
924
-
_phantom: ::core::marker::PhantomData,
925
-
}
926
-
}
927
-
}
928
-
929
-
impl<'a, S> FileBuilder<'a, S>
930
-
where
931
-
S: file_state::State,
932
-
S::Type: file_state::IsSet,
933
-
S::Blob: file_state::IsSet,
934
-
{
935
-
/// Build the final struct
936
-
pub fn build(self) -> File<'a> {
937
-
File {
938
-
base64: self.__unsafe_private_named.0,
939
-
blob: self.__unsafe_private_named.1.unwrap(),
940
-
encoding: self.__unsafe_private_named.2,
941
-
mime_type: self.__unsafe_private_named.3,
942
-
r#type: self.__unsafe_private_named.4.unwrap(),
943
-
extra_data: Default::default(),
944
-
}
945
-
}
946
-
/// Build the final struct with custom extra_data
947
-
pub fn build_with_data(
948
-
self,
949
-
extra_data: std::collections::BTreeMap<
950
-
jacquard_common::smol_str::SmolStr,
951
-
jacquard_common::types::value::Data<'a>,
952
-
>,
953
-
) -> File<'a> {
954
-
File {
955
-
base64: self.__unsafe_private_named.0,
956
-
blob: self.__unsafe_private_named.1.unwrap(),
957
-
encoding: self.__unsafe_private_named.2,
958
-
mime_type: self.__unsafe_private_named.3,
959
-
r#type: self.__unsafe_private_named.4.unwrap(),
960
-
extra_data: Some(extra_data),
961
-
}
962
-
}
963
-
}
964
-
965
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> {
966
-
fn nsid() -> &'static str {
967
-
"place.wisp.fs"
968
-
}
969
-
fn def_name() -> &'static str {
970
-
"file"
971
-
}
972
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
973
-
lexicon_doc_place_wisp_fs()
974
-
}
975
-
fn validate(
976
-
&self,
977
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
978
-
Ok(())
979
-
}
980
-
}
981
-
982
-
/// Virtual filesystem manifest for a Wisp site
983
-
#[jacquard_derive::lexicon]
984
-
#[derive(
985
-
serde::Serialize,
986
-
serde::Deserialize,
987
-
Debug,
988
-
Clone,
989
-
PartialEq,
990
-
Eq,
991
-
jacquard_derive::IntoStatic
992
-
)]
993
-
#[serde(rename_all = "camelCase")]
994
-
pub struct Fs<'a> {
995
-
pub created_at: jacquard_common::types::string::Datetime,
996
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
997
-
pub file_count: Option<i64>,
998
-
#[serde(borrow)]
999
-
pub root: crate::place_wisp::fs::Directory<'a>,
1000
-
#[serde(borrow)]
1001
-
pub site: jacquard_common::CowStr<'a>,
1002
-
}
1003
-
1004
-
pub mod fs_state {
1005
-
1006
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
1007
-
#[allow(unused)]
1008
-
use ::core::marker::PhantomData;
1009
-
mod sealed {
1010
-
pub trait Sealed {}
1011
-
}
1012
-
/// State trait tracking which required fields have been set
1013
-
pub trait State: sealed::Sealed {
1014
-
type Site;
1015
-
type Root;
1016
-
type CreatedAt;
1017
-
}
1018
-
/// Empty state - all required fields are unset
1019
-
pub struct Empty(());
1020
-
impl sealed::Sealed for Empty {}
1021
-
impl State for Empty {
1022
-
type Site = Unset;
1023
-
type Root = Unset;
1024
-
type CreatedAt = Unset;
1025
-
}
1026
-
///State transition - sets the `site` field to Set
1027
-
pub struct SetSite<S: State = Empty>(PhantomData<fn() -> S>);
1028
-
impl<S: State> sealed::Sealed for SetSite<S> {}
1029
-
impl<S: State> State for SetSite<S> {
1030
-
type Site = Set<members::site>;
1031
-
type Root = S::Root;
1032
-
type CreatedAt = S::CreatedAt;
1033
-
}
1034
-
///State transition - sets the `root` field to Set
1035
-
pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>);
1036
-
impl<S: State> sealed::Sealed for SetRoot<S> {}
1037
-
impl<S: State> State for SetRoot<S> {
1038
-
type Site = S::Site;
1039
-
type Root = Set<members::root>;
1040
-
type CreatedAt = S::CreatedAt;
1041
-
}
1042
-
///State transition - sets the `created_at` field to Set
1043
-
pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
1044
-
impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
1045
-
impl<S: State> State for SetCreatedAt<S> {
1046
-
type Site = S::Site;
1047
-
type Root = S::Root;
1048
-
type CreatedAt = Set<members::created_at>;
1049
-
}
1050
-
/// Marker types for field names
1051
-
#[allow(non_camel_case_types)]
1052
-
pub mod members {
1053
-
///Marker type for the `site` field
1054
-
pub struct site(());
1055
-
///Marker type for the `root` field
1056
-
pub struct root(());
1057
-
///Marker type for the `created_at` field
1058
-
pub struct created_at(());
1059
-
}
1060
-
}
1061
-
1062
-
/// Builder for constructing an instance of this type
1063
-
pub struct FsBuilder<'a, S: fs_state::State> {
1064
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1065
-
__unsafe_private_named: (
1066
-
::core::option::Option<jacquard_common::types::string::Datetime>,
1067
-
::core::option::Option<i64>,
1068
-
::core::option::Option<crate::place_wisp::fs::Directory<'a>>,
1069
-
::core::option::Option<jacquard_common::CowStr<'a>>,
1070
-
),
1071
-
_phantom: ::core::marker::PhantomData<&'a ()>,
1072
-
}
1073
-
1074
-
impl<'a> Fs<'a> {
1075
-
/// Create a new builder for this type
1076
-
pub fn new() -> FsBuilder<'a, fs_state::Empty> {
1077
-
FsBuilder::new()
1078
-
}
1079
-
}
1080
-
1081
-
impl<'a> FsBuilder<'a, fs_state::Empty> {
1082
-
/// Create a new builder with all fields unset
1083
-
pub fn new() -> Self {
1084
-
FsBuilder {
1085
-
_phantom_state: ::core::marker::PhantomData,
1086
-
__unsafe_private_named: (None, None, None, None),
1087
-
_phantom: ::core::marker::PhantomData,
1088
-
}
1089
-
}
1090
-
}
1091
-
1092
-
impl<'a, S> FsBuilder<'a, S>
1093
-
where
1094
-
S: fs_state::State,
1095
-
S::CreatedAt: fs_state::IsUnset,
1096
-
{
1097
-
/// Set the `createdAt` field (required)
1098
-
pub fn created_at(
1099
-
mut self,
1100
-
value: impl Into<jacquard_common::types::string::Datetime>,
1101
-
) -> FsBuilder<'a, fs_state::SetCreatedAt<S>> {
1102
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
1103
-
FsBuilder {
1104
-
_phantom_state: ::core::marker::PhantomData,
1105
-
__unsafe_private_named: self.__unsafe_private_named,
1106
-
_phantom: ::core::marker::PhantomData,
1107
-
}
1108
-
}
1109
-
}
1110
-
1111
-
impl<'a, S: fs_state::State> FsBuilder<'a, S> {
1112
-
/// Set the `fileCount` field (optional)
1113
-
pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self {
1114
-
self.__unsafe_private_named.1 = value.into();
1115
-
self
1116
-
}
1117
-
/// Set the `fileCount` field to an Option value (optional)
1118
-
pub fn maybe_file_count(mut self, value: Option<i64>) -> Self {
1119
-
self.__unsafe_private_named.1 = value;
1120
-
self
1121
-
}
1122
-
}
1123
-
1124
-
impl<'a, S> FsBuilder<'a, S>
1125
-
where
1126
-
S: fs_state::State,
1127
-
S::Root: fs_state::IsUnset,
1128
-
{
1129
-
/// Set the `root` field (required)
1130
-
pub fn root(
1131
-
mut self,
1132
-
value: impl Into<crate::place_wisp::fs::Directory<'a>>,
1133
-
) -> FsBuilder<'a, fs_state::SetRoot<S>> {
1134
-
self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
1135
-
FsBuilder {
1136
-
_phantom_state: ::core::marker::PhantomData,
1137
-
__unsafe_private_named: self.__unsafe_private_named,
1138
-
_phantom: ::core::marker::PhantomData,
1139
-
}
1140
-
}
1141
-
}
1142
-
1143
-
impl<'a, S> FsBuilder<'a, S>
1144
-
where
1145
-
S: fs_state::State,
1146
-
S::Site: fs_state::IsUnset,
1147
-
{
1148
-
/// Set the `site` field (required)
1149
-
pub fn site(
1150
-
mut self,
1151
-
value: impl Into<jacquard_common::CowStr<'a>>,
1152
-
) -> FsBuilder<'a, fs_state::SetSite<S>> {
1153
-
self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into());
1154
-
FsBuilder {
1155
-
_phantom_state: ::core::marker::PhantomData,
1156
-
__unsafe_private_named: self.__unsafe_private_named,
1157
-
_phantom: ::core::marker::PhantomData,
1158
-
}
1159
-
}
1160
-
}
1161
-
1162
-
impl<'a, S> FsBuilder<'a, S>
1163
-
where
1164
-
S: fs_state::State,
1165
-
S::Site: fs_state::IsSet,
1166
-
S::Root: fs_state::IsSet,
1167
-
S::CreatedAt: fs_state::IsSet,
1168
-
{
1169
-
/// Build the final struct
1170
-
pub fn build(self) -> Fs<'a> {
1171
-
Fs {
1172
-
created_at: self.__unsafe_private_named.0.unwrap(),
1173
-
file_count: self.__unsafe_private_named.1,
1174
-
root: self.__unsafe_private_named.2.unwrap(),
1175
-
site: self.__unsafe_private_named.3.unwrap(),
1176
-
extra_data: Default::default(),
1177
-
}
1178
-
}
1179
-
/// Build the final struct with custom extra_data
1180
-
pub fn build_with_data(
1181
-
self,
1182
-
extra_data: std::collections::BTreeMap<
1183
-
jacquard_common::smol_str::SmolStr,
1184
-
jacquard_common::types::value::Data<'a>,
1185
-
>,
1186
-
) -> Fs<'a> {
1187
-
Fs {
1188
-
created_at: self.__unsafe_private_named.0.unwrap(),
1189
-
file_count: self.__unsafe_private_named.1,
1190
-
root: self.__unsafe_private_named.2.unwrap(),
1191
-
site: self.__unsafe_private_named.3.unwrap(),
1192
-
extra_data: Some(extra_data),
1193
-
}
1194
-
}
1195
-
}
1196
-
1197
-
impl<'a> Fs<'a> {
1198
-
pub fn uri(
1199
-
uri: impl Into<jacquard_common::CowStr<'a>>,
1200
-
) -> Result<
1201
-
jacquard_common::types::uri::RecordUri<'a, FsRecord>,
1202
-
jacquard_common::types::uri::UriError,
1203
-
> {
1204
-
jacquard_common::types::uri::RecordUri::try_from_uri(
1205
-
jacquard_common::types::string::AtUri::new_cow(uri.into())?,
1206
-
)
1207
-
}
1208
-
}
1209
-
1210
-
/// Typed wrapper for GetRecord response with this collection's record type.
1211
-
#[derive(
1212
-
serde::Serialize,
1213
-
serde::Deserialize,
1214
-
Debug,
1215
-
Clone,
1216
-
PartialEq,
1217
-
Eq,
1218
-
jacquard_derive::IntoStatic
1219
-
)]
1220
-
#[serde(rename_all = "camelCase")]
1221
-
pub struct FsGetRecordOutput<'a> {
1222
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
1223
-
#[serde(borrow)]
1224
-
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
1225
-
#[serde(borrow)]
1226
-
pub uri: jacquard_common::types::string::AtUri<'a>,
1227
-
#[serde(borrow)]
1228
-
pub value: Fs<'a>,
1229
-
}
1230
-
1231
-
impl From<FsGetRecordOutput<'_>> for Fs<'_> {
1232
-
fn from(output: FsGetRecordOutput<'_>) -> Self {
1233
-
use jacquard_common::IntoStatic;
1234
-
output.value.into_static()
1235
-
}
1236
-
}
1237
-
1238
-
impl jacquard_common::types::collection::Collection for Fs<'_> {
1239
-
const NSID: &'static str = "place.wisp.fs";
1240
-
type Record = FsRecord;
1241
-
}
1242
-
1243
-
/// Marker type for deserializing records from this collection.
1244
-
#[derive(Debug, serde::Serialize, serde::Deserialize)]
1245
-
pub struct FsRecord;
1246
-
impl jacquard_common::xrpc::XrpcResp for FsRecord {
1247
-
const NSID: &'static str = "place.wisp.fs";
1248
-
const ENCODING: &'static str = "application/json";
1249
-
type Output<'de> = FsGetRecordOutput<'de>;
1250
-
type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
1251
-
}
1252
-
1253
-
impl jacquard_common::types::collection::Collection for FsRecord {
1254
-
const NSID: &'static str = "place.wisp.fs";
1255
-
type Record = FsRecord;
1256
-
}
1257
-
1258
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Fs<'a> {
1259
-
fn nsid() -> &'static str {
1260
-
"place.wisp.fs"
1261
-
}
1262
-
fn def_name() -> &'static str {
1263
-
"main"
1264
-
}
1265
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1266
-
lexicon_doc_place_wisp_fs()
1267
-
}
1268
-
fn validate(
1269
-
&self,
1270
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1271
-
if let Some(ref value) = self.file_count {
1272
-
if *value > 1000i64 {
1273
-
return Err(::jacquard_lexicon::validation::ConstraintError::Maximum {
1274
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1275
-
"file_count",
1276
-
),
1277
-
max: 1000i64,
1278
-
actual: *value,
1279
-
});
1280
-
}
1281
-
}
1282
-
if let Some(ref value) = self.file_count {
1283
-
if *value < 0i64 {
1284
-
return Err(::jacquard_lexicon::validation::ConstraintError::Minimum {
1285
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1286
-
"file_count",
1287
-
),
1288
-
min: 0i64,
1289
-
actual: *value,
1290
-
});
1291
-
}
1292
-
}
1293
-
Ok(())
1294
-
}
1295
-
}
1296
-
1297
-
#[jacquard_derive::lexicon]
1298
-
#[derive(
1299
-
serde::Serialize,
1300
-
serde::Deserialize,
1301
-
Debug,
1302
-
Clone,
1303
-
PartialEq,
1304
-
Eq,
1305
-
jacquard_derive::IntoStatic
1306
-
)]
1307
-
#[serde(rename_all = "camelCase")]
1308
-
pub struct Subfs<'a> {
1309
-
/// If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure.
1310
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
1311
-
pub flat: Option<bool>,
1312
-
/// AT-URI pointing to a place.wisp.subfs record containing this subtree.
1313
-
#[serde(borrow)]
1314
-
pub subject: jacquard_common::types::string::AtUri<'a>,
1315
-
#[serde(borrow)]
1316
-
pub r#type: jacquard_common::CowStr<'a>,
1317
-
}
1318
-
1319
-
pub mod subfs_state {
1320
-
1321
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
1322
-
#[allow(unused)]
1323
-
use ::core::marker::PhantomData;
1324
-
mod sealed {
1325
-
pub trait Sealed {}
1326
-
}
1327
-
/// State trait tracking which required fields have been set
1328
-
pub trait State: sealed::Sealed {
1329
-
type Type;
1330
-
type Subject;
1331
-
}
1332
-
/// Empty state - all required fields are unset
1333
-
pub struct Empty(());
1334
-
impl sealed::Sealed for Empty {}
1335
-
impl State for Empty {
1336
-
type Type = Unset;
1337
-
type Subject = Unset;
1338
-
}
1339
-
///State transition - sets the `type` field to Set
1340
-
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
1341
-
impl<S: State> sealed::Sealed for SetType<S> {}
1342
-
impl<S: State> State for SetType<S> {
1343
-
type Type = Set<members::r#type>;
1344
-
type Subject = S::Subject;
1345
-
}
1346
-
///State transition - sets the `subject` field to Set
1347
-
pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>);
1348
-
impl<S: State> sealed::Sealed for SetSubject<S> {}
1349
-
impl<S: State> State for SetSubject<S> {
1350
-
type Type = S::Type;
1351
-
type Subject = Set<members::subject>;
1352
-
}
1353
-
/// Marker types for field names
1354
-
#[allow(non_camel_case_types)]
1355
-
pub mod members {
1356
-
///Marker type for the `type` field
1357
-
pub struct r#type(());
1358
-
///Marker type for the `subject` field
1359
-
pub struct subject(());
1360
-
}
1361
-
}
1362
-
1363
-
/// Builder for constructing an instance of this type
1364
-
pub struct SubfsBuilder<'a, S: subfs_state::State> {
1365
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1366
-
__unsafe_private_named: (
1367
-
::core::option::Option<bool>,
1368
-
::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
1369
-
::core::option::Option<jacquard_common::CowStr<'a>>,
1370
-
),
1371
-
_phantom: ::core::marker::PhantomData<&'a ()>,
1372
-
}
1373
-
1374
-
impl<'a> Subfs<'a> {
1375
-
/// Create a new builder for this type
1376
-
pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> {
1377
-
SubfsBuilder::new()
1378
-
}
1379
-
}
1380
-
1381
-
impl<'a> SubfsBuilder<'a, subfs_state::Empty> {
1382
-
/// Create a new builder with all fields unset
1383
-
pub fn new() -> Self {
1384
-
SubfsBuilder {
1385
-
_phantom_state: ::core::marker::PhantomData,
1386
-
__unsafe_private_named: (None, None, None),
1387
-
_phantom: ::core::marker::PhantomData,
1388
-
}
1389
-
}
1390
-
}
1391
-
1392
-
impl<'a, S: subfs_state::State> SubfsBuilder<'a, S> {
1393
-
/// Set the `flat` field (optional)
1394
-
pub fn flat(mut self, value: impl Into<Option<bool>>) -> Self {
1395
-
self.__unsafe_private_named.0 = value.into();
1396
-
self
1397
-
}
1398
-
/// Set the `flat` field to an Option value (optional)
1399
-
pub fn maybe_flat(mut self, value: Option<bool>) -> Self {
1400
-
self.__unsafe_private_named.0 = value;
1401
-
self
1402
-
}
1403
-
}
1404
-
1405
-
impl<'a, S> SubfsBuilder<'a, S>
1406
-
where
1407
-
S: subfs_state::State,
1408
-
S::Subject: subfs_state::IsUnset,
1409
-
{
1410
-
/// Set the `subject` field (required)
1411
-
pub fn subject(
1412
-
mut self,
1413
-
value: impl Into<jacquard_common::types::string::AtUri<'a>>,
1414
-
) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> {
1415
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
1416
-
SubfsBuilder {
1417
-
_phantom_state: ::core::marker::PhantomData,
1418
-
__unsafe_private_named: self.__unsafe_private_named,
1419
-
_phantom: ::core::marker::PhantomData,
1420
-
}
1421
-
}
1422
-
}
1423
-
1424
-
impl<'a, S> SubfsBuilder<'a, S>
1425
-
where
1426
-
S: subfs_state::State,
1427
-
S::Type: subfs_state::IsUnset,
1428
-
{
1429
-
/// Set the `type` field (required)
1430
-
pub fn r#type(
1431
-
mut self,
1432
-
value: impl Into<jacquard_common::CowStr<'a>>,
1433
-
) -> SubfsBuilder<'a, subfs_state::SetType<S>> {
1434
-
self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
1435
-
SubfsBuilder {
1436
-
_phantom_state: ::core::marker::PhantomData,
1437
-
__unsafe_private_named: self.__unsafe_private_named,
1438
-
_phantom: ::core::marker::PhantomData,
1439
-
}
1440
-
}
1441
-
}
1442
-
1443
-
impl<'a, S> SubfsBuilder<'a, S>
1444
-
where
1445
-
S: subfs_state::State,
1446
-
S::Type: subfs_state::IsSet,
1447
-
S::Subject: subfs_state::IsSet,
1448
-
{
1449
-
/// Build the final struct
1450
-
pub fn build(self) -> Subfs<'a> {
1451
-
Subfs {
1452
-
flat: self.__unsafe_private_named.0,
1453
-
subject: self.__unsafe_private_named.1.unwrap(),
1454
-
r#type: self.__unsafe_private_named.2.unwrap(),
1455
-
extra_data: Default::default(),
1456
-
}
1457
-
}
1458
-
/// Build the final struct with custom extra_data
1459
-
pub fn build_with_data(
1460
-
self,
1461
-
extra_data: std::collections::BTreeMap<
1462
-
jacquard_common::smol_str::SmolStr,
1463
-
jacquard_common::types::value::Data<'a>,
1464
-
>,
1465
-
) -> Subfs<'a> {
1466
-
Subfs {
1467
-
flat: self.__unsafe_private_named.0,
1468
-
subject: self.__unsafe_private_named.1.unwrap(),
1469
-
r#type: self.__unsafe_private_named.2.unwrap(),
1470
-
extra_data: Some(extra_data),
1471
-
}
1472
-
}
1473
-
}
1474
-
1475
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> {
1476
-
fn nsid() -> &'static str {
1477
-
"place.wisp.fs"
1478
-
}
1479
-
fn def_name() -> &'static str {
1480
-
"subfs"
1481
-
}
1482
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1483
-
lexicon_doc_place_wisp_fs()
1484
-
}
1485
-
fn validate(
1486
-
&self,
1487
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1488
-
Ok(())
1489
-
}
1490
-
}
···
-653
cli/src/place_wisp/settings.rs
-653
cli/src/place_wisp/settings.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: place.wisp.settings
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
/// Custom HTTP header configuration
9
-
#[jacquard_derive::lexicon]
10
-
#[derive(
11
-
serde::Serialize,
12
-
serde::Deserialize,
13
-
Debug,
14
-
Clone,
15
-
PartialEq,
16
-
Eq,
17
-
jacquard_derive::IntoStatic,
18
-
Default
19
-
)]
20
-
#[serde(rename_all = "camelCase")]
21
-
pub struct CustomHeader<'a> {
22
-
/// HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')
23
-
#[serde(borrow)]
24
-
pub name: jacquard_common::CowStr<'a>,
25
-
/// Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.
26
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
27
-
#[serde(borrow)]
28
-
pub path: std::option::Option<jacquard_common::CowStr<'a>>,
29
-
/// HTTP header value
30
-
#[serde(borrow)]
31
-
pub value: jacquard_common::CowStr<'a>,
32
-
}
33
-
34
-
fn lexicon_doc_place_wisp_settings() -> ::jacquard_lexicon::lexicon::LexiconDoc<
35
-
'static,
36
-
> {
37
-
::jacquard_lexicon::lexicon::LexiconDoc {
38
-
lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
39
-
id: ::jacquard_common::CowStr::new_static("place.wisp.settings"),
40
-
revision: None,
41
-
description: None,
42
-
defs: {
43
-
let mut map = ::std::collections::BTreeMap::new();
44
-
map.insert(
45
-
::jacquard_common::smol_str::SmolStr::new_static("customHeader"),
46
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
47
-
description: Some(
48
-
::jacquard_common::CowStr::new_static(
49
-
"Custom HTTP header configuration",
50
-
),
51
-
),
52
-
required: Some(
53
-
vec![
54
-
::jacquard_common::smol_str::SmolStr::new_static("name"),
55
-
::jacquard_common::smol_str::SmolStr::new_static("value")
56
-
],
57
-
),
58
-
nullable: None,
59
-
properties: {
60
-
#[allow(unused_mut)]
61
-
let mut map = ::std::collections::BTreeMap::new();
62
-
map.insert(
63
-
::jacquard_common::smol_str::SmolStr::new_static("name"),
64
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
65
-
description: Some(
66
-
::jacquard_common::CowStr::new_static(
67
-
"HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')",
68
-
),
69
-
),
70
-
format: None,
71
-
default: None,
72
-
min_length: None,
73
-
max_length: Some(100usize),
74
-
min_graphemes: None,
75
-
max_graphemes: None,
76
-
r#enum: None,
77
-
r#const: None,
78
-
known_values: None,
79
-
}),
80
-
);
81
-
map.insert(
82
-
::jacquard_common::smol_str::SmolStr::new_static("path"),
83
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
84
-
description: Some(
85
-
::jacquard_common::CowStr::new_static(
86
-
"Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.",
87
-
),
88
-
),
89
-
format: None,
90
-
default: None,
91
-
min_length: None,
92
-
max_length: Some(500usize),
93
-
min_graphemes: None,
94
-
max_graphemes: None,
95
-
r#enum: None,
96
-
r#const: None,
97
-
known_values: None,
98
-
}),
99
-
);
100
-
map.insert(
101
-
::jacquard_common::smol_str::SmolStr::new_static("value"),
102
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
103
-
description: Some(
104
-
::jacquard_common::CowStr::new_static("HTTP header value"),
105
-
),
106
-
format: None,
107
-
default: None,
108
-
min_length: None,
109
-
max_length: Some(1000usize),
110
-
min_graphemes: None,
111
-
max_graphemes: None,
112
-
r#enum: None,
113
-
r#const: None,
114
-
known_values: None,
115
-
}),
116
-
);
117
-
map
118
-
},
119
-
}),
120
-
);
121
-
map.insert(
122
-
::jacquard_common::smol_str::SmolStr::new_static("main"),
123
-
::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
124
-
description: Some(
125
-
::jacquard_common::CowStr::new_static(
126
-
"Configuration settings for a static site hosted on wisp.place",
127
-
),
128
-
),
129
-
key: Some(::jacquard_common::CowStr::new_static("any")),
130
-
record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
131
-
description: None,
132
-
required: None,
133
-
nullable: None,
134
-
properties: {
135
-
#[allow(unused_mut)]
136
-
let mut map = ::std::collections::BTreeMap::new();
137
-
map.insert(
138
-
::jacquard_common::smol_str::SmolStr::new_static(
139
-
"cleanUrls",
140
-
),
141
-
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
142
-
description: None,
143
-
default: None,
144
-
r#const: None,
145
-
}),
146
-
);
147
-
map.insert(
148
-
::jacquard_common::smol_str::SmolStr::new_static(
149
-
"custom404",
150
-
),
151
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
152
-
description: Some(
153
-
::jacquard_common::CowStr::new_static(
154
-
"Custom 404 error page file path. Incompatible with directoryListing and spaMode.",
155
-
),
156
-
),
157
-
format: None,
158
-
default: None,
159
-
min_length: None,
160
-
max_length: Some(500usize),
161
-
min_graphemes: None,
162
-
max_graphemes: None,
163
-
r#enum: None,
164
-
r#const: None,
165
-
known_values: None,
166
-
}),
167
-
);
168
-
map.insert(
169
-
::jacquard_common::smol_str::SmolStr::new_static(
170
-
"directoryListing",
171
-
),
172
-
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
173
-
description: None,
174
-
default: None,
175
-
r#const: None,
176
-
}),
177
-
);
178
-
map.insert(
179
-
::jacquard_common::smol_str::SmolStr::new_static("headers"),
180
-
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
181
-
description: Some(
182
-
::jacquard_common::CowStr::new_static(
183
-
"Custom HTTP headers to set on responses",
184
-
),
185
-
),
186
-
items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef {
187
-
description: None,
188
-
r#ref: ::jacquard_common::CowStr::new_static(
189
-
"#customHeader",
190
-
),
191
-
}),
192
-
min_length: None,
193
-
max_length: Some(50usize),
194
-
}),
195
-
);
196
-
map.insert(
197
-
::jacquard_common::smol_str::SmolStr::new_static(
198
-
"indexFiles",
199
-
),
200
-
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
201
-
description: Some(
202
-
::jacquard_common::CowStr::new_static(
203
-
"Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.",
204
-
),
205
-
),
206
-
items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString {
207
-
description: None,
208
-
format: None,
209
-
default: None,
210
-
min_length: None,
211
-
max_length: Some(255usize),
212
-
min_graphemes: None,
213
-
max_graphemes: None,
214
-
r#enum: None,
215
-
r#const: None,
216
-
known_values: None,
217
-
}),
218
-
min_length: None,
219
-
max_length: Some(10usize),
220
-
}),
221
-
);
222
-
map.insert(
223
-
::jacquard_common::smol_str::SmolStr::new_static("spaMode"),
224
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
225
-
description: Some(
226
-
::jacquard_common::CowStr::new_static(
227
-
"File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.",
228
-
),
229
-
),
230
-
format: None,
231
-
default: None,
232
-
min_length: None,
233
-
max_length: Some(500usize),
234
-
min_graphemes: None,
235
-
max_graphemes: None,
236
-
r#enum: None,
237
-
r#const: None,
238
-
known_values: None,
239
-
}),
240
-
);
241
-
map
242
-
},
243
-
}),
244
-
}),
245
-
);
246
-
map
247
-
},
248
-
}
249
-
}
250
-
251
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for CustomHeader<'a> {
252
-
fn nsid() -> &'static str {
253
-
"place.wisp.settings"
254
-
}
255
-
fn def_name() -> &'static str {
256
-
"customHeader"
257
-
}
258
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
259
-
lexicon_doc_place_wisp_settings()
260
-
}
261
-
fn validate(
262
-
&self,
263
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
264
-
{
265
-
let value = &self.name;
266
-
#[allow(unused_comparisons)]
267
-
if <str>::len(value.as_ref()) > 100usize {
268
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
269
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
270
-
"name",
271
-
),
272
-
max: 100usize,
273
-
actual: <str>::len(value.as_ref()),
274
-
});
275
-
}
276
-
}
277
-
if let Some(ref value) = self.path {
278
-
#[allow(unused_comparisons)]
279
-
if <str>::len(value.as_ref()) > 500usize {
280
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
281
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
282
-
"path",
283
-
),
284
-
max: 500usize,
285
-
actual: <str>::len(value.as_ref()),
286
-
});
287
-
}
288
-
}
289
-
{
290
-
let value = &self.value;
291
-
#[allow(unused_comparisons)]
292
-
if <str>::len(value.as_ref()) > 1000usize {
293
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
294
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
295
-
"value",
296
-
),
297
-
max: 1000usize,
298
-
actual: <str>::len(value.as_ref()),
299
-
});
300
-
}
301
-
}
302
-
Ok(())
303
-
}
304
-
}
305
-
306
-
/// Configuration settings for a static site hosted on wisp.place
307
-
#[jacquard_derive::lexicon]
308
-
#[derive(
309
-
serde::Serialize,
310
-
serde::Deserialize,
311
-
Debug,
312
-
Clone,
313
-
PartialEq,
314
-
Eq,
315
-
jacquard_derive::IntoStatic
316
-
)]
317
-
#[serde(rename_all = "camelCase")]
318
-
pub struct Settings<'a> {
319
-
/// Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically.
320
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
321
-
pub clean_urls: std::option::Option<bool>,
322
-
/// Custom 404 error page file path. Incompatible with directoryListing and spaMode.
323
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
324
-
#[serde(borrow)]
325
-
pub custom404: std::option::Option<jacquard_common::CowStr<'a>>,
326
-
/// Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode.
327
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
328
-
pub directory_listing: std::option::Option<bool>,
329
-
/// Custom HTTP headers to set on responses
330
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
331
-
#[serde(borrow)]
332
-
pub headers: std::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>,
333
-
/// Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.
334
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
335
-
#[serde(borrow)]
336
-
pub index_files: std::option::Option<Vec<jacquard_common::CowStr<'a>>>,
337
-
/// File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.
338
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
339
-
#[serde(borrow)]
340
-
pub spa_mode: std::option::Option<jacquard_common::CowStr<'a>>,
341
-
}
342
-
343
-
pub mod settings_state {
344
-
345
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
346
-
#[allow(unused)]
347
-
use ::core::marker::PhantomData;
348
-
mod sealed {
349
-
pub trait Sealed {}
350
-
}
351
-
/// State trait tracking which required fields have been set
352
-
pub trait State: sealed::Sealed {}
353
-
/// Empty state - all required fields are unset
354
-
pub struct Empty(());
355
-
impl sealed::Sealed for Empty {}
356
-
impl State for Empty {}
357
-
/// Marker types for field names
358
-
#[allow(non_camel_case_types)]
359
-
pub mod members {}
360
-
}
361
-
362
-
/// Builder for constructing an instance of this type
363
-
pub struct SettingsBuilder<'a, S: settings_state::State> {
364
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
365
-
__unsafe_private_named: (
366
-
::core::option::Option<bool>,
367
-
::core::option::Option<jacquard_common::CowStr<'a>>,
368
-
::core::option::Option<bool>,
369
-
::core::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>,
370
-
::core::option::Option<Vec<jacquard_common::CowStr<'a>>>,
371
-
::core::option::Option<jacquard_common::CowStr<'a>>,
372
-
),
373
-
_phantom: ::core::marker::PhantomData<&'a ()>,
374
-
}
375
-
376
-
impl<'a> Settings<'a> {
377
-
/// Create a new builder for this type
378
-
pub fn new() -> SettingsBuilder<'a, settings_state::Empty> {
379
-
SettingsBuilder::new()
380
-
}
381
-
}
382
-
383
-
impl<'a> SettingsBuilder<'a, settings_state::Empty> {
384
-
/// Create a new builder with all fields unset
385
-
pub fn new() -> Self {
386
-
SettingsBuilder {
387
-
_phantom_state: ::core::marker::PhantomData,
388
-
__unsafe_private_named: (None, None, None, None, None, None),
389
-
_phantom: ::core::marker::PhantomData,
390
-
}
391
-
}
392
-
}
393
-
394
-
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
395
-
/// Set the `cleanUrls` field (optional)
396
-
pub fn clean_urls(mut self, value: impl Into<Option<bool>>) -> Self {
397
-
self.__unsafe_private_named.0 = value.into();
398
-
self
399
-
}
400
-
/// Set the `cleanUrls` field to an Option value (optional)
401
-
pub fn maybe_clean_urls(mut self, value: Option<bool>) -> Self {
402
-
self.__unsafe_private_named.0 = value;
403
-
self
404
-
}
405
-
}
406
-
407
-
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
408
-
/// Set the `custom404` field (optional)
409
-
pub fn custom404(
410
-
mut self,
411
-
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
412
-
) -> Self {
413
-
self.__unsafe_private_named.1 = value.into();
414
-
self
415
-
}
416
-
/// Set the `custom404` field to an Option value (optional)
417
-
pub fn maybe_custom404(
418
-
mut self,
419
-
value: Option<jacquard_common::CowStr<'a>>,
420
-
) -> Self {
421
-
self.__unsafe_private_named.1 = value;
422
-
self
423
-
}
424
-
}
425
-
426
-
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
427
-
/// Set the `directoryListing` field (optional)
428
-
pub fn directory_listing(mut self, value: impl Into<Option<bool>>) -> Self {
429
-
self.__unsafe_private_named.2 = value.into();
430
-
self
431
-
}
432
-
/// Set the `directoryListing` field to an Option value (optional)
433
-
pub fn maybe_directory_listing(mut self, value: Option<bool>) -> Self {
434
-
self.__unsafe_private_named.2 = value;
435
-
self
436
-
}
437
-
}
438
-
439
-
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
440
-
/// Set the `headers` field (optional)
441
-
pub fn headers(
442
-
mut self,
443
-
value: impl Into<Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>>,
444
-
) -> Self {
445
-
self.__unsafe_private_named.3 = value.into();
446
-
self
447
-
}
448
-
/// Set the `headers` field to an Option value (optional)
449
-
pub fn maybe_headers(
450
-
mut self,
451
-
value: Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>,
452
-
) -> Self {
453
-
self.__unsafe_private_named.3 = value;
454
-
self
455
-
}
456
-
}
457
-
458
-
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
459
-
/// Set the `indexFiles` field (optional)
460
-
pub fn index_files(
461
-
mut self,
462
-
value: impl Into<Option<Vec<jacquard_common::CowStr<'a>>>>,
463
-
) -> Self {
464
-
self.__unsafe_private_named.4 = value.into();
465
-
self
466
-
}
467
-
/// Set the `indexFiles` field to an Option value (optional)
468
-
pub fn maybe_index_files(
469
-
mut self,
470
-
value: Option<Vec<jacquard_common::CowStr<'a>>>,
471
-
) -> Self {
472
-
self.__unsafe_private_named.4 = value;
473
-
self
474
-
}
475
-
}
476
-
477
-
impl<'a, S: settings_state::State> SettingsBuilder<'a, S> {
478
-
/// Set the `spaMode` field (optional)
479
-
pub fn spa_mode(
480
-
mut self,
481
-
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
482
-
) -> Self {
483
-
self.__unsafe_private_named.5 = value.into();
484
-
self
485
-
}
486
-
/// Set the `spaMode` field to an Option value (optional)
487
-
pub fn maybe_spa_mode(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
488
-
self.__unsafe_private_named.5 = value;
489
-
self
490
-
}
491
-
}
492
-
493
-
impl<'a, S> SettingsBuilder<'a, S>
494
-
where
495
-
S: settings_state::State,
496
-
{
497
-
/// Build the final struct
498
-
pub fn build(self) -> Settings<'a> {
499
-
Settings {
500
-
clean_urls: self.__unsafe_private_named.0,
501
-
custom404: self.__unsafe_private_named.1,
502
-
directory_listing: self.__unsafe_private_named.2,
503
-
headers: self.__unsafe_private_named.3,
504
-
index_files: self.__unsafe_private_named.4,
505
-
spa_mode: self.__unsafe_private_named.5,
506
-
extra_data: Default::default(),
507
-
}
508
-
}
509
-
/// Build the final struct with custom extra_data
510
-
pub fn build_with_data(
511
-
self,
512
-
extra_data: std::collections::BTreeMap<
513
-
jacquard_common::smol_str::SmolStr,
514
-
jacquard_common::types::value::Data<'a>,
515
-
>,
516
-
) -> Settings<'a> {
517
-
Settings {
518
-
clean_urls: self.__unsafe_private_named.0,
519
-
custom404: self.__unsafe_private_named.1,
520
-
directory_listing: self.__unsafe_private_named.2,
521
-
headers: self.__unsafe_private_named.3,
522
-
index_files: self.__unsafe_private_named.4,
523
-
spa_mode: self.__unsafe_private_named.5,
524
-
extra_data: Some(extra_data),
525
-
}
526
-
}
527
-
}
528
-
529
-
impl<'a> Settings<'a> {
530
-
pub fn uri(
531
-
uri: impl Into<jacquard_common::CowStr<'a>>,
532
-
) -> Result<
533
-
jacquard_common::types::uri::RecordUri<'a, SettingsRecord>,
534
-
jacquard_common::types::uri::UriError,
535
-
> {
536
-
jacquard_common::types::uri::RecordUri::try_from_uri(
537
-
jacquard_common::types::string::AtUri::new_cow(uri.into())?,
538
-
)
539
-
}
540
-
}
541
-
542
-
/// Typed wrapper for GetRecord response with this collection's record type.
543
-
#[derive(
544
-
serde::Serialize,
545
-
serde::Deserialize,
546
-
Debug,
547
-
Clone,
548
-
PartialEq,
549
-
Eq,
550
-
jacquard_derive::IntoStatic
551
-
)]
552
-
#[serde(rename_all = "camelCase")]
553
-
pub struct SettingsGetRecordOutput<'a> {
554
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
555
-
#[serde(borrow)]
556
-
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
557
-
#[serde(borrow)]
558
-
pub uri: jacquard_common::types::string::AtUri<'a>,
559
-
#[serde(borrow)]
560
-
pub value: Settings<'a>,
561
-
}
562
-
563
-
impl From<SettingsGetRecordOutput<'_>> for Settings<'_> {
564
-
fn from(output: SettingsGetRecordOutput<'_>) -> Self {
565
-
use jacquard_common::IntoStatic;
566
-
output.value.into_static()
567
-
}
568
-
}
569
-
570
-
impl jacquard_common::types::collection::Collection for Settings<'_> {
571
-
const NSID: &'static str = "place.wisp.settings";
572
-
type Record = SettingsRecord;
573
-
}
574
-
575
-
/// Marker type for deserializing records from this collection.
576
-
#[derive(Debug, serde::Serialize, serde::Deserialize)]
577
-
pub struct SettingsRecord;
578
-
impl jacquard_common::xrpc::XrpcResp for SettingsRecord {
579
-
const NSID: &'static str = "place.wisp.settings";
580
-
const ENCODING: &'static str = "application/json";
581
-
type Output<'de> = SettingsGetRecordOutput<'de>;
582
-
type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
583
-
}
584
-
585
-
impl jacquard_common::types::collection::Collection for SettingsRecord {
586
-
const NSID: &'static str = "place.wisp.settings";
587
-
type Record = SettingsRecord;
588
-
}
589
-
590
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Settings<'a> {
591
-
fn nsid() -> &'static str {
592
-
"place.wisp.settings"
593
-
}
594
-
fn def_name() -> &'static str {
595
-
"main"
596
-
}
597
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
598
-
lexicon_doc_place_wisp_settings()
599
-
}
600
-
fn validate(
601
-
&self,
602
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
603
-
if let Some(ref value) = self.custom404 {
604
-
#[allow(unused_comparisons)]
605
-
if <str>::len(value.as_ref()) > 500usize {
606
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
607
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
608
-
"custom404",
609
-
),
610
-
max: 500usize,
611
-
actual: <str>::len(value.as_ref()),
612
-
});
613
-
}
614
-
}
615
-
if let Some(ref value) = self.headers {
616
-
#[allow(unused_comparisons)]
617
-
if value.len() > 50usize {
618
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
619
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
620
-
"headers",
621
-
),
622
-
max: 50usize,
623
-
actual: value.len(),
624
-
});
625
-
}
626
-
}
627
-
if let Some(ref value) = self.index_files {
628
-
#[allow(unused_comparisons)]
629
-
if value.len() > 10usize {
630
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
631
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
632
-
"index_files",
633
-
),
634
-
max: 10usize,
635
-
actual: value.len(),
636
-
});
637
-
}
638
-
}
639
-
if let Some(ref value) = self.spa_mode {
640
-
#[allow(unused_comparisons)]
641
-
if <str>::len(value.as_ref()) > 500usize {
642
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
643
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
644
-
"spa_mode",
645
-
),
646
-
max: 500usize,
647
-
actual: <str>::len(value.as_ref()),
648
-
});
649
-
}
650
-
}
651
-
Ok(())
652
-
}
653
-
}
···
-1408
cli/src/place_wisp/subfs.rs
-1408
cli/src/place_wisp/subfs.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: place.wisp.subfs
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(
10
-
serde::Serialize,
11
-
serde::Deserialize,
12
-
Debug,
13
-
Clone,
14
-
PartialEq,
15
-
Eq,
16
-
jacquard_derive::IntoStatic
17
-
)]
18
-
#[serde(rename_all = "camelCase")]
19
-
pub struct Directory<'a> {
20
-
#[serde(borrow)]
21
-
pub entries: Vec<crate::place_wisp::subfs::Entry<'a>>,
22
-
#[serde(borrow)]
23
-
pub r#type: jacquard_common::CowStr<'a>,
24
-
}
25
-
26
-
pub mod directory_state {
27
-
28
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
29
-
#[allow(unused)]
30
-
use ::core::marker::PhantomData;
31
-
mod sealed {
32
-
pub trait Sealed {}
33
-
}
34
-
/// State trait tracking which required fields have been set
35
-
pub trait State: sealed::Sealed {
36
-
type Type;
37
-
type Entries;
38
-
}
39
-
/// Empty state - all required fields are unset
40
-
pub struct Empty(());
41
-
impl sealed::Sealed for Empty {}
42
-
impl State for Empty {
43
-
type Type = Unset;
44
-
type Entries = Unset;
45
-
}
46
-
///State transition - sets the `type` field to Set
47
-
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
48
-
impl<S: State> sealed::Sealed for SetType<S> {}
49
-
impl<S: State> State for SetType<S> {
50
-
type Type = Set<members::r#type>;
51
-
type Entries = S::Entries;
52
-
}
53
-
///State transition - sets the `entries` field to Set
54
-
pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>);
55
-
impl<S: State> sealed::Sealed for SetEntries<S> {}
56
-
impl<S: State> State for SetEntries<S> {
57
-
type Type = S::Type;
58
-
type Entries = Set<members::entries>;
59
-
}
60
-
/// Marker types for field names
61
-
#[allow(non_camel_case_types)]
62
-
pub mod members {
63
-
///Marker type for the `type` field
64
-
pub struct r#type(());
65
-
///Marker type for the `entries` field
66
-
pub struct entries(());
67
-
}
68
-
}
69
-
70
-
/// Builder for constructing an instance of this type
71
-
pub struct DirectoryBuilder<'a, S: directory_state::State> {
72
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
73
-
__unsafe_private_named: (
74
-
::core::option::Option<Vec<crate::place_wisp::subfs::Entry<'a>>>,
75
-
::core::option::Option<jacquard_common::CowStr<'a>>,
76
-
),
77
-
_phantom: ::core::marker::PhantomData<&'a ()>,
78
-
}
79
-
80
-
impl<'a> Directory<'a> {
81
-
/// Create a new builder for this type
82
-
pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> {
83
-
DirectoryBuilder::new()
84
-
}
85
-
}
86
-
87
-
impl<'a> DirectoryBuilder<'a, directory_state::Empty> {
88
-
/// Create a new builder with all fields unset
89
-
pub fn new() -> Self {
90
-
DirectoryBuilder {
91
-
_phantom_state: ::core::marker::PhantomData,
92
-
__unsafe_private_named: (None, None),
93
-
_phantom: ::core::marker::PhantomData,
94
-
}
95
-
}
96
-
}
97
-
98
-
impl<'a, S> DirectoryBuilder<'a, S>
99
-
where
100
-
S: directory_state::State,
101
-
S::Entries: directory_state::IsUnset,
102
-
{
103
-
/// Set the `entries` field (required)
104
-
pub fn entries(
105
-
mut self,
106
-
value: impl Into<Vec<crate::place_wisp::subfs::Entry<'a>>>,
107
-
) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> {
108
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
109
-
DirectoryBuilder {
110
-
_phantom_state: ::core::marker::PhantomData,
111
-
__unsafe_private_named: self.__unsafe_private_named,
112
-
_phantom: ::core::marker::PhantomData,
113
-
}
114
-
}
115
-
}
116
-
117
-
impl<'a, S> DirectoryBuilder<'a, S>
118
-
where
119
-
S: directory_state::State,
120
-
S::Type: directory_state::IsUnset,
121
-
{
122
-
/// Set the `type` field (required)
123
-
pub fn r#type(
124
-
mut self,
125
-
value: impl Into<jacquard_common::CowStr<'a>>,
126
-
) -> DirectoryBuilder<'a, directory_state::SetType<S>> {
127
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
128
-
DirectoryBuilder {
129
-
_phantom_state: ::core::marker::PhantomData,
130
-
__unsafe_private_named: self.__unsafe_private_named,
131
-
_phantom: ::core::marker::PhantomData,
132
-
}
133
-
}
134
-
}
135
-
136
-
impl<'a, S> DirectoryBuilder<'a, S>
137
-
where
138
-
S: directory_state::State,
139
-
S::Type: directory_state::IsSet,
140
-
S::Entries: directory_state::IsSet,
141
-
{
142
-
/// Build the final struct
143
-
pub fn build(self) -> Directory<'a> {
144
-
Directory {
145
-
entries: self.__unsafe_private_named.0.unwrap(),
146
-
r#type: self.__unsafe_private_named.1.unwrap(),
147
-
extra_data: Default::default(),
148
-
}
149
-
}
150
-
/// Build the final struct with custom extra_data
151
-
pub fn build_with_data(
152
-
self,
153
-
extra_data: std::collections::BTreeMap<
154
-
jacquard_common::smol_str::SmolStr,
155
-
jacquard_common::types::value::Data<'a>,
156
-
>,
157
-
) -> Directory<'a> {
158
-
Directory {
159
-
entries: self.__unsafe_private_named.0.unwrap(),
160
-
r#type: self.__unsafe_private_named.1.unwrap(),
161
-
extra_data: Some(extra_data),
162
-
}
163
-
}
164
-
}
165
-
166
-
fn lexicon_doc_place_wisp_subfs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
167
-
::jacquard_lexicon::lexicon::LexiconDoc {
168
-
lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
169
-
id: ::jacquard_common::CowStr::new_static("place.wisp.subfs"),
170
-
revision: None,
171
-
description: None,
172
-
defs: {
173
-
let mut map = ::std::collections::BTreeMap::new();
174
-
map.insert(
175
-
::jacquard_common::smol_str::SmolStr::new_static("directory"),
176
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
177
-
description: None,
178
-
required: Some(
179
-
vec![
180
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
181
-
::jacquard_common::smol_str::SmolStr::new_static("entries")
182
-
],
183
-
),
184
-
nullable: None,
185
-
properties: {
186
-
#[allow(unused_mut)]
187
-
let mut map = ::std::collections::BTreeMap::new();
188
-
map.insert(
189
-
::jacquard_common::smol_str::SmolStr::new_static("entries"),
190
-
::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
191
-
description: None,
192
-
items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef {
193
-
description: None,
194
-
r#ref: ::jacquard_common::CowStr::new_static("#entry"),
195
-
}),
196
-
min_length: None,
197
-
max_length: Some(500usize),
198
-
}),
199
-
);
200
-
map.insert(
201
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
202
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
203
-
description: None,
204
-
format: None,
205
-
default: None,
206
-
min_length: None,
207
-
max_length: None,
208
-
min_graphemes: None,
209
-
max_graphemes: None,
210
-
r#enum: None,
211
-
r#const: None,
212
-
known_values: None,
213
-
}),
214
-
);
215
-
map
216
-
},
217
-
}),
218
-
);
219
-
map.insert(
220
-
::jacquard_common::smol_str::SmolStr::new_static("entry"),
221
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
222
-
description: None,
223
-
required: Some(
224
-
vec![
225
-
::jacquard_common::smol_str::SmolStr::new_static("name"),
226
-
::jacquard_common::smol_str::SmolStr::new_static("node")
227
-
],
228
-
),
229
-
nullable: None,
230
-
properties: {
231
-
#[allow(unused_mut)]
232
-
let mut map = ::std::collections::BTreeMap::new();
233
-
map.insert(
234
-
::jacquard_common::smol_str::SmolStr::new_static("name"),
235
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
236
-
description: None,
237
-
format: None,
238
-
default: None,
239
-
min_length: None,
240
-
max_length: Some(255usize),
241
-
min_graphemes: None,
242
-
max_graphemes: None,
243
-
r#enum: None,
244
-
r#const: None,
245
-
known_values: None,
246
-
}),
247
-
);
248
-
map.insert(
249
-
::jacquard_common::smol_str::SmolStr::new_static("node"),
250
-
::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion {
251
-
description: None,
252
-
refs: vec![
253
-
::jacquard_common::CowStr::new_static("#file"),
254
-
::jacquard_common::CowStr::new_static("#directory"),
255
-
::jacquard_common::CowStr::new_static("#subfs")
256
-
],
257
-
closed: None,
258
-
}),
259
-
);
260
-
map
261
-
},
262
-
}),
263
-
);
264
-
map.insert(
265
-
::jacquard_common::smol_str::SmolStr::new_static("file"),
266
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
267
-
description: None,
268
-
required: Some(
269
-
vec![
270
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
271
-
::jacquard_common::smol_str::SmolStr::new_static("blob")
272
-
],
273
-
),
274
-
nullable: None,
275
-
properties: {
276
-
#[allow(unused_mut)]
277
-
let mut map = ::std::collections::BTreeMap::new();
278
-
map.insert(
279
-
::jacquard_common::smol_str::SmolStr::new_static("base64"),
280
-
::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean {
281
-
description: None,
282
-
default: None,
283
-
r#const: None,
284
-
}),
285
-
);
286
-
map.insert(
287
-
::jacquard_common::smol_str::SmolStr::new_static("blob"),
288
-
::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob {
289
-
description: None,
290
-
accept: None,
291
-
max_size: None,
292
-
}),
293
-
);
294
-
map.insert(
295
-
::jacquard_common::smol_str::SmolStr::new_static("encoding"),
296
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
297
-
description: Some(
298
-
::jacquard_common::CowStr::new_static(
299
-
"Content encoding (e.g., gzip for compressed files)",
300
-
),
301
-
),
302
-
format: None,
303
-
default: None,
304
-
min_length: None,
305
-
max_length: None,
306
-
min_graphemes: None,
307
-
max_graphemes: None,
308
-
r#enum: None,
309
-
r#const: None,
310
-
known_values: None,
311
-
}),
312
-
);
313
-
map.insert(
314
-
::jacquard_common::smol_str::SmolStr::new_static("mimeType"),
315
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
316
-
description: Some(
317
-
::jacquard_common::CowStr::new_static(
318
-
"Original MIME type before compression",
319
-
),
320
-
),
321
-
format: None,
322
-
default: None,
323
-
min_length: None,
324
-
max_length: None,
325
-
min_graphemes: None,
326
-
max_graphemes: None,
327
-
r#enum: None,
328
-
r#const: None,
329
-
known_values: None,
330
-
}),
331
-
);
332
-
map.insert(
333
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
334
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
335
-
description: None,
336
-
format: None,
337
-
default: None,
338
-
min_length: None,
339
-
max_length: None,
340
-
min_graphemes: None,
341
-
max_graphemes: None,
342
-
r#enum: None,
343
-
r#const: None,
344
-
known_values: None,
345
-
}),
346
-
);
347
-
map
348
-
},
349
-
}),
350
-
);
351
-
map.insert(
352
-
::jacquard_common::smol_str::SmolStr::new_static("main"),
353
-
::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
354
-
description: Some(
355
-
::jacquard_common::CowStr::new_static(
356
-
"Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.",
357
-
),
358
-
),
359
-
key: None,
360
-
record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
361
-
description: None,
362
-
required: Some(
363
-
vec![
364
-
::jacquard_common::smol_str::SmolStr::new_static("root"),
365
-
::jacquard_common::smol_str::SmolStr::new_static("createdAt")
366
-
],
367
-
),
368
-
nullable: None,
369
-
properties: {
370
-
#[allow(unused_mut)]
371
-
let mut map = ::std::collections::BTreeMap::new();
372
-
map.insert(
373
-
::jacquard_common::smol_str::SmolStr::new_static(
374
-
"createdAt",
375
-
),
376
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
377
-
description: None,
378
-
format: Some(
379
-
::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
380
-
),
381
-
default: None,
382
-
min_length: None,
383
-
max_length: None,
384
-
min_graphemes: None,
385
-
max_graphemes: None,
386
-
r#enum: None,
387
-
r#const: None,
388
-
known_values: None,
389
-
}),
390
-
);
391
-
map.insert(
392
-
::jacquard_common::smol_str::SmolStr::new_static(
393
-
"fileCount",
394
-
),
395
-
::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger {
396
-
description: None,
397
-
default: None,
398
-
minimum: Some(0i64),
399
-
maximum: Some(1000i64),
400
-
r#enum: None,
401
-
r#const: None,
402
-
}),
403
-
);
404
-
map.insert(
405
-
::jacquard_common::smol_str::SmolStr::new_static("root"),
406
-
::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
407
-
description: None,
408
-
r#ref: ::jacquard_common::CowStr::new_static("#directory"),
409
-
}),
410
-
);
411
-
map
412
-
},
413
-
}),
414
-
}),
415
-
);
416
-
map.insert(
417
-
::jacquard_common::smol_str::SmolStr::new_static("subfs"),
418
-
::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
419
-
description: None,
420
-
required: Some(
421
-
vec![
422
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
423
-
::jacquard_common::smol_str::SmolStr::new_static("subject")
424
-
],
425
-
),
426
-
nullable: None,
427
-
properties: {
428
-
#[allow(unused_mut)]
429
-
let mut map = ::std::collections::BTreeMap::new();
430
-
map.insert(
431
-
::jacquard_common::smol_str::SmolStr::new_static("subject"),
432
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
433
-
description: Some(
434
-
::jacquard_common::CowStr::new_static(
435
-
"AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.",
436
-
),
437
-
),
438
-
format: Some(
439
-
::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
440
-
),
441
-
default: None,
442
-
min_length: None,
443
-
max_length: None,
444
-
min_graphemes: None,
445
-
max_graphemes: None,
446
-
r#enum: None,
447
-
r#const: None,
448
-
known_values: None,
449
-
}),
450
-
);
451
-
map.insert(
452
-
::jacquard_common::smol_str::SmolStr::new_static("type"),
453
-
::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
454
-
description: None,
455
-
format: None,
456
-
default: None,
457
-
min_length: None,
458
-
max_length: None,
459
-
min_graphemes: None,
460
-
max_graphemes: None,
461
-
r#enum: None,
462
-
r#const: None,
463
-
known_values: None,
464
-
}),
465
-
);
466
-
map
467
-
},
468
-
}),
469
-
);
470
-
map
471
-
},
472
-
}
473
-
}
474
-
475
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> {
476
-
fn nsid() -> &'static str {
477
-
"place.wisp.subfs"
478
-
}
479
-
fn def_name() -> &'static str {
480
-
"directory"
481
-
}
482
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
483
-
lexicon_doc_place_wisp_subfs()
484
-
}
485
-
fn validate(
486
-
&self,
487
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
488
-
{
489
-
let value = &self.entries;
490
-
#[allow(unused_comparisons)]
491
-
if value.len() > 500usize {
492
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
493
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
494
-
"entries",
495
-
),
496
-
max: 500usize,
497
-
actual: value.len(),
498
-
});
499
-
}
500
-
}
501
-
Ok(())
502
-
}
503
-
}
504
-
505
-
#[jacquard_derive::lexicon]
506
-
#[derive(
507
-
serde::Serialize,
508
-
serde::Deserialize,
509
-
Debug,
510
-
Clone,
511
-
PartialEq,
512
-
Eq,
513
-
jacquard_derive::IntoStatic
514
-
)]
515
-
#[serde(rename_all = "camelCase")]
516
-
pub struct Entry<'a> {
517
-
#[serde(borrow)]
518
-
pub name: jacquard_common::CowStr<'a>,
519
-
#[serde(borrow)]
520
-
pub node: EntryNode<'a>,
521
-
}
522
-
523
-
pub mod entry_state {
524
-
525
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
526
-
#[allow(unused)]
527
-
use ::core::marker::PhantomData;
528
-
mod sealed {
529
-
pub trait Sealed {}
530
-
}
531
-
/// State trait tracking which required fields have been set
532
-
pub trait State: sealed::Sealed {
533
-
type Name;
534
-
type Node;
535
-
}
536
-
/// Empty state - all required fields are unset
537
-
pub struct Empty(());
538
-
impl sealed::Sealed for Empty {}
539
-
impl State for Empty {
540
-
type Name = Unset;
541
-
type Node = Unset;
542
-
}
543
-
///State transition - sets the `name` field to Set
544
-
pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>);
545
-
impl<S: State> sealed::Sealed for SetName<S> {}
546
-
impl<S: State> State for SetName<S> {
547
-
type Name = Set<members::name>;
548
-
type Node = S::Node;
549
-
}
550
-
///State transition - sets the `node` field to Set
551
-
pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>);
552
-
impl<S: State> sealed::Sealed for SetNode<S> {}
553
-
impl<S: State> State for SetNode<S> {
554
-
type Name = S::Name;
555
-
type Node = Set<members::node>;
556
-
}
557
-
/// Marker types for field names
558
-
#[allow(non_camel_case_types)]
559
-
pub mod members {
560
-
///Marker type for the `name` field
561
-
pub struct name(());
562
-
///Marker type for the `node` field
563
-
pub struct node(());
564
-
}
565
-
}
566
-
567
-
/// Builder for constructing an instance of this type
568
-
pub struct EntryBuilder<'a, S: entry_state::State> {
569
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
570
-
__unsafe_private_named: (
571
-
::core::option::Option<jacquard_common::CowStr<'a>>,
572
-
::core::option::Option<EntryNode<'a>>,
573
-
),
574
-
_phantom: ::core::marker::PhantomData<&'a ()>,
575
-
}
576
-
577
-
impl<'a> Entry<'a> {
578
-
/// Create a new builder for this type
579
-
pub fn new() -> EntryBuilder<'a, entry_state::Empty> {
580
-
EntryBuilder::new()
581
-
}
582
-
}
583
-
584
-
impl<'a> EntryBuilder<'a, entry_state::Empty> {
585
-
/// Create a new builder with all fields unset
586
-
pub fn new() -> Self {
587
-
EntryBuilder {
588
-
_phantom_state: ::core::marker::PhantomData,
589
-
__unsafe_private_named: (None, None),
590
-
_phantom: ::core::marker::PhantomData,
591
-
}
592
-
}
593
-
}
594
-
595
-
impl<'a, S> EntryBuilder<'a, S>
596
-
where
597
-
S: entry_state::State,
598
-
S::Name: entry_state::IsUnset,
599
-
{
600
-
/// Set the `name` field (required)
601
-
pub fn name(
602
-
mut self,
603
-
value: impl Into<jacquard_common::CowStr<'a>>,
604
-
) -> EntryBuilder<'a, entry_state::SetName<S>> {
605
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
606
-
EntryBuilder {
607
-
_phantom_state: ::core::marker::PhantomData,
608
-
__unsafe_private_named: self.__unsafe_private_named,
609
-
_phantom: ::core::marker::PhantomData,
610
-
}
611
-
}
612
-
}
613
-
614
-
impl<'a, S> EntryBuilder<'a, S>
615
-
where
616
-
S: entry_state::State,
617
-
S::Node: entry_state::IsUnset,
618
-
{
619
-
/// Set the `node` field (required)
620
-
pub fn node(
621
-
mut self,
622
-
value: impl Into<EntryNode<'a>>,
623
-
) -> EntryBuilder<'a, entry_state::SetNode<S>> {
624
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
625
-
EntryBuilder {
626
-
_phantom_state: ::core::marker::PhantomData,
627
-
__unsafe_private_named: self.__unsafe_private_named,
628
-
_phantom: ::core::marker::PhantomData,
629
-
}
630
-
}
631
-
}
632
-
633
-
impl<'a, S> EntryBuilder<'a, S>
634
-
where
635
-
S: entry_state::State,
636
-
S::Name: entry_state::IsSet,
637
-
S::Node: entry_state::IsSet,
638
-
{
639
-
/// Build the final struct
640
-
pub fn build(self) -> Entry<'a> {
641
-
Entry {
642
-
name: self.__unsafe_private_named.0.unwrap(),
643
-
node: self.__unsafe_private_named.1.unwrap(),
644
-
extra_data: Default::default(),
645
-
}
646
-
}
647
-
/// Build the final struct with custom extra_data
648
-
pub fn build_with_data(
649
-
self,
650
-
extra_data: std::collections::BTreeMap<
651
-
jacquard_common::smol_str::SmolStr,
652
-
jacquard_common::types::value::Data<'a>,
653
-
>,
654
-
) -> Entry<'a> {
655
-
Entry {
656
-
name: self.__unsafe_private_named.0.unwrap(),
657
-
node: self.__unsafe_private_named.1.unwrap(),
658
-
extra_data: Some(extra_data),
659
-
}
660
-
}
661
-
}
662
-
663
-
#[jacquard_derive::open_union]
664
-
#[derive(
665
-
serde::Serialize,
666
-
serde::Deserialize,
667
-
Debug,
668
-
Clone,
669
-
PartialEq,
670
-
Eq,
671
-
jacquard_derive::IntoStatic
672
-
)]
673
-
#[serde(tag = "$type")]
674
-
#[serde(bound(deserialize = "'de: 'a"))]
675
-
pub enum EntryNode<'a> {
676
-
#[serde(rename = "place.wisp.subfs#file")]
677
-
File(Box<crate::place_wisp::subfs::File<'a>>),
678
-
#[serde(rename = "place.wisp.subfs#directory")]
679
-
Directory(Box<crate::place_wisp::subfs::Directory<'a>>),
680
-
#[serde(rename = "place.wisp.subfs#subfs")]
681
-
Subfs(Box<crate::place_wisp::subfs::Subfs<'a>>),
682
-
}
683
-
684
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> {
685
-
fn nsid() -> &'static str {
686
-
"place.wisp.subfs"
687
-
}
688
-
fn def_name() -> &'static str {
689
-
"entry"
690
-
}
691
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
692
-
lexicon_doc_place_wisp_subfs()
693
-
}
694
-
fn validate(
695
-
&self,
696
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
697
-
{
698
-
let value = &self.name;
699
-
#[allow(unused_comparisons)]
700
-
if <str>::len(value.as_ref()) > 255usize {
701
-
return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
702
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
703
-
"name",
704
-
),
705
-
max: 255usize,
706
-
actual: <str>::len(value.as_ref()),
707
-
});
708
-
}
709
-
}
710
-
Ok(())
711
-
}
712
-
}
713
-
714
-
#[jacquard_derive::lexicon]
715
-
#[derive(
716
-
serde::Serialize,
717
-
serde::Deserialize,
718
-
Debug,
719
-
Clone,
720
-
PartialEq,
721
-
Eq,
722
-
jacquard_derive::IntoStatic
723
-
)]
724
-
#[serde(rename_all = "camelCase")]
725
-
pub struct File<'a> {
726
-
/// True if blob content is base64-encoded (used to bypass PDS content sniffing)
727
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
728
-
pub base64: Option<bool>,
729
-
/// Content blob ref
730
-
#[serde(borrow)]
731
-
pub blob: jacquard_common::types::blob::BlobRef<'a>,
732
-
/// Content encoding (e.g., gzip for compressed files)
733
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
734
-
#[serde(borrow)]
735
-
pub encoding: Option<jacquard_common::CowStr<'a>>,
736
-
/// Original MIME type before compression
737
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
738
-
#[serde(borrow)]
739
-
pub mime_type: Option<jacquard_common::CowStr<'a>>,
740
-
#[serde(borrow)]
741
-
pub r#type: jacquard_common::CowStr<'a>,
742
-
}
743
-
744
-
pub mod file_state {
745
-
746
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
747
-
#[allow(unused)]
748
-
use ::core::marker::PhantomData;
749
-
mod sealed {
750
-
pub trait Sealed {}
751
-
}
752
-
/// State trait tracking which required fields have been set
753
-
pub trait State: sealed::Sealed {
754
-
type Type;
755
-
type Blob;
756
-
}
757
-
/// Empty state - all required fields are unset
758
-
pub struct Empty(());
759
-
impl sealed::Sealed for Empty {}
760
-
impl State for Empty {
761
-
type Type = Unset;
762
-
type Blob = Unset;
763
-
}
764
-
///State transition - sets the `type` field to Set
765
-
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
766
-
impl<S: State> sealed::Sealed for SetType<S> {}
767
-
impl<S: State> State for SetType<S> {
768
-
type Type = Set<members::r#type>;
769
-
type Blob = S::Blob;
770
-
}
771
-
///State transition - sets the `blob` field to Set
772
-
pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>);
773
-
impl<S: State> sealed::Sealed for SetBlob<S> {}
774
-
impl<S: State> State for SetBlob<S> {
775
-
type Type = S::Type;
776
-
type Blob = Set<members::blob>;
777
-
}
778
-
/// Marker types for field names
779
-
#[allow(non_camel_case_types)]
780
-
pub mod members {
781
-
///Marker type for the `type` field
782
-
pub struct r#type(());
783
-
///Marker type for the `blob` field
784
-
pub struct blob(());
785
-
}
786
-
}
787
-
788
-
/// Builder for constructing an instance of this type
789
-
pub struct FileBuilder<'a, S: file_state::State> {
790
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
791
-
__unsafe_private_named: (
792
-
::core::option::Option<bool>,
793
-
::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>,
794
-
::core::option::Option<jacquard_common::CowStr<'a>>,
795
-
::core::option::Option<jacquard_common::CowStr<'a>>,
796
-
::core::option::Option<jacquard_common::CowStr<'a>>,
797
-
),
798
-
_phantom: ::core::marker::PhantomData<&'a ()>,
799
-
}
800
-
801
-
impl<'a> File<'a> {
802
-
/// Create a new builder for this type
803
-
pub fn new() -> FileBuilder<'a, file_state::Empty> {
804
-
FileBuilder::new()
805
-
}
806
-
}
807
-
808
-
impl<'a> FileBuilder<'a, file_state::Empty> {
809
-
/// Create a new builder with all fields unset
810
-
pub fn new() -> Self {
811
-
FileBuilder {
812
-
_phantom_state: ::core::marker::PhantomData,
813
-
__unsafe_private_named: (None, None, None, None, None),
814
-
_phantom: ::core::marker::PhantomData,
815
-
}
816
-
}
817
-
}
818
-
819
-
impl<'a, S: file_state::State> FileBuilder<'a, S> {
820
-
/// Set the `base64` field (optional)
821
-
pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self {
822
-
self.__unsafe_private_named.0 = value.into();
823
-
self
824
-
}
825
-
/// Set the `base64` field to an Option value (optional)
826
-
pub fn maybe_base64(mut self, value: Option<bool>) -> Self {
827
-
self.__unsafe_private_named.0 = value;
828
-
self
829
-
}
830
-
}
831
-
832
-
impl<'a, S> FileBuilder<'a, S>
833
-
where
834
-
S: file_state::State,
835
-
S::Blob: file_state::IsUnset,
836
-
{
837
-
/// Set the `blob` field (required)
838
-
pub fn blob(
839
-
mut self,
840
-
value: impl Into<jacquard_common::types::blob::BlobRef<'a>>,
841
-
) -> FileBuilder<'a, file_state::SetBlob<S>> {
842
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
843
-
FileBuilder {
844
-
_phantom_state: ::core::marker::PhantomData,
845
-
__unsafe_private_named: self.__unsafe_private_named,
846
-
_phantom: ::core::marker::PhantomData,
847
-
}
848
-
}
849
-
}
850
-
851
-
impl<'a, S: file_state::State> FileBuilder<'a, S> {
852
-
/// Set the `encoding` field (optional)
853
-
pub fn encoding(
854
-
mut self,
855
-
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
856
-
) -> Self {
857
-
self.__unsafe_private_named.2 = value.into();
858
-
self
859
-
}
860
-
/// Set the `encoding` field to an Option value (optional)
861
-
pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
862
-
self.__unsafe_private_named.2 = value;
863
-
self
864
-
}
865
-
}
866
-
867
-
impl<'a, S: file_state::State> FileBuilder<'a, S> {
868
-
/// Set the `mimeType` field (optional)
869
-
pub fn mime_type(
870
-
mut self,
871
-
value: impl Into<Option<jacquard_common::CowStr<'a>>>,
872
-
) -> Self {
873
-
self.__unsafe_private_named.3 = value.into();
874
-
self
875
-
}
876
-
/// Set the `mimeType` field to an Option value (optional)
877
-
pub fn maybe_mime_type(
878
-
mut self,
879
-
value: Option<jacquard_common::CowStr<'a>>,
880
-
) -> Self {
881
-
self.__unsafe_private_named.3 = value;
882
-
self
883
-
}
884
-
}
885
-
886
-
impl<'a, S> FileBuilder<'a, S>
887
-
where
888
-
S: file_state::State,
889
-
S::Type: file_state::IsUnset,
890
-
{
891
-
/// Set the `type` field (required)
892
-
pub fn r#type(
893
-
mut self,
894
-
value: impl Into<jacquard_common::CowStr<'a>>,
895
-
) -> FileBuilder<'a, file_state::SetType<S>> {
896
-
self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into());
897
-
FileBuilder {
898
-
_phantom_state: ::core::marker::PhantomData,
899
-
__unsafe_private_named: self.__unsafe_private_named,
900
-
_phantom: ::core::marker::PhantomData,
901
-
}
902
-
}
903
-
}
904
-
905
-
impl<'a, S> FileBuilder<'a, S>
906
-
where
907
-
S: file_state::State,
908
-
S::Type: file_state::IsSet,
909
-
S::Blob: file_state::IsSet,
910
-
{
911
-
/// Build the final struct
912
-
pub fn build(self) -> File<'a> {
913
-
File {
914
-
base64: self.__unsafe_private_named.0,
915
-
blob: self.__unsafe_private_named.1.unwrap(),
916
-
encoding: self.__unsafe_private_named.2,
917
-
mime_type: self.__unsafe_private_named.3,
918
-
r#type: self.__unsafe_private_named.4.unwrap(),
919
-
extra_data: Default::default(),
920
-
}
921
-
}
922
-
/// Build the final struct with custom extra_data
923
-
pub fn build_with_data(
924
-
self,
925
-
extra_data: std::collections::BTreeMap<
926
-
jacquard_common::smol_str::SmolStr,
927
-
jacquard_common::types::value::Data<'a>,
928
-
>,
929
-
) -> File<'a> {
930
-
File {
931
-
base64: self.__unsafe_private_named.0,
932
-
blob: self.__unsafe_private_named.1.unwrap(),
933
-
encoding: self.__unsafe_private_named.2,
934
-
mime_type: self.__unsafe_private_named.3,
935
-
r#type: self.__unsafe_private_named.4.unwrap(),
936
-
extra_data: Some(extra_data),
937
-
}
938
-
}
939
-
}
940
-
941
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> {
942
-
fn nsid() -> &'static str {
943
-
"place.wisp.subfs"
944
-
}
945
-
fn def_name() -> &'static str {
946
-
"file"
947
-
}
948
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
949
-
lexicon_doc_place_wisp_subfs()
950
-
}
951
-
fn validate(
952
-
&self,
953
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
954
-
Ok(())
955
-
}
956
-
}
957
-
958
-
/// Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.
959
-
#[jacquard_derive::lexicon]
960
-
#[derive(
961
-
serde::Serialize,
962
-
serde::Deserialize,
963
-
Debug,
964
-
Clone,
965
-
PartialEq,
966
-
Eq,
967
-
jacquard_derive::IntoStatic
968
-
)]
969
-
#[serde(rename_all = "camelCase")]
970
-
pub struct SubfsRecord<'a> {
971
-
pub created_at: jacquard_common::types::string::Datetime,
972
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
973
-
pub file_count: Option<i64>,
974
-
#[serde(borrow)]
975
-
pub root: crate::place_wisp::subfs::Directory<'a>,
976
-
}
977
-
978
-
pub mod subfs_record_state {
979
-
980
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
981
-
#[allow(unused)]
982
-
use ::core::marker::PhantomData;
983
-
mod sealed {
984
-
pub trait Sealed {}
985
-
}
986
-
/// State trait tracking which required fields have been set
987
-
pub trait State: sealed::Sealed {
988
-
type Root;
989
-
type CreatedAt;
990
-
}
991
-
/// Empty state - all required fields are unset
992
-
pub struct Empty(());
993
-
impl sealed::Sealed for Empty {}
994
-
impl State for Empty {
995
-
type Root = Unset;
996
-
type CreatedAt = Unset;
997
-
}
998
-
///State transition - sets the `root` field to Set
999
-
pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>);
1000
-
impl<S: State> sealed::Sealed for SetRoot<S> {}
1001
-
impl<S: State> State for SetRoot<S> {
1002
-
type Root = Set<members::root>;
1003
-
type CreatedAt = S::CreatedAt;
1004
-
}
1005
-
///State transition - sets the `created_at` field to Set
1006
-
pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
1007
-
impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
1008
-
impl<S: State> State for SetCreatedAt<S> {
1009
-
type Root = S::Root;
1010
-
type CreatedAt = Set<members::created_at>;
1011
-
}
1012
-
/// Marker types for field names
1013
-
#[allow(non_camel_case_types)]
1014
-
pub mod members {
1015
-
///Marker type for the `root` field
1016
-
pub struct root(());
1017
-
///Marker type for the `created_at` field
1018
-
pub struct created_at(());
1019
-
}
1020
-
}
1021
-
1022
-
/// Builder for constructing an instance of this type
1023
-
pub struct SubfsRecordBuilder<'a, S: subfs_record_state::State> {
1024
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1025
-
__unsafe_private_named: (
1026
-
::core::option::Option<jacquard_common::types::string::Datetime>,
1027
-
::core::option::Option<i64>,
1028
-
::core::option::Option<crate::place_wisp::subfs::Directory<'a>>,
1029
-
),
1030
-
_phantom: ::core::marker::PhantomData<&'a ()>,
1031
-
}
1032
-
1033
-
impl<'a> SubfsRecord<'a> {
1034
-
/// Create a new builder for this type
1035
-
pub fn new() -> SubfsRecordBuilder<'a, subfs_record_state::Empty> {
1036
-
SubfsRecordBuilder::new()
1037
-
}
1038
-
}
1039
-
1040
-
impl<'a> SubfsRecordBuilder<'a, subfs_record_state::Empty> {
1041
-
/// Create a new builder with all fields unset
1042
-
pub fn new() -> Self {
1043
-
SubfsRecordBuilder {
1044
-
_phantom_state: ::core::marker::PhantomData,
1045
-
__unsafe_private_named: (None, None, None),
1046
-
_phantom: ::core::marker::PhantomData,
1047
-
}
1048
-
}
1049
-
}
1050
-
1051
-
impl<'a, S> SubfsRecordBuilder<'a, S>
1052
-
where
1053
-
S: subfs_record_state::State,
1054
-
S::CreatedAt: subfs_record_state::IsUnset,
1055
-
{
1056
-
/// Set the `createdAt` field (required)
1057
-
pub fn created_at(
1058
-
mut self,
1059
-
value: impl Into<jacquard_common::types::string::Datetime>,
1060
-
) -> SubfsRecordBuilder<'a, subfs_record_state::SetCreatedAt<S>> {
1061
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
1062
-
SubfsRecordBuilder {
1063
-
_phantom_state: ::core::marker::PhantomData,
1064
-
__unsafe_private_named: self.__unsafe_private_named,
1065
-
_phantom: ::core::marker::PhantomData,
1066
-
}
1067
-
}
1068
-
}
1069
-
1070
-
impl<'a, S: subfs_record_state::State> SubfsRecordBuilder<'a, S> {
1071
-
/// Set the `fileCount` field (optional)
1072
-
pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self {
1073
-
self.__unsafe_private_named.1 = value.into();
1074
-
self
1075
-
}
1076
-
/// Set the `fileCount` field to an Option value (optional)
1077
-
pub fn maybe_file_count(mut self, value: Option<i64>) -> Self {
1078
-
self.__unsafe_private_named.1 = value;
1079
-
self
1080
-
}
1081
-
}
1082
-
1083
-
impl<'a, S> SubfsRecordBuilder<'a, S>
1084
-
where
1085
-
S: subfs_record_state::State,
1086
-
S::Root: subfs_record_state::IsUnset,
1087
-
{
1088
-
/// Set the `root` field (required)
1089
-
pub fn root(
1090
-
mut self,
1091
-
value: impl Into<crate::place_wisp::subfs::Directory<'a>>,
1092
-
) -> SubfsRecordBuilder<'a, subfs_record_state::SetRoot<S>> {
1093
-
self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
1094
-
SubfsRecordBuilder {
1095
-
_phantom_state: ::core::marker::PhantomData,
1096
-
__unsafe_private_named: self.__unsafe_private_named,
1097
-
_phantom: ::core::marker::PhantomData,
1098
-
}
1099
-
}
1100
-
}
1101
-
1102
-
impl<'a, S> SubfsRecordBuilder<'a, S>
1103
-
where
1104
-
S: subfs_record_state::State,
1105
-
S::Root: subfs_record_state::IsSet,
1106
-
S::CreatedAt: subfs_record_state::IsSet,
1107
-
{
1108
-
/// Build the final struct
1109
-
pub fn build(self) -> SubfsRecord<'a> {
1110
-
SubfsRecord {
1111
-
created_at: self.__unsafe_private_named.0.unwrap(),
1112
-
file_count: self.__unsafe_private_named.1,
1113
-
root: self.__unsafe_private_named.2.unwrap(),
1114
-
extra_data: Default::default(),
1115
-
}
1116
-
}
1117
-
/// Build the final struct with custom extra_data
1118
-
pub fn build_with_data(
1119
-
self,
1120
-
extra_data: std::collections::BTreeMap<
1121
-
jacquard_common::smol_str::SmolStr,
1122
-
jacquard_common::types::value::Data<'a>,
1123
-
>,
1124
-
) -> SubfsRecord<'a> {
1125
-
SubfsRecord {
1126
-
created_at: self.__unsafe_private_named.0.unwrap(),
1127
-
file_count: self.__unsafe_private_named.1,
1128
-
root: self.__unsafe_private_named.2.unwrap(),
1129
-
extra_data: Some(extra_data),
1130
-
}
1131
-
}
1132
-
}
1133
-
1134
-
impl<'a> SubfsRecord<'a> {
1135
-
pub fn uri(
1136
-
uri: impl Into<jacquard_common::CowStr<'a>>,
1137
-
) -> Result<
1138
-
jacquard_common::types::uri::RecordUri<'a, SubfsRecordRecord>,
1139
-
jacquard_common::types::uri::UriError,
1140
-
> {
1141
-
jacquard_common::types::uri::RecordUri::try_from_uri(
1142
-
jacquard_common::types::string::AtUri::new_cow(uri.into())?,
1143
-
)
1144
-
}
1145
-
}
1146
-
1147
-
/// Typed wrapper for GetRecord response with this collection's record type.
1148
-
#[derive(
1149
-
serde::Serialize,
1150
-
serde::Deserialize,
1151
-
Debug,
1152
-
Clone,
1153
-
PartialEq,
1154
-
Eq,
1155
-
jacquard_derive::IntoStatic
1156
-
)]
1157
-
#[serde(rename_all = "camelCase")]
1158
-
pub struct SubfsRecordGetRecordOutput<'a> {
1159
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
1160
-
#[serde(borrow)]
1161
-
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
1162
-
#[serde(borrow)]
1163
-
pub uri: jacquard_common::types::string::AtUri<'a>,
1164
-
#[serde(borrow)]
1165
-
pub value: SubfsRecord<'a>,
1166
-
}
1167
-
1168
-
impl From<SubfsRecordGetRecordOutput<'_>> for SubfsRecord<'_> {
1169
-
fn from(output: SubfsRecordGetRecordOutput<'_>) -> Self {
1170
-
use jacquard_common::IntoStatic;
1171
-
output.value.into_static()
1172
-
}
1173
-
}
1174
-
1175
-
impl jacquard_common::types::collection::Collection for SubfsRecord<'_> {
1176
-
const NSID: &'static str = "place.wisp.subfs";
1177
-
type Record = SubfsRecordRecord;
1178
-
}
1179
-
1180
-
/// Marker type for deserializing records from this collection.
1181
-
#[derive(Debug, serde::Serialize, serde::Deserialize)]
1182
-
pub struct SubfsRecordRecord;
1183
-
impl jacquard_common::xrpc::XrpcResp for SubfsRecordRecord {
1184
-
const NSID: &'static str = "place.wisp.subfs";
1185
-
const ENCODING: &'static str = "application/json";
1186
-
type Output<'de> = SubfsRecordGetRecordOutput<'de>;
1187
-
type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
1188
-
}
1189
-
1190
-
impl jacquard_common::types::collection::Collection for SubfsRecordRecord {
1191
-
const NSID: &'static str = "place.wisp.subfs";
1192
-
type Record = SubfsRecordRecord;
1193
-
}
1194
-
1195
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubfsRecord<'a> {
1196
-
fn nsid() -> &'static str {
1197
-
"place.wisp.subfs"
1198
-
}
1199
-
fn def_name() -> &'static str {
1200
-
"main"
1201
-
}
1202
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1203
-
lexicon_doc_place_wisp_subfs()
1204
-
}
1205
-
fn validate(
1206
-
&self,
1207
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1208
-
if let Some(ref value) = self.file_count {
1209
-
if *value > 1000i64 {
1210
-
return Err(::jacquard_lexicon::validation::ConstraintError::Maximum {
1211
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1212
-
"file_count",
1213
-
),
1214
-
max: 1000i64,
1215
-
actual: *value,
1216
-
});
1217
-
}
1218
-
}
1219
-
if let Some(ref value) = self.file_count {
1220
-
if *value < 0i64 {
1221
-
return Err(::jacquard_lexicon::validation::ConstraintError::Minimum {
1222
-
path: ::jacquard_lexicon::validation::ValidationPath::from_field(
1223
-
"file_count",
1224
-
),
1225
-
min: 0i64,
1226
-
actual: *value,
1227
-
});
1228
-
}
1229
-
}
1230
-
Ok(())
1231
-
}
1232
-
}
1233
-
1234
-
#[jacquard_derive::lexicon]
1235
-
#[derive(
1236
-
serde::Serialize,
1237
-
serde::Deserialize,
1238
-
Debug,
1239
-
Clone,
1240
-
PartialEq,
1241
-
Eq,
1242
-
jacquard_derive::IntoStatic
1243
-
)]
1244
-
#[serde(rename_all = "camelCase")]
1245
-
pub struct Subfs<'a> {
1246
-
/// AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.
1247
-
#[serde(borrow)]
1248
-
pub subject: jacquard_common::types::string::AtUri<'a>,
1249
-
#[serde(borrow)]
1250
-
pub r#type: jacquard_common::CowStr<'a>,
1251
-
}
1252
-
1253
-
pub mod subfs_state {
1254
-
1255
-
pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
1256
-
#[allow(unused)]
1257
-
use ::core::marker::PhantomData;
1258
-
mod sealed {
1259
-
pub trait Sealed {}
1260
-
}
1261
-
/// State trait tracking which required fields have been set
1262
-
pub trait State: sealed::Sealed {
1263
-
type Type;
1264
-
type Subject;
1265
-
}
1266
-
/// Empty state - all required fields are unset
1267
-
pub struct Empty(());
1268
-
impl sealed::Sealed for Empty {}
1269
-
impl State for Empty {
1270
-
type Type = Unset;
1271
-
type Subject = Unset;
1272
-
}
1273
-
///State transition - sets the `type` field to Set
1274
-
pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>);
1275
-
impl<S: State> sealed::Sealed for SetType<S> {}
1276
-
impl<S: State> State for SetType<S> {
1277
-
type Type = Set<members::r#type>;
1278
-
type Subject = S::Subject;
1279
-
}
1280
-
///State transition - sets the `subject` field to Set
1281
-
pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>);
1282
-
impl<S: State> sealed::Sealed for SetSubject<S> {}
1283
-
impl<S: State> State for SetSubject<S> {
1284
-
type Type = S::Type;
1285
-
type Subject = Set<members::subject>;
1286
-
}
1287
-
/// Marker types for field names
1288
-
#[allow(non_camel_case_types)]
1289
-
pub mod members {
1290
-
///Marker type for the `type` field
1291
-
pub struct r#type(());
1292
-
///Marker type for the `subject` field
1293
-
pub struct subject(());
1294
-
}
1295
-
}
1296
-
1297
-
/// Builder for constructing an instance of this type
1298
-
pub struct SubfsBuilder<'a, S: subfs_state::State> {
1299
-
_phantom_state: ::core::marker::PhantomData<fn() -> S>,
1300
-
__unsafe_private_named: (
1301
-
::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
1302
-
::core::option::Option<jacquard_common::CowStr<'a>>,
1303
-
),
1304
-
_phantom: ::core::marker::PhantomData<&'a ()>,
1305
-
}
1306
-
1307
-
impl<'a> Subfs<'a> {
1308
-
/// Create a new builder for this type
1309
-
pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> {
1310
-
SubfsBuilder::new()
1311
-
}
1312
-
}
1313
-
1314
-
impl<'a> SubfsBuilder<'a, subfs_state::Empty> {
1315
-
/// Create a new builder with all fields unset
1316
-
pub fn new() -> Self {
1317
-
SubfsBuilder {
1318
-
_phantom_state: ::core::marker::PhantomData,
1319
-
__unsafe_private_named: (None, None),
1320
-
_phantom: ::core::marker::PhantomData,
1321
-
}
1322
-
}
1323
-
}
1324
-
1325
-
impl<'a, S> SubfsBuilder<'a, S>
1326
-
where
1327
-
S: subfs_state::State,
1328
-
S::Subject: subfs_state::IsUnset,
1329
-
{
1330
-
/// Set the `subject` field (required)
1331
-
pub fn subject(
1332
-
mut self,
1333
-
value: impl Into<jacquard_common::types::string::AtUri<'a>>,
1334
-
) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> {
1335
-
self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
1336
-
SubfsBuilder {
1337
-
_phantom_state: ::core::marker::PhantomData,
1338
-
__unsafe_private_named: self.__unsafe_private_named,
1339
-
_phantom: ::core::marker::PhantomData,
1340
-
}
1341
-
}
1342
-
}
1343
-
1344
-
impl<'a, S> SubfsBuilder<'a, S>
1345
-
where
1346
-
S: subfs_state::State,
1347
-
S::Type: subfs_state::IsUnset,
1348
-
{
1349
-
/// Set the `type` field (required)
1350
-
pub fn r#type(
1351
-
mut self,
1352
-
value: impl Into<jacquard_common::CowStr<'a>>,
1353
-
) -> SubfsBuilder<'a, subfs_state::SetType<S>> {
1354
-
self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
1355
-
SubfsBuilder {
1356
-
_phantom_state: ::core::marker::PhantomData,
1357
-
__unsafe_private_named: self.__unsafe_private_named,
1358
-
_phantom: ::core::marker::PhantomData,
1359
-
}
1360
-
}
1361
-
}
1362
-
1363
-
impl<'a, S> SubfsBuilder<'a, S>
1364
-
where
1365
-
S: subfs_state::State,
1366
-
S::Type: subfs_state::IsSet,
1367
-
S::Subject: subfs_state::IsSet,
1368
-
{
1369
-
/// Build the final struct
1370
-
pub fn build(self) -> Subfs<'a> {
1371
-
Subfs {
1372
-
subject: self.__unsafe_private_named.0.unwrap(),
1373
-
r#type: self.__unsafe_private_named.1.unwrap(),
1374
-
extra_data: Default::default(),
1375
-
}
1376
-
}
1377
-
/// Build the final struct with custom extra_data
1378
-
pub fn build_with_data(
1379
-
self,
1380
-
extra_data: std::collections::BTreeMap<
1381
-
jacquard_common::smol_str::SmolStr,
1382
-
jacquard_common::types::value::Data<'a>,
1383
-
>,
1384
-
) -> Subfs<'a> {
1385
-
Subfs {
1386
-
subject: self.__unsafe_private_named.0.unwrap(),
1387
-
r#type: self.__unsafe_private_named.1.unwrap(),
1388
-
extra_data: Some(extra_data),
1389
-
}
1390
-
}
1391
-
}
1392
-
1393
-
impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> {
1394
-
fn nsid() -> &'static str {
1395
-
"place.wisp.subfs"
1396
-
}
1397
-
fn def_name() -> &'static str {
1398
-
"subfs"
1399
-
}
1400
-
fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
1401
-
lexicon_doc_place_wisp_subfs()
1402
-
}
1403
-
fn validate(
1404
-
&self,
1405
-
) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
1406
-
Ok(())
1407
-
}
1408
-
}
···
-8
cli/src/place_wisp.rs
-8
cli/src/place_wisp.rs
+12
-12
cli/src/pull.rs
+12
-12
cli/src/pull.rs
···
1
use crate::blob_map;
2
use crate::download;
3
use crate::metadata::SiteMetadata;
4
-
use crate::place_wisp::fs::*;
5
use crate::subfs_utils;
6
use jacquard::CowStr;
7
use jacquard::prelude::IdentityResolver;
···
410
) -> miette::Result<Directory<'static>> {
411
use jacquard_common::IntoStatic;
412
use jacquard_common::types::value::from_data;
413
-
use crate::place_wisp::subfs::SubfsRecord;
414
415
-
let mut all_subfs_map: HashMap<String, crate::place_wisp::subfs::Directory> = HashMap::new();
416
let mut to_fetch = subfs_utils::extract_subfs_uris(directory, String::new());
417
418
if to_fetch.is_empty() {
···
516
517
/// Extract subfs URIs from a subfs::Directory (helper for pull)
518
fn extract_subfs_uris_from_subfs_dir(
519
-
directory: &crate::place_wisp::subfs::Directory,
520
current_path: String,
521
) -> Vec<(String, String)> {
522
let mut uris = Vec::new();
···
529
};
530
531
match &entry.node {
532
-
crate::place_wisp::subfs::EntryNode::Subfs(subfs_node) => {
533
uris.push((subfs_node.subject.to_string(), full_path.clone()));
534
}
535
-
crate::place_wisp::subfs::EntryNode::Directory(subdir) => {
536
let nested = extract_subfs_uris_from_subfs_dir(subdir, full_path);
537
uris.extend(nested);
538
}
···
546
/// Recursively replace subfs nodes with their actual content
547
fn replace_subfs_with_content(
548
directory: Directory,
549
-
subfs_map: &HashMap<String, crate::place_wisp::subfs::Directory>,
550
current_path: String,
551
) -> Directory<'static> {
552
use jacquard_common::IntoStatic;
···
628
}
629
630
/// Convert a subfs entry to a fs entry (they have the same structure but different types)
631
-
fn convert_subfs_entry_to_fs(subfs_entry: crate::place_wisp::subfs::Entry<'static>) -> Entry<'static> {
632
use jacquard_common::IntoStatic;
633
634
let node = match subfs_entry.node {
635
-
crate::place_wisp::subfs::EntryNode::File(file) => {
636
EntryNode::File(Box::new(
637
File::new()
638
.r#type(file.r#type.into_static())
···
643
.build()
644
))
645
}
646
-
crate::place_wisp::subfs::EntryNode::Directory(dir) => {
647
let converted_entries: Vec<Entry<'static>> = dir
648
.entries
649
.into_iter()
···
657
.build()
658
))
659
}
660
-
crate::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => {
661
// Nested subfs should have been expanded already - if we get here, it means expansion failed
662
// Treat it like a directory reference that should have been expanded
663
eprintln!(" โ ๏ธ Warning: unexpanded nested subfs at path, treating as empty directory");
···
668
.build()
669
))
670
}
671
-
crate::place_wisp::subfs::EntryNode::Unknown(unknown) => {
672
EntryNode::Unknown(unknown)
673
}
674
};
···
1
use crate::blob_map;
2
use crate::download;
3
use crate::metadata::SiteMetadata;
4
+
use wisp_lexicons::place_wisp::fs::*;
5
use crate::subfs_utils;
6
use jacquard::CowStr;
7
use jacquard::prelude::IdentityResolver;
···
410
) -> miette::Result<Directory<'static>> {
411
use jacquard_common::IntoStatic;
412
use jacquard_common::types::value::from_data;
413
+
use wisp_lexicons::place_wisp::subfs::SubfsRecord;
414
415
+
let mut all_subfs_map: HashMap<String, wisp_lexicons::place_wisp::subfs::Directory> = HashMap::new();
416
let mut to_fetch = subfs_utils::extract_subfs_uris(directory, String::new());
417
418
if to_fetch.is_empty() {
···
516
517
/// Extract subfs URIs from a subfs::Directory (helper for pull)
518
fn extract_subfs_uris_from_subfs_dir(
519
+
directory: &wisp_lexicons::place_wisp::subfs::Directory,
520
current_path: String,
521
) -> Vec<(String, String)> {
522
let mut uris = Vec::new();
···
529
};
530
531
match &entry.node {
532
+
wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(subfs_node) => {
533
uris.push((subfs_node.subject.to_string(), full_path.clone()));
534
}
535
+
wisp_lexicons::place_wisp::subfs::EntryNode::Directory(subdir) => {
536
let nested = extract_subfs_uris_from_subfs_dir(subdir, full_path);
537
uris.extend(nested);
538
}
···
546
/// Recursively replace subfs nodes with their actual content
547
fn replace_subfs_with_content(
548
directory: Directory,
549
+
subfs_map: &HashMap<String, wisp_lexicons::place_wisp::subfs::Directory>,
550
current_path: String,
551
) -> Directory<'static> {
552
use jacquard_common::IntoStatic;
···
628
}
629
630
/// Convert a subfs entry to a fs entry (they have the same structure but different types)
631
+
fn convert_subfs_entry_to_fs(subfs_entry: wisp_lexicons::place_wisp::subfs::Entry<'static>) -> Entry<'static> {
632
use jacquard_common::IntoStatic;
633
634
let node = match subfs_entry.node {
635
+
wisp_lexicons::place_wisp::subfs::EntryNode::File(file) => {
636
EntryNode::File(Box::new(
637
File::new()
638
.r#type(file.r#type.into_static())
···
643
.build()
644
))
645
}
646
+
wisp_lexicons::place_wisp::subfs::EntryNode::Directory(dir) => {
647
let converted_entries: Vec<Entry<'static>> = dir
648
.entries
649
.into_iter()
···
657
.build()
658
))
659
}
660
+
wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => {
661
// Nested subfs should have been expanded already - if we get here, it means expansion failed
662
// Treat it like a directory reference that should have been expanded
663
eprintln!(" โ ๏ธ Warning: unexpanded nested subfs at path, treating as empty directory");
···
668
.build()
669
))
670
}
671
+
wisp_lexicons::place_wisp::subfs::EntryNode::Unknown(unknown) => {
672
EntryNode::Unknown(unknown)
673
}
674
};
+1
-1
cli/src/serve.rs
+1
-1
cli/src/serve.rs
+14
-14
cli/src/subfs_utils.rs
+14
-14
cli/src/subfs_utils.rs
···
6
use miette::IntoDiagnostic;
7
use std::collections::HashMap;
8
9
-
use crate::place_wisp::fs::{Directory as FsDirectory, EntryNode as FsEntryNode};
10
-
use crate::place_wisp::subfs::SubfsRecord;
11
12
/// Extract all subfs URIs from a directory tree with their mount paths
13
pub fn extract_subfs_uris(directory: &FsDirectory, current_path: String) -> Vec<(String, String)> {
···
145
146
/// Extract subfs URIs from a subfs::Directory
147
fn extract_subfs_uris_from_subfs_dir(
148
-
directory: &crate::place_wisp::subfs::Directory,
149
current_path: String,
150
) -> Vec<(String, String)> {
151
let mut uris = Vec::new();
152
153
for entry in &directory.entries {
154
match &entry.node {
155
-
crate::place_wisp::subfs::EntryNode::Subfs(subfs_node) => {
156
// Check if this is a chunk entry (chunk0, chunk1, etc.)
157
// Chunks should be flat-merged, so use the parent's path
158
let mount_path = if entry.name.starts_with("chunk") &&
···
171
172
uris.push((subfs_node.subject.to_string(), mount_path));
173
}
174
-
crate::place_wisp::subfs::EntryNode::Directory(subdir) => {
175
let full_path = if current_path.is_empty() {
176
entry.name.to_string()
177
} else {
···
204
for (mount_path, subfs_record) in all_subfs {
205
// Check if this record only contains chunk subfs references (no files)
206
let only_has_chunks = subfs_record.root.entries.iter().all(|e| {
207
-
matches!(&e.node, crate::place_wisp::subfs::EntryNode::Subfs(_)) &&
208
e.name.starts_with("chunk") &&
209
e.name.chars().skip(5).all(|c| c.is_ascii_digit())
210
});
···
232
/// Extract blobs from a subfs directory (works with subfs::Directory)
233
/// Returns a map of file paths to their blob refs and CIDs
234
fn extract_subfs_blobs(
235
-
directory: &crate::place_wisp::subfs::Directory,
236
current_path: String,
237
) -> HashMap<String, (BlobRef<'static>, String)> {
238
let mut blob_map = HashMap::new();
···
245
};
246
247
match &entry.node {
248
-
crate::place_wisp::subfs::EntryNode::File(file_node) => {
249
let blob_ref = &file_node.blob;
250
let cid_string = blob_ref.blob().r#ref.to_string();
251
blob_map.insert(
···
253
(blob_ref.clone().into_static(), cid_string)
254
);
255
}
256
-
crate::place_wisp::subfs::EntryNode::Directory(subdir) => {
257
let sub_map = extract_subfs_blobs(subdir, full_path);
258
blob_map.extend(sub_map);
259
}
260
-
crate::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => {
261
// Nested subfs - these should be resolved recursively in the main flow
262
// For now, we skip them (they'll be fetched separately)
263
eprintln!(" โ ๏ธ Found nested subfs at {}, skipping (should be fetched separately)", full_path);
264
}
265
-
crate::place_wisp::subfs::EntryNode::Unknown(_) => {
266
// Skip unknown nodes
267
}
268
}
···
352
flat: bool,
353
) -> miette::Result<FsDirectory<'static>> {
354
use jacquard_common::CowStr;
355
-
use crate::place_wisp::fs::{Entry, Subfs};
356
357
let path_parts: Vec<&str> = target_path.split('/').collect();
358
···
430
431
// Construct AT-URI and convert to RecordUri
432
let at_uri = AtUri::new(uri).into_diagnostic()?;
433
-
let record_uri: RecordUri<'_, crate::place_wisp::subfs::SubfsRecordRecord> = RecordUri::try_from_uri(at_uri).into_diagnostic()?;
434
435
let rkey = record_uri.rkey()
436
.ok_or_else(|| miette::miette!("Invalid subfs URI: missing rkey"))?
···
489
}
490
491
/// Estimate the JSON size of a single entry
492
-
fn estimate_entry_size(entry: &crate::place_wisp::fs::Entry) -> usize {
493
match serde_json::to_string(entry) {
494
Ok(json) => json.len(),
495
Err(_) => 500, // Conservative estimate if serialization fails
···
6
use miette::IntoDiagnostic;
7
use std::collections::HashMap;
8
9
+
use wisp_lexicons::place_wisp::fs::{Directory as FsDirectory, EntryNode as FsEntryNode};
10
+
use wisp_lexicons::place_wisp::subfs::SubfsRecord;
11
12
/// Extract all subfs URIs from a directory tree with their mount paths
13
pub fn extract_subfs_uris(directory: &FsDirectory, current_path: String) -> Vec<(String, String)> {
···
145
146
/// Extract subfs URIs from a subfs::Directory
147
fn extract_subfs_uris_from_subfs_dir(
148
+
directory: &wisp_lexicons::place_wisp::subfs::Directory,
149
current_path: String,
150
) -> Vec<(String, String)> {
151
let mut uris = Vec::new();
152
153
for entry in &directory.entries {
154
match &entry.node {
155
+
wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(subfs_node) => {
156
// Check if this is a chunk entry (chunk0, chunk1, etc.)
157
// Chunks should be flat-merged, so use the parent's path
158
let mount_path = if entry.name.starts_with("chunk") &&
···
171
172
uris.push((subfs_node.subject.to_string(), mount_path));
173
}
174
+
wisp_lexicons::place_wisp::subfs::EntryNode::Directory(subdir) => {
175
let full_path = if current_path.is_empty() {
176
entry.name.to_string()
177
} else {
···
204
for (mount_path, subfs_record) in all_subfs {
205
// Check if this record only contains chunk subfs references (no files)
206
let only_has_chunks = subfs_record.root.entries.iter().all(|e| {
207
+
matches!(&e.node, wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(_)) &&
208
e.name.starts_with("chunk") &&
209
e.name.chars().skip(5).all(|c| c.is_ascii_digit())
210
});
···
232
/// Extract blobs from a subfs directory (works with subfs::Directory)
233
/// Returns a map of file paths to their blob refs and CIDs
234
fn extract_subfs_blobs(
235
+
directory: &wisp_lexicons::place_wisp::subfs::Directory,
236
current_path: String,
237
) -> HashMap<String, (BlobRef<'static>, String)> {
238
let mut blob_map = HashMap::new();
···
245
};
246
247
match &entry.node {
248
+
wisp_lexicons::place_wisp::subfs::EntryNode::File(file_node) => {
249
let blob_ref = &file_node.blob;
250
let cid_string = blob_ref.blob().r#ref.to_string();
251
blob_map.insert(
···
253
(blob_ref.clone().into_static(), cid_string)
254
);
255
}
256
+
wisp_lexicons::place_wisp::subfs::EntryNode::Directory(subdir) => {
257
let sub_map = extract_subfs_blobs(subdir, full_path);
258
blob_map.extend(sub_map);
259
}
260
+
wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => {
261
// Nested subfs - these should be resolved recursively in the main flow
262
// For now, we skip them (they'll be fetched separately)
263
eprintln!(" โ ๏ธ Found nested subfs at {}, skipping (should be fetched separately)", full_path);
264
}
265
+
wisp_lexicons::place_wisp::subfs::EntryNode::Unknown(_) => {
266
// Skip unknown nodes
267
}
268
}
···
352
flat: bool,
353
) -> miette::Result<FsDirectory<'static>> {
354
use jacquard_common::CowStr;
355
+
use wisp_lexicons::place_wisp::fs::{Entry, Subfs};
356
357
let path_parts: Vec<&str> = target_path.split('/').collect();
358
···
430
431
// Construct AT-URI and convert to RecordUri
432
let at_uri = AtUri::new(uri).into_diagnostic()?;
433
+
let record_uri: RecordUri<'_, wisp_lexicons::place_wisp::subfs::SubfsRecordRecord> = RecordUri::try_from_uri(at_uri).into_diagnostic()?;
434
435
let rkey = record_uri.rkey()
436
.ok_or_else(|| miette::miette!("Invalid subfs URI: missing rkey"))?
···
489
}
490
491
/// Estimate the JSON size of a single entry
492
+
fn estimate_entry_size(entry: &wisp_lexicons::place_wisp::fs::Entry) -> usize {
493
match serde_json::to_string(entry) {
494
Ok(json) => json.len(),
495
Err(_) => 500, // Conservative estimate if serialization fails
+21
docker-compose.yml
+21
docker-compose.yml
···
···
1
+
services:
2
+
postgres:
3
+
image: postgres:16-alpine
4
+
container_name: wisp-postgres
5
+
restart: unless-stopped
6
+
environment:
7
+
POSTGRES_USER: postgres
8
+
POSTGRES_PASSWORD: postgres
9
+
POSTGRES_DB: wisp
10
+
ports:
11
+
- "5432:5432"
12
+
volumes:
13
+
- postgres_data:/var/lib/postgresql/data
14
+
healthcheck:
15
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
16
+
interval: 5s
17
+
timeout: 5s
18
+
retries: 5
19
+
20
+
volumes:
21
+
postgres_data:
+4
-1
docs/astro.config.mjs
+4
-1
docs/astro.config.mjs
···
7
integrations: [
8
starlight({
9
title: 'Wisp.place Docs',
10
-
social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/tangled-org/wisp.place' }],
11
sidebar: [
12
{
13
label: 'Getting Started',
···
24
label: 'Guides',
25
items: [
26
{ label: 'Self-Hosting', slug: 'deployment' },
27
{ label: 'Redirects & Rewrites', slug: 'redirects' },
28
],
29
},
···
7
integrations: [
8
starlight({
9
title: 'Wisp.place Docs',
10
+
components: {
11
+
SocialIcons: './src/components/SocialIcons.astro',
12
+
},
13
sidebar: [
14
{
15
label: 'Getting Started',
···
26
label: 'Guides',
27
items: [
28
{ label: 'Self-Hosting', slug: 'deployment' },
29
+
{ label: 'Monitoring & Metrics', slug: 'monitoring' },
30
{ label: 'Redirects & Rewrites', slug: 'redirects' },
31
],
32
},
+9
docs/src/assets/tangled-icon.svg
+9
docs/src/assets/tangled-icon.svg
···
···
1
+
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg1" width="25" height="25" viewBox="0 0 25 25" sodipodi:docname="tangled_dolly_silhouette.png">
2
+
<defs id="defs1"/>
3
+
<sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1">
4
+
<inkscape:page x="0" y="0" width="25" height="25" id="page2" margin="0" bleed="0"/>
5
+
</sodipodi:namedview>
6
+
<g inkscape:groupmode="layer" inkscape:label="Image" id="g1">
7
+
<path style="fill:#000000;stroke-width:1.12248" d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z" id="path1"/>
8
+
</g>
9
+
</svg>
+26
docs/src/components/SocialIcons.astro
+26
docs/src/components/SocialIcons.astro
···
···
1
+
---
2
+
// Custom social icons component to use the Tangled icon
3
+
---
4
+
5
+
<div class="sl-flex">
6
+
<a
7
+
href="https://tangled.org/nekomimi.pet/wisp.place-monorepo"
8
+
rel="me"
9
+
class="sl-flex"
10
+
aria-label="Tangled"
11
+
>
12
+
<svg
13
+
xmlns="http://www.w3.org/2000/svg"
14
+
viewBox="0 0 25 25"
15
+
width="16"
16
+
height="16"
17
+
aria-hidden="true"
18
+
focusable="false"
19
+
>
20
+
<path
21
+
style="fill:currentColor;stroke-width:1.12248"
22
+
d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z"
23
+
></path>
24
+
</svg>
25
+
</a>
26
+
</div>
+8
-11
docs/src/content/docs/cli.md
+8
-11
docs/src/content/docs/cli.md
···
71
72
engine: 'nixery'
73
74
-
clone:
75
-
skip: false
76
-
depth: 1
77
-
submodules: false
78
-
79
dependencies:
80
nixpkgs:
81
- nodejs
···
141
# Pull a site to a specific directory
142
wisp-cli pull your-handle.bsky.social \
143
--site my-site \
144
-
--output ./my-site
145
146
# Pull to current directory
147
wisp-cli pull your-handle.bsky.social \
···
204
205
The CLI handles all file processing automatically to ensure reliable storage and delivery. Files are compressed with gzip at level 9 for optimal size reduction, then base64 encoded to bypass PDS content sniffing restrictions. Everything is uploaded as `application/octet-stream` blobs while preserving the original MIME type as metadata. When serving your site, the hosting service automatically decompresses non-HTML/CSS/JS files, ensuring your content is delivered correctly to visitors.
206
207
## Incremental Updates
208
209
The CLI tracks file changes using CID-based content addressing to minimize upload times and bandwidth usage. On your first deploy, all files are uploaded to establish the initial site. For subsequent deploys, the CLI compares content-addressed CIDs to detect which files have actually changed, uploading only those that differ from the previous version. This makes fast iterations possible even for large sites, with deploys completing in seconds when only a few files have changed.
···
236
### Pull Command
237
238
```bash
239
-
wisp-cli pull [OPTIONS] <INPUT>
240
241
Arguments:
242
<INPUT> Handle or DID
243
244
Options:
245
-s, --site <SITE> Site name to download
246
-
-o, --output <OUTPUT> Output directory [default: .]
247
-h, --help Print help
248
```
249
250
### Serve Command
251
252
```bash
253
-
wisp-cli serve [OPTIONS] <INPUT>
254
255
Arguments:
256
<INPUT> Handle or DID
257
258
Options:
259
-s, --site <SITE> Site name to serve
260
-
-o, --output <OUTPUT> Site files directory [default: .]
261
-
-p, --port <PORT> Port to serve on [default: 8080]
262
--spa Enable SPA mode (serve index.html for all routes)
263
--directory Enable directory listing mode for paths without index files
264
-h, --help Print help
···
71
72
engine: 'nixery'
73
74
dependencies:
75
nixpkgs:
76
- nodejs
···
136
# Pull a site to a specific directory
137
wisp-cli pull your-handle.bsky.social \
138
--site my-site \
139
+
--path ./my-site
140
141
# Pull to current directory
142
wisp-cli pull your-handle.bsky.social \
···
199
200
The CLI handles all file processing automatically to ensure reliable storage and delivery. Files are compressed with gzip at level 9 for optimal size reduction, then base64 encoded to bypass PDS content sniffing restrictions. Everything is uploaded as `application/octet-stream` blobs while preserving the original MIME type as metadata. When serving your site, the hosting service automatically decompresses non-HTML/CSS/JS files, ensuring your content is delivered correctly to visitors.
201
202
+
**File Filtering**: The CLI automatically excludes common files like `.git`, `node_modules`, `.env`, and other development artifacts. Customize this with a [`.wispignore` file](/file-filtering).
203
+
204
## Incremental Updates
205
206
The CLI tracks file changes using CID-based content addressing to minimize upload times and bandwidth usage. On your first deploy, all files are uploaded to establish the initial site. For subsequent deploys, the CLI compares content-addressed CIDs to detect which files have actually changed, uploading only those that differ from the previous version. This makes fast iterations possible even for large sites, with deploys completing in seconds when only a few files have changed.
···
233
### Pull Command
234
235
```bash
236
+
wisp-cli pull [OPTIONS] --site <SITE> <INPUT>
237
238
Arguments:
239
<INPUT> Handle or DID
240
241
Options:
242
-s, --site <SITE> Site name to download
243
+
-p, --path <PATH> Output directory [default: .]
244
-h, --help Print help
245
```
246
247
### Serve Command
248
249
```bash
250
+
wisp-cli serve [OPTIONS] --site <SITE> <INPUT>
251
252
Arguments:
253
<INPUT> Handle or DID
254
255
Options:
256
-s, --site <SITE> Site name to serve
257
+
-p, --path <PATH> Site files directory [default: .]
258
+
-P, --port <PORT> Port to serve on [default: 8080]
259
--spa Enable SPA mode (serve index.html for all routes)
260
--directory Enable directory listing mode for paths without index files
261
-h, --help Print help
+58
docs/src/content/docs/file-filtering.md
+58
docs/src/content/docs/file-filtering.md
···
···
1
+
---
2
+
title: File Filtering & .wispignore
3
+
description: Control which files are uploaded to your Wisp site
4
+
---
5
+
6
+
# File Filtering & .wispignore
7
+
8
+
Wisp automatically excludes common files that shouldn't be deployed (`.git`, `node_modules`, `.env`, etc.).
9
+
10
+
## Default Exclusions
11
+
12
+
- Version control: `.git`, `.github`, `.gitlab`
13
+
- Dependencies: `node_modules`, `__pycache__`, `*.pyc`
14
+
- Secrets: `.env`, `.env.*`
15
+
- OS files: `.DS_Store`, `Thumbs.db`, `._*`
16
+
- Cache: `.cache`, `.temp`, `.tmp`
17
+
- Dev tools: `.vscode`, `*.swp`, `*~`, `.tangled`
18
+
- Virtual envs: `.venv`, `venv`, `env`
19
+
20
+
## Custom Patterns
21
+
22
+
Create a `.wispignore` file in your site root using gitignore syntax:
23
+
24
+
```
25
+
# Build outputs
26
+
dist/
27
+
*.map
28
+
29
+
# Logs and temp files
30
+
*.log
31
+
temp/
32
+
33
+
# Keep one exception
34
+
!important.log
35
+
```
36
+
37
+
### Pattern Syntax
38
+
39
+
- `file.txt` - exact match
40
+
- `*.log` - wildcard
41
+
- `logs/` - directory
42
+
- `src/**/*.test.js` - glob pattern
43
+
- `!keep.txt` - exception (don't ignore)
44
+
45
+
## Usage
46
+
47
+
**CLI**: Place `.wispignore` in your upload directory
48
+
```bash
49
+
wisp-cli handle.bsky.social --path ./my-site --site my-site
50
+
```
51
+
52
+
**Web**: Include `.wispignore` when uploading files
53
+
54
+
## Notes
55
+
56
+
- Custom patterns add to (not replace) default patterns
57
+
- Works in both CLI and web uploads
58
+
- The CLI logs which files are skipped
+85
docs/src/content/docs/guides/grafana-setup.md
+85
docs/src/content/docs/guides/grafana-setup.md
···
···
1
+
---
2
+
title: Grafana Setup Example
3
+
description: Quick setup for Grafana Cloud monitoring
4
+
---
5
+
6
+
Example setup for monitoring Wisp.place with Grafana Cloud.
7
+
8
+
## 1. Create Grafana Cloud Account
9
+
10
+
Sign up at [grafana.com](https://grafana.com) for a free tier account.
11
+
12
+
## 2. Get Credentials
13
+
14
+
Navigate to your stack and find:
15
+
16
+
**Loki** (Connections โ Loki โ Details):
17
+
- Push endpoint: `https://logs-prod-XXX.grafana.net`
18
+
- Create API token with write permissions
19
+
20
+
**Prometheus** (Connections โ Prometheus โ Details):
21
+
- Remote Write endpoint: `https://prometheus-prod-XXX.grafana.net/api/prom`
22
+
- Create API token with write permissions
23
+
24
+
## 3. Configure Wisp.place
25
+
26
+
Add to your `.env`:
27
+
28
+
```bash
29
+
GRAFANA_LOKI_URL=https://logs-prod-XXX.grafana.net
30
+
GRAFANA_LOKI_TOKEN=glc_eyJ...
31
+
32
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-XXX.grafana.net/api/prom
33
+
GRAFANA_PROMETHEUS_TOKEN=glc_eyJ...
34
+
```
35
+
36
+
## 4. Create Dashboard
37
+
38
+
Import this dashboard JSON or build your own:
39
+
40
+
```json
41
+
{
42
+
"panels": [
43
+
{
44
+
"title": "Request Rate",
45
+
"targets": [{
46
+
"expr": "sum(rate(http_requests_total[1m])) by (service)"
47
+
}]
48
+
},
49
+
{
50
+
"title": "P95 Latency",
51
+
"targets": [{
52
+
"expr": "histogram_quantile(0.95, rate(http_request_duration_ms_bucket[5m]))"
53
+
}]
54
+
},
55
+
{
56
+
"title": "Error Rate",
57
+
"targets": [{
58
+
"expr": "sum(rate(errors_total[5m])) / sum(rate(http_requests_total[5m]))"
59
+
}]
60
+
}
61
+
]
62
+
}
63
+
```
64
+
65
+
## 5. Set Alerts
66
+
67
+
Example alert for high error rate:
68
+
69
+
```yaml
70
+
alert: HighErrorRate
71
+
expr: |
72
+
sum(rate(errors_total[5m])) by (service) /
73
+
sum(rate(http_requests_total[5m])) by (service) > 0.05
74
+
for: 5m
75
+
annotations:
76
+
summary: "High error rate in {{ $labels.service }}"
77
+
```
78
+
79
+
## Verify Data Flow
80
+
81
+
Check Grafana Explore:
82
+
- Loki: `{job="main-app"} | json`
83
+
- Prometheus: `http_requests_total`
84
+
85
+
Data should appear within 30 seconds of service startup.
+156
docs/src/content/docs/monitoring.md
+156
docs/src/content/docs/monitoring.md
···
···
1
+
---
2
+
title: Monitoring & Metrics
3
+
description: Track performance and debug issues with Grafana integration
4
+
---
5
+
6
+
Wisp.place includes built-in observability with automatic Grafana integration for logs and metrics. Monitor request performance, track errors, and analyze usage patterns across both the main backend and hosting service.
7
+
8
+
## Quick Start
9
+
10
+
Set environment variables to enable Grafana export:
11
+
12
+
```bash
13
+
# Grafana Cloud
14
+
GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net
15
+
GRAFANA_LOKI_TOKEN=glc_xxx
16
+
17
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom
18
+
GRAFANA_PROMETHEUS_TOKEN=glc_xxx
19
+
20
+
# Self-hosted Grafana
21
+
GRAFANA_LOKI_USERNAME=your-username
22
+
GRAFANA_LOKI_PASSWORD=your-password
23
+
```
24
+
25
+
Restart services. Metrics and logs now flow to Grafana automatically.
26
+
27
+
## Metrics Collected
28
+
29
+
### HTTP Requests
30
+
- `http_requests_total` - Total request count by path, method, status
31
+
- `http_request_duration_ms` - Request duration histogram
32
+
- `errors_total` - Error count by service
33
+
34
+
### Performance Stats
35
+
- P50, P95, P99 response times
36
+
- Requests per minute
37
+
- Error rates
38
+
- Average duration by endpoint
39
+
40
+
## Log Aggregation
41
+
42
+
Logs are sent to Loki with automatic categorization:
43
+
44
+
```
45
+
{job="main-app"} |= "error" # OAuth and upload errors
46
+
{job="hosting-service"} |= "cache" # Cache operations
47
+
{service="hosting-service", level="warn"} # Warnings only
48
+
```
49
+
50
+
## Service Identification
51
+
52
+
Each service is tagged separately:
53
+
- `main-app` - OAuth, uploads, domain management
54
+
- `hosting-service` - Firehose, caching, content serving
55
+
56
+
## Configuration Options
57
+
58
+
### Environment Variables
59
+
60
+
```bash
61
+
# Required
62
+
GRAFANA_LOKI_URL # Loki endpoint
63
+
GRAFANA_PROMETHEUS_URL # Prometheus endpoint (add /api/prom for OTLP)
64
+
65
+
# Authentication (use one)
66
+
GRAFANA_LOKI_TOKEN # Bearer token (Grafana Cloud)
67
+
GRAFANA_LOKI_USERNAME # Basic auth (self-hosted)
68
+
GRAFANA_LOKI_PASSWORD
69
+
70
+
# Optional
71
+
GRAFANA_BATCH_SIZE=100 # Batch size before flush
72
+
GRAFANA_FLUSH_INTERVAL=5000 # Flush interval in ms
73
+
```
74
+
75
+
### Programmatic Setup
76
+
77
+
```typescript
78
+
import { initializeGrafanaExporters } from '@wisp/observability'
79
+
80
+
initializeGrafanaExporters({
81
+
lokiUrl: 'https://logs.grafana.net',
82
+
lokiAuth: { bearerToken: 'token' },
83
+
prometheusUrl: 'https://prometheus.grafana.net/api/prom',
84
+
prometheusAuth: { bearerToken: 'token' },
85
+
serviceName: 'my-service',
86
+
batchSize: 100,
87
+
flushIntervalMs: 5000
88
+
})
89
+
```
90
+
91
+
## Grafana Dashboard Queries
92
+
93
+
### Request Performance
94
+
```promql
95
+
# Average response time by endpoint
96
+
avg by (path) (
97
+
rate(http_request_duration_ms_sum[5m]) /
98
+
rate(http_request_duration_ms_count[5m])
99
+
)
100
+
101
+
# Request rate
102
+
sum(rate(http_requests_total[1m])) by (service)
103
+
104
+
# Error rate
105
+
sum(rate(errors_total[5m])) by (service) /
106
+
sum(rate(http_requests_total[5m])) by (service)
107
+
```
108
+
109
+
### Log Analysis
110
+
```logql
111
+
# Recent errors
112
+
{job="main-app"} |= "error" | json
113
+
114
+
# Slow requests (>1s)
115
+
{job="hosting-service"} |~ "duration.*[1-9][0-9]{3,}"
116
+
117
+
# Failed OAuth attempts
118
+
{job="main-app"} |= "OAuth" |= "failed"
119
+
```
120
+
121
+
## Troubleshooting
122
+
123
+
### Logs not appearing
124
+
- Check `GRAFANA_LOKI_URL` is correct (no trailing `/loki/api/v1/push`)
125
+
- Verify authentication token/credentials
126
+
- Look for connection errors in service logs
127
+
128
+
### Metrics missing
129
+
- Ensure `GRAFANA_PROMETHEUS_URL` includes `/api/prom` suffix
130
+
- Check firewall rules allow outbound HTTPS
131
+
- Verify OpenTelemetry export errors in logs
132
+
133
+
### High memory usage
134
+
- Reduce `GRAFANA_BATCH_SIZE` (default: 100)
135
+
- Lower `GRAFANA_FLUSH_INTERVAL` to flush more frequently
136
+
137
+
## Local Development
138
+
139
+
Metrics and logs are stored in-memory when Grafana isn't configured. Access them via:
140
+
141
+
- `http://localhost:8000/api/observability/logs`
142
+
- `http://localhost:8000/api/observability/metrics`
143
+
- `http://localhost:8000/api/observability/errors`
144
+
145
+
## Testing Integration
146
+
147
+
Run integration tests to verify setup:
148
+
149
+
```bash
150
+
cd packages/@wisp/observability
151
+
bun test src/integration-test.test.ts
152
+
153
+
# Test with live Grafana
154
+
GRAFANA_LOKI_URL=... GRAFANA_LOKI_USERNAME=... GRAFANA_LOKI_PASSWORD=... \
155
+
bun test src/integration-test.test.ts
156
+
```
+15
-15
docs/src/styles/custom.css
+15
-15
docs/src/styles/custom.css
···
5
/* Increase base font size by 10% */
6
font-size: 110%;
7
8
-
/* Light theme - Warm beige background from app */
9
-
--sl-color-bg: oklch(0.90 0.012 35);
10
-
--sl-color-bg-sidebar: oklch(0.93 0.01 35);
11
-
--sl-color-bg-nav: oklch(0.93 0.01 35);
12
-
--sl-color-text: oklch(0.18 0.01 30);
13
-
--sl-color-text-accent: oklch(0.78 0.15 345);
14
-
--sl-color-accent: oklch(0.78 0.15 345);
15
-
--sl-color-accent-low: oklch(0.95 0.03 345);
16
-
--sl-color-border: oklch(0.75 0.015 30);
17
-
--sl-color-gray-1: oklch(0.52 0.015 30);
18
-
--sl-color-gray-2: oklch(0.42 0.015 30);
19
-
--sl-color-gray-3: oklch(0.33 0.015 30);
20
-
--sl-color-gray-4: oklch(0.25 0.015 30);
21
-
--sl-color-gray-5: oklch(0.75 0.015 30);
22
--sl-color-bg-accent: oklch(0.88 0.01 35);
23
}
24
···
70
/* Sidebar active/hover state text contrast fix */
71
.sidebar a[aria-current="page"],
72
.sidebar a[aria-current="page"] span {
73
-
color: oklch(0.23 0.015 285) !important;
74
}
75
76
[data-theme="dark"] .sidebar a[aria-current="page"],
···
5
/* Increase base font size by 10% */
6
font-size: 110%;
7
8
+
/* Light theme - Warm beige with improved contrast */
9
+
--sl-color-bg: oklch(0.92 0.012 35);
10
+
--sl-color-bg-sidebar: oklch(0.95 0.008 35);
11
+
--sl-color-bg-nav: oklch(0.95 0.008 35);
12
+
--sl-color-text: oklch(0.15 0.015 30);
13
+
--sl-color-text-accent: oklch(0.65 0.18 345);
14
+
--sl-color-accent: oklch(0.65 0.18 345);
15
+
--sl-color-accent-low: oklch(0.92 0.05 345);
16
+
--sl-color-border: oklch(0.65 0.02 30);
17
+
--sl-color-gray-1: oklch(0.45 0.02 30);
18
+
--sl-color-gray-2: oklch(0.35 0.02 30);
19
+
--sl-color-gray-3: oklch(0.28 0.02 30);
20
+
--sl-color-gray-4: oklch(0.20 0.015 30);
21
+
--sl-color-gray-5: oklch(0.65 0.02 30);
22
--sl-color-bg-accent: oklch(0.88 0.01 35);
23
}
24
···
70
/* Sidebar active/hover state text contrast fix */
71
.sidebar a[aria-current="page"],
72
.sidebar a[aria-current="page"] span {
73
+
color: oklch(0.15 0.015 30) !important;
74
}
75
76
[data-theme="dark"] .sidebar a[aria-current="page"],
-318
flake.lock
-318
flake.lock
···
1
-
{
2
-
"nodes": {
3
-
"crane": {
4
-
"flake": false,
5
-
"locked": {
6
-
"lastModified": 1758758545,
7
-
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
8
-
"owner": "ipetkov",
9
-
"repo": "crane",
10
-
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
11
-
"type": "github"
12
-
},
13
-
"original": {
14
-
"owner": "ipetkov",
15
-
"ref": "v0.21.1",
16
-
"repo": "crane",
17
-
"type": "github"
18
-
}
19
-
},
20
-
"dream2nix": {
21
-
"inputs": {
22
-
"nixpkgs": [
23
-
"nci",
24
-
"nixpkgs"
25
-
],
26
-
"purescript-overlay": "purescript-overlay",
27
-
"pyproject-nix": "pyproject-nix"
28
-
},
29
-
"locked": {
30
-
"lastModified": 1754978539,
31
-
"narHash": "sha256-nrDovydywSKRbWim9Ynmgj8SBm8LK3DI2WuhIqzOHYI=",
32
-
"owner": "nix-community",
33
-
"repo": "dream2nix",
34
-
"rev": "fbec3263cb4895ac86ee9506cdc4e6919a1a2214",
35
-
"type": "github"
36
-
},
37
-
"original": {
38
-
"owner": "nix-community",
39
-
"repo": "dream2nix",
40
-
"type": "github"
41
-
}
42
-
},
43
-
"fenix": {
44
-
"inputs": {
45
-
"nixpkgs": [
46
-
"nixpkgs"
47
-
],
48
-
"rust-analyzer-src": "rust-analyzer-src"
49
-
},
50
-
"locked": {
51
-
"lastModified": 1762584108,
52
-
"narHash": "sha256-wZUW7dlXMXaRdvNbaADqhF8gg9bAfFiMV+iyFQiDv+Y=",
53
-
"owner": "nix-community",
54
-
"repo": "fenix",
55
-
"rev": "32f3ad3b6c690061173e1ac16708874975ec6056",
56
-
"type": "github"
57
-
},
58
-
"original": {
59
-
"owner": "nix-community",
60
-
"repo": "fenix",
61
-
"type": "github"
62
-
}
63
-
},
64
-
"flake-compat": {
65
-
"flake": false,
66
-
"locked": {
67
-
"lastModified": 1696426674,
68
-
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
69
-
"owner": "edolstra",
70
-
"repo": "flake-compat",
71
-
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
72
-
"type": "github"
73
-
},
74
-
"original": {
75
-
"owner": "edolstra",
76
-
"repo": "flake-compat",
77
-
"type": "github"
78
-
}
79
-
},
80
-
"mk-naked-shell": {
81
-
"flake": false,
82
-
"locked": {
83
-
"lastModified": 1681286841,
84
-
"narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
85
-
"owner": "90-008",
86
-
"repo": "mk-naked-shell",
87
-
"rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
88
-
"type": "github"
89
-
},
90
-
"original": {
91
-
"owner": "90-008",
92
-
"repo": "mk-naked-shell",
93
-
"type": "github"
94
-
}
95
-
},
96
-
"nci": {
97
-
"inputs": {
98
-
"crane": "crane",
99
-
"dream2nix": "dream2nix",
100
-
"mk-naked-shell": "mk-naked-shell",
101
-
"nixpkgs": [
102
-
"nixpkgs"
103
-
],
104
-
"parts": "parts",
105
-
"rust-overlay": "rust-overlay",
106
-
"treefmt": "treefmt"
107
-
},
108
-
"locked": {
109
-
"lastModified": 1762582646,
110
-
"narHash": "sha256-MMzE4xccG+8qbLhdaZoeFDUKWUOn3B4lhp5dZmgukmM=",
111
-
"owner": "90-008",
112
-
"repo": "nix-cargo-integration",
113
-
"rev": "0993c449377049fa8868a664e8290ac6658e0b9a",
114
-
"type": "github"
115
-
},
116
-
"original": {
117
-
"owner": "90-008",
118
-
"repo": "nix-cargo-integration",
119
-
"type": "github"
120
-
}
121
-
},
122
-
"nixpkgs": {
123
-
"locked": {
124
-
"lastModified": 1762361079,
125
-
"narHash": "sha256-lz718rr1BDpZBYk7+G8cE6wee3PiBUpn8aomG/vLLiY=",
126
-
"owner": "nixos",
127
-
"repo": "nixpkgs",
128
-
"rev": "ffcdcf99d65c61956d882df249a9be53e5902ea5",
129
-
"type": "github"
130
-
},
131
-
"original": {
132
-
"owner": "nixos",
133
-
"ref": "nixpkgs-unstable",
134
-
"repo": "nixpkgs",
135
-
"type": "github"
136
-
}
137
-
},
138
-
"parts": {
139
-
"inputs": {
140
-
"nixpkgs-lib": [
141
-
"nci",
142
-
"nixpkgs"
143
-
]
144
-
},
145
-
"locked": {
146
-
"lastModified": 1762440070,
147
-
"narHash": "sha256-xxdepIcb39UJ94+YydGP221rjnpkDZUlykKuF54PsqI=",
148
-
"owner": "hercules-ci",
149
-
"repo": "flake-parts",
150
-
"rev": "26d05891e14c88eb4a5d5bee659c0db5afb609d8",
151
-
"type": "github"
152
-
},
153
-
"original": {
154
-
"owner": "hercules-ci",
155
-
"repo": "flake-parts",
156
-
"type": "github"
157
-
}
158
-
},
159
-
"parts_2": {
160
-
"inputs": {
161
-
"nixpkgs-lib": [
162
-
"nixpkgs"
163
-
]
164
-
},
165
-
"locked": {
166
-
"lastModified": 1762440070,
167
-
"narHash": "sha256-xxdepIcb39UJ94+YydGP221rjnpkDZUlykKuF54PsqI=",
168
-
"owner": "hercules-ci",
169
-
"repo": "flake-parts",
170
-
"rev": "26d05891e14c88eb4a5d5bee659c0db5afb609d8",
171
-
"type": "github"
172
-
},
173
-
"original": {
174
-
"owner": "hercules-ci",
175
-
"repo": "flake-parts",
176
-
"type": "github"
177
-
}
178
-
},
179
-
"purescript-overlay": {
180
-
"inputs": {
181
-
"flake-compat": "flake-compat",
182
-
"nixpkgs": [
183
-
"nci",
184
-
"dream2nix",
185
-
"nixpkgs"
186
-
],
187
-
"slimlock": "slimlock"
188
-
},
189
-
"locked": {
190
-
"lastModified": 1728546539,
191
-
"narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=",
192
-
"owner": "thomashoneyman",
193
-
"repo": "purescript-overlay",
194
-
"rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4",
195
-
"type": "github"
196
-
},
197
-
"original": {
198
-
"owner": "thomashoneyman",
199
-
"repo": "purescript-overlay",
200
-
"type": "github"
201
-
}
202
-
},
203
-
"pyproject-nix": {
204
-
"inputs": {
205
-
"nixpkgs": [
206
-
"nci",
207
-
"dream2nix",
208
-
"nixpkgs"
209
-
]
210
-
},
211
-
"locked": {
212
-
"lastModified": 1752481895,
213
-
"narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=",
214
-
"owner": "pyproject-nix",
215
-
"repo": "pyproject.nix",
216
-
"rev": "16ee295c25107a94e59a7fc7f2e5322851781162",
217
-
"type": "github"
218
-
},
219
-
"original": {
220
-
"owner": "pyproject-nix",
221
-
"repo": "pyproject.nix",
222
-
"type": "github"
223
-
}
224
-
},
225
-
"root": {
226
-
"inputs": {
227
-
"fenix": "fenix",
228
-
"nci": "nci",
229
-
"nixpkgs": "nixpkgs",
230
-
"parts": "parts_2"
231
-
}
232
-
},
233
-
"rust-analyzer-src": {
234
-
"flake": false,
235
-
"locked": {
236
-
"lastModified": 1762438844,
237
-
"narHash": "sha256-ApIKJf6CcMsV2nYBXhGF95BmZMO/QXPhgfSnkA/rVUo=",
238
-
"owner": "rust-lang",
239
-
"repo": "rust-analyzer",
240
-
"rev": "4bf516ee5a960c1e2eee9fedd9b1c9e976a19c86",
241
-
"type": "github"
242
-
},
243
-
"original": {
244
-
"owner": "rust-lang",
245
-
"ref": "nightly",
246
-
"repo": "rust-analyzer",
247
-
"type": "github"
248
-
}
249
-
},
250
-
"rust-overlay": {
251
-
"inputs": {
252
-
"nixpkgs": [
253
-
"nci",
254
-
"nixpkgs"
255
-
]
256
-
},
257
-
"locked": {
258
-
"lastModified": 1762569282,
259
-
"narHash": "sha256-vINZAJpXQTZd5cfh06Rcw7hesH7sGSvi+Tn+HUieJn8=",
260
-
"owner": "oxalica",
261
-
"repo": "rust-overlay",
262
-
"rev": "a35a6144b976f70827c2fe2f5c89d16d8f9179d8",
263
-
"type": "github"
264
-
},
265
-
"original": {
266
-
"owner": "oxalica",
267
-
"repo": "rust-overlay",
268
-
"type": "github"
269
-
}
270
-
},
271
-
"slimlock": {
272
-
"inputs": {
273
-
"nixpkgs": [
274
-
"nci",
275
-
"dream2nix",
276
-
"purescript-overlay",
277
-
"nixpkgs"
278
-
]
279
-
},
280
-
"locked": {
281
-
"lastModified": 1688756706,
282
-
"narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=",
283
-
"owner": "thomashoneyman",
284
-
"repo": "slimlock",
285
-
"rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c",
286
-
"type": "github"
287
-
},
288
-
"original": {
289
-
"owner": "thomashoneyman",
290
-
"repo": "slimlock",
291
-
"type": "github"
292
-
}
293
-
},
294
-
"treefmt": {
295
-
"inputs": {
296
-
"nixpkgs": [
297
-
"nci",
298
-
"nixpkgs"
299
-
]
300
-
},
301
-
"locked": {
302
-
"lastModified": 1762410071,
303
-
"narHash": "sha256-aF5fvoZeoXNPxT0bejFUBXeUjXfHLSL7g+mjR/p5TEg=",
304
-
"owner": "numtide",
305
-
"repo": "treefmt-nix",
306
-
"rev": "97a30861b13c3731a84e09405414398fbf3e109f",
307
-
"type": "github"
308
-
},
309
-
"original": {
310
-
"owner": "numtide",
311
-
"repo": "treefmt-nix",
312
-
"type": "github"
313
-
}
314
-
}
315
-
},
316
-
"root": "root",
317
-
"version": 7
318
-
}
···
-59
flake.nix
-59
flake.nix
···
1
-
{
2
-
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
3
-
inputs.nci.url = "github:90-008/nix-cargo-integration";
4
-
inputs.nci.inputs.nixpkgs.follows = "nixpkgs";
5
-
inputs.parts.url = "github:hercules-ci/flake-parts";
6
-
inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs";
7
-
inputs.fenix = {
8
-
url = "github:nix-community/fenix";
9
-
inputs.nixpkgs.follows = "nixpkgs";
10
-
};
11
-
12
-
outputs = inputs @ {
13
-
parts,
14
-
nci,
15
-
...
16
-
}:
17
-
parts.lib.mkFlake {inherit inputs;} {
18
-
systems = ["x86_64-linux" "aarch64-darwin"];
19
-
imports = [
20
-
nci.flakeModule
21
-
./crates.nix
22
-
];
23
-
perSystem = {
24
-
pkgs,
25
-
config,
26
-
...
27
-
}: let
28
-
crateOutputs = config.nci.outputs."wisp-cli";
29
-
mkRenamedPackage = name: pkg: isWindows: pkgs.runCommand name {} ''
30
-
mkdir -p $out/bin
31
-
if [ -f ${pkg}/bin/wisp-cli.exe ]; then
32
-
cp ${pkg}/bin/wisp-cli.exe $out/bin/${name}
33
-
elif [ -f ${pkg}/bin/wisp-cli ]; then
34
-
cp ${pkg}/bin/wisp-cli $out/bin/${name}
35
-
else
36
-
echo "Error: Could not find wisp-cli binary in ${pkg}/bin/"
37
-
ls -la ${pkg}/bin/ || true
38
-
exit 1
39
-
fi
40
-
'';
41
-
in {
42
-
devShells.default = crateOutputs.devShell;
43
-
packages.default = crateOutputs.packages.release;
44
-
packages.wisp-cli-x86_64-linux = mkRenamedPackage "wisp-cli-x86_64-linux" crateOutputs.packages.release false;
45
-
packages.wisp-cli-aarch64-linux = mkRenamedPackage "wisp-cli-aarch64-linux" crateOutputs.allTargets."aarch64-unknown-linux-gnu".packages.release false;
46
-
packages.wisp-cli-x86_64-windows = mkRenamedPackage "wisp-cli-x86_64-windows.exe" crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release true;
47
-
packages.wisp-cli-aarch64-darwin = mkRenamedPackage "wisp-cli-aarch64-darwin" crateOutputs.allTargets."aarch64-apple-darwin".packages.release false;
48
-
packages.all = pkgs.symlinkJoin {
49
-
name = "wisp-cli-all";
50
-
paths = [
51
-
config.packages.wisp-cli-x86_64-linux
52
-
config.packages.wisp-cli-aarch64-linux
53
-
config.packages.wisp-cli-x86_64-windows
54
-
config.packages.wisp-cli-aarch64-darwin
55
-
];
56
-
};
57
-
};
58
-
};
59
-
}
···
+59
lexicons/fs.json
+59
lexicons/fs.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "place.wisp.fs",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Virtual filesystem manifest for a Wisp site",
8
+
"record": {
9
+
"type": "object",
10
+
"required": ["site", "root", "createdAt"],
11
+
"properties": {
12
+
"site": { "type": "string" },
13
+
"root": { "type": "ref", "ref": "#directory" },
14
+
"fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
15
+
"createdAt": { "type": "string", "format": "datetime" }
16
+
}
17
+
}
18
+
},
19
+
"file": {
20
+
"type": "object",
21
+
"required": ["type", "blob"],
22
+
"properties": {
23
+
"type": { "type": "string", "const": "file" },
24
+
"blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" },
25
+
"encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" },
26
+
"mimeType": { "type": "string", "description": "Original MIME type before compression" },
27
+
"base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } }
28
+
},
29
+
"directory": {
30
+
"type": "object",
31
+
"required": ["type", "entries"],
32
+
"properties": {
33
+
"type": { "type": "string", "const": "directory" },
34
+
"entries": {
35
+
"type": "array",
36
+
"maxLength": 500,
37
+
"items": { "type": "ref", "ref": "#entry" }
38
+
}
39
+
}
40
+
},
41
+
"entry": {
42
+
"type": "object",
43
+
"required": ["name", "node"],
44
+
"properties": {
45
+
"name": { "type": "string", "maxLength": 255 },
46
+
"node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] }
47
+
}
48
+
},
49
+
"subfs": {
50
+
"type": "object",
51
+
"required": ["type", "subject"],
52
+
"properties": {
53
+
"type": { "type": "string", "const": "subfs" },
54
+
"subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree." },
55
+
"flat": { "type": "boolean", "description": "If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure." }
56
+
}
57
+
}
58
+
}
59
+
}
+76
lexicons/settings.json
+76
lexicons/settings.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "place.wisp.settings",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Configuration settings for a static site hosted on wisp.place",
8
+
"key": "any",
9
+
"record": {
10
+
"type": "object",
11
+
"properties": {
12
+
"directoryListing": {
13
+
"type": "boolean",
14
+
"description": "Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode.",
15
+
"default": false
16
+
},
17
+
"spaMode": {
18
+
"type": "string",
19
+
"description": "File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.",
20
+
"maxLength": 500
21
+
},
22
+
"custom404": {
23
+
"type": "string",
24
+
"description": "Custom 404 error page file path. Incompatible with directoryListing and spaMode.",
25
+
"maxLength": 500
26
+
},
27
+
"indexFiles": {
28
+
"type": "array",
29
+
"description": "Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.",
30
+
"items": {
31
+
"type": "string",
32
+
"maxLength": 255
33
+
},
34
+
"maxLength": 10
35
+
},
36
+
"cleanUrls": {
37
+
"type": "boolean",
38
+
"description": "Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically.",
39
+
"default": false
40
+
},
41
+
"headers": {
42
+
"type": "array",
43
+
"description": "Custom HTTP headers to set on responses",
44
+
"items": {
45
+
"type": "ref",
46
+
"ref": "#customHeader"
47
+
},
48
+
"maxLength": 50
49
+
}
50
+
}
51
+
}
52
+
},
53
+
"customHeader": {
54
+
"type": "object",
55
+
"description": "Custom HTTP header configuration",
56
+
"required": ["name", "value"],
57
+
"properties": {
58
+
"name": {
59
+
"type": "string",
60
+
"description": "HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')",
61
+
"maxLength": 100
62
+
},
63
+
"value": {
64
+
"type": "string",
65
+
"description": "HTTP header value",
66
+
"maxLength": 1000
67
+
},
68
+
"path": {
69
+
"type": "string",
70
+
"description": "Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.",
71
+
"maxLength": 500
72
+
}
73
+
}
74
+
}
75
+
}
76
+
}
+59
lexicons/subfs.json
+59
lexicons/subfs.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "place.wisp.subfs",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.",
8
+
"record": {
9
+
"type": "object",
10
+
"required": ["root", "createdAt"],
11
+
"properties": {
12
+
"root": { "type": "ref", "ref": "#directory" },
13
+
"fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
14
+
"createdAt": { "type": "string", "format": "datetime" }
15
+
}
16
+
}
17
+
},
18
+
"file": {
19
+
"type": "object",
20
+
"required": ["type", "blob"],
21
+
"properties": {
22
+
"type": { "type": "string", "const": "file" },
23
+
"blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" },
24
+
"encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" },
25
+
"mimeType": { "type": "string", "description": "Original MIME type before compression" },
26
+
"base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" }
27
+
}
28
+
},
29
+
"directory": {
30
+
"type": "object",
31
+
"required": ["type", "entries"],
32
+
"properties": {
33
+
"type": { "type": "string", "const": "directory" },
34
+
"entries": {
35
+
"type": "array",
36
+
"maxLength": 500,
37
+
"items": { "type": "ref", "ref": "#entry" }
38
+
}
39
+
}
40
+
},
41
+
"entry": {
42
+
"type": "object",
43
+
"required": ["name", "node"],
44
+
"properties": {
45
+
"name": { "type": "string", "maxLength": 255 },
46
+
"node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] }
47
+
}
48
+
},
49
+
"subfs": {
50
+
"type": "object",
51
+
"required": ["type", "subject"],
52
+
"properties": {
53
+
"type": { "type": "string", "const": "subfs" },
54
+
"subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures." }
55
+
}
56
+
}
57
+
}
58
+
}
59
+
+13
-1
package.json
+13
-1
package.json
···
9
],
10
"dependencies": {
11
"@tailwindcss/cli": "^4.1.17",
12
"bun-plugin-tailwind": "^0.1.2",
13
"tailwindcss": "^4.1.17"
14
},
15
"scripts": {
···
19
"build": "cd apps/main-app && bun run build.ts",
20
"build:hosting": "cd apps/hosting-service && npm run build",
21
"build:all": "bun run build && npm run build:hosting",
22
"screenshot": "bun run apps/main-app/scripts/screenshot-sites.ts",
23
"hosting:dev": "cd apps/hosting-service && npm run dev",
24
-
"hosting:start": "cd apps/hosting-service && npm run start"
25
}
26
}
···
9
],
10
"dependencies": {
11
"@tailwindcss/cli": "^4.1.17",
12
+
"atproto-ui": "^0.12.0",
13
"bun-plugin-tailwind": "^0.1.2",
14
+
"elysia": "^1.4.18",
15
"tailwindcss": "^4.1.17"
16
},
17
"scripts": {
···
21
"build": "cd apps/main-app && bun run build.ts",
22
"build:hosting": "cd apps/hosting-service && npm run build",
23
"build:all": "bun run build && npm run build:hosting",
24
+
"check": "cd apps/main-app && npm run check && cd ../hosting-service && npm run check",
25
"screenshot": "bun run apps/main-app/scripts/screenshot-sites.ts",
26
"hosting:dev": "cd apps/hosting-service && npm run dev",
27
+
"hosting:start": "cd apps/hosting-service && npm run start",
28
+
"codegen": "./scripts/codegen.sh"
29
+
},
30
+
"trustedDependencies": [
31
+
"@parcel/watcher",
32
+
"bun",
33
+
"esbuild"
34
+
],
35
+
"devDependencies": {
36
+
"@types/bun": "^1.3.5"
37
}
38
}
+3
packages/@wisp/atproto-utils/package.json
+3
packages/@wisp/atproto-utils/package.json
+1
-1
packages/@wisp/constants/src/index.ts
+1
-1
packages/@wisp/constants/src/index.ts
+244
packages/@wisp/fs-utils/src/tree.test.ts
+244
packages/@wisp/fs-utils/src/tree.test.ts
···
···
1
+
import { describe, test, expect } from 'bun:test'
2
+
import { processUploadedFiles, type UploadedFile } from './tree'
3
+
4
+
describe('processUploadedFiles', () => {
5
+
test('should preserve nested directory structure', () => {
6
+
const files: UploadedFile[] = [
7
+
{
8
+
name: 'mysite/index.html',
9
+
content: Buffer.from('<html>'),
10
+
mimeType: 'text/html',
11
+
size: 6
12
+
},
13
+
{
14
+
name: 'mysite/_astro/main.js',
15
+
content: Buffer.from('console.log()'),
16
+
mimeType: 'application/javascript',
17
+
size: 13
18
+
},
19
+
{
20
+
name: 'mysite/_astro/styles.css',
21
+
content: Buffer.from('body {}'),
22
+
mimeType: 'text/css',
23
+
size: 7
24
+
},
25
+
{
26
+
name: 'mysite/images/logo.png',
27
+
content: Buffer.from([0x89, 0x50, 0x4e, 0x47]),
28
+
mimeType: 'image/png',
29
+
size: 4
30
+
}
31
+
]
32
+
33
+
const result = processUploadedFiles(files)
34
+
35
+
expect(result.fileCount).toBe(4)
36
+
expect(result.directory.entries).toHaveLength(3) // index.html, _astro/, images/
37
+
38
+
// Check _astro directory exists
39
+
const astroEntry = result.directory.entries.find(e => e.name === '_astro')
40
+
expect(astroEntry).toBeTruthy()
41
+
expect('type' in astroEntry!.node && astroEntry!.node.type).toBe('directory')
42
+
43
+
if ('entries' in astroEntry!.node) {
44
+
const astroDir = astroEntry!.node
45
+
expect(astroDir.entries).toHaveLength(2) // main.js, styles.css
46
+
expect(astroDir.entries.find(e => e.name === 'main.js')).toBeTruthy()
47
+
expect(astroDir.entries.find(e => e.name === 'styles.css')).toBeTruthy()
48
+
}
49
+
50
+
// Check images directory exists
51
+
const imagesEntry = result.directory.entries.find(e => e.name === 'images')
52
+
expect(imagesEntry).toBeTruthy()
53
+
expect('type' in imagesEntry!.node && imagesEntry!.node.type).toBe('directory')
54
+
55
+
if ('entries' in imagesEntry!.node) {
56
+
const imagesDir = imagesEntry!.node
57
+
expect(imagesDir.entries).toHaveLength(1) // logo.png
58
+
expect(imagesDir.entries.find(e => e.name === 'logo.png')).toBeTruthy()
59
+
}
60
+
})
61
+
62
+
test('should handle deeply nested directories', () => {
63
+
const files: UploadedFile[] = [
64
+
{
65
+
name: 'site/a/b/c/d/deep.txt',
66
+
content: Buffer.from('deep'),
67
+
mimeType: 'text/plain',
68
+
size: 4
69
+
}
70
+
]
71
+
72
+
const result = processUploadedFiles(files)
73
+
74
+
expect(result.fileCount).toBe(1)
75
+
76
+
// Navigate through nested structure
77
+
const aEntry = result.directory.entries.find(e => e.name === 'a')
78
+
expect(aEntry).toBeTruthy()
79
+
expect('type' in aEntry!.node && aEntry!.node.type).toBe('directory')
80
+
81
+
if ('entries' in aEntry!.node) {
82
+
const bEntry = aEntry!.node.entries.find(e => e.name === 'b')
83
+
expect(bEntry).toBeTruthy()
84
+
expect('type' in bEntry!.node && bEntry!.node.type).toBe('directory')
85
+
86
+
if ('entries' in bEntry!.node) {
87
+
const cEntry = bEntry!.node.entries.find(e => e.name === 'c')
88
+
expect(cEntry).toBeTruthy()
89
+
expect('type' in cEntry!.node && cEntry!.node.type).toBe('directory')
90
+
91
+
if ('entries' in cEntry!.node) {
92
+
const dEntry = cEntry!.node.entries.find(e => e.name === 'd')
93
+
expect(dEntry).toBeTruthy()
94
+
expect('type' in dEntry!.node && dEntry!.node.type).toBe('directory')
95
+
96
+
if ('entries' in dEntry!.node) {
97
+
const fileEntry = dEntry!.node.entries.find(e => e.name === 'deep.txt')
98
+
expect(fileEntry).toBeTruthy()
99
+
expect('type' in fileEntry!.node && fileEntry!.node.type).toBe('file')
100
+
}
101
+
}
102
+
}
103
+
}
104
+
})
105
+
106
+
test('should handle files at root level', () => {
107
+
const files: UploadedFile[] = [
108
+
{
109
+
name: 'mysite/index.html',
110
+
content: Buffer.from('<html>'),
111
+
mimeType: 'text/html',
112
+
size: 6
113
+
},
114
+
{
115
+
name: 'mysite/robots.txt',
116
+
content: Buffer.from('User-agent: *'),
117
+
mimeType: 'text/plain',
118
+
size: 13
119
+
}
120
+
]
121
+
122
+
const result = processUploadedFiles(files)
123
+
124
+
expect(result.fileCount).toBe(2)
125
+
expect(result.directory.entries).toHaveLength(2)
126
+
expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
127
+
expect(result.directory.entries.find(e => e.name === 'robots.txt')).toBeTruthy()
128
+
})
129
+
130
+
test('should skip .git directories', () => {
131
+
const files: UploadedFile[] = [
132
+
{
133
+
name: 'mysite/index.html',
134
+
content: Buffer.from('<html>'),
135
+
mimeType: 'text/html',
136
+
size: 6
137
+
},
138
+
{
139
+
name: 'mysite/.git/config',
140
+
content: Buffer.from('[core]'),
141
+
mimeType: 'text/plain',
142
+
size: 6
143
+
},
144
+
{
145
+
name: 'mysite/.gitignore',
146
+
content: Buffer.from('node_modules'),
147
+
mimeType: 'text/plain',
148
+
size: 12
149
+
}
150
+
]
151
+
152
+
const result = processUploadedFiles(files)
153
+
154
+
expect(result.fileCount).toBe(2) // Only index.html and .gitignore
155
+
expect(result.directory.entries).toHaveLength(2)
156
+
expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
157
+
expect(result.directory.entries.find(e => e.name === '.gitignore')).toBeTruthy()
158
+
expect(result.directory.entries.find(e => e.name === '.git')).toBeFalsy()
159
+
})
160
+
161
+
test('should handle mixed root and nested files', () => {
162
+
const files: UploadedFile[] = [
163
+
{
164
+
name: 'mysite/index.html',
165
+
content: Buffer.from('<html>'),
166
+
mimeType: 'text/html',
167
+
size: 6
168
+
},
169
+
{
170
+
name: 'mysite/about/index.html',
171
+
content: Buffer.from('<html>'),
172
+
mimeType: 'text/html',
173
+
size: 6
174
+
},
175
+
{
176
+
name: 'mysite/about/team.html',
177
+
content: Buffer.from('<html>'),
178
+
mimeType: 'text/html',
179
+
size: 6
180
+
},
181
+
{
182
+
name: 'mysite/robots.txt',
183
+
content: Buffer.from('User-agent: *'),
184
+
mimeType: 'text/plain',
185
+
size: 13
186
+
}
187
+
]
188
+
189
+
const result = processUploadedFiles(files)
190
+
191
+
expect(result.fileCount).toBe(4)
192
+
expect(result.directory.entries).toHaveLength(3) // index.html, about/, robots.txt
193
+
194
+
const aboutEntry = result.directory.entries.find(e => e.name === 'about')
195
+
expect(aboutEntry).toBeTruthy()
196
+
expect('type' in aboutEntry!.node && aboutEntry!.node.type).toBe('directory')
197
+
198
+
if ('entries' in aboutEntry!.node) {
199
+
const aboutDir = aboutEntry!.node
200
+
expect(aboutDir.entries).toHaveLength(2) // index.html, team.html
201
+
expect(aboutDir.entries.find(e => e.name === 'index.html')).toBeTruthy()
202
+
expect(aboutDir.entries.find(e => e.name === 'team.html')).toBeTruthy()
203
+
}
204
+
})
205
+
206
+
test('should handle empty file array', () => {
207
+
const files: UploadedFile[] = []
208
+
209
+
const result = processUploadedFiles(files)
210
+
211
+
expect(result.fileCount).toBe(0)
212
+
expect(result.directory.entries).toHaveLength(0)
213
+
})
214
+
215
+
test('should strip base folder name from paths', () => {
216
+
// This tests the behavior where file.name includes the base folder
217
+
// e.g., "mysite/index.html" should become "index.html" at root
218
+
const files: UploadedFile[] = [
219
+
{
220
+
name: 'build-output/index.html',
221
+
content: Buffer.from('<html>'),
222
+
mimeType: 'text/html',
223
+
size: 6
224
+
},
225
+
{
226
+
name: 'build-output/assets/main.js',
227
+
content: Buffer.from('console.log()'),
228
+
mimeType: 'application/javascript',
229
+
size: 13
230
+
}
231
+
]
232
+
233
+
const result = processUploadedFiles(files)
234
+
235
+
expect(result.fileCount).toBe(2)
236
+
237
+
// Should have index.html at root and assets/ directory
238
+
expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
239
+
expect(result.directory.entries.find(e => e.name === 'assets')).toBeTruthy()
240
+
241
+
// Should NOT have 'build-output' directory
242
+
expect(result.directory.entries.find(e => e.name === 'build-output')).toBeFalsy()
243
+
})
244
+
})
-59
packages/@wisp/lexicons/lexicons/fs.json
-59
packages/@wisp/lexicons/lexicons/fs.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "place.wisp.fs",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"description": "Virtual filesystem manifest for a Wisp site",
8
-
"record": {
9
-
"type": "object",
10
-
"required": ["site", "root", "createdAt"],
11
-
"properties": {
12
-
"site": { "type": "string" },
13
-
"root": { "type": "ref", "ref": "#directory" },
14
-
"fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
15
-
"createdAt": { "type": "string", "format": "datetime" }
16
-
}
17
-
}
18
-
},
19
-
"file": {
20
-
"type": "object",
21
-
"required": ["type", "blob"],
22
-
"properties": {
23
-
"type": { "type": "string", "const": "file" },
24
-
"blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" },
25
-
"encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" },
26
-
"mimeType": { "type": "string", "description": "Original MIME type before compression" },
27
-
"base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } }
28
-
},
29
-
"directory": {
30
-
"type": "object",
31
-
"required": ["type", "entries"],
32
-
"properties": {
33
-
"type": { "type": "string", "const": "directory" },
34
-
"entries": {
35
-
"type": "array",
36
-
"maxLength": 500,
37
-
"items": { "type": "ref", "ref": "#entry" }
38
-
}
39
-
}
40
-
},
41
-
"entry": {
42
-
"type": "object",
43
-
"required": ["name", "node"],
44
-
"properties": {
45
-
"name": { "type": "string", "maxLength": 255 },
46
-
"node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] }
47
-
}
48
-
},
49
-
"subfs": {
50
-
"type": "object",
51
-
"required": ["type", "subject"],
52
-
"properties": {
53
-
"type": { "type": "string", "const": "subfs" },
54
-
"subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree." },
55
-
"flat": { "type": "boolean", "description": "If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure." }
56
-
}
57
-
}
58
-
}
59
-
}
···
-76
packages/@wisp/lexicons/lexicons/settings.json
-76
packages/@wisp/lexicons/lexicons/settings.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "place.wisp.settings",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"description": "Configuration settings for a static site hosted on wisp.place",
8
-
"key": "any",
9
-
"record": {
10
-
"type": "object",
11
-
"properties": {
12
-
"directoryListing": {
13
-
"type": "boolean",
14
-
"description": "Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode.",
15
-
"default": false
16
-
},
17
-
"spaMode": {
18
-
"type": "string",
19
-
"description": "File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.",
20
-
"maxLength": 500
21
-
},
22
-
"custom404": {
23
-
"type": "string",
24
-
"description": "Custom 404 error page file path. Incompatible with directoryListing and spaMode.",
25
-
"maxLength": 500
26
-
},
27
-
"indexFiles": {
28
-
"type": "array",
29
-
"description": "Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.",
30
-
"items": {
31
-
"type": "string",
32
-
"maxLength": 255
33
-
},
34
-
"maxLength": 10
35
-
},
36
-
"cleanUrls": {
37
-
"type": "boolean",
38
-
"description": "Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically.",
39
-
"default": false
40
-
},
41
-
"headers": {
42
-
"type": "array",
43
-
"description": "Custom HTTP headers to set on responses",
44
-
"items": {
45
-
"type": "ref",
46
-
"ref": "#customHeader"
47
-
},
48
-
"maxLength": 50
49
-
}
50
-
}
51
-
}
52
-
},
53
-
"customHeader": {
54
-
"type": "object",
55
-
"description": "Custom HTTP header configuration",
56
-
"required": ["name", "value"],
57
-
"properties": {
58
-
"name": {
59
-
"type": "string",
60
-
"description": "HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')",
61
-
"maxLength": 100
62
-
},
63
-
"value": {
64
-
"type": "string",
65
-
"description": "HTTP header value",
66
-
"maxLength": 1000
67
-
},
68
-
"path": {
69
-
"type": "string",
70
-
"description": "Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.",
71
-
"maxLength": 500
72
-
}
73
-
}
74
-
}
75
-
}
76
-
}
···
-59
packages/@wisp/lexicons/lexicons/subfs.json
-59
packages/@wisp/lexicons/lexicons/subfs.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "place.wisp.subfs",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"description": "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.",
8
-
"record": {
9
-
"type": "object",
10
-
"required": ["root", "createdAt"],
11
-
"properties": {
12
-
"root": { "type": "ref", "ref": "#directory" },
13
-
"fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
14
-
"createdAt": { "type": "string", "format": "datetime" }
15
-
}
16
-
}
17
-
},
18
-
"file": {
19
-
"type": "object",
20
-
"required": ["type", "blob"],
21
-
"properties": {
22
-
"type": { "type": "string", "const": "file" },
23
-
"blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" },
24
-
"encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" },
25
-
"mimeType": { "type": "string", "description": "Original MIME type before compression" },
26
-
"base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" }
27
-
}
28
-
},
29
-
"directory": {
30
-
"type": "object",
31
-
"required": ["type", "entries"],
32
-
"properties": {
33
-
"type": { "type": "string", "const": "directory" },
34
-
"entries": {
35
-
"type": "array",
36
-
"maxLength": 500,
37
-
"items": { "type": "ref", "ref": "#entry" }
38
-
}
39
-
}
40
-
},
41
-
"entry": {
42
-
"type": "object",
43
-
"required": ["name", "node"],
44
-
"properties": {
45
-
"name": { "type": "string", "maxLength": 255 },
46
-
"node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] }
47
-
}
48
-
},
49
-
"subfs": {
50
-
"type": "object",
51
-
"required": ["type", "subject"],
52
-
"properties": {
53
-
"type": { "type": "string", "const": "subfs" },
54
-
"subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures." }
55
-
}
56
-
}
57
-
}
58
-
}
59
-
···
+3
-2
packages/@wisp/lexicons/package.json
+3
-2
packages/@wisp/lexicons/package.json
+1
-1
packages/@wisp/lexicons/src/index.ts
+1
-1
packages/@wisp/lexicons/src/index.ts
+1
-1
packages/@wisp/lexicons/src/lexicons.ts
+1
-1
packages/@wisp/lexicons/src/lexicons.ts
+33
packages/@wisp/observability/.env.example
+33
packages/@wisp/observability/.env.example
···
···
1
+
# Grafana Cloud Configuration for @wisp/observability
2
+
# Copy this file to .env and fill in your actual values
3
+
4
+
# ============================================================================
5
+
# Grafana Loki (for logs)
6
+
# ============================================================================
7
+
GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net
8
+
9
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
10
+
GRAFANA_LOKI_TOKEN=glc_xxx
11
+
12
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
13
+
# GRAFANA_LOKI_USERNAME=your-username
14
+
# GRAFANA_LOKI_PASSWORD=your-password
15
+
16
+
# ============================================================================
17
+
# Grafana Prometheus (for metrics)
18
+
# ============================================================================
19
+
# Note: Add /api/prom to the base URL for OTLP export
20
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom
21
+
22
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
23
+
GRAFANA_PROMETHEUS_TOKEN=glc_xxx
24
+
25
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
26
+
# GRAFANA_PROMETHEUS_USERNAME=your-username
27
+
# GRAFANA_PROMETHEUS_PASSWORD=your-password
28
+
29
+
# ============================================================================
30
+
# Optional: Override service metadata
31
+
# ============================================================================
32
+
# SERVICE_NAME=wisp-app
33
+
# SERVICE_VERSION=1.0.0
+217
packages/@wisp/observability/README.md
+217
packages/@wisp/observability/README.md
···
···
1
+
# @wisp/observability
2
+
3
+
Framework-agnostic observability package with Grafana integration for logs and metrics persistence.
4
+
5
+
## Features
6
+
7
+
- **In-memory storage** for local development
8
+
- **Grafana Loki** integration for log persistence
9
+
- **Prometheus/OTLP** integration for metrics
10
+
- Framework middleware for Elysia and Hono
11
+
- Automatic batching and buffering for efficient data transmission
12
+
13
+
## Installation
14
+
15
+
```bash
16
+
bun add @wisp/observability
17
+
```
18
+
19
+
## Basic Usage
20
+
21
+
### Without Grafana (In-Memory Only)
22
+
23
+
```typescript
24
+
import { createLogger, metricsCollector } from '@wisp/observability'
25
+
26
+
const logger = createLogger('my-service')
27
+
28
+
// Log messages
29
+
logger.info('Server started')
30
+
logger.error('Failed to connect', new Error('Connection refused'))
31
+
32
+
// Record metrics
33
+
metricsCollector.recordRequest('/api/users', 'GET', 200, 45, 'my-service')
34
+
```
35
+
36
+
### With Grafana Integration
37
+
38
+
```typescript
39
+
import { initializeGrafanaExporters, createLogger } from '@wisp/observability'
40
+
41
+
// Initialize at application startup
42
+
initializeGrafanaExporters({
43
+
lokiUrl: 'https://logs-prod.grafana.net',
44
+
lokiAuth: {
45
+
bearerToken: 'your-loki-api-key'
46
+
},
47
+
prometheusUrl: 'https://prometheus-prod.grafana.net',
48
+
prometheusAuth: {
49
+
bearerToken: 'your-prometheus-api-key'
50
+
},
51
+
serviceName: 'wisp-app',
52
+
serviceVersion: '1.0.0',
53
+
batchSize: 100,
54
+
flushIntervalMs: 5000
55
+
})
56
+
57
+
// Now all logs and metrics will be sent to Grafana automatically
58
+
const logger = createLogger('my-service')
59
+
logger.info('This will be sent to Grafana Loki')
60
+
```
61
+
62
+
## Configuration
63
+
64
+
### Environment Variables
65
+
66
+
You can configure Grafana integration using environment variables:
67
+
68
+
```bash
69
+
# Loki configuration
70
+
GRAFANA_LOKI_URL=https://logs-prod.grafana.net
71
+
72
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
73
+
GRAFANA_LOKI_TOKEN=your-loki-api-key
74
+
75
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
76
+
GRAFANA_LOKI_USERNAME=your-username
77
+
GRAFANA_LOKI_PASSWORD=your-password
78
+
79
+
# Prometheus configuration
80
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod.grafana.net/api/prom
81
+
82
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
83
+
GRAFANA_PROMETHEUS_TOKEN=your-prometheus-api-key
84
+
85
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
86
+
GRAFANA_PROMETHEUS_USERNAME=your-username
87
+
GRAFANA_PROMETHEUS_PASSWORD=your-password
88
+
```
89
+
90
+
### Programmatic Configuration
91
+
92
+
```typescript
93
+
import { initializeGrafanaExporters } from '@wisp/observability'
94
+
95
+
initializeGrafanaExporters({
96
+
// Loki configuration for logs
97
+
lokiUrl: 'https://logs-prod.grafana.net',
98
+
lokiAuth: {
99
+
// Option 1: Bearer token (recommended for Grafana Cloud)
100
+
bearerToken: 'your-api-key',
101
+
102
+
// Option 2: Basic auth
103
+
username: 'your-username',
104
+
password: 'your-password'
105
+
},
106
+
107
+
// Prometheus/OTLP configuration for metrics
108
+
prometheusUrl: 'https://prometheus-prod.grafana.net',
109
+
prometheusAuth: {
110
+
bearerToken: 'your-api-key'
111
+
},
112
+
113
+
// Service metadata
114
+
serviceName: 'wisp-app',
115
+
serviceVersion: '1.0.0',
116
+
117
+
// Batching configuration
118
+
batchSize: 100, // Flush after this many entries
119
+
flushIntervalMs: 5000, // Flush every 5 seconds
120
+
121
+
// Enable/disable exporters
122
+
enabled: true
123
+
})
124
+
```
125
+
126
+
## Middleware Integration
127
+
128
+
### Elysia
129
+
130
+
```typescript
131
+
import { Elysia } from 'elysia'
132
+
import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
133
+
import { initializeGrafanaExporters } from '@wisp/observability'
134
+
135
+
// Initialize Grafana exporters
136
+
initializeGrafanaExporters({
137
+
lokiUrl: process.env.GRAFANA_LOKI_URL,
138
+
lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN }
139
+
})
140
+
141
+
const app = new Elysia()
142
+
.use(observabilityMiddleware({ service: 'main-app' }))
143
+
.get('/', () => 'Hello World')
144
+
.listen(3000)
145
+
```
146
+
147
+
### Hono
148
+
149
+
```typescript
150
+
import { Hono } from 'hono'
151
+
import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
152
+
import { initializeGrafanaExporters } from '@wisp/observability'
153
+
154
+
// Initialize Grafana exporters
155
+
initializeGrafanaExporters({
156
+
lokiUrl: process.env.GRAFANA_LOKI_URL,
157
+
lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN }
158
+
})
159
+
160
+
const app = new Hono()
161
+
app.use('*', observabilityMiddleware({ service: 'hosting-service' }))
162
+
app.onError(observabilityErrorHandler({ service: 'hosting-service' }))
163
+
```
164
+
165
+
## Grafana Cloud Setup
166
+
167
+
1. **Create a Grafana Cloud account** at https://grafana.com/
168
+
169
+
2. **Get your Loki credentials:**
170
+
- Go to your Grafana Cloud portal
171
+
- Navigate to "Loki" โ "Details"
172
+
- Copy the Push endpoint URL and create an API key
173
+
174
+
3. **Get your Prometheus credentials:**
175
+
- Navigate to "Prometheus" โ "Details"
176
+
- Copy the Remote Write endpoint and create an API key
177
+
178
+
4. **Configure your application:**
179
+
```typescript
180
+
initializeGrafanaExporters({
181
+
lokiUrl: 'https://logs-prod-xxx.grafana.net',
182
+
lokiAuth: { bearerToken: 'glc_xxx' },
183
+
prometheusUrl: 'https://prometheus-prod-xxx.grafana.net/api/prom',
184
+
prometheusAuth: { bearerToken: 'glc_xxx' }
185
+
})
186
+
```
187
+
188
+
## Data Flow
189
+
190
+
1. **Logs** โ Buffered โ Batched โ Sent to Grafana Loki
191
+
2. **Metrics** โ Aggregated โ Exported via OTLP โ Sent to Prometheus
192
+
3. **Errors** โ Deduplicated โ Sent to Loki with error tag
193
+
194
+
## Performance Considerations
195
+
196
+
- Logs and metrics are batched to reduce network overhead
197
+
- Default batch size: 100 entries
198
+
- Default flush interval: 5 seconds
199
+
- Failed exports are logged but don't block application
200
+
- In-memory buffers are capped to prevent memory leaks
201
+
202
+
## Graceful Shutdown
203
+
204
+
The exporters automatically register shutdown handlers:
205
+
206
+
```typescript
207
+
import { shutdownGrafanaExporters } from '@wisp/observability'
208
+
209
+
// Manual shutdown if needed
210
+
process.on('beforeExit', async () => {
211
+
await shutdownGrafanaExporters()
212
+
})
213
+
```
214
+
215
+
## License
216
+
217
+
MIT
+13
-1
packages/@wisp/observability/package.json
+13
-1
packages/@wisp/observability/package.json
···
24
}
25
},
26
"peerDependencies": {
27
+
"hono": "^4.10.7"
28
},
29
"peerDependenciesMeta": {
30
"hono": {
31
"optional": true
32
}
33
+
},
34
+
"dependencies": {
35
+
"@opentelemetry/api": "^1.9.0",
36
+
"@opentelemetry/sdk-metrics": "^1.29.0",
37
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
38
+
"@opentelemetry/resources": "^1.29.0",
39
+
"@opentelemetry/semantic-conventions": "^1.29.0"
40
+
},
41
+
"devDependencies": {
42
+
"@hono/node-server": "^1.19.6",
43
+
"bun-types": "^1.3.3",
44
+
"typescript": "^5.9.3"
45
}
46
}
+12
-2
packages/@wisp/observability/src/core.ts
+12
-2
packages/@wisp/observability/src/core.ts
···
3
* Framework-agnostic logging, error tracking, and metrics collection
4
*/
5
6
// ============================================================================
7
// Types
8
// ============================================================================
···
128
logs.splice(MAX_LOGS)
129
}
130
131
// Also log to console for compatibility
132
const contextStr = context ? ` ${JSON.stringify(context)}` : ''
133
const traceStr = traceId ? ` [trace:${traceId}]` : ''
···
163
},
164
165
debug(message: string, service: string, context?: Record<string, any>, traceId?: string) {
166
-
const env = typeof Bun !== 'undefined' ? Bun.env.NODE_ENV : process.env.NODE_ENV;
167
-
if (env !== 'production') {
168
this.log('debug', message, service, context, traceId)
169
}
170
},
···
233
234
errors.set(key, entry)
235
236
// Rotate if needed
237
if (errors.size > MAX_ERRORS) {
238
const oldest = Array.from(errors.keys())[0]
···
284
}
285
286
metrics.unshift(entry)
287
288
// Rotate if needed
289
if (metrics.length > MAX_METRICS) {
···
3
* Framework-agnostic logging, error tracking, and metrics collection
4
*/
5
6
+
import { lokiExporter, metricsExporter } from './exporters'
7
+
8
// ============================================================================
9
// Types
10
// ============================================================================
···
130
logs.splice(MAX_LOGS)
131
}
132
133
+
// Send to Loki exporter
134
+
lokiExporter.pushLog(entry)
135
+
136
// Also log to console for compatibility
137
const contextStr = context ? ` ${JSON.stringify(context)}` : ''
138
const traceStr = traceId ? ` [trace:${traceId}]` : ''
···
168
},
169
170
debug(message: string, service: string, context?: Record<string, any>, traceId?: string) {
171
+
if (process.env.NODE_ENV !== 'production') {
172
this.log('debug', message, service, context, traceId)
173
}
174
},
···
237
238
errors.set(key, entry)
239
240
+
// Send to Loki exporter
241
+
lokiExporter.pushError(entry)
242
+
243
// Rotate if needed
244
if (errors.size > MAX_ERRORS) {
245
const oldest = Array.from(errors.keys())[0]
···
291
}
292
293
metrics.unshift(entry)
294
+
295
+
// Send to Prometheus/OTLP exporter
296
+
metricsExporter.recordMetric(entry)
297
298
// Rotate if needed
299
if (metrics.length > MAX_METRICS) {
+433
packages/@wisp/observability/src/exporters.ts
+433
packages/@wisp/observability/src/exporters.ts
···
···
1
+
/**
2
+
* Grafana exporters for logs and metrics
3
+
* Integrates with Grafana Loki for logs and Prometheus/OTLP for metrics
4
+
*/
5
+
6
+
import type { LogEntry, ErrorEntry, MetricEntry } from './core'
7
+
import { metrics, type MeterProvider } from '@opentelemetry/api'
8
+
import { MeterProvider as SdkMeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
9
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
10
+
import { Resource } from '@opentelemetry/resources'
11
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
12
+
13
+
// ============================================================================
14
+
// Types
15
+
// ============================================================================
16
+
17
+
export interface GrafanaConfig {
18
+
lokiUrl?: string
19
+
lokiAuth?: {
20
+
username?: string
21
+
password?: string
22
+
bearerToken?: string
23
+
}
24
+
prometheusUrl?: string
25
+
prometheusAuth?: {
26
+
username?: string
27
+
password?: string
28
+
bearerToken?: string
29
+
}
30
+
serviceName?: string
31
+
serviceVersion?: string
32
+
batchSize?: number
33
+
flushIntervalMs?: number
34
+
enabled?: boolean
35
+
}
36
+
37
+
interface LokiStream {
38
+
stream: Record<string, string>
39
+
values: Array<[string, string]>
40
+
}
41
+
42
+
interface LokiBatch {
43
+
streams: LokiStream[]
44
+
}
45
+
46
+
// ============================================================================
47
+
// Configuration
48
+
// ============================================================================
49
+
50
+
class GrafanaExporterConfig {
51
+
private config: GrafanaConfig = {
52
+
enabled: false,
53
+
batchSize: 100,
54
+
flushIntervalMs: 5000,
55
+
serviceName: 'wisp-app',
56
+
serviceVersion: '1.0.0'
57
+
}
58
+
59
+
initialize(config: GrafanaConfig) {
60
+
this.config = { ...this.config, ...config }
61
+
62
+
// Load from environment variables if not provided
63
+
if (!this.config.lokiUrl) {
64
+
this.config.lokiUrl = process.env.GRAFANA_LOKI_URL
65
+
}
66
+
67
+
if (!this.config.prometheusUrl) {
68
+
this.config.prometheusUrl = process.env.GRAFANA_PROMETHEUS_URL
69
+
}
70
+
71
+
// Load Loki authentication from environment
72
+
if (!this.config.lokiAuth?.bearerToken && !this.config.lokiAuth?.username) {
73
+
const token = process.env.GRAFANA_LOKI_TOKEN
74
+
const username = process.env.GRAFANA_LOKI_USERNAME
75
+
const password = process.env.GRAFANA_LOKI_PASSWORD
76
+
77
+
if (token) {
78
+
this.config.lokiAuth = { ...this.config.lokiAuth, bearerToken: token }
79
+
} else if (username && password) {
80
+
this.config.lokiAuth = { ...this.config.lokiAuth, username, password }
81
+
}
82
+
}
83
+
84
+
// Load Prometheus authentication from environment
85
+
if (!this.config.prometheusAuth?.bearerToken && !this.config.prometheusAuth?.username) {
86
+
const token = process.env.GRAFANA_PROMETHEUS_TOKEN
87
+
const username = process.env.GRAFANA_PROMETHEUS_USERNAME
88
+
const password = process.env.GRAFANA_PROMETHEUS_PASSWORD
89
+
90
+
if (token) {
91
+
this.config.prometheusAuth = { ...this.config.prometheusAuth, bearerToken: token }
92
+
} else if (username && password) {
93
+
this.config.prometheusAuth = { ...this.config.prometheusAuth, username, password }
94
+
}
95
+
}
96
+
97
+
// Enable if URLs are configured
98
+
if (this.config.lokiUrl || this.config.prometheusUrl) {
99
+
this.config.enabled = true
100
+
}
101
+
102
+
return this
103
+
}
104
+
105
+
getConfig(): GrafanaConfig {
106
+
return { ...this.config }
107
+
}
108
+
109
+
isEnabled(): boolean {
110
+
return this.config.enabled === true
111
+
}
112
+
}
113
+
114
+
export const grafanaConfig = new GrafanaExporterConfig()
115
+
116
+
// ============================================================================
117
+
// Loki Exporter for Logs
118
+
// ============================================================================
119
+
120
+
class LokiExporter {
121
+
private buffer: LogEntry[] = []
122
+
private errorBuffer: ErrorEntry[] = []
123
+
private flushTimer?: NodeJS.Timeout
124
+
private config: GrafanaConfig = {}
125
+
126
+
initialize(config: GrafanaConfig) {
127
+
this.config = config
128
+
129
+
if (this.config.enabled && this.config.lokiUrl) {
130
+
this.startBatching()
131
+
}
132
+
}
133
+
134
+
private startBatching() {
135
+
const interval = this.config.flushIntervalMs || 5000
136
+
137
+
this.flushTimer = setInterval(() => {
138
+
this.flush()
139
+
}, interval)
140
+
}
141
+
142
+
stop() {
143
+
if (this.flushTimer) {
144
+
clearInterval(this.flushTimer)
145
+
this.flushTimer = undefined
146
+
}
147
+
// Final flush
148
+
this.flush()
149
+
}
150
+
151
+
pushLog(entry: LogEntry) {
152
+
if (!this.config.enabled || !this.config.lokiUrl) return
153
+
154
+
this.buffer.push(entry)
155
+
156
+
const batchSize = this.config.batchSize || 100
157
+
if (this.buffer.length >= batchSize) {
158
+
this.flush()
159
+
}
160
+
}
161
+
162
+
pushError(entry: ErrorEntry) {
163
+
if (!this.config.enabled || !this.config.lokiUrl) return
164
+
165
+
this.errorBuffer.push(entry)
166
+
167
+
const batchSize = this.config.batchSize || 100
168
+
if (this.errorBuffer.length >= batchSize) {
169
+
this.flush()
170
+
}
171
+
}
172
+
173
+
private async flush() {
174
+
if (!this.config.lokiUrl) return
175
+
176
+
const logsToSend = [...this.buffer]
177
+
const errorsToSend = [...this.errorBuffer]
178
+
179
+
this.buffer = []
180
+
this.errorBuffer = []
181
+
182
+
if (logsToSend.length === 0 && errorsToSend.length === 0) return
183
+
184
+
try {
185
+
const batch = this.createLokiBatch(logsToSend, errorsToSend)
186
+
await this.sendToLoki(batch)
187
+
} catch (error) {
188
+
console.error('[LokiExporter] Failed to send logs to Loki:', error)
189
+
// Optionally re-queue failed logs
190
+
}
191
+
}
192
+
193
+
private createLokiBatch(logs: LogEntry[], errors: ErrorEntry[]): LokiBatch {
194
+
const streams: LokiStream[] = []
195
+
196
+
// Group logs by service and level
197
+
const logGroups = new Map<string, LogEntry[]>()
198
+
199
+
for (const log of logs) {
200
+
const key = `${log.service}-${log.level}`
201
+
const group = logGroups.get(key) || []
202
+
group.push(log)
203
+
logGroups.set(key, group)
204
+
}
205
+
206
+
// Create streams for logs
207
+
for (const [key, entries] of logGroups) {
208
+
const [service, level] = key.split('-')
209
+
const values: Array<[string, string]> = entries.map(entry => {
210
+
const logLine = JSON.stringify({
211
+
message: entry.message,
212
+
context: entry.context,
213
+
traceId: entry.traceId,
214
+
eventType: entry.eventType
215
+
})
216
+
217
+
// Loki expects nanosecond timestamp as string
218
+
const nanoTimestamp = String(entry.timestamp.getTime() * 1000000)
219
+
return [nanoTimestamp, logLine]
220
+
})
221
+
222
+
streams.push({
223
+
stream: {
224
+
service: service || 'unknown',
225
+
level: level || 'info',
226
+
job: this.config.serviceName || 'wisp-app'
227
+
},
228
+
values
229
+
})
230
+
}
231
+
232
+
// Create streams for errors
233
+
if (errors.length > 0) {
234
+
const errorValues: Array<[string, string]> = errors.map(entry => {
235
+
const logLine = JSON.stringify({
236
+
message: entry.message,
237
+
stack: entry.stack,
238
+
context: entry.context,
239
+
count: entry.count
240
+
})
241
+
242
+
const nanoTimestamp = String(entry.timestamp.getTime() * 1000000)
243
+
return [nanoTimestamp, logLine]
244
+
})
245
+
246
+
streams.push({
247
+
stream: {
248
+
service: errors[0]?.service || 'unknown',
249
+
level: 'error',
250
+
job: this.config.serviceName || 'wisp-app',
251
+
type: 'aggregated_error'
252
+
},
253
+
values: errorValues
254
+
})
255
+
}
256
+
257
+
return { streams }
258
+
}
259
+
260
+
private async sendToLoki(batch: LokiBatch) {
261
+
if (!this.config.lokiUrl) return
262
+
263
+
const headers: Record<string, string> = {
264
+
'Content-Type': 'application/json'
265
+
}
266
+
267
+
// Add authentication
268
+
if (this.config.lokiAuth?.bearerToken) {
269
+
headers['Authorization'] = `Bearer ${this.config.lokiAuth.bearerToken}`
270
+
} else if (this.config.lokiAuth?.username && this.config.lokiAuth?.password) {
271
+
const auth = Buffer.from(`${this.config.lokiAuth.username}:${this.config.lokiAuth.password}`).toString('base64')
272
+
headers['Authorization'] = `Basic ${auth}`
273
+
}
274
+
275
+
const response = await fetch(`${this.config.lokiUrl}/loki/api/v1/push`, {
276
+
method: 'POST',
277
+
headers,
278
+
body: JSON.stringify(batch)
279
+
})
280
+
281
+
if (!response.ok) {
282
+
const text = await response.text()
283
+
throw new Error(`Loki push failed: ${response.status} - ${text}`)
284
+
}
285
+
}
286
+
}
287
+
288
+
// ============================================================================
289
+
// OpenTelemetry Metrics Exporter
290
+
// ============================================================================
291
+
292
+
class MetricsExporter {
293
+
private meterProvider?: MeterProvider
294
+
private requestCounter?: any
295
+
private requestDuration?: any
296
+
private errorCounter?: any
297
+
private config: GrafanaConfig = {}
298
+
299
+
initialize(config: GrafanaConfig) {
300
+
this.config = config
301
+
302
+
if (!this.config.enabled || !this.config.prometheusUrl) return
303
+
304
+
// Create OTLP exporter with Prometheus endpoint
305
+
const exporter = new OTLPMetricExporter({
306
+
url: `${this.config.prometheusUrl}/v1/metrics`,
307
+
headers: this.getAuthHeaders(),
308
+
timeoutMillis: 10000
309
+
})
310
+
311
+
// Create meter provider with periodic exporting
312
+
const meterProvider = new SdkMeterProvider({
313
+
resource: new Resource({
314
+
[ATTR_SERVICE_NAME]: this.config.serviceName || 'wisp-app',
315
+
[ATTR_SERVICE_VERSION]: this.config.serviceVersion || '1.0.0'
316
+
}),
317
+
readers: [
318
+
new PeriodicExportingMetricReader({
319
+
exporter,
320
+
exportIntervalMillis: this.config.flushIntervalMs || 5000
321
+
})
322
+
]
323
+
})
324
+
325
+
// Set global meter provider
326
+
metrics.setGlobalMeterProvider(meterProvider)
327
+
this.meterProvider = meterProvider
328
+
329
+
// Create metrics instruments
330
+
const meter = metrics.getMeter(this.config.serviceName || 'wisp-app')
331
+
332
+
this.requestCounter = meter.createCounter('http_requests_total', {
333
+
description: 'Total number of HTTP requests'
334
+
})
335
+
336
+
this.requestDuration = meter.createHistogram('http_request_duration_ms', {
337
+
description: 'HTTP request duration in milliseconds',
338
+
unit: 'ms'
339
+
})
340
+
341
+
this.errorCounter = meter.createCounter('errors_total', {
342
+
description: 'Total number of errors'
343
+
})
344
+
}
345
+
346
+
private getAuthHeaders(): Record<string, string> {
347
+
const headers: Record<string, string> = {}
348
+
349
+
if (this.config.prometheusAuth?.bearerToken) {
350
+
headers['Authorization'] = `Bearer ${this.config.prometheusAuth.bearerToken}`
351
+
} else if (this.config.prometheusAuth?.username && this.config.prometheusAuth?.password) {
352
+
const auth = Buffer.from(`${this.config.prometheusAuth.username}:${this.config.prometheusAuth.password}`).toString('base64')
353
+
headers['Authorization'] = `Basic ${auth}`
354
+
}
355
+
356
+
return headers
357
+
}
358
+
359
+
recordMetric(entry: MetricEntry) {
360
+
if (!this.config.enabled) return
361
+
362
+
const attributes = {
363
+
method: entry.method,
364
+
path: entry.path,
365
+
status: String(entry.statusCode),
366
+
service: entry.service
367
+
}
368
+
369
+
// Record request count
370
+
this.requestCounter?.add(1, attributes)
371
+
372
+
// Record request duration
373
+
this.requestDuration?.record(entry.duration, attributes)
374
+
375
+
// Record errors
376
+
if (entry.statusCode >= 400) {
377
+
this.errorCounter?.add(1, attributes)
378
+
}
379
+
}
380
+
381
+
async shutdown() {
382
+
if (this.meterProvider && 'shutdown' in this.meterProvider) {
383
+
await (this.meterProvider as SdkMeterProvider).shutdown()
384
+
}
385
+
}
386
+
}
387
+
388
+
// ============================================================================
389
+
// Singleton Instances
390
+
// ============================================================================
391
+
392
+
export const lokiExporter = new LokiExporter()
393
+
export const metricsExporter = new MetricsExporter()
394
+
395
+
// ============================================================================
396
+
// Initialization
397
+
// ============================================================================
398
+
399
+
export function initializeGrafanaExporters(config?: GrafanaConfig) {
400
+
const finalConfig = grafanaConfig.initialize(config || {}).getConfig()
401
+
402
+
if (finalConfig.enabled) {
403
+
console.log('[Observability] Initializing Grafana exporters', {
404
+
lokiEnabled: !!finalConfig.lokiUrl,
405
+
prometheusEnabled: !!finalConfig.prometheusUrl,
406
+
serviceName: finalConfig.serviceName
407
+
})
408
+
409
+
lokiExporter.initialize(finalConfig)
410
+
metricsExporter.initialize(finalConfig)
411
+
}
412
+
413
+
return {
414
+
lokiExporter,
415
+
metricsExporter,
416
+
config: finalConfig
417
+
}
418
+
}
419
+
420
+
// ============================================================================
421
+
// Cleanup
422
+
// ============================================================================
423
+
424
+
export async function shutdownGrafanaExporters() {
425
+
lokiExporter.stop()
426
+
await metricsExporter.shutdown()
427
+
}
428
+
429
+
// Graceful shutdown handlers
430
+
if (typeof process !== 'undefined') {
431
+
process.on('SIGTERM', shutdownGrafanaExporters)
432
+
process.on('SIGINT', shutdownGrafanaExporters)
433
+
}
+8
packages/@wisp/observability/src/index.ts
+8
packages/@wisp/observability/src/index.ts
···
6
// Export everything from core
7
export * from './core'
8
9
// Note: Middleware should be imported from specific subpaths:
10
// - import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
11
// - import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
···
6
// Export everything from core
7
export * from './core'
8
9
+
// Export Grafana integration
10
+
export {
11
+
initializeGrafanaExporters,
12
+
shutdownGrafanaExporters,
13
+
grafanaConfig,
14
+
type GrafanaConfig
15
+
} from './exporters'
16
+
17
// Note: Middleware should be imported from specific subpaths:
18
// - import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
19
// - import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
+336
packages/@wisp/observability/src/integration-test.test.ts
+336
packages/@wisp/observability/src/integration-test.test.ts
···
···
1
+
/**
2
+
* Integration tests for Grafana exporters
3
+
* Tests both mock server and live server connections
4
+
*/
5
+
6
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
7
+
import { createLogger, metricsCollector, initializeGrafanaExporters, shutdownGrafanaExporters } from './index'
8
+
import { Hono } from 'hono'
9
+
import { serve } from '@hono/node-server'
10
+
import type { ServerType } from '@hono/node-server'
11
+
12
+
// ============================================================================
13
+
// Mock Grafana Server
14
+
// ============================================================================
15
+
16
+
interface MockRequest {
17
+
method: string
18
+
path: string
19
+
headers: Record<string, string>
20
+
body: any
21
+
}
22
+
23
+
class MockGrafanaServer {
24
+
private app: Hono
25
+
private server?: ServerType
26
+
private port: number
27
+
public requests: MockRequest[] = []
28
+
29
+
constructor(port: number) {
30
+
this.port = port
31
+
this.app = new Hono()
32
+
33
+
// Mock Loki endpoint
34
+
this.app.post('/loki/api/v1/push', async (c) => {
35
+
const body = await c.req.json()
36
+
this.requests.push({
37
+
method: 'POST',
38
+
path: '/loki/api/v1/push',
39
+
headers: Object.fromEntries(c.req.raw.headers.entries()),
40
+
body
41
+
})
42
+
return c.json({ status: 'success' })
43
+
})
44
+
45
+
// Mock Prometheus/OTLP endpoint
46
+
this.app.post('/v1/metrics', async (c) => {
47
+
const body = await c.req.json()
48
+
this.requests.push({
49
+
method: 'POST',
50
+
path: '/v1/metrics',
51
+
headers: Object.fromEntries(c.req.raw.headers.entries()),
52
+
body
53
+
})
54
+
return c.json({ status: 'success' })
55
+
})
56
+
57
+
// Health check
58
+
this.app.get('/health', (c) => c.json({ status: 'ok' }))
59
+
}
60
+
61
+
async start() {
62
+
this.server = serve({
63
+
fetch: this.app.fetch,
64
+
port: this.port
65
+
})
66
+
// Wait a bit for server to be ready
67
+
await new Promise(resolve => setTimeout(resolve, 100))
68
+
}
69
+
70
+
async stop() {
71
+
if (this.server) {
72
+
this.server.close()
73
+
this.server = undefined
74
+
}
75
+
}
76
+
77
+
clearRequests() {
78
+
this.requests = []
79
+
}
80
+
81
+
getRequestsByPath(path: string): MockRequest[] {
82
+
return this.requests.filter(r => r.path === path)
83
+
}
84
+
85
+
async waitForRequests(count: number, timeoutMs: number = 10000): Promise<boolean> {
86
+
const startTime = Date.now()
87
+
while (this.requests.length < count) {
88
+
if (Date.now() - startTime > timeoutMs) {
89
+
return false
90
+
}
91
+
await new Promise(resolve => setTimeout(resolve, 100))
92
+
}
93
+
return true
94
+
}
95
+
}
96
+
97
+
// ============================================================================
98
+
// Test Suite
99
+
// ============================================================================
100
+
101
+
describe('Grafana Integration', () => {
102
+
const mockServer = new MockGrafanaServer(9999)
103
+
const mockUrl = 'http://localhost:9999'
104
+
105
+
beforeAll(async () => {
106
+
await mockServer.start()
107
+
})
108
+
109
+
afterAll(async () => {
110
+
await mockServer.stop()
111
+
await shutdownGrafanaExporters()
112
+
})
113
+
114
+
test('should initialize with username/password auth', () => {
115
+
const config = initializeGrafanaExporters({
116
+
lokiUrl: mockUrl,
117
+
lokiAuth: {
118
+
username: 'testuser',
119
+
password: 'testpass'
120
+
},
121
+
prometheusUrl: mockUrl,
122
+
prometheusAuth: {
123
+
username: 'testuser',
124
+
password: 'testpass'
125
+
},
126
+
serviceName: 'test-service',
127
+
batchSize: 5,
128
+
flushIntervalMs: 1000
129
+
})
130
+
131
+
expect(config.config.enabled).toBe(true)
132
+
expect(config.config.lokiUrl).toBe(mockUrl)
133
+
expect(config.config.prometheusUrl).toBe(mockUrl)
134
+
expect(config.config.lokiAuth?.username).toBe('testuser')
135
+
expect(config.config.prometheusAuth?.username).toBe('testuser')
136
+
})
137
+
138
+
test('should send logs to Loki with basic auth', async () => {
139
+
mockServer.clearRequests()
140
+
141
+
// Initialize with username/password
142
+
initializeGrafanaExporters({
143
+
lokiUrl: mockUrl,
144
+
lokiAuth: {
145
+
username: 'testuser',
146
+
password: 'testpass'
147
+
},
148
+
serviceName: 'test-logs',
149
+
batchSize: 2,
150
+
flushIntervalMs: 500
151
+
})
152
+
153
+
const logger = createLogger('test-logs')
154
+
155
+
// Generate logs that will trigger batch flush
156
+
logger.info('Test message 1')
157
+
logger.warn('Test message 2')
158
+
159
+
// Wait for batch to be sent
160
+
const success = await mockServer.waitForRequests(1, 5000)
161
+
expect(success).toBe(true)
162
+
163
+
const lokiRequests = mockServer.getRequestsByPath('/loki/api/v1/push')
164
+
expect(lokiRequests.length).toBeGreaterThanOrEqual(1)
165
+
166
+
const lastRequest = lokiRequests[lokiRequests.length - 1]!
167
+
168
+
// Verify basic auth header
169
+
expect(lastRequest.headers['authorization']).toMatch(/^Basic /)
170
+
171
+
// Verify Loki batch format
172
+
expect(lastRequest.body).toHaveProperty('streams')
173
+
expect(Array.isArray(lastRequest.body.streams)).toBe(true)
174
+
expect(lastRequest.body.streams.length).toBeGreaterThan(0)
175
+
176
+
const stream = lastRequest.body.streams[0]!
177
+
expect(stream).toHaveProperty('stream')
178
+
expect(stream).toHaveProperty('values')
179
+
expect(stream.stream.job).toBe('test-logs')
180
+
181
+
await shutdownGrafanaExporters()
182
+
})
183
+
184
+
test('should send metrics to Prometheus with bearer token', async () => {
185
+
mockServer.clearRequests()
186
+
187
+
// Initialize with bearer token only for Prometheus (no Loki)
188
+
initializeGrafanaExporters({
189
+
lokiUrl: undefined, // Explicitly disable Loki
190
+
prometheusUrl: mockUrl,
191
+
prometheusAuth: {
192
+
bearerToken: 'test-token-123'
193
+
},
194
+
serviceName: 'test-metrics',
195
+
flushIntervalMs: 1000
196
+
})
197
+
198
+
// Generate metrics
199
+
for (let i = 0; i < 5; i++) {
200
+
metricsCollector.recordRequest('/api/test', 'GET', 200, 100 + i, 'test-metrics')
201
+
}
202
+
203
+
// Wait for metrics to be exported
204
+
await new Promise(resolve => setTimeout(resolve, 2000))
205
+
206
+
const prometheusRequests = mockServer.getRequestsByPath('/v1/metrics')
207
+
expect(prometheusRequests.length).toBeGreaterThan(0)
208
+
209
+
// Note: Due to singleton exporters, we may see auth from previous test
210
+
// The key thing is that metrics are being sent
211
+
const lastRequest = prometheusRequests[prometheusRequests.length - 1]!
212
+
expect(lastRequest.headers['authorization']).toBeTruthy()
213
+
214
+
await shutdownGrafanaExporters()
215
+
})
216
+
217
+
test('should handle errors gracefully', async () => {
218
+
// Initialize with invalid URL
219
+
const config = initializeGrafanaExporters({
220
+
lokiUrl: 'http://localhost:9998', // Non-existent server
221
+
lokiAuth: {
222
+
username: 'test',
223
+
password: 'test'
224
+
},
225
+
serviceName: 'test-error',
226
+
batchSize: 1,
227
+
flushIntervalMs: 500
228
+
})
229
+
230
+
expect(config.config.enabled).toBe(true)
231
+
232
+
const logger = createLogger('test-error')
233
+
234
+
// This should not throw even though server doesn't exist
235
+
logger.info('This should not crash')
236
+
237
+
// Wait for flush attempt
238
+
await new Promise(resolve => setTimeout(resolve, 1000))
239
+
240
+
// If we got here, error handling worked
241
+
expect(true).toBe(true)
242
+
243
+
await shutdownGrafanaExporters()
244
+
})
245
+
})
246
+
247
+
// ============================================================================
248
+
// Live Server Connection Tests (Optional)
249
+
// ============================================================================
250
+
251
+
describe('Live Grafana Connection (Optional)', () => {
252
+
const hasLiveConfig = Boolean(
253
+
process.env.GRAFANA_LOKI_URL &&
254
+
(process.env.GRAFANA_LOKI_TOKEN ||
255
+
(process.env.GRAFANA_LOKI_USERNAME && process.env.GRAFANA_LOKI_PASSWORD))
256
+
)
257
+
258
+
test.skipIf(!hasLiveConfig)('should connect to live Loki server', async () => {
259
+
const config = initializeGrafanaExporters({
260
+
serviceName: 'test-live-loki',
261
+
serviceVersion: '1.0.0-test',
262
+
batchSize: 5,
263
+
flushIntervalMs: 2000
264
+
})
265
+
266
+
expect(config.config.enabled).toBe(true)
267
+
expect(config.config.lokiUrl).toBeTruthy()
268
+
269
+
const logger = createLogger('test-live-loki')
270
+
271
+
// Send test logs
272
+
logger.info('Live connection test log', { test: true, timestamp: Date.now() })
273
+
logger.warn('Test warning from integration test')
274
+
logger.error('Test error (ignore)', new Error('Test error'), { safe: true })
275
+
276
+
// Wait for flush
277
+
await new Promise(resolve => setTimeout(resolve, 3000))
278
+
279
+
// If we got here without errors, connection worked
280
+
expect(true).toBe(true)
281
+
282
+
await shutdownGrafanaExporters()
283
+
})
284
+
285
+
test.skipIf(!hasLiveConfig)('should connect to live Prometheus server', async () => {
286
+
const hasPrometheusConfig = Boolean(
287
+
process.env.GRAFANA_PROMETHEUS_URL &&
288
+
(process.env.GRAFANA_PROMETHEUS_TOKEN ||
289
+
(process.env.GRAFANA_PROMETHEUS_USERNAME && process.env.GRAFANA_PROMETHEUS_PASSWORD))
290
+
)
291
+
292
+
if (!hasPrometheusConfig) {
293
+
console.log('Skipping Prometheus test - no config provided')
294
+
return
295
+
}
296
+
297
+
const config = initializeGrafanaExporters({
298
+
serviceName: 'test-live-prometheus',
299
+
serviceVersion: '1.0.0-test',
300
+
flushIntervalMs: 2000
301
+
})
302
+
303
+
expect(config.config.enabled).toBe(true)
304
+
expect(config.config.prometheusUrl).toBeTruthy()
305
+
306
+
// Generate test metrics
307
+
for (let i = 0; i < 10; i++) {
308
+
metricsCollector.recordRequest(
309
+
'/test/endpoint',
310
+
'GET',
311
+
200,
312
+
50 + Math.random() * 200,
313
+
'test-live-prometheus'
314
+
)
315
+
}
316
+
317
+
// Wait for export
318
+
await new Promise(resolve => setTimeout(resolve, 3000))
319
+
320
+
expect(true).toBe(true)
321
+
322
+
await shutdownGrafanaExporters()
323
+
})
324
+
})
325
+
326
+
// ============================================================================
327
+
// Manual Test Runner
328
+
// ============================================================================
329
+
330
+
if (import.meta.main) {
331
+
console.log('๐งช Running Grafana integration tests...\n')
332
+
console.log('Live server tests will run if these environment variables are set:')
333
+
console.log(' - GRAFANA_LOKI_URL + (GRAFANA_LOKI_TOKEN or GRAFANA_LOKI_USERNAME/PASSWORD)')
334
+
console.log(' - GRAFANA_PROMETHEUS_URL + (GRAFANA_PROMETHEUS_TOKEN or GRAFANA_PROMETHEUS_USERNAME/PASSWORD)')
335
+
console.log('')
336
+
}
+128
-23
packages/@wisp/safe-fetch/src/index.ts
+128
-23
packages/@wisp/safe-fetch/src/index.ts
···
28
const MAX_BLOB_SIZE = 500 * 1024 * 1024; // 500MB
29
const MAX_REDIRECTS = 10;
30
31
function isBlockedHost(hostname: string): boolean {
32
const lowerHost = hostname.toLowerCase();
33
···
44
return false;
45
}
46
47
export async function safeFetch(
48
url: string,
49
-
options?: RequestInit & { maxSize?: number; timeout?: number }
50
): Promise<Response> {
51
const timeoutMs = options?.timeout ?? FETCH_TIMEOUT;
52
const maxSize = options?.maxSize ?? MAX_RESPONSE_SIZE;
53
54
-
// Parse and validate URL
55
let parsedUrl: URL;
56
try {
57
parsedUrl = new URL(url);
···
68
throw new Error(`Blocked host: ${hostname}`);
69
}
70
71
-
const controller = new AbortController();
72
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
73
74
-
try {
75
-
const response = await fetch(url, {
76
-
...options,
77
-
signal: controller.signal,
78
-
redirect: 'follow',
79
-
});
80
81
-
const contentLength = response.headers.get('content-length');
82
-
if (contentLength && parseInt(contentLength, 10) > maxSize) {
83
-
throw new Error(`Response too large: ${contentLength} bytes`);
84
}
85
86
-
return response;
87
-
} catch (err) {
88
-
if (err instanceof Error && err.name === 'AbortError') {
89
-
throw new Error(`Request timeout after ${timeoutMs}ms`);
90
-
}
91
-
throw err;
92
-
} finally {
93
-
clearTimeout(timeoutId);
94
}
95
}
96
97
export async function safeFetchJson<T = any>(
98
url: string,
99
-
options?: RequestInit & { maxSize?: number; timeout?: number }
100
): Promise<T> {
101
const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE;
102
const response = await safeFetch(url, { ...options, maxSize: maxJsonSize });
···
142
143
export async function safeFetchBlob(
144
url: string,
145
-
options?: RequestInit & { maxSize?: number; timeout?: number }
146
): Promise<Uint8Array> {
147
const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE;
148
const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
···
28
const MAX_BLOB_SIZE = 500 * 1024 * 1024; // 500MB
29
const MAX_REDIRECTS = 10;
30
31
+
// Retry configuration
32
+
const MAX_RETRIES = 3;
33
+
const INITIAL_RETRY_DELAY = 1000; // 1 second
34
+
const MAX_RETRY_DELAY = 10000; // 10 seconds
35
+
36
function isBlockedHost(hostname: string): boolean {
37
const lowerHost = hostname.toLowerCase();
38
···
49
return false;
50
}
51
52
+
/**
53
+
* Check if an error is retryable (network/SSL errors, not HTTP errors)
54
+
*/
55
+
function isRetryableError(err: unknown): boolean {
56
+
if (!(err instanceof Error)) return false;
57
+
58
+
// Network errors (ECONNRESET, ENOTFOUND, etc.)
59
+
const errorCode = (err as any).code;
60
+
if (errorCode) {
61
+
const retryableCodes = [
62
+
'ECONNRESET',
63
+
'ECONNREFUSED',
64
+
'ETIMEDOUT',
65
+
'ENOTFOUND',
66
+
'ENETUNREACH',
67
+
'EAI_AGAIN',
68
+
'EPIPE',
69
+
'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', // SSL/TLS handshake failures
70
+
'ERR_SSL_WRONG_VERSION_NUMBER',
71
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
72
+
];
73
+
if (retryableCodes.includes(errorCode)) {
74
+
return true;
75
+
}
76
+
}
77
+
78
+
// Timeout errors
79
+
if (err.name === 'AbortError' || err.message.includes('timeout')) {
80
+
return true;
81
+
}
82
+
83
+
// Fetch failures (generic network errors)
84
+
if (err.message.includes('fetch failed')) {
85
+
return true;
86
+
}
87
+
88
+
return false;
89
+
}
90
+
91
+
/**
92
+
* Sleep for a given number of milliseconds
93
+
*/
94
+
function sleep(ms: number): Promise<void> {
95
+
return new Promise(resolve => setTimeout(resolve, ms));
96
+
}
97
+
98
+
/**
99
+
* Retry a function with exponential backoff
100
+
*/
101
+
async function withRetry<T>(
102
+
fn: () => Promise<T>,
103
+
options: { maxRetries?: number; initialDelay?: number; maxDelay?: number; context?: string } = {}
104
+
): Promise<T> {
105
+
const maxRetries = options.maxRetries ?? MAX_RETRIES;
106
+
const initialDelay = options.initialDelay ?? INITIAL_RETRY_DELAY;
107
+
const maxDelay = options.maxDelay ?? MAX_RETRY_DELAY;
108
+
const context = options.context ?? 'Request';
109
+
110
+
let lastError: unknown;
111
+
112
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
113
+
try {
114
+
return await fn();
115
+
} catch (err) {
116
+
lastError = err;
117
+
118
+
// Don't retry if this is the last attempt or error is not retryable
119
+
if (attempt === maxRetries || !isRetryableError(err)) {
120
+
throw err;
121
+
}
122
+
123
+
// Calculate delay with exponential backoff
124
+
const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
125
+
126
+
const errorCode = (err as any)?.code;
127
+
const errorMsg = err instanceof Error ? err.message : String(err);
128
+
console.warn(
129
+
`${context} failed (attempt ${attempt + 1}/${maxRetries + 1}): ${errorMsg}${errorCode ? ` [${errorCode}]` : ''} - retrying in ${delay}ms`
130
+
);
131
+
132
+
await sleep(delay);
133
+
}
134
+
}
135
+
136
+
throw lastError;
137
+
}
138
+
139
export async function safeFetch(
140
url: string,
141
+
options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean }
142
): Promise<Response> {
143
+
const shouldRetry = options?.retry !== false; // Default to true
144
const timeoutMs = options?.timeout ?? FETCH_TIMEOUT;
145
const maxSize = options?.maxSize ?? MAX_RESPONSE_SIZE;
146
147
+
// Parse and validate URL (done once, outside retry loop)
148
let parsedUrl: URL;
149
try {
150
parsedUrl = new URL(url);
···
161
throw new Error(`Blocked host: ${hostname}`);
162
}
163
164
+
const fetchFn = async () => {
165
+
const controller = new AbortController();
166
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
167
+
168
+
try {
169
+
const response = await fetch(url, {
170
+
...options,
171
+
signal: controller.signal,
172
+
redirect: 'follow',
173
+
headers: {
174
+
'User-Agent': 'wisp-place hosting-service',
175
+
...(options?.headers || {}),
176
+
},
177
+
});
178
179
+
const contentLength = response.headers.get('content-length');
180
+
if (contentLength && parseInt(contentLength, 10) > maxSize) {
181
+
throw new Error(`Response too large: ${contentLength} bytes`);
182
+
}
183
184
+
return response;
185
+
} catch (err) {
186
+
if (err instanceof Error && err.name === 'AbortError') {
187
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
188
+
}
189
+
throw err;
190
+
} finally {
191
+
clearTimeout(timeoutId);
192
}
193
+
};
194
195
+
if (shouldRetry) {
196
+
return withRetry(fetchFn, { context: `Fetch ${parsedUrl.hostname}` });
197
+
} else {
198
+
return fetchFn();
199
}
200
}
201
202
export async function safeFetchJson<T = any>(
203
url: string,
204
+
options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean }
205
): Promise<T> {
206
const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE;
207
const response = await safeFetch(url, { ...options, maxSize: maxJsonSize });
···
247
248
export async function safeFetchBlob(
249
url: string,
250
+
options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean }
251
): Promise<Uint8Array> {
252
const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE;
253
const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
+28
scripts/codegen.sh
+28
scripts/codegen.sh
···
···
1
+
#!/bin/bash
2
+
set -e
3
+
4
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
6
+
7
+
# Parse arguments
8
+
AUTO_ACCEPT=""
9
+
if [[ "$1" == "-y" || "$1" == "--yes" ]]; then
10
+
AUTO_ACCEPT="yes |"
11
+
fi
12
+
13
+
echo "=== Generating TypeScript lexicons ==="
14
+
cd "$ROOT_DIR/packages/@wisp/lexicons"
15
+
eval "$AUTO_ACCEPT npm run codegen"
16
+
17
+
echo "=== Generating Rust lexicons ==="
18
+
echo "Installing jacquard-lexgen..."
19
+
cargo install jacquard-lexgen --version 0.9.5 2>/dev/null || true
20
+
echo "Running jacquard-codegen..."
21
+
echo " Input: $ROOT_DIR/lexicons"
22
+
echo " Output: $ROOT_DIR/cli/crates/lexicons/src"
23
+
jacquard-codegen -i "$ROOT_DIR/lexicons" -o "$ROOT_DIR/cli/crates/lexicons/src"
24
+
25
+
# Add extern crate alloc for the macro to work
26
+
sed -i '' '1s/^/extern crate alloc;\n\n/' "$ROOT_DIR/cli/crates/lexicons/src/lib.rs"
27
+
28
+
echo "=== Done ==="
+1
-1
tsconfig.json
+1
-1
tsconfig.json
···
33
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35
"types": [
36
-
"bun-types"
37
] /* Specify type package names to be included without being referenced in a source file. */,
38
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
39
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
···
33
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35
"types": [
36
+
"bun"
37
] /* Specify type package names to be included without being referenced in a source file. */,
38
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
39
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */