+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"}
+53
-48
.tangled/workflows/deploy-wisp.yml
+53
-48
.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
-
- bun
20
-
21
environment:
22
-
WISP_HANDLE: 'wisp.place'
23
-
SITE_PATH: 'docs/dist'
24
-
SITE_NAME: 'docs'
25
steps:
26
-
- name: 'Initialize submodules'
27
-
command: |
28
-
git submodule update --init --recursive
29
30
-
- name: 'Build wisp-cli'
31
-
command: |
32
-
cd cli
33
-
export PATH="$HOME/.nix-profile/bin:$PATH"
34
-
nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
35
-
nix-channel --update
36
-
nix-shell -p pkg-config openssl --run '
37
-
export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)"
38
-
export OPENSSL_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.dev)"
39
-
export OPENSSL_NO_VENDOR=1
40
-
export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.out)/lib"
41
-
cargo build --release
42
-
'
43
-
cd ..
44
-
- name: 'Build docs'
45
-
command: |
46
-
cd docs
47
-
bun run build
48
-
- name: 'Deploy to Wisp.place'
49
-
command: |
50
-
./cli/target/release/wisp-cli \
51
-
"$WISP_HANDLE" \
52
-
--path "$SITE_PATH" \
53
-
--site "$SITE_NAME" \
54
-
--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
+
bun run build
53
+
- name: Deploy to Wisp.place
54
+
command: |
55
+
./cli/target/release/wisp-cli \
56
+
"$WISP_HANDLE" \
57
+
--path "$SITE_PATH" \
58
+
--site "$SITE_NAME" \
59
+
--password "$WISP_APP_PASSWORD"
+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"]
+3
-3
README.md
+3
-3
README.md
+4
-2
apps/hosting-service/package.json
+4
-2
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": {
···
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",
···
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": {
···
19
"@wisp/safe-fetch": "workspace:*",
20
"@atproto/api": "^0.17.4",
21
"@atproto/identity": "^0.4.9",
22
+
"@atproto/lexicon": "^0.5.2",
23
"@atproto/sync": "^0.1.36",
24
"@atproto/xrpc": "^0.7.5",
25
"@hono/node-server": "^1.19.6",
···
32
"@types/bun": "^1.3.1",
33
"@types/mime-types": "^2.1.4",
34
"@types/node": "^22.10.5",
35
+
"tsx": "^4.19.2",
36
+
"typescript": "^5.9.3"
37
}
38
}
+15
-3
apps/hosting-service/src/index.ts
+15
-3
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);
···
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) => {
···
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
+
9
+
// Initialize Grafana exporters if configured
10
+
initializeGrafanaExporters({
11
+
serviceName: 'hosting-service',
12
+
serviceVersion: '1.0.0'
13
+
});
14
15
const logger = createLogger('hosting-service');
16
17
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001;
18
const CACHE_DIR = process.env.CACHE_DIR || './cache/sites';
19
+
const BACKFILL_CONCURRENCY = process.env.BACKFILL_CONCURRENCY
20
+
? parseInt(process.env.BACKFILL_CONCURRENCY)
21
+
: undefined; // Let backfill.ts default (10) apply
22
23
// Parse CLI arguments
24
const args = process.argv.slice(2);
···
55
console.log('๐ Backfill requested, starting cache backfill...');
56
backfillCache({
57
skipExisting: true,
58
+
concurrency: BACKFILL_CONCURRENCY,
59
}).then((stats) => {
60
console.log('โ
Cache backfill completed');
61
}).catch((err) => {
···
86
Cache: ${CACHE_DIR}
87
Firehose: Connected to Firehose
88
Cache-Only: ${CACHE_ONLY_MODE ? 'ENABLED (no DB writes)' : 'DISABLED'}
89
+
Backfill: ${backfillOnStartup ? `ENABLED (concurrency: ${BACKFILL_CONCURRENCY || 10})` : 'DISABLED'}
90
`);
91
92
// Graceful shutdown
···
94
console.log('\n๐ Shutting down...');
95
firehose.stop();
96
stopDomainCacheCleanup();
97
+
await closeDatabase();
98
server.close();
99
process.exit(0);
100
});
···
103
console.log('\n๐ Shutting down...');
104
firehose.stop();
105
stopDomainCacheCleanup();
106
+
await closeDatabase();
107
server.close();
108
process.exit(0);
109
});
+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 && 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
+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
+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
+
})
+108
-18
apps/hosting-service/src/lib/utils.ts
+108
-18
apps/hosting-service/src/lib/utils.ts
···
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
···
299
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);
···
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) {
···
7
import { safeFetchJson, safeFetchBlob } from '@wisp/safe-fetch';
8
import { CID } from 'multiformats';
9
import { extractBlobCid } from '@wisp/atproto-utils';
10
+
import { sanitizePath, collectFileCidsFromEntries, countFilesInDirectory } from '@wisp/fs-utils';
11
import { shouldCompressMimeType } from '@wisp/atproto-utils/compression';
12
+
import { MAX_BLOB_SIZE, MAX_FILE_COUNT, MAX_SITE_SIZE } from '@wisp/constants';
13
14
// Re-export shared utilities for local usage and tests
15
export { extractBlobCid, sanitizePath };
···
90
export async function fetchSiteRecord(did: string, rkey: string): Promise<{ record: WispFsRecord; cid: string } | null> {
91
try {
92
const pdsEndpoint = await getPdsForDid(did);
93
+
if (!pdsEndpoint) {
94
+
console.error('[hosting-service] Failed to get PDS endpoint for DID', { did, rkey });
95
+
return null;
96
+
}
97
98
const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`;
99
const data = await safeFetchJson(url);
···
103
cid: data.cid || ''
104
};
105
} catch (err) {
106
+
const errorCode = (err as any)?.code;
107
+
const errorMsg = err instanceof Error ? err.message : String(err);
108
+
109
+
// Better error logging to distinguish between network errors and 404s
110
+
if (errorMsg.includes('HTTP 404') || errorMsg.includes('Not Found')) {
111
+
console.log('[hosting-service] Site record not found', { did, rkey });
112
+
} else if (errorCode && ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', 'ETIMEDOUT'].includes(errorCode)) {
113
+
console.error('[hosting-service] Network/SSL error fetching site record (after retries)', {
114
+
did,
115
+
rkey,
116
+
error: errorMsg,
117
+
code: errorCode
118
+
});
119
+
} else {
120
+
console.error('[hosting-service] Failed to fetch site record', {
121
+
did,
122
+
rkey,
123
+
error: errorMsg,
124
+
code: errorCode
125
+
});
126
+
}
127
+
128
return null;
129
}
130
}
···
145
}
146
147
/**
148
+
* Calculate total size of all blobs in a directory tree from manifest metadata
149
+
*/
150
+
function calculateTotalBlobSize(directory: Directory): number {
151
+
let totalSize = 0;
152
+
153
+
function sumBlobSizes(entries: Entry[]) {
154
+
for (const entry of entries) {
155
+
const node = entry.node;
156
+
157
+
if ('type' in node && node.type === 'directory' && 'entries' in node) {
158
+
// Recursively sum subdirectories
159
+
sumBlobSizes(node.entries);
160
+
} else if ('type' in node && node.type === 'file' && 'blob' in node) {
161
+
// Add blob size from manifest
162
+
const fileNode = node as File;
163
+
const blobSize = (fileNode.blob as any)?.size || 0;
164
+
totalSize += blobSize;
165
+
}
166
+
}
167
+
}
168
+
169
+
sumBlobSizes(directory.entries);
170
+
return totalSize;
171
+
}
172
+
173
+
/**
174
* Extract all subfs URIs from a directory tree with their mount paths
175
*/
176
+
export function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> {
177
const uris: Array<{ uri: string; path: string }> = [];
178
179
for (const entry of directory.entries) {
···
233
* Replace subfs nodes in a directory tree with their actual content
234
* Subfs entries are "merged" - their root entries are hoisted into the parent directory
235
* This function is recursive - it will keep expanding until no subfs nodes remain
236
+
* Uses a cache to avoid re-fetching the same subfs records across recursion depths
237
*/
238
+
export async function expandSubfsNodes(
239
+
directory: Directory,
240
+
pdsEndpoint: string,
241
+
depth: number = 0,
242
+
subfsCache: Map<string, SubfsRecord | null> = new Map()
243
+
): Promise<Directory> {
244
const MAX_DEPTH = 10; // Prevent infinite loops
245
246
if (depth >= MAX_DEPTH) {
···
256
return directory;
257
}
258
259
+
// Filter to only URIs we haven't fetched yet
260
+
const uncachedUris = subfsUris.filter(({ uri }) => !subfsCache.has(uri));
261
262
+
if (uncachedUris.length > 0) {
263
+
console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, fetching ${uncachedUris.length} new records (${subfsUris.length - uncachedUris.length} cached)...`);
264
265
+
// Fetch only uncached subfs records in parallel
266
+
const fetchedRecords = await Promise.all(
267
+
uncachedUris.map(async ({ uri }) => {
268
+
const record = await fetchSubfsRecord(uri, pdsEndpoint);
269
+
return { uri, record };
270
+
})
271
+
);
272
+
273
+
// Add fetched records to cache
274
+
for (const { uri, record } of fetchedRecords) {
275
+
subfsCache.set(uri, record);
276
+
}
277
+
} else {
278
+
console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, all cached`);
279
+
}
280
+
281
+
// Build a map of path -> root entries to merge using the cache
282
// Note: SubFS entries are compatible with FS entries at runtime
283
const subfsMap = new Map<string, Entry[]>();
284
+
for (const { uri, path } of subfsUris) {
285
+
const record = subfsCache.get(uri);
286
if (record && record.root && record.root.entries) {
287
subfsMap.set(path, record.root.entries as unknown as Entry[]);
288
}
···
350
};
351
352
// Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs)
353
+
// Pass the cache to avoid re-fetching records
354
+
return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1, subfsCache);
355
}
356
357
···
370
371
// Expand subfs nodes before caching
372
const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint);
373
+
374
+
// Verify all subfs nodes were expanded
375
+
const remainingSubfs = extractSubfsUris(expandedRoot);
376
+
if (remainingSubfs.length > 0) {
377
+
console.warn(`[Cache] Warning: ${remainingSubfs.length} subfs nodes remain unexpanded after expansion`, remainingSubfs);
378
+
}
379
+
380
+
// Validate file count limit
381
+
const fileCount = countFilesInDirectory(expandedRoot);
382
+
if (fileCount > MAX_FILE_COUNT) {
383
+
throw new Error(`Site exceeds file count limit: ${fileCount} files (max ${MAX_FILE_COUNT})`);
384
+
}
385
+
console.log(`[Cache] File count validation passed: ${fileCount} files (limit: ${MAX_FILE_COUNT})`);
386
+
387
+
// Validate total size from blob metadata
388
+
const totalBlobSize = calculateTotalBlobSize(expandedRoot);
389
+
if (totalBlobSize > MAX_SITE_SIZE) {
390
+
throw new Error(`Site exceeds size limit: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (max ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`);
391
+
}
392
+
console.log(`[Cache] Size validation passed: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (limit: ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`);
393
394
// Get existing cache metadata to check for incremental updates
395
const existingMetadata = await getCacheMetadata(did, rkey);
···
605
606
console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`);
607
608
+
let content = await safeFetchBlob(blobUrl, { maxSize: MAX_BLOB_SIZE, timeout: 300000 });
609
610
// If content is base64-encoded, decode it back to raw binary (gzipped or not)
611
if (base64) {
+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,
+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);
+317
-163
bun.lock
+317
-163
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",
···
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",
···
102
"@wisp/lexicons": "workspace:*",
103
"multiformats": "^13.3.1",
104
},
105
},
106
"packages/@wisp/constants": {
107
"name": "@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
···
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
···
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
···
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
···
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
···
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
···
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
···
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
···
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",
···
42
"@types/mime-types": "^2.1.4",
43
"@types/node": "^22.10.5",
44
"tsx": "^4.19.2",
45
+
"typescript": "^5.9.3",
46
},
47
},
48
"apps/main-app": {
49
"name": "@wisp/main-app",
50
"version": "1.0.50",
51
"dependencies": {
52
+
"@atproto-labs/did-resolver": "^0.2.4",
53
+
"@atproto/api": "^0.17.7",
54
+
"@atproto/common-web": "^0.4.6",
55
"@atproto/jwk-jose": "^0.1.11",
56
+
"@atproto/lex-cli": "^0.9.7",
57
+
"@atproto/oauth-client-node": "^0.3.12",
58
+
"@atproto/xrpc-server": "^0.9.6",
59
"@elysiajs/cors": "^1.4.0",
60
"@elysiajs/eden": "^1.4.3",
61
"@elysiajs/openapi": "^1.4.11",
···
75
"@wisp/lexicons": "workspace:*",
76
"@wisp/observability": "workspace:*",
77
"actor-typeahead": "^0.1.1",
78
+
"atproto-ui": "^0.12.0",
79
"bun-plugin-tailwind": "^0.1.2",
80
"class-variance-authority": "^0.7.1",
81
"clsx": "^2.1.1",
82
+
"elysia": "^1.4.18",
83
"ignore": "^7.0.5",
84
"iron-session": "^8.0.4",
85
"lucide-react": "^0.546.0",
···
93
"zlib": "^1.0.5",
94
},
95
"devDependencies": {
96
+
"@atproto-labs/handle-resolver": "^0.3.4",
97
+
"@atproto/did": "^0.2.3",
98
"@types/react": "^19.2.2",
99
"@types/react-dom": "^19.2.1",
100
"bun-types": "latest",
···
111
"@wisp/lexicons": "workspace:*",
112
"multiformats": "^13.3.1",
113
},
114
+
"devDependencies": {
115
+
"@atproto/lexicon": "^0.5.2",
116
+
},
117
},
118
"packages/@wisp/constants": {
119
"name": "@wisp/constants",
···
143
},
144
"devDependencies": {
145
"@atproto/lex-cli": "^0.9.5",
146
+
"multiformats": "^13.4.1",
147
},
148
},
149
"packages/@wisp/observability": {
150
"name": "@wisp/observability",
151
"version": "1.0.0",
152
+
"dependencies": {
153
+
"@opentelemetry/api": "^1.9.0",
154
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
155
+
"@opentelemetry/resources": "^1.29.0",
156
+
"@opentelemetry/sdk-metrics": "^1.29.0",
157
+
"@opentelemetry/semantic-conventions": "^1.29.0",
158
+
},
159
+
"devDependencies": {
160
+
"@hono/node-server": "^1.19.6",
161
+
"bun-types": "^1.3.3",
162
+
"typescript": "^5.9.3",
163
+
},
164
"peerDependencies": {
165
+
"hono": "^4.10.7",
166
},
167
"optionalPeers": [
168
"hono",
···
174
},
175
},
176
"trustedDependencies": [
177
+
"esbuild",
178
"cbor-extract",
179
"protobufjs",
180
+
"core-js",
181
+
"bun",
182
+
"@parcel/watcher",
183
],
184
"packages": {
185
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
186
187
+
"@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=="],
188
189
+
"@atcute/client": ["@atcute/client@4.1.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5" } }, "sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ=="],
190
191
+
"@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="],
192
193
"@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=="],
194
195
+
"@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="],
196
197
+
"@atcute/tangled": ["@atcute/tangled@1.0.12", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.3" } }, "sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA=="],
198
199
+
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.4", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg=="],
200
201
+
"@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=="],
202
203
"@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="],
204
205
"@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=="],
206
207
+
"@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=="],
208
209
+
"@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=="],
210
211
+
"@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=="],
212
213
"@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="],
214
···
220
221
"@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=="],
222
223
+
"@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=="],
224
225
+
"@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=="],
226
227
+
"@atproto/did": ["@atproto/did@0.2.3", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg=="],
228
229
"@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="],
230
···
234
235
"@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=="],
236
237
+
"@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=="],
238
239
+
"@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=="],
240
241
+
"@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=="],
242
243
+
"@atproto/lex-json": ["@atproto/lex-json@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "tslib": "^2.8.1" } }, "sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g=="],
244
245
"@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=="],
246
247
+
"@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=="],
248
249
+
"@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=="],
250
251
+
"@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=="],
252
253
"@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=="],
254
255
"@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=="],
256
257
+
"@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="],
258
259
+
"@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=="],
260
261
"@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="],
262
263
+
"@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=="],
264
265
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
266
···
280
281
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
282
283
+
"@elysiajs/eden": ["@elysiajs/eden@1.4.5", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g=="],
284
285
"@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="],
286
287
+
"@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=="],
288
289
+
"@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="],
290
291
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="],
292
···
340
341
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="],
342
343
+
"@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=="],
344
345
"@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=="],
346
···
370
371
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
372
373
+
"@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=="],
374
375
"@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=="],
376
···
380
381
"@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=="],
382
383
+
"@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=="],
384
385
"@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=="],
386
···
396
397
"@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=="],
398
399
+
"@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=="],
400
401
"@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=="],
402
403
+
"@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=="],
404
405
"@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=="],
406
407
"@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=="],
408
409
+
"@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=="],
410
411
"@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=="],
412
413
+
"@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=="],
414
415
"@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=="],
416
···
420
421
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
422
423
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="],
424
425
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow=="],
426
427
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g=="],
428
429
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw=="],
430
431
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw=="],
432
433
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw=="],
434
435
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w=="],
436
437
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w=="],
438
439
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ=="],
440
441
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ=="],
442
443
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA=="],
444
445
"@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=="],
446
···
576
577
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
578
579
+
"@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
580
581
+
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
582
583
+
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
584
585
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
586
587
"@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=="],
588
589
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
590
591
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
592
593
"@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
594
595
+
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
596
597
+
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
598
599
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
600
···
622
623
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
624
625
+
"actor-typeahead": ["actor-typeahead@0.1.2", "", {}, "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A=="],
626
627
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
628
···
634
635
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
636
637
+
"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=="],
638
639
"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],
640
···
642
643
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
644
645
+
"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=="],
646
647
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
648
···
650
651
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
652
653
+
"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=="],
654
655
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
656
657
+
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
658
659
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
660
···
690
691
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
692
693
+
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
694
695
+
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
696
697
+
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
698
699
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
700
701
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
702
···
712
713
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
714
715
+
"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=="],
716
717
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
718
···
742
743
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
744
745
+
"exact-mirror": ["exact-mirror@0.2.5", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ=="],
746
747
+
"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=="],
748
749
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
750
···
752
753
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
754
755
+
"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=="],
756
757
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
758
759
+
"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=="],
760
761
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
762
···
780
781
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
782
783
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
784
785
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
786
787
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
788
789
+
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
790
791
+
"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=="],
792
793
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
794
···
922
923
"pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="],
924
925
+
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
926
927
+
"playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
928
929
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
930
931
+
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
932
933
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
934
···
940
941
"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=="],
942
943
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
944
945
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
946
···
948
949
"rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="],
950
951
+
"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=="],
952
953
+
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
954
955
+
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
956
957
+
"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=="],
958
959
"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=="],
960
···
980
981
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
982
983
+
"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=="],
984
985
"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=="],
986
···
1002
1003
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
1004
1005
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1006
1007
"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=="],
1008
···
1016
1017
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
1018
1019
+
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
1020
1021
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
1022
···
1038
1039
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1040
1041
+
"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=="],
1042
1043
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
1044
···
1088
1089
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
1090
1091
+
"@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
1092
1093
"@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=="],
1094
···
1096
1097
"@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1098
1099
"@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1100
1101
"@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1102
1103
"@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1104
1105
"@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1106
1107
"@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1108
1109
"@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1110
1111
+
"@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=="],
1112
1113
"@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1114
1115
+
"@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=="],
1116
1117
+
"@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=="],
1118
1119
"@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1120
1121
+
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1122
1123
+
"@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1124
1125
+
"@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=="],
1126
1127
+
"@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=="],
1128
+
1129
+
"@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=="],
1130
+
1131
+
"@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=="],
1132
+
1133
+
"@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=="],
1134
+
1135
+
"@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=="],
1136
+
1137
+
"@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=="],
1138
+
1139
+
"@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=="],
1140
+
1141
+
"@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=="],
1142
+
1143
+
"@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=="],
1144
+
1145
+
"@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=="],
1146
+
1147
+
"@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=="],
1148
+
1149
+
"@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=="],
1150
+
1151
+
"@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=="],
1152
+
1153
+
"@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=="],
1154
+
1155
+
"@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=="],
1156
+
1157
+
"@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=="],
1158
+
1159
+
"@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=="],
1160
+
1161
+
"@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=="],
1162
+
1163
+
"@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=="],
1164
+
1165
+
"@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=="],
1166
+
1167
+
"@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=="],
1168
+
1169
+
"@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=="],
1170
+
1171
+
"@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=="],
1172
+
1173
+
"@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=="],
1174
+
1175
+
"@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=="],
1176
+
1177
+
"@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=="],
1178
+
1179
+
"@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=="],
1180
+
1181
+
"@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=="],
1182
+
1183
+
"@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=="],
1184
+
1185
+
"@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=="],
1186
+
1187
+
"@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=="],
1188
+
1189
+
"@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=="],
1190
+
1191
+
"@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=="],
1192
+
1193
+
"@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=="],
1194
+
1195
+
"@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=="],
1196
+
1197
+
"@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=="],
1198
+
1199
+
"@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=="],
1200
+
1201
+
"@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=="],
1202
+
1203
+
"@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=="],
1204
+
1205
+
"@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=="],
1206
+
1207
+
"@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=="],
1208
+
1209
+
"@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=="],
1210
+
1211
+
"@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=="],
1212
+
1213
+
"@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g=="],
1214
+
1215
+
"@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=="],
1216
+
1217
+
"@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=="],
1218
+
1219
+
"@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=="],
1220
+
1221
+
"@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=="],
1222
+
1223
+
"@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=="],
1224
+
1225
+
"@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=="],
1226
+
1227
+
"@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=="],
1228
+
1229
+
"@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1230
+
1231
+
"@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=="],
1232
+
1233
+
"@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=="],
1234
+
1235
+
"@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=="],
1236
+
1237
+
"@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=="],
1238
+
1239
+
"@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=="],
1240
+
1241
+
"@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=="],
1242
+
1243
+
"@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=="],
1244
+
1245
+
"@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=="],
1246
+
1247
+
"@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=="],
1248
+
1249
+
"@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=="],
1250
1251
"@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=="],
1252
···
1262
1263
"@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=="],
1264
1265
+
"@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=="],
1266
1267
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
1268
···
1270
1271
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1272
1273
+
"@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
1274
1275
+
"@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=="],
1276
1277
+
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1278
1279
"iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1280
···
1282
1283
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
1284
1285
"require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1286
1287
+
"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=="],
1288
1289
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1290
1291
+
"send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
1292
+
1293
+
"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=="],
1294
+
1295
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1296
1297
+
"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=="],
1298
1299
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1300
···
1302
1303
"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=="],
1304
1305
+
"wisp-hosting-service/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
1306
1307
+
"@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=="],
1308
1309
+
"@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=="],
1310
1311
+
"@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=="],
1312
1313
+
"@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=="],
1314
1315
+
"@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=="],
1316
1317
+
"@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=="],
1318
1319
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1320
1321
+
"@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=="],
1322
1323
+
"@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=="],
1324
1325
+
"@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=="],
1326
1327
+
"@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=="],
1328
1329
+
"@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=="],
1330
+
1331
+
"@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1332
+
1333
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1334
+
1335
+
"@opentelemetry/sdk-metrics/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1336
+
1337
+
"@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=="],
1338
+
1339
+
"@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=="],
1340
+
1341
+
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1342
+
1343
+
"@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1344
1345
"require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1346
1347
+
"serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
1348
1349
+
"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=="],
1350
1351
+
"serve-static/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1352
1353
+
"serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
1354
1355
+
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="],
1356
1357
+
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="],
1358
1359
+
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="],
1360
1361
+
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="],
1362
1363
+
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="],
1364
1365
+
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="],
1366
1367
+
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="],
1368
1369
+
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="],
1370
1371
+
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="],
1372
1373
+
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="],
1374
1375
+
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="],
1376
1377
+
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="],
1378
1379
+
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="],
1380
1381
+
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="],
1382
1383
+
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="],
1384
1385
+
"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=="],
1386
1387
+
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="],
1388
1389
+
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="],
1390
1391
+
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="],
1392
1393
+
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="],
1394
1395
+
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="],
1396
1397
+
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="],
1398
1399
+
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="],
1400
+
1401
+
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="],
1402
1403
+
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="],
1404
1405
+
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="],
1406
1407
"wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
1408
1409
+
"wisp-hosting-service/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
1410
}
1411
}
+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
+162
-10
cli/Cargo.lock
+162
-10
cli/Cargo.lock
···
162
]
163
164
[[package]]
165
name = "atomic-waker"
166
version = "1.1.2"
167
source = "registry+https://github.com/rust-lang/crates.io-index"
···
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"
···
605
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
606
607
[[package]]
608
name = "const-oid"
609
version = "0.9.6"
610
source = "registry+https://github.com/rust-lang/crates.io-index"
···
678
dependencies = [
679
"cfg-if",
680
]
681
682
[[package]]
683
name = "crossbeam-channel"
···
959
]
960
961
[[package]]
962
name = "encoding_rs"
963
version = "0.8.35"
964
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1329
]
1330
1331
[[package]]
1332
name = "hashbrown"
1333
version = "0.12.3"
1334
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"
···
1733
]
1734
1735
[[package]]
1736
name = "indoc"
1737
version = "2.0.7"
1738
source = "registry+https://github.com/rust-lang/crates.io-index"
···
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",
···
1826
"regex",
1827
"regex-lite",
1828
"reqwest",
1829
"serde",
1830
"serde_html_form",
1831
"serde_json",
···
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",
···
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",
···
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",
···
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",
···
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",
···
1923
"jacquard-lexicon",
1924
"miette",
1925
"mini-moka",
1926
"percent-encoding",
1927
"reqwest",
1928
"serde",
1929
"serde_html_form",
1930
"serde_json",
···
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",
···
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",
···
1979
"miette",
1980
"p256",
1981
"rand 0.8.5",
1982
"rouille",
1983
"serde",
1984
"serde_html_form",
···
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",
···
2513
]
2514
2515
[[package]]
2516
name = "objc2"
2517
version = "0.6.3"
2518
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2758
]
2759
2760
[[package]]
2761
name = "potential_utf"
2762
version = "0.1.4"
2763
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"
···
3367
"core-foundation-sys",
3368
"libc",
3369
]
3370
3371
[[package]]
3372
name = "send_wrapper"
···
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"
···
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"
···
5008
"futures",
5009
"globset",
5010
"ignore",
5011
"jacquard",
5012
"jacquard-api",
5013
"jacquard-common",
···
162
]
163
164
[[package]]
165
+
name = "atomic-polyfill"
166
+
version = "1.0.3"
167
+
source = "registry+https://github.com/rust-lang/crates.io-index"
168
+
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
169
+
dependencies = [
170
+
"critical-section",
171
+
]
172
+
173
+
[[package]]
174
name = "atomic-waker"
175
version = "1.1.2"
176
source = "registry+https://github.com/rust-lang/crates.io-index"
···
581
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
582
583
[[package]]
584
+
name = "cobs"
585
+
version = "0.3.0"
586
+
source = "registry+https://github.com/rust-lang/crates.io-index"
587
+
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
588
+
dependencies = [
589
+
"thiserror 2.0.17",
590
+
]
591
+
592
+
[[package]]
593
name = "colorchoice"
594
version = "1.0.4"
595
source = "registry+https://github.com/rust-lang/crates.io-index"
···
623
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
624
625
[[package]]
626
+
name = "console"
627
+
version = "0.15.11"
628
+
source = "registry+https://github.com/rust-lang/crates.io-index"
629
+
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
630
+
dependencies = [
631
+
"encode_unicode",
632
+
"libc",
633
+
"once_cell",
634
+
"unicode-width 0.2.2",
635
+
"windows-sys 0.59.0",
636
+
]
637
+
638
+
[[package]]
639
name = "const-oid"
640
version = "0.9.6"
641
source = "registry+https://github.com/rust-lang/crates.io-index"
···
709
dependencies = [
710
"cfg-if",
711
]
712
+
713
+
[[package]]
714
+
name = "critical-section"
715
+
version = "1.2.0"
716
+
source = "registry+https://github.com/rust-lang/crates.io-index"
717
+
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
718
719
[[package]]
720
name = "crossbeam-channel"
···
996
]
997
998
[[package]]
999
+
name = "embedded-io"
1000
+
version = "0.4.0"
1001
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1002
+
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
1003
+
1004
+
[[package]]
1005
+
name = "embedded-io"
1006
+
version = "0.6.1"
1007
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1008
+
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
1009
+
1010
+
[[package]]
1011
+
name = "encode_unicode"
1012
+
version = "1.0.0"
1013
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1014
+
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
1015
+
1016
+
[[package]]
1017
name = "encoding_rs"
1018
version = "0.8.35"
1019
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1384
]
1385
1386
[[package]]
1387
+
name = "hash32"
1388
+
version = "0.2.1"
1389
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1390
+
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
1391
+
dependencies = [
1392
+
"byteorder",
1393
+
]
1394
+
1395
+
[[package]]
1396
name = "hashbrown"
1397
version = "0.12.3"
1398
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1411
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1412
1413
[[package]]
1414
+
name = "heapless"
1415
+
version = "0.7.17"
1416
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1417
+
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
1418
+
dependencies = [
1419
+
"atomic-polyfill",
1420
+
"hash32",
1421
+
"rustc_version",
1422
+
"serde",
1423
+
"spin 0.9.8",
1424
+
"stable_deref_trait",
1425
+
]
1426
+
1427
+
[[package]]
1428
name = "heck"
1429
version = "0.4.1"
1430
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1811
]
1812
1813
[[package]]
1814
+
name = "indicatif"
1815
+
version = "0.17.11"
1816
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1817
+
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
1818
+
dependencies = [
1819
+
"console",
1820
+
"number_prefix",
1821
+
"portable-atomic",
1822
+
"unicode-width 0.2.2",
1823
+
"web-time",
1824
+
]
1825
+
1826
+
[[package]]
1827
name = "indoc"
1828
version = "2.0.7"
1829
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1900
1901
[[package]]
1902
name = "jacquard"
1903
+
version = "0.9.4"
1904
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
1905
dependencies = [
1906
"bytes",
1907
"getrandom 0.2.16",
···
1917
"regex",
1918
"regex-lite",
1919
"reqwest",
1920
+
"ring",
1921
"serde",
1922
"serde_html_form",
1923
"serde_json",
···
1932
[[package]]
1933
name = "jacquard-api"
1934
version = "0.9.2"
1935
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
1936
dependencies = [
1937
"bon",
1938
"bytes",
···
1942
"miette",
1943
"rustversion",
1944
"serde",
1945
+
"serde_bytes",
1946
"serde_ipld_dagcbor",
1947
"thiserror 2.0.17",
1948
"unicode-segmentation",
···
1951
[[package]]
1952
name = "jacquard-common"
1953
version = "0.9.2"
1954
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
1955
dependencies = [
1956
"base64 0.22.1",
1957
"bon",
···
1972
"n0-future 0.1.3",
1973
"ouroboros",
1974
"p256",
1975
+
"postcard",
1976
"rand 0.9.2",
1977
"regex",
1978
"regex-lite",
1979
"reqwest",
1980
+
"ring",
1981
"serde",
1982
+
"serde_bytes",
1983
"serde_html_form",
1984
"serde_ipld_dagcbor",
1985
"serde_json",
···
1995
1996
[[package]]
1997
name = "jacquard-derive"
1998
+
version = "0.9.4"
1999
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
2000
dependencies = [
2001
"heck 0.5.0",
2002
"jacquard-lexicon",
···
2008
[[package]]
2009
name = "jacquard-identity"
2010
version = "0.9.2"
2011
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
2012
dependencies = [
2013
"bon",
2014
"bytes",
···
2019
"jacquard-lexicon",
2020
"miette",
2021
"mini-moka",
2022
+
"n0-future 0.1.3",
2023
"percent-encoding",
2024
"reqwest",
2025
+
"ring",
2026
"serde",
2027
"serde_html_form",
2028
"serde_json",
···
2036
[[package]]
2037
name = "jacquard-lexicon"
2038
version = "0.9.2"
2039
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
2040
dependencies = [
2041
"cid",
2042
"dashmap",
···
2062
[[package]]
2063
name = "jacquard-oauth"
2064
version = "0.9.2"
2065
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
2066
dependencies = [
2067
"base64 0.22.1",
2068
"bytes",
···
2077
"miette",
2078
"p256",
2079
"rand 0.8.5",
2080
+
"ring",
2081
"rouille",
2082
"serde",
2083
"serde_html_form",
···
2388
[[package]]
2389
name = "mini-moka"
2390
version = "0.10.99"
2391
+
source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df"
2392
dependencies = [
2393
"crossbeam-channel",
2394
"crossbeam-utils",
···
2612
]
2613
2614
[[package]]
2615
+
name = "number_prefix"
2616
+
version = "0.4.0"
2617
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2618
+
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
2619
+
2620
+
[[package]]
2621
name = "objc2"
2622
version = "0.6.3"
2623
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2863
]
2864
2865
[[package]]
2866
+
name = "portable-atomic"
2867
+
version = "1.11.1"
2868
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2869
+
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
2870
+
2871
+
[[package]]
2872
+
name = "postcard"
2873
+
version = "1.1.3"
2874
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2875
+
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
2876
+
dependencies = [
2877
+
"cobs",
2878
+
"embedded-io 0.4.0",
2879
+
"embedded-io 0.6.1",
2880
+
"heapless",
2881
+
"serde",
2882
+
]
2883
+
2884
+
[[package]]
2885
name = "potential_utf"
2886
version = "0.1.4"
2887
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3324
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
3325
3326
[[package]]
3327
+
name = "rustc_version"
3328
+
version = "0.4.1"
3329
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3330
+
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
3331
+
dependencies = [
3332
+
"semver",
3333
+
]
3334
+
3335
+
[[package]]
3336
name = "rustix"
3337
version = "1.1.2"
3338
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3500
"core-foundation-sys",
3501
"libc",
3502
]
3503
+
3504
+
[[package]]
3505
+
name = "semver"
3506
+
version = "1.0.27"
3507
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3508
+
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
3509
3510
[[package]]
3511
name = "send_wrapper"
···
3786
version = "0.9.8"
3787
source = "registry+https://github.com/rust-lang/crates.io-index"
3788
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
3789
+
dependencies = [
3790
+
"lock_api",
3791
+
]
3792
3793
[[package]]
3794
name = "spin"
···
4859
4860
[[package]]
4861
name = "windows-sys"
4862
+
version = "0.59.0"
4863
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4864
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
4865
+
dependencies = [
4866
+
"windows-targets 0.52.6",
4867
+
]
4868
+
4869
+
[[package]]
4870
+
name = "windows-sys"
4871
version = "0.60.2"
4872
source = "registry+https://github.com/rust-lang/crates.io-index"
4873
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
···
5159
"futures",
5160
"globset",
5161
"ignore",
5162
+
"indicatif",
5163
"jacquard",
5164
"jacquard-api",
5165
"jacquard-common",
+8
cli/Cargo.toml
+8
cli/Cargo.toml
···
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"
···
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
+
#jacquard = { path = "../../jacquard/crates/jacquard", features = ["loopback"] }
19
+
#jacquard-oauth = { path = "../../jacquard/crates/jacquard-oauth" }
20
+
#jacquard-api = { path = "../../jacquard/crates/jacquard-api", features = ["streaming"] }
21
+
#jacquard-common = { path = "../../jacquard/crates/jacquard-common", features = ["websocket"] }
22
+
#jacquard-identity = { path = "../../jacquard/crates/jacquard-identity", features = ["dns"] }
23
+
#jacquard-derive = { path = "../../jacquard/crates/jacquard-derive" }
24
+
#jacquard-lexicon = { path = "../../jacquard/crates/jacquard-lexicon" }
25
clap = { version = "4.5.51", features = ["derive"] }
26
tokio = { version = "1.48", features = ["full"] }
27
miette = { version = "7.6.0", features = ["fancy"] }
···
49
regex = "1.11"
50
ignore = "0.4"
51
globset = "0.4"
52
+
indicatif = "0.17"
+163
-24
cli/src/main.rs
+163
-24
cli/src/main.rs
···
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")]
···
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 {
···
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 }) => {
···
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)
···
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"))
···
26
use std::io::Write;
27
use base64::Engine;
28
use futures::stream::{self, StreamExt};
29
+
use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
30
31
use place_wisp::fs::*;
32
use place_wisp::settings::*;
33
+
34
+
/// Maximum number of concurrent file uploads to the PDS
35
+
const MAX_CONCURRENT_UPLOADS: usize = 2;
36
+
37
+
/// Limits for caching on wisp.place (from @wisp/constants)
38
+
const MAX_FILE_COUNT: usize = 1000;
39
+
const MAX_SITE_SIZE: usize = 300 * 1024 * 1024; // 300MB
40
41
#[derive(Parser, Debug)]
42
#[command(author, version, about = "wisp.place CLI tool")]
···
72
/// Enable SPA mode (serve index.html for all routes)
73
#[arg(long, global = true, conflicts_with = "command")]
74
spa: bool,
75
+
76
+
/// Skip confirmation prompts (automatically accept warnings)
77
+
#[arg(short = 'y', long, global = true, conflicts_with = "command")]
78
+
yes: bool,
79
}
80
81
#[derive(Subcommand, Debug)]
···
108
/// Enable SPA mode (serve index.html for all routes)
109
#[arg(long)]
110
spa: bool,
111
+
112
+
/// Skip confirmation prompts (automatically accept warnings)
113
+
#[arg(short = 'y', long)]
114
+
yes: bool,
115
},
116
/// Pull a site from the PDS to a local directory
117
Pull {
···
150
let args = Args::parse();
151
152
let result = match args.command {
153
+
Some(Commands::Deploy { input, path, site, store, password, directory, spa, yes }) => {
154
// Dispatch to appropriate authentication method
155
if let Some(password) = password {
156
+
run_with_app_password(input, password, path, site, directory, spa, yes).await
157
} else {
158
+
run_with_oauth(input, store, path, site, directory, spa, yes).await
159
}
160
}
161
Some(Commands::Pull { input, site, output }) => {
···
172
173
// Dispatch to appropriate authentication method
174
if let Some(password) = args.password {
175
+
run_with_app_password(input, password, path, args.site, args.directory, args.spa, args.yes).await
176
} else {
177
+
run_with_oauth(input, store, path, args.site, args.directory, args.spa, args.yes).await
178
}
179
} else {
180
// No command and no input, show help
···
203
site: Option<String>,
204
directory: bool,
205
spa: bool,
206
+
yes: bool,
207
) -> miette::Result<()> {
208
let (session, auth) =
209
MemoryCredentialSession::authenticated(input, password, None, None).await?;
210
println!("Signed in as {}", auth.handle);
211
212
let agent: Agent<_> = Agent::from(session);
213
+
deploy_site(&agent, path, site, directory, spa, yes).await
214
}
215
216
/// Run deployment with OAuth authentication
···
221
site: Option<String>,
222
directory: bool,
223
spa: bool,
224
+
yes: bool,
225
) -> miette::Result<()> {
226
use jacquard::oauth::scopes::Scope;
227
use jacquard::oauth::atproto::AtprotoClientMetadata;
···
254
.await?;
255
256
let agent: Agent<_> = Agent::from(session);
257
+
deploy_site(&agent, path, site, directory, spa, yes).await
258
+
}
259
+
260
+
/// Scan directory to count files and calculate total size
261
+
/// Returns (file_count, total_size_bytes)
262
+
fn scan_directory_stats(
263
+
dir_path: &Path,
264
+
ignore_matcher: &ignore_patterns::IgnoreMatcher,
265
+
current_path: String,
266
+
) -> miette::Result<(usize, u64)> {
267
+
let mut file_count = 0;
268
+
let mut total_size = 0u64;
269
+
270
+
let dir_entries: Vec<_> = std::fs::read_dir(dir_path)
271
+
.into_diagnostic()?
272
+
.collect::<Result<Vec<_>, _>>()
273
+
.into_diagnostic()?;
274
+
275
+
for entry in dir_entries {
276
+
let path = entry.path();
277
+
let name = entry.file_name();
278
+
let name_str = name.to_str()
279
+
.ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?
280
+
.to_string();
281
+
282
+
let full_path = if current_path.is_empty() {
283
+
name_str.clone()
284
+
} else {
285
+
format!("{}/{}", current_path, name_str)
286
+
};
287
+
288
+
// Skip files/directories that match ignore patterns
289
+
if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) {
290
+
continue;
291
+
}
292
+
293
+
let metadata = entry.metadata().into_diagnostic()?;
294
+
295
+
if metadata.is_file() {
296
+
file_count += 1;
297
+
total_size += metadata.len();
298
+
} else if metadata.is_dir() {
299
+
let subdir_path = if current_path.is_empty() {
300
+
name_str
301
+
} else {
302
+
format!("{}/{}", current_path, name_str)
303
+
};
304
+
let (sub_count, sub_size) = scan_directory_stats(&path, ignore_matcher, subdir_path)?;
305
+
file_count += sub_count;
306
+
total_size += sub_size;
307
+
}
308
+
}
309
+
310
+
Ok((file_count, total_size))
311
}
312
313
/// Deploy the site using the provided agent
···
317
site: Option<String>,
318
directory_listing: bool,
319
spa_mode: bool,
320
+
skip_prompts: bool,
321
) -> miette::Result<()> {
322
// Verify the path exists
323
if !path.exists() {
···
335
336
println!("Deploying site '{}'...", site_name);
337
338
+
// Scan directory to check file count and size
339
+
let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?;
340
+
let (file_count, total_size) = scan_directory_stats(&path, &ignore_matcher, String::new())?;
341
+
342
+
let size_mb = total_size as f64 / (1024.0 * 1024.0);
343
+
println!("Scanned: {} files, {:.1} MB total", file_count, size_mb);
344
+
345
+
// Check if limits are exceeded
346
+
let exceeds_file_count = file_count > MAX_FILE_COUNT;
347
+
let exceeds_size = total_size > MAX_SITE_SIZE as u64;
348
+
349
+
if exceeds_file_count || exceeds_size {
350
+
println!("\nโ ๏ธ Warning: Your site exceeds wisp.place caching limits:");
351
+
352
+
if exceeds_file_count {
353
+
println!(" โข File count: {} (limit: {})", file_count, MAX_FILE_COUNT);
354
+
}
355
+
356
+
if exceeds_size {
357
+
let size_mb = total_size as f64 / (1024.0 * 1024.0);
358
+
let limit_mb = MAX_SITE_SIZE as f64 / (1024.0 * 1024.0);
359
+
println!(" โข Total size: {:.1} MB (limit: {:.0} MB)", size_mb, limit_mb);
360
+
}
361
+
362
+
println!("\nwisp.place will NOT serve your site if you proceed.");
363
+
println!("Your site will be uploaded to your PDS, but will only be accessible via:");
364
+
println!(" โข wisp-cli serve (local hosting)");
365
+
println!(" โข Other hosting services with more generous limits");
366
+
367
+
if !skip_prompts {
368
+
// Prompt for confirmation
369
+
use std::io::{self, Write};
370
+
print!("\nDo you want to upload anyway? (y/N): ");
371
+
io::stdout().flush().into_diagnostic()?;
372
+
373
+
let mut input = String::new();
374
+
io::stdin().read_line(&mut input).into_diagnostic()?;
375
+
let input = input.trim().to_lowercase();
376
+
377
+
if input != "y" && input != "yes" {
378
+
println!("Upload cancelled.");
379
+
return Ok(());
380
+
}
381
+
} else {
382
+
println!("\nSkipping confirmation (--yes flag set).");
383
+
}
384
+
385
+
println!("\nProceeding with upload...\n");
386
+
}
387
+
388
// Try to fetch existing manifest for incremental updates
389
let (existing_blob_map, old_subfs_uris): (HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, Vec<(String, String)>) = {
390
use jacquard_common::types::string::AtUri;
···
446
}
447
};
448
449
+
// Create progress tracking (spinner style since we don't know total count upfront)
450
+
let multi_progress = MultiProgress::new();
451
+
let progress = multi_progress.add(ProgressBar::new_spinner());
452
+
progress.set_style(
453
+
ProgressStyle::default_spinner()
454
+
.template("[{elapsed_precise}] {spinner:.cyan} {pos} files {msg}")
455
+
.into_diagnostic()?
456
+
.tick_chars("โ โ โ โกโขโ โ โ ")
457
+
);
458
+
progress.set_message("Scanning files...");
459
+
progress.enable_steady_tick(std::time::Duration::from_millis(100));
460
+
461
+
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher, &progress).await?;
462
let uploaded_count = total_files - reused_count;
463
+
464
+
progress.finish_with_message(format!("โ {} files ({} uploaded, {} reused)", total_files, uploaded_count, reused_count));
465
466
// Check if we need to split into subfs records
467
const MAX_MANIFEST_SIZE: usize = 140 * 1024; // 140KB (PDS limit is 150KB)
···
740
existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
741
current_path: String,
742
ignore_matcher: &'a ignore_patterns::IgnoreMatcher,
743
+
progress: &'a ProgressBar,
744
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>>
745
{
746
Box::pin(async move {
···
788
}
789
}
790
791
+
// Process files concurrently with a limit of 2
792
let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks)
793
.map(|(name, path, full_path)| async move {
794
+
let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs, progress).await?;
795
+
progress.inc(1);
796
let entry = Entry::new()
797
.name(CowStr::from(name))
798
.node(EntryNode::File(Box::new(file_node)))
799
.build();
800
Ok::<_, miette::Report>((entry, reused))
801
})
802
+
.buffer_unordered(MAX_CONCURRENT_UPLOADS)
803
.collect::<Vec<_>>()
804
.await
805
.into_iter()
···
826
} else {
827
format!("{}/{}", current_path, name)
828
};
829
+
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher, progress).await?;
830
dir_entries.push(Entry::new()
831
.name(CowStr::from(name))
832
.node(EntryNode::Directory(Box::new(subdir)))
···
858
file_path: &Path,
859
file_path_key: &str,
860
existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
861
+
progress: &ProgressBar,
862
) -> miette::Result<(File<'static>, bool)>
863
{
864
// Read file
···
898
if let Some((existing_blob_ref, existing_cid)) = existing_blob {
899
if existing_cid == &file_cid {
900
// CIDs match - reuse existing blob
901
+
progress.set_message(format!("โ Reused {}", file_path_key));
902
let mut file_builder = File::new()
903
.r#type(CowStr::from("file"))
904
.blob(existing_blob_ref.clone())
···
912
}
913
914
return Ok((file_builder.build(), true));
915
}
916
}
917
···
922
MimeType::new_static("application/octet-stream")
923
};
924
925
+
// Format file size nicely
926
+
let size_str = if upload_bytes.len() < 1024 {
927
+
format!("{} B", upload_bytes.len())
928
+
} else if upload_bytes.len() < 1024 * 1024 {
929
+
format!("{:.1} KB", upload_bytes.len() as f64 / 1024.0)
930
+
} else {
931
+
format!("{:.1} MB", upload_bytes.len() as f64 / (1024.0 * 1024.0))
932
+
};
933
+
934
+
progress.set_message(format!("โ Uploading {} ({})", file_path_key, size_str));
935
let blob = agent.upload_blob(upload_bytes, mime_type).await?;
936
+
progress.set_message(format!("โ Uploaded {}", file_path_key));
937
938
let mut file_builder = File::new()
939
.r#type(CowStr::from("file"))
+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>
+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"],
+11
package.json
+11
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
+
},
29
+
"trustedDependencies": [
30
+
"@parcel/watcher",
31
+
"bun",
32
+
"esbuild"
33
+
],
34
+
"devDependencies": {
35
+
"@types/bun": "^1.3.5"
36
}
37
}
+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
+
})
+2
-1
packages/@wisp/lexicons/package.json
+2
-1
packages/@wisp/lexicons/package.json
+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 { LogEntry, ErrorEntry, MetricEntry } from './core'
7
+
import { metrics, 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;
+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. */