mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Merge branch 'main' into unfavourite
This commit is contained in:
commit
484c162463
410 changed files with 9350 additions and 6396 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
image: denoland/deno:2.1.1
|
image: denoland/deno:2.2.0
|
||||||
|
|
||||||
default:
|
default:
|
||||||
interruptible: true
|
interruptible: true
|
||||||
|
|
@ -10,7 +10,7 @@ test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- deno fmt --check
|
- deno fmt --check
|
||||||
- deno lint
|
- deno task lint
|
||||||
- deno task check
|
- deno task check
|
||||||
- deno task test --coverage=cov_profile
|
- deno task test --coverage=cov_profile
|
||||||
- deno coverage cov_profile
|
- deno coverage cov_profile
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
deno 2.1.1
|
deno 2.2.0
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -8,7 +8,7 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch Program",
|
"name": "Launch Program",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"program": "${workspaceFolder}/src/server.ts",
|
"program": "${workspaceFolder}/packages/ditto/server.ts",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"runtimeExecutable": "deno",
|
"runtimeExecutable": "deno",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
FROM denoland/deno:2.1.1
|
FROM denoland/deno:2.2.0
|
||||||
ENV PORT 5000
|
ENV PORT 5000
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN mkdir -p data && chown -R deno data
|
RUN mkdir -p data && chown -R deno data
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN deno cache --allow-import src/server.ts
|
RUN deno cache --allow-import packages/ditto/server.ts
|
||||||
RUN apt-get update && apt-get install -y unzip curl
|
RUN apt-get update && apt-get install -y unzip curl
|
||||||
RUN deno task soapbox
|
RUN deno task soapbox
|
||||||
CMD deno task start
|
CMD deno task start
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
---
|
|
||||||
- name: Update Ditto
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
tasks:
|
|
||||||
- name: Update Deno
|
|
||||||
shell:
|
|
||||||
cmd: curl -fsSL https://deno.land/x/install/install.sh | sh
|
|
||||||
environment:
|
|
||||||
DENO_INSTALL: /usr/local
|
|
||||||
become_user: root
|
|
||||||
|
|
||||||
- name: Update Soapbox
|
|
||||||
shell:
|
|
||||||
cmd: deno task soapbox
|
|
||||||
chdir: /opt/ditto
|
|
||||||
become_user: ditto
|
|
||||||
|
|
||||||
- name: Update ditto from the main branch
|
|
||||||
git:
|
|
||||||
repo: 'https://gitlab.com/soapbox-pub/ditto.git'
|
|
||||||
dest: '/opt/ditto'
|
|
||||||
version: main
|
|
||||||
become_user: ditto
|
|
||||||
|
|
||||||
- name: Restart ditto service
|
|
||||||
systemd:
|
|
||||||
name: ditto
|
|
||||||
state: restarted
|
|
||||||
become_user: root
|
|
||||||
53
deno.json
53
deno.json
|
|
@ -1,17 +1,31 @@
|
||||||
{
|
{
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
"workspace": [
|
||||||
|
"./packages/conf",
|
||||||
|
"./packages/db",
|
||||||
|
"./packages/ditto",
|
||||||
|
"./packages/lang",
|
||||||
|
"./packages/mastoapi",
|
||||||
|
"./packages/metrics",
|
||||||
|
"./packages/nip98",
|
||||||
|
"./packages/policies",
|
||||||
|
"./packages/ratelimiter",
|
||||||
|
"./packages/translators",
|
||||||
|
"./packages/uploaders"
|
||||||
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run -A --env-file --deny-read=.env src/server.ts",
|
"start": "deno run -A --env-file --deny-read=.env packages/ditto/server.ts",
|
||||||
"dev": "deno run -A --env-file --deny-read=.env --watch src/server.ts",
|
"dev": "deno run -A --env-file --deny-read=.env --watch packages/ditto/server.ts",
|
||||||
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts",
|
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts",
|
||||||
"db:export": "deno run -A --env-file --deny-read=.env scripts/db-export.ts",
|
"db:export": "deno run -A --env-file --deny-read=.env scripts/db-export.ts",
|
||||||
"db:import": "deno run -A --env-file --deny-read=.env scripts/db-import.ts",
|
"db:import": "deno run -A --env-file --deny-read=.env scripts/db-import.ts",
|
||||||
"db:cleanup": "deno run -A --env-file --deny-read=.env scripts/db-policy.ts",
|
"db:cleanup": "deno run -A --env-file --deny-read=.env scripts/db-policy.ts",
|
||||||
"db:migrate": "deno run -A --env-file --deny-read=.env scripts/db-migrate.ts",
|
"db:migrate": "deno run -A --env-file --deny-read=.env scripts/db-migrate.ts",
|
||||||
"nostr:pull": "deno run -A --env-file --deny-read=.env scripts/nostr-pull.ts",
|
"nostr:pull": "deno run -A --env-file --deny-read=.env scripts/nostr-pull.ts",
|
||||||
"debug": "deno run -A --env-file --deny-read=.env --inspect src/server.ts",
|
"debug": "deno run -A --env-file --deny-read=.env --inspect packages/ditto/server.ts",
|
||||||
"test": "deno test -A --env-file=.env.test --deny-read=.env --junit-path=./deno-test.xml",
|
"test": "deno test -A --env-file=.env.test --deny-read=.env --junit-path=./deno-test.xml",
|
||||||
"check": "deno check --allow-import .",
|
"check": "deno check --allow-import .",
|
||||||
|
"lint": "deno lint --allow-import",
|
||||||
"nsec": "deno run scripts/nsec.ts",
|
"nsec": "deno run scripts/nsec.ts",
|
||||||
"admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts",
|
"admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts",
|
||||||
"admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts",
|
"admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts",
|
||||||
|
|
@ -20,8 +34,11 @@
|
||||||
"stats:recompute": "deno run -A --env-file --deny-read=.env scripts/stats-recompute.ts",
|
"stats:recompute": "deno run -A --env-file --deny-read=.env scripts/stats-recompute.ts",
|
||||||
"soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip -o soapbox.zip && rm soapbox.zip",
|
"soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip -o soapbox.zip && rm soapbox.zip",
|
||||||
"trends": "deno run -A --env-file --deny-read=.env scripts/trends.ts",
|
"trends": "deno run -A --env-file --deny-read=.env scripts/trends.ts",
|
||||||
"clean:deps": "deno cache --reload src/app.ts",
|
"clean:deps": "deno cache --reload packages/ditto/app.ts",
|
||||||
|
"db:populate:nip05": "deno run -A --env-file --deny-read=.env scripts/db-populate-nip05.ts",
|
||||||
"db:populate-search": "deno run -A --env-file --deny-read=.env scripts/db-populate-search.ts",
|
"db:populate-search": "deno run -A --env-file --deny-read=.env scripts/db-populate-search.ts",
|
||||||
|
"db:populate-extensions": "deno run -A --env-file --deny-read=.env scripts/db-populate-extensions.ts",
|
||||||
|
"db:streak:recompute": "deno run -A --env-file --deny-read=.env scripts/db-streak-recompute.ts",
|
||||||
"vapid": "deno run scripts/vapid.ts"
|
"vapid": "deno run scripts/vapid.ts"
|
||||||
},
|
},
|
||||||
"unstable": [
|
"unstable": [
|
||||||
|
|
@ -34,25 +51,26 @@
|
||||||
"./public"
|
"./public"
|
||||||
],
|
],
|
||||||
"imports": {
|
"imports": {
|
||||||
"@/": "./src/",
|
|
||||||
"@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47",
|
"@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47",
|
||||||
"@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
|
"@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
|
||||||
|
"@cashu/cashu-ts": "npm:@cashu/cashu-ts@^2.2.0",
|
||||||
|
"@core/asyncutil": "jsr:@core/asyncutil@^1.2.0",
|
||||||
"@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.8",
|
"@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.8",
|
||||||
"@esroyo/scoped-performance": "jsr:@esroyo/scoped-performance@^3.1.0",
|
"@esroyo/scoped-performance": "jsr:@esroyo/scoped-performance@^3.1.0",
|
||||||
"@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2",
|
"@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2",
|
||||||
"@hono/hono": "jsr:@hono/hono@^4.4.6",
|
"@hono/hono": "jsr:@hono/hono@^4.4.6",
|
||||||
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
|
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
|
||||||
"@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1",
|
|
||||||
"@negrel/webpush": "jsr:@negrel/webpush@^0.3.0",
|
"@negrel/webpush": "jsr:@negrel/webpush@^0.3.0",
|
||||||
"@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0",
|
"@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0",
|
||||||
"@nostrify/db": "jsr:@nostrify/db@^0.36.1",
|
"@nostrify/db": "jsr:@nostrify/db@^0.39.4",
|
||||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.0",
|
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.39.1",
|
||||||
"@nostrify/policies": "jsr:@nostrify/policies@^0.35.0",
|
"@nostrify/policies": "jsr:@nostrify/policies@^0.36.1",
|
||||||
|
"@nostrify/types": "jsr:@nostrify/types@^0.36.0",
|
||||||
"@scure/base": "npm:@scure/base@^1.1.6",
|
"@scure/base": "npm:@scure/base@^1.1.6",
|
||||||
"@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs",
|
"@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs",
|
||||||
"@soapbox/kysely-pglite": "jsr:@soapbox/kysely-pglite@^1.0.0",
|
"@soapbox/kysely-pglite": "jsr:@soapbox/kysely-pglite@^1.0.0",
|
||||||
|
"@soapbox/logi": "jsr:@soapbox/logi@^0.3.0",
|
||||||
"@soapbox/safe-fetch": "jsr:@soapbox/safe-fetch@^2.0.0",
|
"@soapbox/safe-fetch": "jsr:@soapbox/safe-fetch@^2.0.0",
|
||||||
"@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0",
|
|
||||||
"@std/assert": "jsr:@std/assert@^0.225.1",
|
"@std/assert": "jsr:@std/assert@^0.225.1",
|
||||||
"@std/cli": "jsr:@std/cli@^0.223.0",
|
"@std/cli": "jsr:@std/cli@^0.223.0",
|
||||||
"@std/crypto": "jsr:@std/crypto@^0.224.0",
|
"@std/crypto": "jsr:@std/crypto@^0.224.0",
|
||||||
|
|
@ -61,16 +79,16 @@
|
||||||
"@std/json": "jsr:@std/json@^0.223.0",
|
"@std/json": "jsr:@std/json@^0.223.0",
|
||||||
"@std/media-types": "jsr:@std/media-types@^0.224.1",
|
"@std/media-types": "jsr:@std/media-types@^0.224.1",
|
||||||
"@std/streams": "jsr:@std/streams@^0.223.0",
|
"@std/streams": "jsr:@std/streams@^0.223.0",
|
||||||
|
"@std/testing": "jsr:@std/testing@^1.0.9",
|
||||||
"blurhash": "npm:blurhash@2.0.5",
|
"blurhash": "npm:blurhash@2.0.5",
|
||||||
"comlink": "npm:comlink@^4.4.1",
|
"comlink": "npm:comlink@^4.4.1",
|
||||||
"comlink-async-generator": "npm:comlink-async-generator@^0.0.1",
|
"comlink-async-generator": "npm:comlink-async-generator@^0.0.1",
|
||||||
"commander": "npm:commander@12.1.0",
|
"commander": "npm:commander@12.1.0",
|
||||||
"deno.json": "./deno.json",
|
|
||||||
"entities": "npm:entities@^4.5.0",
|
"entities": "npm:entities@^4.5.0",
|
||||||
"fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0",
|
"fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0",
|
||||||
"formdata-helper": "npm:formdata-helper@^0.3.0",
|
"formdata-helper": "npm:formdata-helper@^0.3.0",
|
||||||
"hono-rate-limiter": "npm:hono-rate-limiter@^0.3.0",
|
"hono-rate-limiter": "npm:hono-rate-limiter@^0.3.0",
|
||||||
"iso-639-1": "npm:iso-639-1@2.1.15",
|
"iso-639-1": "npm:iso-639-1@^3.1.5",
|
||||||
"isomorphic-dompurify": "npm:isomorphic-dompurify@^2.16.0",
|
"isomorphic-dompurify": "npm:isomorphic-dompurify@^2.16.0",
|
||||||
"kysely": "npm:kysely@^0.27.4",
|
"kysely": "npm:kysely@^0.27.4",
|
||||||
"kysely-postgres-js": "npm:kysely-postgres-js@2.0.0",
|
"kysely-postgres-js": "npm:kysely-postgres-js@2.0.0",
|
||||||
|
|
@ -83,7 +101,6 @@
|
||||||
"nostr-tools": "npm:nostr-tools@2.5.1",
|
"nostr-tools": "npm:nostr-tools@2.5.1",
|
||||||
"nostr-wasm": "npm:nostr-wasm@^0.1.0",
|
"nostr-wasm": "npm:nostr-wasm@^0.1.0",
|
||||||
"path-to-regexp": "npm:path-to-regexp@^7.1.0",
|
"path-to-regexp": "npm:path-to-regexp@^7.1.0",
|
||||||
"png-to-ico": "npm:png-to-ico@^2.1.8",
|
|
||||||
"postgres": "https://gitlab.com/soapbox-pub/postgres.js/-/raw/e79d7d2039446fbf7a37d4eca0d17e94a94b8b53/deno/mod.js",
|
"postgres": "https://gitlab.com/soapbox-pub/postgres.js/-/raw/e79d7d2039446fbf7a37d4eca0d17e94a94b8b53/deno/mod.js",
|
||||||
"prom-client": "npm:prom-client@^15.1.2",
|
"prom-client": "npm:prom-client@^15.1.2",
|
||||||
"question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts",
|
"question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts",
|
||||||
|
|
@ -95,16 +112,6 @@
|
||||||
"zod": "npm:zod@^3.23.8",
|
"zod": "npm:zod@^3.23.8",
|
||||||
"~/fixtures/": "./fixtures/"
|
"~/fixtures/": "./fixtures/"
|
||||||
},
|
},
|
||||||
"lint": {
|
|
||||||
"rules": {
|
|
||||||
"tags": [
|
|
||||||
"recommended"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"no-explicit-any"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fmt": {
|
"fmt": {
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"lineWidth": 120,
|
"lineWidth": 120,
|
||||||
|
|
|
||||||
324
deno.lock
generated
324
deno.lock
generated
|
|
@ -3,6 +3,7 @@
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@b-fuze/deno-dom@~0.1.47": "0.1.48",
|
"jsr:@b-fuze/deno-dom@~0.1.47": "0.1.48",
|
||||||
"jsr:@bradenmacdonald/s3-lite-client@~0.7.4": "0.7.6",
|
"jsr:@bradenmacdonald/s3-lite-client@~0.7.4": "0.7.6",
|
||||||
|
"jsr:@core/asyncutil@^1.2.0": "1.2.0",
|
||||||
"jsr:@denosaurs/plug@1.0.3": "1.0.3",
|
"jsr:@denosaurs/plug@1.0.3": "1.0.3",
|
||||||
"jsr:@esroyo/scoped-performance@^3.1.0": "3.1.0",
|
"jsr:@esroyo/scoped-performance@^3.1.0": "3.1.0",
|
||||||
"jsr:@gfx/canvas-wasm@~0.4.2": "0.4.2",
|
"jsr:@gfx/canvas-wasm@~0.4.2": "0.4.2",
|
||||||
|
|
@ -26,33 +27,35 @@
|
||||||
"jsr:@gleasonator/policy@0.9.1": "0.9.1",
|
"jsr:@gleasonator/policy@0.9.1": "0.9.1",
|
||||||
"jsr:@gleasonator/policy@0.9.2": "0.9.2",
|
"jsr:@gleasonator/policy@0.9.2": "0.9.2",
|
||||||
"jsr:@gleasonator/policy@0.9.3": "0.9.3",
|
"jsr:@gleasonator/policy@0.9.3": "0.9.3",
|
||||||
"jsr:@hono/hono@^4.4.6": "4.6.2",
|
"jsr:@gleasonator/policy@0.9.4": "0.9.4",
|
||||||
"jsr:@lambdalisue/async@^2.1.1": "2.1.1",
|
"jsr:@hono/hono@^4.4.6": "4.6.15",
|
||||||
"jsr:@negrel/http-ece@0.6.0": "0.6.0",
|
"jsr:@negrel/http-ece@0.6.0": "0.6.0",
|
||||||
"jsr:@negrel/webpush@0.3": "0.3.0",
|
"jsr:@negrel/webpush@0.3": "0.3.0",
|
||||||
"jsr:@nostrify/db@~0.36.1": "0.36.1",
|
"jsr:@nostrify/db@~0.39.4": "0.39.4",
|
||||||
"jsr:@nostrify/nostrify@0.31": "0.31.0",
|
"jsr:@nostrify/nostrify@0.31": "0.31.0",
|
||||||
"jsr:@nostrify/nostrify@0.32": "0.32.0",
|
"jsr:@nostrify/nostrify@0.32": "0.32.0",
|
||||||
"jsr:@nostrify/nostrify@0.35": "0.35.0",
|
"jsr:@nostrify/nostrify@0.36": "0.36.2",
|
||||||
"jsr:@nostrify/nostrify@0.36": "0.36.0",
|
"jsr:@nostrify/nostrify@0.39": "0.39.1",
|
||||||
"jsr:@nostrify/nostrify@~0.22.1": "0.22.5",
|
"jsr:@nostrify/nostrify@~0.22.1": "0.22.5",
|
||||||
"jsr:@nostrify/nostrify@~0.22.4": "0.22.4",
|
"jsr:@nostrify/nostrify@~0.22.4": "0.22.4",
|
||||||
"jsr:@nostrify/nostrify@~0.22.5": "0.22.5",
|
"jsr:@nostrify/nostrify@~0.22.5": "0.22.5",
|
||||||
|
"jsr:@nostrify/nostrify@~0.39.1": "0.39.1",
|
||||||
"jsr:@nostrify/policies@0.33": "0.33.0",
|
"jsr:@nostrify/policies@0.33": "0.33.0",
|
||||||
"jsr:@nostrify/policies@0.33.1": "0.33.1",
|
"jsr:@nostrify/policies@0.33.1": "0.33.1",
|
||||||
"jsr:@nostrify/policies@0.34": "0.34.0",
|
"jsr:@nostrify/policies@0.34": "0.34.0",
|
||||||
"jsr:@nostrify/policies@0.35": "0.35.0",
|
|
||||||
"jsr:@nostrify/policies@0.36": "0.36.0",
|
"jsr:@nostrify/policies@0.36": "0.36.0",
|
||||||
"jsr:@nostrify/policies@~0.33.1": "0.33.1",
|
"jsr:@nostrify/policies@~0.33.1": "0.33.1",
|
||||||
"jsr:@nostrify/policies@~0.36.1": "0.36.1",
|
"jsr:@nostrify/policies@~0.36.1": "0.36.1",
|
||||||
"jsr:@nostrify/types@0.30": "0.30.1",
|
"jsr:@nostrify/types@0.30": "0.30.1",
|
||||||
"jsr:@nostrify/types@0.35": "0.35.0",
|
"jsr:@nostrify/types@0.35": "0.35.0",
|
||||||
|
"jsr:@nostrify/types@0.36": "0.36.0",
|
||||||
"jsr:@nostrify/types@~0.30.1": "0.30.1",
|
"jsr:@nostrify/types@~0.30.1": "0.30.1",
|
||||||
"jsr:@soapbox/kysely-pglite@1": "1.0.0",
|
"jsr:@soapbox/kysely-pglite@1": "1.0.0",
|
||||||
|
"jsr:@soapbox/logi@0.3": "0.3.0",
|
||||||
"jsr:@soapbox/safe-fetch@2": "2.0.0",
|
"jsr:@soapbox/safe-fetch@2": "2.0.0",
|
||||||
"jsr:@soapbox/stickynotes@0.4": "0.4.0",
|
|
||||||
"jsr:@std/assert@0.223": "0.223.0",
|
"jsr:@std/assert@0.223": "0.223.0",
|
||||||
"jsr:@std/assert@0.224": "0.224.0",
|
"jsr:@std/assert@0.224": "0.224.0",
|
||||||
|
"jsr:@std/assert@^1.0.10": "1.0.11",
|
||||||
"jsr:@std/assert@~0.213.1": "0.213.1",
|
"jsr:@std/assert@~0.213.1": "0.213.1",
|
||||||
"jsr:@std/assert@~0.225.1": "0.225.3",
|
"jsr:@std/assert@~0.225.1": "0.225.3",
|
||||||
"jsr:@std/bytes@0.223": "0.223.0",
|
"jsr:@std/bytes@0.223": "0.223.0",
|
||||||
|
|
@ -60,10 +63,11 @@
|
||||||
"jsr:@std/bytes@0.224.0": "0.224.0",
|
"jsr:@std/bytes@0.224.0": "0.224.0",
|
||||||
"jsr:@std/bytes@^1.0.0-rc.3": "1.0.0",
|
"jsr:@std/bytes@^1.0.0-rc.3": "1.0.0",
|
||||||
"jsr:@std/bytes@^1.0.1-rc.3": "1.0.2",
|
"jsr:@std/bytes@^1.0.1-rc.3": "1.0.2",
|
||||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
"jsr:@std/bytes@^1.0.2": "1.0.4",
|
||||||
"jsr:@std/bytes@^1.0.2-rc.3": "1.0.2",
|
"jsr:@std/bytes@^1.0.2-rc.3": "1.0.2",
|
||||||
"jsr:@std/cli@0.223": "0.223.0",
|
"jsr:@std/cli@0.223": "0.223.0",
|
||||||
"jsr:@std/crypto@0.224": "0.224.0",
|
"jsr:@std/crypto@0.224": "0.224.0",
|
||||||
|
"jsr:@std/data-structures@^1.0.6": "1.0.6",
|
||||||
"jsr:@std/encoding@0.213.1": "0.213.1",
|
"jsr:@std/encoding@0.213.1": "0.213.1",
|
||||||
"jsr:@std/encoding@0.224": "0.224.3",
|
"jsr:@std/encoding@0.224": "0.224.3",
|
||||||
"jsr:@std/encoding@0.224.0": "0.224.0",
|
"jsr:@std/encoding@0.224.0": "0.224.0",
|
||||||
|
|
@ -71,18 +75,23 @@
|
||||||
"jsr:@std/encoding@~0.224.1": "0.224.3",
|
"jsr:@std/encoding@~0.224.1": "0.224.3",
|
||||||
"jsr:@std/fmt@0.213.1": "0.213.1",
|
"jsr:@std/fmt@0.213.1": "0.213.1",
|
||||||
"jsr:@std/fs@0.213.1": "0.213.1",
|
"jsr:@std/fs@0.213.1": "0.213.1",
|
||||||
|
"jsr:@std/fs@^1.0.9": "1.0.11",
|
||||||
"jsr:@std/fs@~0.229.3": "0.229.3",
|
"jsr:@std/fs@~0.229.3": "0.229.3",
|
||||||
"jsr:@std/internal@1": "1.0.4",
|
"jsr:@std/internal@1": "1.0.5",
|
||||||
|
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||||
"jsr:@std/io@0.223": "0.223.0",
|
"jsr:@std/io@0.223": "0.223.0",
|
||||||
"jsr:@std/io@0.224": "0.224.8",
|
"jsr:@std/io@0.224": "0.224.9",
|
||||||
"jsr:@std/json@0.223": "0.223.0",
|
"jsr:@std/json@0.223": "0.223.0",
|
||||||
"jsr:@std/media-types@0.224.0": "0.224.0",
|
"jsr:@std/media-types@0.224.0": "0.224.0",
|
||||||
"jsr:@std/media-types@~0.224.1": "0.224.1",
|
"jsr:@std/media-types@~0.224.1": "0.224.1",
|
||||||
"jsr:@std/path@0.213.1": "0.213.1",
|
"jsr:@std/path@0.213.1": "0.213.1",
|
||||||
"jsr:@std/path@0.224.0": "0.224.0",
|
"jsr:@std/path@0.224.0": "0.224.0",
|
||||||
"jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1",
|
"jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1",
|
||||||
|
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||||
"jsr:@std/path@~0.213.1": "0.213.1",
|
"jsr:@std/path@~0.213.1": "0.213.1",
|
||||||
"jsr:@std/streams@0.223": "0.223.0",
|
"jsr:@std/streams@0.223": "0.223.0",
|
||||||
|
"jsr:@std/testing@^1.0.9": "1.0.9",
|
||||||
|
"npm:@cashu/cashu-ts@^2.2.0": "2.2.0",
|
||||||
"npm:@electric-sql/pglite@~0.2.8": "0.2.8",
|
"npm:@electric-sql/pglite@~0.2.8": "0.2.8",
|
||||||
"npm:@isaacs/ttlcache@^1.4.1": "1.4.1",
|
"npm:@isaacs/ttlcache@^1.4.1": "1.4.1",
|
||||||
"npm:@noble/hashes@^1.4.0": "1.4.0",
|
"npm:@noble/hashes@^1.4.0": "1.4.0",
|
||||||
|
|
@ -90,7 +99,7 @@
|
||||||
"npm:@scure/base@^1.1.6": "1.1.6",
|
"npm:@scure/base@^1.1.6": "1.1.6",
|
||||||
"npm:@scure/bip32@^1.4.0": "1.4.0",
|
"npm:@scure/bip32@^1.4.0": "1.4.0",
|
||||||
"npm:@scure/bip39@^1.3.0": "1.3.0",
|
"npm:@scure/bip39@^1.3.0": "1.3.0",
|
||||||
"npm:@types/node@*": "18.16.19",
|
"npm:@types/node@*": "22.5.4",
|
||||||
"npm:blurhash@2.0.5": "2.0.5",
|
"npm:blurhash@2.0.5": "2.0.5",
|
||||||
"npm:comlink-async-generator@*": "0.0.1",
|
"npm:comlink-async-generator@*": "0.0.1",
|
||||||
"npm:comlink-async-generator@^0.0.1": "0.0.1",
|
"npm:comlink-async-generator@^0.0.1": "0.0.1",
|
||||||
|
|
@ -100,7 +109,7 @@
|
||||||
"npm:fast-stable-stringify@1": "1.0.0",
|
"npm:fast-stable-stringify@1": "1.0.0",
|
||||||
"npm:formdata-helper@0.3": "0.3.0",
|
"npm:formdata-helper@0.3": "0.3.0",
|
||||||
"npm:hono-rate-limiter@0.3": "0.3.0_hono@4.2.5",
|
"npm:hono-rate-limiter@0.3": "0.3.0_hono@4.2.5",
|
||||||
"npm:iso-639-1@2.1.15": "2.1.15",
|
"npm:iso-639-1@^3.1.5": "3.1.5",
|
||||||
"npm:isomorphic-dompurify@^2.16.0": "2.16.0",
|
"npm:isomorphic-dompurify@^2.16.0": "2.16.0",
|
||||||
"npm:kysely-postgres-js@2.0.0": "2.0.0_kysely@0.27.3_postgres@3.4.4",
|
"npm:kysely-postgres-js@2.0.0": "2.0.0_kysely@0.27.3_postgres@3.4.4",
|
||||||
"npm:kysely@~0.27.2": "0.27.4",
|
"npm:kysely@~0.27.2": "0.27.4",
|
||||||
|
|
@ -115,11 +124,11 @@
|
||||||
"npm:lru-cache@^10.2.0": "10.2.2",
|
"npm:lru-cache@^10.2.0": "10.2.2",
|
||||||
"npm:lru-cache@^10.2.2": "10.2.2",
|
"npm:lru-cache@^10.2.2": "10.2.2",
|
||||||
"npm:nostr-tools@2.5.1": "2.5.1",
|
"npm:nostr-tools@2.5.1": "2.5.1",
|
||||||
|
"npm:nostr-tools@^2.10.4": "2.10.4",
|
||||||
"npm:nostr-tools@^2.5.0": "2.5.1",
|
"npm:nostr-tools@^2.5.0": "2.5.1",
|
||||||
"npm:nostr-tools@^2.7.0": "2.7.0",
|
"npm:nostr-tools@^2.7.0": "2.7.0",
|
||||||
"npm:nostr-wasm@0.1": "0.1.0",
|
"npm:nostr-wasm@0.1": "0.1.0",
|
||||||
"npm:path-to-regexp@^7.1.0": "7.1.0",
|
"npm:path-to-regexp@^7.1.0": "7.1.0",
|
||||||
"npm:png-to-ico@^2.1.8": "2.1.8",
|
|
||||||
"npm:postgres@3.4.4": "3.4.4",
|
"npm:postgres@3.4.4": "3.4.4",
|
||||||
"npm:prom-client@^15.1.2": "15.1.2",
|
"npm:prom-client@^15.1.2": "15.1.2",
|
||||||
"npm:sharp@~0.33.5": "0.33.5",
|
"npm:sharp@~0.33.5": "0.33.5",
|
||||||
|
|
@ -129,6 +138,7 @@
|
||||||
"npm:type-fest@^4.3.0": "4.18.2",
|
"npm:type-fest@^4.3.0": "4.18.2",
|
||||||
"npm:unfurl.js@^6.4.0": "6.4.0",
|
"npm:unfurl.js@^6.4.0": "6.4.0",
|
||||||
"npm:websocket-ts@^2.1.5": "2.1.5",
|
"npm:websocket-ts@^2.1.5": "2.1.5",
|
||||||
|
"npm:websocket-ts@^2.2.1": "2.2.1",
|
||||||
"npm:zod@^3.23.8": "3.23.8"
|
"npm:zod@^3.23.8": "3.23.8"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
|
|
@ -139,7 +149,10 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@b-fuze/deno-dom@0.1.48": {
|
"@b-fuze/deno-dom@0.1.48": {
|
||||||
"integrity": "bf5b591aef2e9e9c59adfcbb93a9ecd45bab5b7c8263625beafa5c8f1662e7da"
|
"integrity": "bf5b591aef2e9e9c59adfcbb93a9ecd45bab5b7c8263625beafa5c8f1662e7da",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@denosaurs/plug"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"@bradenmacdonald/s3-lite-client@0.7.6": {
|
"@bradenmacdonald/s3-lite-client@0.7.6": {
|
||||||
"integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1",
|
"integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1",
|
||||||
|
|
@ -147,6 +160,9 @@
|
||||||
"jsr:@std/io@0.224"
|
"jsr:@std/io@0.224"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@core/asyncutil@1.2.0": {
|
||||||
|
"integrity": "9967f15190c60df032c13f72ce5ac73d185c34f31c53dc918d8800025854c118"
|
||||||
|
},
|
||||||
"@denosaurs/plug@1.0.3": {
|
"@denosaurs/plug@1.0.3": {
|
||||||
"integrity": "b010544e386bea0ff3a1d05e0c88f704ea28cbd4d753439c2f1ee021a85d4640",
|
"integrity": "b010544e386bea0ff3a1d05e0c88f704ea28cbd4d753439c2f1ee021a85d4640",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -294,6 +310,13 @@
|
||||||
"jsr:@nostrify/policies@~0.36.1"
|
"jsr:@nostrify/policies@~0.36.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@gleasonator/policy@0.9.4": {
|
||||||
|
"integrity": "5d5b8a585b8e3cd6e6b7daed2cfa61cd1a3e5945691f092eb98f8671384c3657",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@nostrify/nostrify@0.36",
|
||||||
|
"jsr:@nostrify/policies@~0.36.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@hono/hono@4.4.6": {
|
"@hono/hono@4.4.6": {
|
||||||
"integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453"
|
"integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453"
|
||||||
},
|
},
|
||||||
|
|
@ -321,8 +344,8 @@
|
||||||
"@hono/hono@4.6.2": {
|
"@hono/hono@4.6.2": {
|
||||||
"integrity": "35fcf3be4687825080b01bed7bbe2ac66f8d8b8939f0bad459661bf3b46d916f"
|
"integrity": "35fcf3be4687825080b01bed7bbe2ac66f8d8b8939f0bad459661bf3b46d916f"
|
||||||
},
|
},
|
||||||
"@lambdalisue/async@2.1.1": {
|
"@hono/hono@4.6.15": {
|
||||||
"integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4"
|
"integrity": "935b3b12e98e4b22bcd1aa4dbe6587321e431c79829eba61f535b4ede39fd8b1"
|
||||||
},
|
},
|
||||||
"@negrel/http-ece@0.6.0": {
|
"@negrel/http-ece@0.6.0": {
|
||||||
"integrity": "7afdd81b86ea5b21a9677b323c01c3338705e11cc2bfed250870f5349d8f86f7",
|
"integrity": "7afdd81b86ea5b21a9677b323c01c3338705e11cc2bfed250870f5349d8f86f7",
|
||||||
|
|
@ -341,13 +364,13 @@
|
||||||
"jsr:@std/path@0.224.0"
|
"jsr:@std/path@0.224.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@nostrify/db@0.36.1": {
|
"@nostrify/db@0.39.4": {
|
||||||
"integrity": "b65b89ca6fe98d9dbcc0402b5c9c07b8430c2c91f84ba4128ff2eeed70c3d49f",
|
"integrity": "53fecea3b67394cf4f52795f89d1d065bdeb0627b8655cc7fc3a89d6b21adf01",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@nostrify/nostrify@0.36",
|
"jsr:@nostrify/nostrify@0.39",
|
||||||
"jsr:@nostrify/types@0.35",
|
"jsr:@nostrify/types@0.36",
|
||||||
"npm:kysely@~0.27.3",
|
"npm:kysely@~0.27.3",
|
||||||
"npm:nostr-tools@^2.7.0"
|
"npm:nostr-tools@^2.10.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@nostrify/nostrify@0.22.4": {
|
"@nostrify/nostrify@0.22.4": {
|
||||||
|
|
@ -361,7 +384,7 @@
|
||||||
"npm:kysely@~0.27.3",
|
"npm:kysely@~0.27.3",
|
||||||
"npm:lru-cache@^10.2.0",
|
"npm:lru-cache@^10.2.0",
|
||||||
"npm:nostr-tools@^2.5.0",
|
"npm:nostr-tools@^2.5.0",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts@^2.1.5",
|
||||||
"npm:zod"
|
"npm:zod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -375,7 +398,7 @@
|
||||||
"npm:kysely@~0.27.3",
|
"npm:kysely@~0.27.3",
|
||||||
"npm:lru-cache@^10.2.0",
|
"npm:lru-cache@^10.2.0",
|
||||||
"npm:nostr-tools@^2.7.0",
|
"npm:nostr-tools@^2.7.0",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts@^2.1.5",
|
||||||
"npm:zod"
|
"npm:zod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -390,7 +413,7 @@
|
||||||
"npm:@scure/bip39",
|
"npm:@scure/bip39",
|
||||||
"npm:lru-cache@^10.2.0",
|
"npm:lru-cache@^10.2.0",
|
||||||
"npm:nostr-tools@^2.7.0",
|
"npm:nostr-tools@^2.7.0",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts@^2.1.5",
|
||||||
"npm:zod"
|
"npm:zod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -403,7 +426,7 @@
|
||||||
"npm:@scure/bip39",
|
"npm:@scure/bip39",
|
||||||
"npm:lru-cache@^10.2.0",
|
"npm:lru-cache@^10.2.0",
|
||||||
"npm:nostr-tools@^2.7.0",
|
"npm:nostr-tools@^2.7.0",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts@^2.1.5",
|
||||||
"npm:zod"
|
"npm:zod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -416,20 +439,7 @@
|
||||||
"npm:@scure/bip39",
|
"npm:@scure/bip39",
|
||||||
"npm:lru-cache@^10.2.0",
|
"npm:lru-cache@^10.2.0",
|
||||||
"npm:nostr-tools@^2.7.0",
|
"npm:nostr-tools@^2.7.0",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts@^2.1.5",
|
||||||
"npm:zod"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@nostrify/nostrify@0.35.0": {
|
|
||||||
"integrity": "9bfef4883838b8b4cb2e2b28a60b72de95391ca5b789bc7206a2baea054dea55",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@nostrify/types@0.35",
|
|
||||||
"jsr:@std/encoding@~0.224.1",
|
|
||||||
"npm:@scure/bip32",
|
|
||||||
"npm:@scure/bip39",
|
|
||||||
"npm:lru-cache@^10.2.0",
|
|
||||||
"npm:nostr-tools@^2.7.0",
|
|
||||||
"npm:websocket-ts",
|
|
||||||
"npm:zod"
|
"npm:zod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -444,7 +454,67 @@
|
||||||
"npm:@scure/bip39",
|
"npm:@scure/bip39",
|
||||||
"npm:lru-cache@^10.2.0",
|
"npm:lru-cache@^10.2.0",
|
||||||
"npm:nostr-tools@^2.7.0",
|
"npm:nostr-tools@^2.7.0",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts@^2.1.5",
|
||||||
|
"npm:zod"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nostrify/nostrify@0.36.2": {
|
||||||
|
"integrity": "cc4787ca170b623a2e5dfed1baa4426077daa6143af728ea7dd325d58f4d04d6",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@nostrify/types@0.35",
|
||||||
|
"jsr:@std/encoding@~0.224.1",
|
||||||
|
"npm:@scure/bip32",
|
||||||
|
"npm:@scure/bip39",
|
||||||
|
"npm:lru-cache@^10.2.0",
|
||||||
|
"npm:nostr-tools@^2.7.0",
|
||||||
|
"npm:websocket-ts@^2.1.5",
|
||||||
|
"npm:zod"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nostrify/nostrify@0.38.0": {
|
||||||
|
"integrity": "9ec7920057ee3a4dcbaef7e706dedea622bfdfdf0f6aac11047443f88d953deb",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@nostrify/types@0.36",
|
||||||
|
"jsr:@std/crypto",
|
||||||
|
"jsr:@std/encoding@~0.224.1",
|
||||||
|
"npm:@scure/base",
|
||||||
|
"npm:@scure/bip32",
|
||||||
|
"npm:@scure/bip39",
|
||||||
|
"npm:lru-cache@^10.2.0",
|
||||||
|
"npm:nostr-tools@^2.10.4",
|
||||||
|
"npm:websocket-ts@^2.1.5",
|
||||||
|
"npm:zod"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nostrify/nostrify@0.39.0": {
|
||||||
|
"integrity": "f7e052c32b8b9bafe0f2517dcf090e7d3df5aed38452a0cf61ade817d34067ee",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@nostrify/nostrify@0.39",
|
||||||
|
"jsr:@nostrify/types@0.36",
|
||||||
|
"jsr:@std/crypto",
|
||||||
|
"jsr:@std/encoding@~0.224.1",
|
||||||
|
"npm:@scure/base",
|
||||||
|
"npm:@scure/bip32",
|
||||||
|
"npm:@scure/bip39",
|
||||||
|
"npm:lru-cache@^10.2.0",
|
||||||
|
"npm:nostr-tools@^2.10.4",
|
||||||
|
"npm:websocket-ts@^2.2.1",
|
||||||
|
"npm:zod"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nostrify/nostrify@0.39.1": {
|
||||||
|
"integrity": "84f98c815a07f4151bd02188a3525e438c416e9de632c79c9da9edbfca580d7f",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@nostrify/nostrify@~0.39.1",
|
||||||
|
"jsr:@nostrify/types@0.36",
|
||||||
|
"jsr:@std/crypto",
|
||||||
|
"jsr:@std/encoding@~0.224.1",
|
||||||
|
"npm:@scure/base",
|
||||||
|
"npm:@scure/bip32",
|
||||||
|
"npm:@scure/bip39",
|
||||||
|
"npm:lru-cache@^10.2.0",
|
||||||
|
"npm:nostr-tools@^2.10.4",
|
||||||
|
"npm:websocket-ts@^2.2.1",
|
||||||
"npm:zod"
|
"npm:zod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -470,14 +540,6 @@
|
||||||
"npm:nostr-tools@^2.7.0"
|
"npm:nostr-tools@^2.7.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@nostrify/policies@0.35.0": {
|
|
||||||
"integrity": "b828fac9f253e460a9587c05588b7dae6a0a32c5a9c9083e449219887b9e8e20",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@nostrify/nostrify@0.35",
|
|
||||||
"jsr:@nostrify/types@0.35",
|
|
||||||
"npm:nostr-tools@^2.7.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@nostrify/policies@0.36.0": {
|
"@nostrify/policies@0.36.0": {
|
||||||
"integrity": "ad1930de48ce03cdf34da456af1563b487581d1d86683cd416ad760ae40b1fb3",
|
"integrity": "ad1930de48ce03cdf34da456af1563b487581d1d86683cd416ad760ae40b1fb3",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -503,21 +565,24 @@
|
||||||
"@nostrify/types@0.35.0": {
|
"@nostrify/types@0.35.0": {
|
||||||
"integrity": "b8d515563d467072694557d5626fa1600f74e83197eef45dd86a9a99c64f7fe6"
|
"integrity": "b8d515563d467072694557d5626fa1600f74e83197eef45dd86a9a99c64f7fe6"
|
||||||
},
|
},
|
||||||
|
"@nostrify/types@0.36.0": {
|
||||||
|
"integrity": "b3413467debcbd298d217483df4e2aae6c335a34765c90ac7811cf7c637600e7"
|
||||||
|
},
|
||||||
"@soapbox/kysely-pglite@1.0.0": {
|
"@soapbox/kysely-pglite@1.0.0": {
|
||||||
"integrity": "0954b1bf3deab051c479cba966b1e6ed5a0a966aa21d1f40143ec8f5efcd475d",
|
"integrity": "0954b1bf3deab051c479cba966b1e6ed5a0a966aa21d1f40143ec8f5efcd475d",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:kysely@~0.27.4"
|
"npm:kysely@~0.27.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@soapbox/logi@0.3.0": {
|
||||||
|
"integrity": "5aa5121e82422b0a1b5ec81790f75407c16c788d10af629cecef9a35d1b4c290"
|
||||||
|
},
|
||||||
"@soapbox/safe-fetch@2.0.0": {
|
"@soapbox/safe-fetch@2.0.0": {
|
||||||
"integrity": "f451d686501c76a0faa058fe9d2073676282a8a42c3b93c59159eb9191f11b5f",
|
"integrity": "f451d686501c76a0faa058fe9d2073676282a8a42c3b93c59159eb9191f11b5f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:tldts@^6.1.61"
|
"npm:tldts@^6.1.61"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@soapbox/stickynotes@0.4.0": {
|
|
||||||
"integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec"
|
|
||||||
},
|
|
||||||
"@std/assert@0.213.1": {
|
"@std/assert@0.213.1": {
|
||||||
"integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe"
|
"integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe"
|
||||||
},
|
},
|
||||||
|
|
@ -530,7 +595,13 @@
|
||||||
"@std/assert@0.225.3": {
|
"@std/assert@0.225.3": {
|
||||||
"integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f",
|
"integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/internal"
|
"jsr:@std/internal@1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/assert@1.0.11": {
|
||||||
|
"integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal@^1.0.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/bytes@0.223.0": {
|
"@std/bytes@0.223.0": {
|
||||||
|
|
@ -545,6 +616,9 @@
|
||||||
"@std/bytes@1.0.2": {
|
"@std/bytes@1.0.2": {
|
||||||
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
|
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
|
||||||
},
|
},
|
||||||
|
"@std/bytes@1.0.4": {
|
||||||
|
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
|
||||||
|
},
|
||||||
"@std/cli@0.223.0": {
|
"@std/cli@0.223.0": {
|
||||||
"integrity": "2feb7970f2028904c3edc22ea916ce9538113dfc170844f3eae03578c333c356",
|
"integrity": "2feb7970f2028904c3edc22ea916ce9538113dfc170844f3eae03578c333c356",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -558,6 +632,9 @@
|
||||||
"jsr:@std/encoding@0.224"
|
"jsr:@std/encoding@0.224"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@std/data-structures@1.0.6": {
|
||||||
|
"integrity": "76a7fd8080c66604c0496220a791860492ab21a04a63a969c0b9a0609bbbb760"
|
||||||
|
},
|
||||||
"@std/dotenv@0.224.0": {
|
"@std/dotenv@0.224.0": {
|
||||||
"integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d"
|
"integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d"
|
||||||
},
|
},
|
||||||
|
|
@ -589,6 +666,12 @@
|
||||||
"jsr:@std/path@1.0.0-rc.1"
|
"jsr:@std/path@1.0.0-rc.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@std/fs@1.0.11": {
|
||||||
|
"integrity": "ba674672693340c5ebdd018b4fe1af46cb08741f42b4c538154e97d217b55bdd",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/path@^1.0.8"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/internal@1.0.0": {
|
"@std/internal@1.0.0": {
|
||||||
"integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a"
|
"integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a"
|
||||||
},
|
},
|
||||||
|
|
@ -601,6 +684,9 @@
|
||||||
"@std/internal@1.0.4": {
|
"@std/internal@1.0.4": {
|
||||||
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
|
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
|
||||||
},
|
},
|
||||||
|
"@std/internal@1.0.5": {
|
||||||
|
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||||
|
},
|
||||||
"@std/io@0.223.0": {
|
"@std/io@0.223.0": {
|
||||||
"integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1",
|
"integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -650,6 +736,12 @@
|
||||||
"jsr:@std/bytes@^1.0.2"
|
"jsr:@std/bytes@^1.0.2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@std/io@0.224.9": {
|
||||||
|
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/bytes@^1.0.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/json@0.223.0": {
|
"@std/json@0.223.0": {
|
||||||
"integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f",
|
"integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -677,6 +769,9 @@
|
||||||
"@std/path@1.0.0-rc.1": {
|
"@std/path@1.0.0-rc.1": {
|
||||||
"integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6"
|
"integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6"
|
||||||
},
|
},
|
||||||
|
"@std/path@1.0.8": {
|
||||||
|
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||||
|
},
|
||||||
"@std/streams@0.223.0": {
|
"@std/streams@0.223.0": {
|
||||||
"integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99",
|
"integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -684,9 +779,38 @@
|
||||||
"jsr:@std/bytes@0.223",
|
"jsr:@std/bytes@0.223",
|
||||||
"jsr:@std/io@0.223"
|
"jsr:@std/io@0.223"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"@std/testing@1.0.9": {
|
||||||
|
"integrity": "9bdd4ac07cb13e7594ac30e90f6ceef7254ac83a9aeaa089be0008f33aab5cd4",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/assert@^1.0.10",
|
||||||
|
"jsr:@std/data-structures",
|
||||||
|
"jsr:@std/fs@^1.0.9",
|
||||||
|
"jsr:@std/internal@^1.0.5",
|
||||||
|
"jsr:@std/path@^1.0.8"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"@cashu/cashu-ts@2.2.0": {
|
||||||
|
"integrity": "sha512-7b6pGyjjpm3uAJvmOL+ztpRxqp1qnmzGpydp+Pu30pOjxj93EhejPTJVrZMDJ0P35y6u5+5jIjHF4k0fpovvmg==",
|
||||||
|
"dependencies": [
|
||||||
|
"@cashu/crypto",
|
||||||
|
"@noble/curves@1.4.0",
|
||||||
|
"@noble/hashes@1.4.0",
|
||||||
|
"buffer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@cashu/crypto@0.3.4": {
|
||||||
|
"integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@noble/curves@1.8.1",
|
||||||
|
"@noble/hashes@1.7.1",
|
||||||
|
"@scure/bip32@1.6.2",
|
||||||
|
"@scure/bip39@1.5.4",
|
||||||
|
"buffer"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@electric-sql/pglite@0.2.8": {
|
"@electric-sql/pglite@0.2.8": {
|
||||||
"integrity": "sha512-0wSmQu22euBRzR5ghqyIHnBH4MfwlkL5WstOrrA3KOsjEWEglvoL/gH92JajEUA6Ufei/+qbkB2hVloC/K/RxQ=="
|
"integrity": "sha512-0wSmQu22euBRzR5ghqyIHnBH4MfwlkL5WstOrrA3KOsjEWEglvoL/gH92JajEUA6Ufei/+qbkB2hVloC/K/RxQ=="
|
||||||
},
|
},
|
||||||
|
|
@ -804,6 +928,12 @@
|
||||||
"@noble/hashes@1.4.0"
|
"@noble/hashes@1.4.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@noble/curves@1.8.1": {
|
||||||
|
"integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"@noble/hashes@1.7.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@noble/hashes@1.3.1": {
|
"@noble/hashes@1.3.1": {
|
||||||
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
|
||||||
},
|
},
|
||||||
|
|
@ -813,6 +943,9 @@
|
||||||
"@noble/hashes@1.4.0": {
|
"@noble/hashes@1.4.0": {
|
||||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="
|
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="
|
||||||
},
|
},
|
||||||
|
"@noble/hashes@1.7.1": {
|
||||||
|
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="
|
||||||
|
},
|
||||||
"@noble/secp256k1@2.1.0": {
|
"@noble/secp256k1@2.1.0": {
|
||||||
"integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw=="
|
"integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw=="
|
||||||
},
|
},
|
||||||
|
|
@ -825,6 +958,9 @@
|
||||||
"@scure/base@1.1.6": {
|
"@scure/base@1.1.6": {
|
||||||
"integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g=="
|
"integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g=="
|
||||||
},
|
},
|
||||||
|
"@scure/base@1.2.4": {
|
||||||
|
"integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="
|
||||||
|
},
|
||||||
"@scure/bip32@1.3.1": {
|
"@scure/bip32@1.3.1": {
|
||||||
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -841,6 +977,14 @@
|
||||||
"@scure/base@1.1.6"
|
"@scure/base@1.1.6"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@scure/bip32@1.6.2": {
|
||||||
|
"integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==",
|
||||||
|
"dependencies": [
|
||||||
|
"@noble/curves@1.8.1",
|
||||||
|
"@noble/hashes@1.7.1",
|
||||||
|
"@scure/base@1.2.4"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@scure/bip39@1.2.1": {
|
"@scure/bip39@1.2.1": {
|
||||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -855,17 +999,24 @@
|
||||||
"@scure/base@1.1.6"
|
"@scure/base@1.1.6"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@scure/bip39@1.5.4": {
|
||||||
|
"integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@noble/hashes@1.7.1",
|
||||||
|
"@scure/base@1.2.4"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@types/dompurify@3.0.5": {
|
"@types/dompurify@3.0.5": {
|
||||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/trusted-types"
|
"@types/trusted-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/node@17.0.45": {
|
"@types/node@22.5.4": {
|
||||||
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
|
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||||
},
|
"dependencies": [
|
||||||
"@types/node@18.16.19": {
|
"undici-types"
|
||||||
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA=="
|
]
|
||||||
},
|
},
|
||||||
"@types/trusted-types@2.0.7": {
|
"@types/trusted-types@2.0.7": {
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||||
|
|
@ -891,6 +1042,9 @@
|
||||||
"asynckit@0.4.0": {
|
"asynckit@0.4.0": {
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
|
"base64-js@1.5.1": {
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||||
|
},
|
||||||
"bintrees@1.0.2": {
|
"bintrees@1.0.2": {
|
||||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
|
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
|
||||||
},
|
},
|
||||||
|
|
@ -903,6 +1057,13 @@
|
||||||
"fill-range"
|
"fill-range"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"buffer@6.0.3": {
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"dependencies": [
|
||||||
|
"base64-js",
|
||||||
|
"ieee754"
|
||||||
|
]
|
||||||
|
},
|
||||||
"chalk@5.3.0": {
|
"chalk@5.3.0": {
|
||||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
|
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
|
||||||
},
|
},
|
||||||
|
|
@ -1141,6 +1302,9 @@
|
||||||
"safer-buffer"
|
"safer-buffer"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ieee754@1.2.1": {
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||||
|
},
|
||||||
"image-size@1.1.1": {
|
"image-size@1.1.1": {
|
||||||
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -1174,8 +1338,8 @@
|
||||||
"isexe@2.0.0": {
|
"isexe@2.0.0": {
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||||
},
|
},
|
||||||
"iso-639-1@2.1.15": {
|
"iso-639-1@3.1.5": {
|
||||||
"integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg=="
|
"integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA=="
|
||||||
},
|
},
|
||||||
"isomorphic-dompurify@2.16.0": {
|
"isomorphic-dompurify@2.16.0": {
|
||||||
"integrity": "sha512-cXhX2owp8rPxafCr0ywqy2CGI/4ceLNgWkWBEvUz64KTbtg3oRL2ZRqq/zW0pzt4YtDjkHLbwcp/lozpKzAQjg==",
|
"integrity": "sha512-cXhX2owp8rPxafCr0ywqy2CGI/4ceLNgWkWBEvUz64KTbtg3oRL2ZRqq/zW0pzt4YtDjkHLbwcp/lozpKzAQjg==",
|
||||||
|
|
@ -1333,6 +1497,18 @@
|
||||||
"whatwg-url@5.0.0"
|
"whatwg-url@5.0.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"nostr-tools@2.10.4": {
|
||||||
|
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||||
|
"dependencies": [
|
||||||
|
"@noble/ciphers",
|
||||||
|
"@noble/curves@1.2.0",
|
||||||
|
"@noble/hashes@1.3.1",
|
||||||
|
"@scure/base@1.1.1",
|
||||||
|
"@scure/bip32@1.3.1",
|
||||||
|
"@scure/bip39@1.2.1",
|
||||||
|
"nostr-wasm"
|
||||||
|
]
|
||||||
|
},
|
||||||
"nostr-tools@2.5.1": {
|
"nostr-tools@2.5.1": {
|
||||||
"integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==",
|
"integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -1402,14 +1578,6 @@
|
||||||
"pidtree@0.6.0": {
|
"pidtree@0.6.0": {
|
||||||
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="
|
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="
|
||||||
},
|
},
|
||||||
"png-to-ico@2.1.8": {
|
|
||||||
"integrity": "sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==",
|
|
||||||
"dependencies": [
|
|
||||||
"@types/node@17.0.45",
|
|
||||||
"minimist",
|
|
||||||
"pngjs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pngjs@6.0.0": {
|
"pngjs@6.0.0": {
|
||||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="
|
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="
|
||||||
},
|
},
|
||||||
|
|
@ -1611,6 +1779,9 @@
|
||||||
"type-fest@4.18.2": {
|
"type-fest@4.18.2": {
|
||||||
"integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg=="
|
"integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg=="
|
||||||
},
|
},
|
||||||
|
"undici-types@6.19.8": {
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
|
},
|
||||||
"unfurl.js@6.4.0": {
|
"unfurl.js@6.4.0": {
|
||||||
"integrity": "sha512-DogJFWPkOWMcu2xPdpmbcsL+diOOJInD3/jXOv6saX1upnWmMK8ndAtDWUfJkuInqNI9yzADud4ID9T+9UeWCw==",
|
"integrity": "sha512-DogJFWPkOWMcu2xPdpmbcsL+diOOJInD3/jXOv6saX1upnWmMK8ndAtDWUfJkuInqNI9yzADud4ID9T+9UeWCw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -1636,6 +1807,9 @@
|
||||||
"websocket-ts@2.1.5": {
|
"websocket-ts@2.1.5": {
|
||||||
"integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA=="
|
"integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA=="
|
||||||
},
|
},
|
||||||
|
"websocket-ts@2.2.1": {
|
||||||
|
"integrity": "sha512-YKPDfxlK5qOheLZ2bTIiktZO1bpfGdNCPJmTEaPW7G9UXI1GKjDdeacOrsULUS000OPNxDVOyAuKLuIWPqWM0Q=="
|
||||||
|
},
|
||||||
"whatwg-encoding@3.1.1": {
|
"whatwg-encoding@3.1.1": {
|
||||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -2302,17 +2476,18 @@
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@b-fuze/deno-dom@~0.1.47",
|
"jsr:@b-fuze/deno-dom@~0.1.47",
|
||||||
"jsr:@bradenmacdonald/s3-lite-client@~0.7.4",
|
"jsr:@bradenmacdonald/s3-lite-client@~0.7.4",
|
||||||
|
"jsr:@core/asyncutil@^1.2.0",
|
||||||
"jsr:@esroyo/scoped-performance@^3.1.0",
|
"jsr:@esroyo/scoped-performance@^3.1.0",
|
||||||
"jsr:@gfx/canvas-wasm@~0.4.2",
|
"jsr:@gfx/canvas-wasm@~0.4.2",
|
||||||
"jsr:@hono/hono@^4.4.6",
|
"jsr:@hono/hono@^4.4.6",
|
||||||
"jsr:@lambdalisue/async@^2.1.1",
|
|
||||||
"jsr:@negrel/webpush@0.3",
|
"jsr:@negrel/webpush@0.3",
|
||||||
"jsr:@nostrify/db@~0.36.1",
|
"jsr:@nostrify/db@~0.39.4",
|
||||||
"jsr:@nostrify/nostrify@0.36",
|
"jsr:@nostrify/nostrify@~0.39.1",
|
||||||
"jsr:@nostrify/policies@0.35",
|
"jsr:@nostrify/policies@~0.36.1",
|
||||||
|
"jsr:@nostrify/types@0.36",
|
||||||
"jsr:@soapbox/kysely-pglite@1",
|
"jsr:@soapbox/kysely-pglite@1",
|
||||||
|
"jsr:@soapbox/logi@0.3",
|
||||||
"jsr:@soapbox/safe-fetch@2",
|
"jsr:@soapbox/safe-fetch@2",
|
||||||
"jsr:@soapbox/stickynotes@0.4",
|
|
||||||
"jsr:@std/assert@~0.225.1",
|
"jsr:@std/assert@~0.225.1",
|
||||||
"jsr:@std/cli@0.223",
|
"jsr:@std/cli@0.223",
|
||||||
"jsr:@std/crypto@0.224",
|
"jsr:@std/crypto@0.224",
|
||||||
|
|
@ -2321,6 +2496,8 @@
|
||||||
"jsr:@std/json@0.223",
|
"jsr:@std/json@0.223",
|
||||||
"jsr:@std/media-types@~0.224.1",
|
"jsr:@std/media-types@~0.224.1",
|
||||||
"jsr:@std/streams@0.223",
|
"jsr:@std/streams@0.223",
|
||||||
|
"jsr:@std/testing@^1.0.9",
|
||||||
|
"npm:@cashu/cashu-ts@^2.2.0",
|
||||||
"npm:@electric-sql/pglite@~0.2.8",
|
"npm:@electric-sql/pglite@~0.2.8",
|
||||||
"npm:@isaacs/ttlcache@^1.4.1",
|
"npm:@isaacs/ttlcache@^1.4.1",
|
||||||
"npm:@noble/secp256k1@2",
|
"npm:@noble/secp256k1@2",
|
||||||
|
|
@ -2333,7 +2510,7 @@
|
||||||
"npm:fast-stable-stringify@1",
|
"npm:fast-stable-stringify@1",
|
||||||
"npm:formdata-helper@0.3",
|
"npm:formdata-helper@0.3",
|
||||||
"npm:hono-rate-limiter@0.3",
|
"npm:hono-rate-limiter@0.3",
|
||||||
"npm:iso-639-1@2.1.15",
|
"npm:iso-639-1@^3.1.5",
|
||||||
"npm:isomorphic-dompurify@^2.16.0",
|
"npm:isomorphic-dompurify@^2.16.0",
|
||||||
"npm:kysely-postgres-js@2.0.0",
|
"npm:kysely-postgres-js@2.0.0",
|
||||||
"npm:kysely@~0.27.4",
|
"npm:kysely@~0.27.4",
|
||||||
|
|
@ -2346,7 +2523,6 @@
|
||||||
"npm:nostr-tools@2.5.1",
|
"npm:nostr-tools@2.5.1",
|
||||||
"npm:nostr-wasm@0.1",
|
"npm:nostr-wasm@0.1",
|
||||||
"npm:path-to-regexp@^7.1.0",
|
"npm:path-to-regexp@^7.1.0",
|
||||||
"npm:png-to-ico@^2.1.8",
|
|
||||||
"npm:prom-client@^15.1.2",
|
"npm:prom-client@^15.1.2",
|
||||||
"npm:sharp@~0.33.5",
|
"npm:sharp@~0.33.5",
|
||||||
"npm:tldts@^6.0.14",
|
"npm:tldts@^6.0.14",
|
||||||
|
|
|
||||||
23
docs/auth.md
23
docs/auth.md
|
|
@ -1,23 +0,0 @@
|
||||||
# Authentication in Ditto
|
|
||||||
|
|
||||||
One of the main benefits of Nostr is that users control their keys. Instead of a username and password, the user has a public key (`npub` or `pubkey`) and private key (`nsec`). The public key is a globally-unique identifier for the user, and the private key can be used to sign events, producing a signature that only the pubkey could have produced.
|
|
||||||
|
|
||||||
With keys, users have full control over their identity. They can move between servers freely, and post to multiple servers at once. But with such power comes great responsibilities. Users cannot lose control of their key, or they'll lose control over their account forever.
|
|
||||||
|
|
||||||
## Managing Keys
|
|
||||||
|
|
||||||
There are several ways to manage keys in Nostr, and they all come with trade-offs. It's new territory, and people are still coming up with new ideas.
|
|
||||||
|
|
||||||
The main concerns are how to **conveniently log in on multiple devices**, and **who/what to trust with your key.**
|
|
||||||
|
|
||||||
### Current Solutions
|
|
||||||
|
|
||||||
1. **Private key text.** Users copy their key between devices/apps, giving apps full control over their key. Users might email the key to themselves, or better yet use a password manager, or apps might even provide a QR code for other apps to scan. This method is convenient, but it's not secure. Keys can get compromised in transit, or by a malicious or vulnerable app.
|
|
||||||
|
|
||||||
2. **Browser extension.** For web clients, an extension can expose `getPublicKey` and `signEvent` functions to web-pages without exposing the private key directly. This option is secure, but it only works well for laptop/desktop devices. On mobile, only FireFox can do it, with no support from Safari or Chrome. It also offers no way to share a key across devices on its own.
|
|
||||||
|
|
||||||
3. **Remote signer**. Users can run a remote signer program and then connect apps to it. The signer should be running 24/7, so it's best suited for running on a server. This idea has evolved into the creation of "bunker" services. Bunkers allow users to have a traditional username and password and login from anywhere. This method solves a lot of problems, but it also creates some problems. Users have to create an account on a separate website before they can log into your website. This makes it an option for more advanced users. Also, it's concerning that the administrator of the bunker server has full control over your keys. None of this is a problem if you run your own remote signer, but it's not a mainstream option.
|
|
||||||
|
|
||||||
4. **Custodial**. Apps which make you log you in with a username/password, and then keep Nostr keys for each user in their database. You might not even be able to export your keys. This option may be easier for users at first, but it puts a whole lot of liability on the server, since leaks can cause permanent damage. It also gives up a lot of the benefits of Nostr.
|
|
||||||
|
|
||||||
Each of these ideas could be improved upon greatly with new experiments and technical progress. But to Ditto, user freedom matters the most, so we're focusing on non-custodial solution. Even though there are security risks to copying around keys, the onus is on the user. The user may fall victim to a targeted attack (or make a stupid mistake), whereas custodial servers have the ability to wipe out entire demographics of users at once. Therefore we believe that custodial solutions are actually _less_ secure than users copying around keys. Users must take precautions about which apps to trust with their private key until we improve upon the area to make it more secure (likely with better support of browser extensions, OS key management, and more).
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# Debugging Ditto
|
|
||||||
|
|
||||||
Running the command `deno task debug` will start the Ditto server in debug mode, making it possible to inspect with Chromium-based browsers by visiting `chrome://inspect`.
|
|
||||||
|
|
||||||
From there, go to the "Performance" tab and click "Start profiling". Perform the actions you want to profile, then click "Stop profiling". You can then inspect the call stack and see where the time is being spent.
|
|
||||||
|
|
||||||
## Remote debugging
|
|
||||||
|
|
||||||
If the Ditto server is on a separate machine, you will first need to put it into debug mode. Edit its systemd file (usually located at `/etc/systemd/system/ditto.service`) and change `deno task start` to `deno task debug` in the `ExecStart` line. Then run `systemctl daemon-reload` and `systemctl restart ditto`.
|
|
||||||
|
|
||||||
To access the debugger remotely, you can use SSH port forwarding. Run this command on your local machine, replacing `<user>@<host>` with the SSH login for the remote machine:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ssh -L 9229:localhost:9229 <user>@<host>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, in Chromium, go to `chrome://inspect` and the Ditto server should be available.
|
|
||||||
|
|
||||||
## SQL performance
|
|
||||||
|
|
||||||
To track slow queries, first set `DEBUG=ditto:sql` in the environment so only SQL logs are shown.
|
|
||||||
|
|
||||||
Then, grep for any logs above 0.001s:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
journalctl -fu ditto | grep -v '(0.00s)'
|
|
||||||
```
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# Installing Ditto
|
|
||||||
|
|
||||||
First, install Deno:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, run Ditto:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
deno run -A https://gitlab.com/soapbox-pub/ditto/-/raw/main/src/server.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it! Ditto is now running on your machine.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# Mastodon API
|
|
||||||
|
|
||||||
Ditto implements Mastodon's client-server API, a REST API used by Mastodon mobile apps and frontends to interact with Mastodon servers. While it was originally designed for Mastodon, it has been adopted by other ActivityPub servers such as Pleroma, Mitra, Friendica, and many others.
|
|
||||||
|
|
||||||
Note that Mastodon API is **not** ActivityPub. It is not the API used to federate between servers. Instead, it enables user interfaces, mobile apps, bots, and other clients to interact with Mastodon servers.
|
|
||||||
|
|
||||||
Mastodon is built in Ruby on Rails, and its API is inspired by Twitter's legacy REST API. Rails, being an MVC framework, has "models", which it maps directly to "Entities" in its API.
|
|
||||||
|
|
||||||
Endpoints return either a single Entity, or an array of Entities. Entities Entities are JSON objects with a specific structure, and are documented in the [Mastodon API documentation](https://docs.joinmastodon.org/api/).
|
|
||||||
34
installation/Caddyfile
Normal file
34
installation/Caddyfile
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Cloudflare real IP configuration for rate-limiting
|
||||||
|
# {
|
||||||
|
# servers {
|
||||||
|
# # https://www.cloudflare.com/ips/
|
||||||
|
# trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
|
||||||
|
# trusted_proxies_strict
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
log
|
||||||
|
request_header X-Real-IP {client_ip}
|
||||||
|
|
||||||
|
@public path /packs/* /instance/* /images/* /favicon.ico /sw.js /sw.js.map
|
||||||
|
|
||||||
|
handle /packs/* {
|
||||||
|
root * /opt/ditto/public
|
||||||
|
header Cache-Control "max-age=31536000, public, immutable"
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
handle @public {
|
||||||
|
root * /opt/ditto/public
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /metrics {
|
||||||
|
respond "Access denied" 403
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy :4036
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ After=network-online.target
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=ditto
|
User=ditto
|
||||||
|
SyslogIdentifier=ditto
|
||||||
WorkingDirectory=/opt/ditto
|
WorkingDirectory=/opt/ditto
|
||||||
ExecStart=/usr/local/bin/deno task start
|
ExecStart=/usr/local/bin/deno task start
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
|
||||||
31
packages/conf/DittoConf.test.ts
Normal file
31
packages/conf/DittoConf.test.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { assertEquals, assertThrows } from '@std/assert';
|
||||||
|
|
||||||
|
import { DittoConf } from './DittoConf.ts';
|
||||||
|
|
||||||
|
Deno.test('DittoConfig', async (t) => {
|
||||||
|
const env = new Map<string, string>([
|
||||||
|
['DITTO_NSEC', 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const config = new DittoConf(env);
|
||||||
|
|
||||||
|
await t.step('signer', async () => {
|
||||||
|
assertEquals(
|
||||||
|
await config.signer.getPublicKey(),
|
||||||
|
'1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('DittoConfig defaults', async (t) => {
|
||||||
|
const env = new Map<string, string>();
|
||||||
|
const config = new DittoConf(env);
|
||||||
|
|
||||||
|
await t.step('signer throws', () => {
|
||||||
|
assertThrows(() => config.signer);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step('port', () => {
|
||||||
|
assertEquals(config.port, 4036);
|
||||||
|
});
|
||||||
|
});
|
||||||
468
packages/conf/DittoConf.ts
Normal file
468
packages/conf/DittoConf.ts
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { NSecSigner } from '@nostrify/nostrify';
|
||||||
|
import { decodeBase64 } from '@std/encoding/base64';
|
||||||
|
import { encodeBase64Url } from '@std/encoding/base64url';
|
||||||
|
import ISO6391, { type LanguageCode } from 'iso-639-1';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { getEcdsaPublicKey } from './utils/crypto.ts';
|
||||||
|
import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts';
|
||||||
|
import { mergeURLPath } from './utils/url.ts';
|
||||||
|
|
||||||
|
/** Ditto application-wide configuration. */
|
||||||
|
export class DittoConf {
|
||||||
|
constructor(private env: { get(key: string): string | undefined }) {}
|
||||||
|
|
||||||
|
/** Cached parsed admin signer. */
|
||||||
|
private _signer: NSecSigner | undefined;
|
||||||
|
|
||||||
|
/** Cached parsed VAPID public key value. */
|
||||||
|
private _vapidPublicKey: Promise<string | undefined> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ditto admin secret key in hex format.
|
||||||
|
* @deprecated Use `signer` instead. TODO: handle auth tokens.
|
||||||
|
*/
|
||||||
|
get seckey(): Uint8Array {
|
||||||
|
const nsec = this.env.get('DITTO_NSEC');
|
||||||
|
|
||||||
|
if (!nsec) {
|
||||||
|
throw new Error('Missing DITTO_NSEC');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nsec.startsWith('nsec1')) {
|
||||||
|
throw new Error('Invalid DITTO_NSEC');
|
||||||
|
}
|
||||||
|
|
||||||
|
return nip19.decode(nsec as `nsec1${string}`).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ditto admin signer. */
|
||||||
|
get signer(): NSecSigner {
|
||||||
|
if (!this._signer) {
|
||||||
|
this._signer = new NSecSigner(this.seckey);
|
||||||
|
}
|
||||||
|
return this._signer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Port to use when serving the HTTP server. */
|
||||||
|
get port(): number {
|
||||||
|
return parseInt(this.env.get('PORT') || '4036');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IP addresses not affected by rate limiting. */
|
||||||
|
get ipWhitelist(): string[] {
|
||||||
|
return this.env.get('IP_WHITELIST')?.split(',') || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Relay URL to the Ditto server's relay. */
|
||||||
|
get relay(): `wss://${string}` | `ws://${string}` {
|
||||||
|
const { protocol, host } = this.url;
|
||||||
|
return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Relay to use for NIP-50 `search` queries. */
|
||||||
|
get searchRelay(): string | undefined {
|
||||||
|
return this.env.get('SEARCH_RELAY');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Origin of the Ditto server, including the protocol and port. */
|
||||||
|
get localDomain(): string {
|
||||||
|
return this.env.get('LOCAL_DOMAIN') || `http://localhost:${this.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Link to an external nostr viewer. */
|
||||||
|
get externalDomain(): string {
|
||||||
|
return this.env.get('NOSTR_EXTERNAL') || 'https://njump.me';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a link to a nip19-encoded entity in the configured external viewer. */
|
||||||
|
external(path: string): string {
|
||||||
|
return new URL(path, this.externalDomain).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heroku-style database URL. This is used in production to connect to the
|
||||||
|
* database.
|
||||||
|
*
|
||||||
|
* Follows the format:
|
||||||
|
*
|
||||||
|
* ```txt
|
||||||
|
* protocol://username:password@host:port/database_name
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get databaseUrl(): string {
|
||||||
|
return this.env.get('DATABASE_URL') ?? 'file://data/pgdata';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PGlite debug level. 0 disables logging. */
|
||||||
|
get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 {
|
||||||
|
return Number(this.env.get('PGLITE_DEBUG') || 0) as 0 | 1 | 2 | 3 | 4 | 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
get vapidPublicKey(): Promise<string | undefined> {
|
||||||
|
if (!this._vapidPublicKey) {
|
||||||
|
this._vapidPublicKey = (async () => {
|
||||||
|
const keys = await this.vapidKeys;
|
||||||
|
if (keys) {
|
||||||
|
const { publicKey } = keys;
|
||||||
|
const bytes = await crypto.subtle.exportKey('raw', publicKey);
|
||||||
|
return encodeBase64Url(bytes);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._vapidPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get vapidKeys(): Promise<CryptoKeyPair | undefined> {
|
||||||
|
return (async () => {
|
||||||
|
const encoded = this.env.get('VAPID_PRIVATE_KEY');
|
||||||
|
|
||||||
|
if (!encoded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyData = decodeBase64(encoded);
|
||||||
|
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
'pkcs8',
|
||||||
|
keyData,
|
||||||
|
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||||
|
true,
|
||||||
|
['sign'],
|
||||||
|
);
|
||||||
|
const publicKey = await getEcdsaPublicKey(privateKey, true);
|
||||||
|
|
||||||
|
return { privateKey, publicKey };
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
get db(): { timeouts: { default: number; relay: number; timelines: number } } {
|
||||||
|
const env = this.env;
|
||||||
|
return {
|
||||||
|
/** Database query timeout configurations. */
|
||||||
|
timeouts: {
|
||||||
|
/** Default query timeout when another setting isn't more specific. */
|
||||||
|
get default(): number {
|
||||||
|
return Number(env.get('DB_TIMEOUT_DEFAULT') || 5_000);
|
||||||
|
},
|
||||||
|
/** Timeout used for queries made through the Nostr relay. */
|
||||||
|
get relay(): number {
|
||||||
|
return Number(env.get('DB_TIMEOUT_RELAY') || 1_000);
|
||||||
|
},
|
||||||
|
/** Timeout used for timelines such as home, notifications, hashtag, etc. */
|
||||||
|
get timelines(): number {
|
||||||
|
return Number(env.get('DB_TIMEOUT_TIMELINES') || 15_000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time-to-live for captchas in milliseconds. */
|
||||||
|
get captchaTTL(): number {
|
||||||
|
return Number(this.env.get('CAPTCHA_TTL') || 5 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Character limit to enforce for posts made through Mastodon API. */
|
||||||
|
get postCharLimit(): number {
|
||||||
|
return Number(this.env.get('POST_CHAR_LIMIT') || 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** S3 media storage configuration. */
|
||||||
|
get s3(): {
|
||||||
|
endPoint?: string;
|
||||||
|
region?: string;
|
||||||
|
accessKey?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
bucket?: string;
|
||||||
|
pathStyle?: boolean;
|
||||||
|
port?: number;
|
||||||
|
sessionToken?: string;
|
||||||
|
useSSL?: boolean;
|
||||||
|
} {
|
||||||
|
const env = this.env;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get endPoint(): string | undefined {
|
||||||
|
return env.get('S3_ENDPOINT');
|
||||||
|
},
|
||||||
|
get region(): string | undefined {
|
||||||
|
return env.get('S3_REGION');
|
||||||
|
},
|
||||||
|
get accessKey(): string | undefined {
|
||||||
|
return env.get('S3_ACCESS_KEY');
|
||||||
|
},
|
||||||
|
get secretKey(): string | undefined {
|
||||||
|
return env.get('S3_SECRET_KEY');
|
||||||
|
},
|
||||||
|
get bucket(): string | undefined {
|
||||||
|
return env.get('S3_BUCKET');
|
||||||
|
},
|
||||||
|
get pathStyle(): boolean | undefined {
|
||||||
|
return optionalBooleanSchema.parse(env.get('S3_PATH_STYLE'));
|
||||||
|
},
|
||||||
|
get port(): number | undefined {
|
||||||
|
return optionalNumberSchema.parse(env.get('S3_PORT'));
|
||||||
|
},
|
||||||
|
get sessionToken(): string | undefined {
|
||||||
|
return env.get('S3_SESSION_TOKEN');
|
||||||
|
},
|
||||||
|
get useSSL(): boolean | undefined {
|
||||||
|
return optionalBooleanSchema.parse(env.get('S3_USE_SSL'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IPFS uploader configuration. */
|
||||||
|
get ipfs(): { apiUrl: string } {
|
||||||
|
const env = this.env;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Base URL for private IPFS API calls. */
|
||||||
|
get apiUrl(): string {
|
||||||
|
return env.get('IPFS_API_URL') || 'http://localhost:5001';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** nostr.build API endpoint when the `nostrbuild` uploader is used. */
|
||||||
|
get nostrbuildEndpoint(): string {
|
||||||
|
return this.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default Blossom servers to use when the `blossom` uploader is set. */
|
||||||
|
get blossomServers(): string[] {
|
||||||
|
return this.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Module to upload files with. */
|
||||||
|
get uploader(): string | undefined {
|
||||||
|
return this.env.get('DITTO_UPLOADER');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Location to use for local uploads. */
|
||||||
|
get uploadsDir(): string {
|
||||||
|
return this.env.get('UPLOADS_DIR') || 'data/uploads';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Media base URL for uploads. */
|
||||||
|
get mediaDomain(): string {
|
||||||
|
const value = this.env.get('MEDIA_DOMAIN');
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
const url = this.url;
|
||||||
|
url.host = `media.${url.host}`;
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to analyze media metadata with [blurhash](https://www.npmjs.com/package/blurhash) and [sharp](https://www.npmjs.com/package/sharp).
|
||||||
|
* This is prone to security vulnerabilities, which is why it's not enabled by default.
|
||||||
|
*/
|
||||||
|
get mediaAnalyze(): boolean {
|
||||||
|
return optionalBooleanSchema.parse(this.env.get('MEDIA_ANALYZE')) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Max upload size for files in number of bytes. Default 100MiB. */
|
||||||
|
get maxUploadSize(): number {
|
||||||
|
return Number(this.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Usernames that regular users cannot sign up with. */
|
||||||
|
get forbiddenUsernames(): string[] {
|
||||||
|
return this.env.get('FORBIDDEN_USERNAMES')?.split(',') || [
|
||||||
|
'_',
|
||||||
|
'admin',
|
||||||
|
'administrator',
|
||||||
|
'root',
|
||||||
|
'sysadmin',
|
||||||
|
'system',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */
|
||||||
|
get url(): URL {
|
||||||
|
return new URL(this.localDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merges the path with the localDomain. */
|
||||||
|
local(path: string): string {
|
||||||
|
return mergeURLPath(this.localDomain, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL to send Sentry errors to. */
|
||||||
|
get sentryDsn(): string | undefined {
|
||||||
|
return this.env.get('SENTRY_DSN');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Postgres settings. */
|
||||||
|
get pg(): { poolSize: number } {
|
||||||
|
const env = this.env;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Number of connections to use in the pool. */
|
||||||
|
get poolSize(): number {
|
||||||
|
return Number(env.get('PG_POOL_SIZE') ?? 20);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether to enable requesting events from known relays. */
|
||||||
|
get firehoseEnabled(): boolean {
|
||||||
|
return optionalBooleanSchema.parse(this.env.get('FIREHOSE_ENABLED')) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Number of events the firehose is allowed to process at one time before they have to wait in a queue. */
|
||||||
|
get firehoseConcurrency(): number {
|
||||||
|
return Math.ceil(Number(this.env.get('FIREHOSE_CONCURRENCY') ?? 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nostr event kinds of events to listen for on the firehose. */
|
||||||
|
get firehoseKinds(): number[] {
|
||||||
|
return (this.env.get('FIREHOSE_KINDS') ?? '0, 1, 3, 5, 6, 7, 20, 9735, 10002')
|
||||||
|
.split(/[, ]+/g)
|
||||||
|
.map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Ditto should subscribe to Nostr events from the Postgres database itself.
|
||||||
|
* This would make Nostr events inserted directly into Postgres available to the streaming API and relay.
|
||||||
|
*/
|
||||||
|
get notifyEnabled(): boolean {
|
||||||
|
return optionalBooleanSchema.parse(this.env.get('NOTIFY_ENABLED')) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether to enable Ditto cron jobs. */
|
||||||
|
get cronEnabled(): boolean {
|
||||||
|
return optionalBooleanSchema.parse(this.env.get('CRON_ENABLED')) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User-Agent to use when fetching link previews. Pretend to be Facebook by default. */
|
||||||
|
get fetchUserAgent(): string {
|
||||||
|
return this.env.get('DITTO_FETCH_USER_AGENT') ?? 'facebookexternalhit';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */
|
||||||
|
get policy(): string {
|
||||||
|
return this.env.get('DITTO_POLICY') || path.join(this.dataDir, 'policy.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Absolute path to the data directory used by Ditto. */
|
||||||
|
get dataDir(): string {
|
||||||
|
return this.env.get('DITTO_DATA_DIR') || path.join(Deno.cwd(), 'data');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Absolute path of the Deno directory. */
|
||||||
|
get denoDir(): string {
|
||||||
|
return this.env.get('DENO_DIR') || `${os.userInfo().homedir}/.cache/deno`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether zap splits should be enabled. */
|
||||||
|
get zapSplitsEnabled(): boolean {
|
||||||
|
return optionalBooleanSchema.parse(this.env.get('ZAP_SPLITS_ENABLED')) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Languages this server wishes to highlight. Used when querying trends.*/
|
||||||
|
get preferredLanguages(): LanguageCode[] | undefined {
|
||||||
|
return this.env.get('DITTO_LANGUAGES')?.split(',')?.filter(ISO6391.validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mints to be displayed in the UI when the user decides to create a wallet.*/
|
||||||
|
get cashuMints(): string[] {
|
||||||
|
return this.env.get('CASHU_MINTS')?.split(',') ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Translation provider used to translate posts. */
|
||||||
|
get translationProvider(): string | undefined {
|
||||||
|
return this.env.get('TRANSLATION_PROVIDER');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DeepL URL endpoint. */
|
||||||
|
get deeplBaseUrl(): string | undefined {
|
||||||
|
return this.env.get('DEEPL_BASE_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DeepL API KEY. */
|
||||||
|
get deeplApiKey(): string | undefined {
|
||||||
|
return this.env.get('DEEPL_API_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LibreTranslate URL endpoint. */
|
||||||
|
get libretranslateBaseUrl(): string | undefined {
|
||||||
|
return this.env.get('LIBRETRANSLATE_BASE_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LibreTranslate API KEY. */
|
||||||
|
get libretranslateApiKey(): string | undefined {
|
||||||
|
return this.env.get('LIBRETRANSLATE_API_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cache settings. */
|
||||||
|
get caches(): {
|
||||||
|
nip05: { max: number; ttl: number };
|
||||||
|
favicon: { max: number; ttl: number };
|
||||||
|
linkPreview: { max: number; ttl: number };
|
||||||
|
translation: { max: number; ttl: number };
|
||||||
|
} {
|
||||||
|
const env = this.env;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** NIP-05 cache settings. */
|
||||||
|
get nip05(): { max: number; ttl: number } {
|
||||||
|
return {
|
||||||
|
max: Number(env.get('DITTO_CACHE_NIP05_MAX') || 3000),
|
||||||
|
ttl: Number(env.get('DITTO_CACHE_NIP05_TTL') || 1 * 60 * 60 * 1000),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** Favicon cache settings. */
|
||||||
|
get favicon(): { max: number; ttl: number } {
|
||||||
|
return {
|
||||||
|
max: Number(env.get('DITTO_CACHE_FAVICON_MAX') || 500),
|
||||||
|
ttl: Number(env.get('DITTO_CACHE_FAVICON_TTL') || 1 * 60 * 60 * 1000),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** Link preview cache settings. */
|
||||||
|
get linkPreview(): { max: number; ttl: number } {
|
||||||
|
return {
|
||||||
|
max: Number(env.get('DITTO_CACHE_LINK_PREVIEW_MAX') || 3000),
|
||||||
|
ttl: Number(env.get('DITTO_CACHE_LINK_PREVIEW_TTL') || 12 * 60 * 60 * 1000),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/** Translation cache settings. */
|
||||||
|
get translation(): { max: number; ttl: number } {
|
||||||
|
return {
|
||||||
|
max: Number(env.get('DITTO_CACHE_TRANSLATION_MAX') || 1000),
|
||||||
|
ttl: Number(env.get('DITTO_CACHE_TRANSLATION_TTL') || 6 * 60 * 60 * 1000),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Custom profile fields configuration. */
|
||||||
|
get profileFields(): { maxFields: number; nameLength: number; valueLength: number } {
|
||||||
|
const env = this.env;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get maxFields(): number {
|
||||||
|
return Number(env.get('PROFILE_FIELDS_MAX_FIELDS') || 10);
|
||||||
|
},
|
||||||
|
get nameLength(): number {
|
||||||
|
return Number(env.get('PROFILE_FIELDS_NAME_LENGTH') || 255);
|
||||||
|
},
|
||||||
|
get valueLength(): number {
|
||||||
|
return Number(env.get('PROFILE_FIELDS_VALUE_LENGTH') || 2047);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maximum time between events before a streak is broken, *in seconds*. */
|
||||||
|
get streakWindow(): number {
|
||||||
|
return Number(this.env.get('STREAK_WINDOW') || 129600);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/conf/deno.json
Normal file
7
packages/conf/deno.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "@ditto/conf",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"exports": {
|
||||||
|
".": "./mod.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/conf/mod.ts
Normal file
1
packages/conf/mod.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { DittoConf } from './DittoConf.ts';
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { getEcdsaPublicKey } from '@/utils/crypto.ts';
|
import { getEcdsaPublicKey } from './crypto.ts';
|
||||||
|
|
||||||
Deno.test('getEcdsaPublicKey', async () => {
|
Deno.test('getEcdsaPublicKey', async () => {
|
||||||
const { publicKey, privateKey } = await crypto.subtle.generateKey(
|
const { publicKey, privateKey } = await crypto.subtle.generateKey(
|
||||||
17
packages/conf/utils/schema.test.ts
Normal file
17
packages/conf/utils/schema.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { assertEquals, assertThrows } from '@std/assert';
|
||||||
|
|
||||||
|
import { optionalBooleanSchema, optionalNumberSchema } from './schema.ts';
|
||||||
|
|
||||||
|
Deno.test('optionalBooleanSchema', () => {
|
||||||
|
assertEquals(optionalBooleanSchema.parse('true'), true);
|
||||||
|
assertEquals(optionalBooleanSchema.parse('false'), false);
|
||||||
|
assertEquals(optionalBooleanSchema.parse(undefined), undefined);
|
||||||
|
|
||||||
|
assertThrows(() => optionalBooleanSchema.parse('invalid'));
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('optionalNumberSchema', () => {
|
||||||
|
assertEquals(optionalNumberSchema.parse('123'), 123);
|
||||||
|
assertEquals(optionalNumberSchema.parse('invalid'), NaN); // maybe this should throw?
|
||||||
|
assertEquals(optionalNumberSchema.parse(undefined), undefined);
|
||||||
|
});
|
||||||
11
packages/conf/utils/schema.ts
Normal file
11
packages/conf/utils/schema.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const optionalBooleanSchema = z
|
||||||
|
.enum(['true', 'false'])
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value !== undefined ? value === 'true' : undefined);
|
||||||
|
|
||||||
|
export const optionalNumberSchema = z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value !== undefined ? Number(value) : undefined);
|
||||||
9
packages/conf/utils/url.test.ts
Normal file
9
packages/conf/utils/url.test.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { mergeURLPath } from './url.ts';
|
||||||
|
|
||||||
|
Deno.test('mergeURLPath', () => {
|
||||||
|
assertEquals(mergeURLPath('https://mario.com', '/path'), 'https://mario.com/path');
|
||||||
|
assertEquals(mergeURLPath('https://mario.com', 'https://luigi.com/path'), 'https://mario.com/path');
|
||||||
|
assertEquals(mergeURLPath('https://mario.com', 'https://luigi.com/path?q=1'), 'https://mario.com/path?q=1');
|
||||||
|
});
|
||||||
23
packages/conf/utils/url.ts
Normal file
23
packages/conf/utils/url.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Produce a URL whose origin is guaranteed to be the same as the base URL.
|
||||||
|
* The path is either an absolute path (starting with `/`), or a full URL. In either case, only its path is used.
|
||||||
|
*/
|
||||||
|
export function mergeURLPath(
|
||||||
|
/** Base URL. Result is guaranteed to use this URL's origin. */
|
||||||
|
base: string,
|
||||||
|
/** Either an absolute path (starting with `/`), or a full URL. If a full URL, its path */
|
||||||
|
path: string,
|
||||||
|
): string {
|
||||||
|
const url = new URL(
|
||||||
|
path.startsWith('/') ? path : new URL(path).pathname,
|
||||||
|
base,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
// Copy query parameters from the original URL to the new URL
|
||||||
|
const originalUrl = new URL(path);
|
||||||
|
url.search = originalUrl.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
import { DittoTables } from '@/db/DittoTables.ts';
|
import type { DittoTables } from './DittoTables.ts';
|
||||||
|
|
||||||
export interface DittoDatabase {
|
export interface DittoDB extends AsyncDisposable {
|
||||||
readonly kysely: Kysely<DittoTables>;
|
readonly kysely: Kysely<DittoTables>;
|
||||||
readonly poolSize: number;
|
readonly poolSize: number;
|
||||||
readonly availableConnections: number;
|
readonly availableConnections: number;
|
||||||
|
migrate(): Promise<void>;
|
||||||
listen(channel: string, callback: (payload: string) => void): void;
|
listen(channel: string, callback: (payload: string) => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DittoDatabaseOpts {
|
export interface DittoDBOpts {
|
||||||
poolSize?: number;
|
poolSize?: number;
|
||||||
debug?: 0 | 1 | 2 | 3 | 4 | 5;
|
debug?: 0 | 1 | 2 | 3 | 4 | 5;
|
||||||
}
|
}
|
||||||
52
packages/db/DittoPgMigrator.ts
Normal file
52
packages/db/DittoPgMigrator.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { logi } from '@soapbox/logi';
|
||||||
|
import { FileMigrationProvider, type Kysely, Migrator } from 'kysely';
|
||||||
|
|
||||||
|
import type { JsonValue } from '@std/json';
|
||||||
|
|
||||||
|
export class DittoPgMigrator {
|
||||||
|
private migrator: Migrator;
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
constructor(private kysely: Kysely<any>) {
|
||||||
|
this.migrator = new Migrator({
|
||||||
|
db: this.kysely,
|
||||||
|
provider: new FileMigrationProvider({
|
||||||
|
fs,
|
||||||
|
path,
|
||||||
|
migrationFolder: new URL(import.meta.resolve('./migrations')).pathname,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Running migrations...', state: 'started' });
|
||||||
|
const { results, error } = await this.migrator.migrateToLatest();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logi({
|
||||||
|
level: 'fatal',
|
||||||
|
ns: 'ditto.db.migration',
|
||||||
|
msg: 'Migration failed.',
|
||||||
|
state: 'failed',
|
||||||
|
results: results as unknown as JsonValue,
|
||||||
|
error: error instanceof Error ? error : null,
|
||||||
|
});
|
||||||
|
throw new Error('Migration failed.');
|
||||||
|
} else {
|
||||||
|
if (!results?.length) {
|
||||||
|
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Everything up-to-date.', state: 'skipped' });
|
||||||
|
} else {
|
||||||
|
logi({
|
||||||
|
level: 'info',
|
||||||
|
ns: 'ditto.db.migration',
|
||||||
|
msg: 'Migrations finished!',
|
||||||
|
state: 'migrated',
|
||||||
|
results: results as unknown as JsonValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,29 @@
|
||||||
import { Generated } from 'kysely';
|
import type { NPostgresSchema } from '@nostrify/db';
|
||||||
|
import type { Generated } from 'kysely';
|
||||||
import { NPostgresSchema } from '@nostrify/db';
|
|
||||||
|
|
||||||
export interface DittoTables extends NPostgresSchema {
|
export interface DittoTables extends NPostgresSchema {
|
||||||
nostr_events: NostrEventsRow;
|
|
||||||
auth_tokens: AuthTokenRow;
|
auth_tokens: AuthTokenRow;
|
||||||
author_stats: AuthorStatsRow;
|
author_stats: AuthorStatsRow;
|
||||||
|
domain_favicons: DomainFaviconRow;
|
||||||
event_stats: EventStatsRow;
|
event_stats: EventStatsRow;
|
||||||
pubkey_domains: PubkeyDomainRow;
|
|
||||||
event_zaps: EventZapRow;
|
event_zaps: EventZapRow;
|
||||||
push_subscriptions: PushSubscriptionRow;
|
push_subscriptions: PushSubscriptionRow;
|
||||||
|
/** This is a materialized view of `author_stats` pre-sorted by followers_count. */
|
||||||
|
top_authors: Pick<AuthorStatsRow, 'pubkey' | 'followers_count' | 'search'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NostrEventsRow = NPostgresSchema['nostr_events'] & {
|
|
||||||
language: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AuthorStatsRow {
|
interface AuthorStatsRow {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
followers_count: number;
|
followers_count: number;
|
||||||
following_count: number;
|
following_count: number;
|
||||||
notes_count: number;
|
notes_count: number;
|
||||||
search: string;
|
search: string;
|
||||||
|
streak_start: number | null;
|
||||||
|
streak_end: number | null;
|
||||||
|
nip05: string | null;
|
||||||
|
nip05_domain: string | null;
|
||||||
|
nip05_hostname: string | null;
|
||||||
|
nip05_last_verified_at: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventStatsRow {
|
interface EventStatsRow {
|
||||||
|
|
@ -43,9 +45,9 @@ interface AuthTokenRow {
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PubkeyDomainRow {
|
interface DomainFaviconRow {
|
||||||
pubkey: string;
|
|
||||||
domain: string;
|
domain: string;
|
||||||
|
favicon: string;
|
||||||
last_updated_at: number;
|
last_updated_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
30
packages/db/KyselyLogger.ts
Normal file
30
packages/db/KyselyLogger.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { dbQueriesCounter, dbQueryDurationHistogram } from '@ditto/metrics';
|
||||||
|
import { logi, type LogiValue } from '@soapbox/logi';
|
||||||
|
|
||||||
|
import type { Logger } from 'kysely';
|
||||||
|
|
||||||
|
/** Log the SQL for queries. */
|
||||||
|
export const KyselyLogger: Logger = (event) => {
|
||||||
|
const { query, queryDurationMillis } = event;
|
||||||
|
const { parameters, sql } = query;
|
||||||
|
|
||||||
|
const duration = queryDurationMillis / 1000;
|
||||||
|
|
||||||
|
dbQueriesCounter.inc();
|
||||||
|
dbQueryDurationHistogram.observe(duration);
|
||||||
|
|
||||||
|
if (event.level === 'query') {
|
||||||
|
logi({ level: 'debug', ns: 'ditto.sql', sql, parameters: parameters as LogiValue, duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.level === 'error') {
|
||||||
|
logi({
|
||||||
|
level: 'error',
|
||||||
|
ns: 'ditto.sql',
|
||||||
|
sql,
|
||||||
|
parameters: parameters as LogiValue,
|
||||||
|
error: event.error instanceof Error ? event.error : null,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
14
packages/db/adapters/DittoPglite.test.ts
Normal file
14
packages/db/adapters/DittoPglite.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { DittoPglite } from './DittoPglite.ts';
|
||||||
|
|
||||||
|
Deno.test('DittoPglite', async () => {
|
||||||
|
const db = new DittoPglite('memory://');
|
||||||
|
await db.migrate();
|
||||||
|
|
||||||
|
assertEquals(db.poolSize, 1);
|
||||||
|
assertEquals(db.availableConnections, 1);
|
||||||
|
|
||||||
|
await db.kysely.destroy();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
52
packages/db/adapters/DittoPglite.ts
Normal file
52
packages/db/adapters/DittoPglite.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { PGlite } from '@electric-sql/pglite';
|
||||||
|
import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm';
|
||||||
|
import { PgliteDialect } from '@soapbox/kysely-pglite';
|
||||||
|
import { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
import { KyselyLogger } from '../KyselyLogger.ts';
|
||||||
|
import { DittoPgMigrator } from '../DittoPgMigrator.ts';
|
||||||
|
import { isWorker } from '../utils/worker.ts';
|
||||||
|
|
||||||
|
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
||||||
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
|
export class DittoPglite implements DittoDB {
|
||||||
|
readonly poolSize = 1;
|
||||||
|
readonly availableConnections = 1;
|
||||||
|
readonly kysely: Kysely<DittoTables>;
|
||||||
|
|
||||||
|
private pglite: PGlite;
|
||||||
|
private migrator: DittoPgMigrator;
|
||||||
|
|
||||||
|
constructor(databaseUrl: string, opts?: DittoDBOpts) {
|
||||||
|
const url = new URL(databaseUrl);
|
||||||
|
|
||||||
|
if (url.protocol === 'file:' && isWorker()) {
|
||||||
|
throw new Error('PGlite is not supported in worker threads.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pglite = new PGlite(databaseUrl, {
|
||||||
|
extensions: { pg_trgm },
|
||||||
|
debug: opts?.debug,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.kysely = new Kysely<DittoTables>({
|
||||||
|
dialect: new PgliteDialect({ database: this.pglite }),
|
||||||
|
log: KyselyLogger,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.migrator = new DittoPgMigrator(this.kysely);
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
|
this.pglite.listen(channel, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
await this.migrator.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
|
await this.kysely.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/db/adapters/DittoPolyPg.test.ts
Normal file
6
packages/db/adapters/DittoPolyPg.test.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { DittoPolyPg } from './DittoPolyPg.ts';
|
||||||
|
|
||||||
|
Deno.test('DittoPolyPg', async () => {
|
||||||
|
const db = new DittoPolyPg('memory://');
|
||||||
|
await db.migrate();
|
||||||
|
});
|
||||||
53
packages/db/adapters/DittoPolyPg.ts
Normal file
53
packages/db/adapters/DittoPolyPg.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { DittoPglite } from './DittoPglite.ts';
|
||||||
|
import { DittoPostgres } from './DittoPostgres.ts';
|
||||||
|
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
||||||
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
|
/** Creates either a PGlite or Postgres connection depending on the databaseUrl. */
|
||||||
|
export class DittoPolyPg implements DittoDB {
|
||||||
|
private adapter: DittoDB;
|
||||||
|
|
||||||
|
/** Open a new database connection. */
|
||||||
|
constructor(databaseUrl: string, opts?: DittoDBOpts) {
|
||||||
|
const { protocol } = new URL(databaseUrl);
|
||||||
|
|
||||||
|
switch (protocol) {
|
||||||
|
case 'file:':
|
||||||
|
case 'memory:':
|
||||||
|
this.adapter = new DittoPglite(databaseUrl, opts);
|
||||||
|
break;
|
||||||
|
case 'postgres:':
|
||||||
|
case 'postgresql:':
|
||||||
|
this.adapter = new DittoPostgres(databaseUrl, opts);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported database URL.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get kysely(): Kysely<DittoTables> {
|
||||||
|
return this.adapter.kysely;
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
await this.adapter.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
|
this.adapter.listen(channel, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
get poolSize(): number {
|
||||||
|
return this.adapter.poolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get availableConnections(): number {
|
||||||
|
return this.adapter.availableConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
|
await this.adapter[Symbol.asyncDispose]();
|
||||||
|
}
|
||||||
|
}
|
||||||
79
packages/db/adapters/DittoPostgres.ts
Normal file
79
packages/db/adapters/DittoPostgres.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import {
|
||||||
|
type BinaryOperationNode,
|
||||||
|
FunctionNode,
|
||||||
|
Kysely,
|
||||||
|
OperatorNode,
|
||||||
|
PostgresAdapter,
|
||||||
|
PostgresIntrospector,
|
||||||
|
PostgresQueryCompiler,
|
||||||
|
PrimitiveValueListNode,
|
||||||
|
ValueNode,
|
||||||
|
} from 'kysely';
|
||||||
|
import { type PostgresJSDialectConfig, PostgresJSDriver } from 'kysely-postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
import { DittoPgMigrator } from '../DittoPgMigrator.ts';
|
||||||
|
import { KyselyLogger } from '../KyselyLogger.ts';
|
||||||
|
|
||||||
|
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
||||||
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
|
export class DittoPostgres implements DittoDB {
|
||||||
|
private pg: ReturnType<typeof postgres>;
|
||||||
|
private migrator: DittoPgMigrator;
|
||||||
|
|
||||||
|
readonly kysely: Kysely<DittoTables>;
|
||||||
|
|
||||||
|
constructor(databaseUrl: string, opts?: DittoDBOpts) {
|
||||||
|
this.pg = postgres(databaseUrl, { max: opts?.poolSize });
|
||||||
|
|
||||||
|
this.kysely = new Kysely<DittoTables>({
|
||||||
|
dialect: {
|
||||||
|
createAdapter: () => new PostgresAdapter(),
|
||||||
|
createDriver: () =>
|
||||||
|
new PostgresJSDriver({ postgres: this.pg as unknown as PostgresJSDialectConfig['postgres'] }),
|
||||||
|
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||||
|
createQueryCompiler: () => new DittoPostgresQueryCompiler(),
|
||||||
|
},
|
||||||
|
log: KyselyLogger,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.migrator = new DittoPgMigrator(this.kysely);
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
|
this.pg.listen(channel, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
await this.migrator.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
get poolSize(): number {
|
||||||
|
return this.pg.connections.open;
|
||||||
|
}
|
||||||
|
|
||||||
|
get availableConnections(): number {
|
||||||
|
return this.pg.connections.idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
|
await this.pg.end();
|
||||||
|
await this.kysely.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts `in` queries to `any` to improve prepared statements on Postgres. */
|
||||||
|
class DittoPostgresQueryCompiler extends PostgresQueryCompiler {
|
||||||
|
protected override visitBinaryOperation(node: BinaryOperationNode): void {
|
||||||
|
if (
|
||||||
|
OperatorNode.is(node.operator) && node.operator.operator === 'in' && PrimitiveValueListNode.is(node.rightOperand)
|
||||||
|
) {
|
||||||
|
this.visitNode(node.leftOperand);
|
||||||
|
this.append(' = ');
|
||||||
|
this.visitNode(FunctionNode.create('any', [ValueNode.create(node.rightOperand.values)]));
|
||||||
|
} else {
|
||||||
|
super.visitBinaryOperation(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/db/adapters/DummyDB.test.ts
Normal file
11
packages/db/adapters/DummyDB.test.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
import { DummyDB } from './DummyDB.ts';
|
||||||
|
|
||||||
|
Deno.test('DummyDB', async () => {
|
||||||
|
const db = new DummyDB();
|
||||||
|
await db.migrate();
|
||||||
|
|
||||||
|
const rows = await db.kysely.selectFrom('nostr_events').selectAll().execute();
|
||||||
|
|
||||||
|
assertEquals(rows, []);
|
||||||
|
});
|
||||||
33
packages/db/adapters/DummyDB.ts
Normal file
33
packages/db/adapters/DummyDB.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely';
|
||||||
|
|
||||||
|
import type { DittoDB } from '../DittoDB.ts';
|
||||||
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
|
export class DummyDB implements DittoDB {
|
||||||
|
readonly kysely: Kysely<DittoTables>;
|
||||||
|
readonly poolSize = 0;
|
||||||
|
readonly availableConnections = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.kysely = new Kysely<DittoTables>({
|
||||||
|
dialect: {
|
||||||
|
createAdapter: () => new PostgresAdapter(),
|
||||||
|
createDriver: () => new DummyDriver(),
|
||||||
|
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||||
|
createQueryCompiler: () => new PostgresQueryCompiler(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(): void {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.asyncDispose](): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/db/deno.json
Normal file
6
packages/db/deno.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@ditto/db",
|
||||||
|
"exports": {
|
||||||
|
".": "./mod.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('events')
|
.createTable('events')
|
||||||
.addColumn('id', 'text', (col) => col.primaryKey())
|
.addColumn('id', 'text', (col) => col.primaryKey())
|
||||||
|
|
@ -52,7 +52,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('events').execute();
|
await db.schema.dropTable('events').execute();
|
||||||
await db.schema.dropTable('tags').execute();
|
await db.schema.dropTable('tags').execute();
|
||||||
await db.schema.dropTable('users').execute();
|
await db.schema.dropTable('users').execute();
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('relays')
|
.createTable('relays')
|
||||||
.addColumn('url', 'text', (col) => col.primaryKey())
|
.addColumn('url', 'text', (col) => col.primaryKey())
|
||||||
|
|
@ -9,6 +9,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('relays').execute();
|
await db.schema.dropTable('relays').execute();
|
||||||
}
|
}
|
||||||
8
packages/db/migrations/002_events_fts.ts
Normal file
8
packages/db/migrations/002_events_fts.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
// This migration used to create an FTS table for SQLite, but SQLite support was removed.
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
8
packages/db/migrations/003_events_admin.ts
Normal file
8
packages/db/migrations/003_events_admin.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.alterTable('users').dropColumn('admin').execute();
|
||||||
|
}
|
||||||
9
packages/db/migrations/004_add_user_indexes.ts
Normal file
9
packages/db/migrations/004_add_user_indexes.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.dropIndex('idx_users_pubkey').execute();
|
||||||
|
await db.schema.dropIndex('idx_users_username').execute();
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('tags_new')
|
.createTable('tags_new')
|
||||||
.addColumn('tag', 'text', (col) => col.notNull())
|
.addColumn('tag', 'text', (col) => col.notNull())
|
||||||
|
|
@ -42,7 +42,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('tags').execute();
|
await db.schema.dropTable('tags').execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
7
packages/db/migrations/006_pragma.ts
Normal file
7
packages/db/migrations/006_pragma.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('unattached_media')
|
.createTable('unattached_media')
|
||||||
.addColumn('id', 'text', (c) => c.primaryKey())
|
.addColumn('id', 'text', (c) => c.primaryKey())
|
||||||
|
|
@ -29,6 +29,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('unattached_media').execute();
|
await db.schema.dropTable('unattached_media').execute();
|
||||||
}
|
}
|
||||||
7
packages/db/migrations/008_wal.ts
Normal file
7
packages/db/migrations/008_wal.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('author_stats')
|
.createTable('author_stats')
|
||||||
.addColumn('pubkey', 'text', (col) => col.primaryKey())
|
.addColumn('pubkey', 'text', (col) => col.primaryKey())
|
||||||
|
|
@ -18,7 +18,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('author_stats').execute();
|
await db.schema.dropTable('author_stats').execute();
|
||||||
await db.schema.dropTable('event_stats').execute();
|
await db.schema.dropTable('event_stats').execute();
|
||||||
}
|
}
|
||||||
8
packages/db/migrations/010_drop_users.ts
Normal file
8
packages/db/migrations/010_drop_users.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.dropTable('users').ifExists().execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('idx_events_kind_pubkey_created_at')
|
.createIndex('idx_events_kind_pubkey_created_at')
|
||||||
.on('events')
|
.on('events')
|
||||||
|
|
@ -8,6 +8,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_events_kind_pubkey_created_at').execute();
|
await db.schema.dropIndex('idx_events_kind_pubkey_created_at').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_tags_tag').execute();
|
await db.schema.dropIndex('idx_tags_tag').execute();
|
||||||
await db.schema.dropIndex('idx_tags_value').execute();
|
await db.schema.dropIndex('idx_tags_value').execute();
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_tags_tag_value').execute();
|
await db.schema.dropIndex('idx_tags_tag_value').execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('events').addColumn('deleted_at', 'integer').execute();
|
await db.schema.alterTable('events').addColumn('deleted_at', 'integer').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('events').dropColumn('deleted_at').execute();
|
await db.schema.alterTable('events').dropColumn('deleted_at').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.createIndex('idx_author_stats_pubkey').on('author_stats').column('pubkey').execute();
|
await db.schema.createIndex('idx_author_stats_pubkey').on('author_stats').column('pubkey').execute();
|
||||||
await db.schema.createIndex('idx_event_stats_event_id').on('event_stats').column('event_id').execute();
|
await db.schema.createIndex('idx_event_stats_event_id').on('event_stats').column('event_id').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_author_stats_pubkey').on('author_stats').execute();
|
await db.schema.dropIndex('idx_author_stats_pubkey').on('author_stats').execute();
|
||||||
await db.schema.dropIndex('idx_event_stats_event_id').on('event_stats').execute();
|
await db.schema.dropIndex('idx_event_stats_event_id').on('event_stats').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('pubkey_domains')
|
.createTable('pubkey_domains')
|
||||||
.ifNotExists()
|
.ifNotExists()
|
||||||
|
|
@ -16,6 +16,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('pubkey_domains').execute();
|
await db.schema.dropTable('pubkey_domains').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('pubkey_domains')
|
.alterTable('pubkey_domains')
|
||||||
.addColumn('last_updated_at', 'integer', (col) => col.notNull().defaultTo(0))
|
.addColumn('last_updated_at', 'integer', (col) => col.notNull().defaultTo(0))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('pubkey_domains').dropColumn('last_updated_at').execute();
|
await db.schema.alterTable('pubkey_domains').dropColumn('last_updated_at').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('relays').execute();
|
await db.schema.dropTable('relays').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('relays')
|
.createTable('relays')
|
||||||
.addColumn('url', 'text', (col) => col.primaryKey())
|
.addColumn('url', 'text', (col) => col.primaryKey())
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('idx_events_created_at_kind')
|
.createIndex('idx_events_created_at_kind')
|
||||||
.on('events')
|
.on('events')
|
||||||
|
|
@ -9,6 +9,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_events_created_at_kind').ifExists().execute();
|
await db.schema.dropIndex('idx_events_created_at_kind').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('events').renameTo('nostr_events').execute();
|
await db.schema.alterTable('events').renameTo('nostr_events').execute();
|
||||||
await db.schema.alterTable('tags').renameTo('nostr_tags').execute();
|
await db.schema.alterTable('tags').renameTo('nostr_tags').execute();
|
||||||
await db.schema.alterTable('nostr_tags').renameColumn('tag', 'name').execute();
|
await db.schema.alterTable('nostr_tags').renameColumn('tag', 'name').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('nostr_events').renameTo('events').execute();
|
await db.schema.alterTable('nostr_events').renameTo('events').execute();
|
||||||
await db.schema.alterTable('nostr_tags').renameTo('tags').execute();
|
await db.schema.alterTable('nostr_tags').renameTo('tags').execute();
|
||||||
await db.schema.alterTable('tags').renameColumn('name', 'tag').execute();
|
await db.schema.alterTable('tags').renameColumn('name', 'tag').execute();
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.deleteFrom('nostr_events').where('deleted_at', 'is not', null).execute();
|
await db.deleteFrom('nostr_events').where('deleted_at', 'is not', null).execute();
|
||||||
await db.schema.alterTable('nostr_events').dropColumn('deleted_at').execute();
|
await db.schema.alterTable('nostr_events').dropColumn('deleted_at').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('nostr_events').addColumn('deleted_at', 'integer').execute();
|
await db.schema.alterTable('nostr_events').addColumn('deleted_at', 'integer').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.createTable('nostr_pgfts')
|
await db.schema.createTable('nostr_pgfts')
|
||||||
.ifNotExists()
|
.ifNotExists()
|
||||||
.addColumn('event_id', 'text', (c) => c.primaryKey().references('nostr_events.id').onDelete('cascade'))
|
.addColumn('event_id', 'text', (c) => c.primaryKey().references('nostr_events.id').onDelete('cascade'))
|
||||||
|
|
@ -8,6 +8,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('nostr_pgfts').ifExists().execute();
|
await db.schema.dropTable('nostr_pgfts').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('nostr_pgfts_gin_search_vec')
|
.createIndex('nostr_pgfts_gin_search_vec')
|
||||||
.ifNotExists()
|
.ifNotExists()
|
||||||
|
|
@ -10,6 +10,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('nostr_pgfts_gin_search_vec').ifExists().execute();
|
await db.schema.dropIndex('nostr_pgfts_gin_search_vec').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('event_stats')
|
.alterTable('event_stats')
|
||||||
.addColumn('reactions', 'text', (col) => col.defaultTo('{}'))
|
.addColumn('reactions', 'text', (col) => col.defaultTo('{}'))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('event_stats').dropColumn('reactions').execute();
|
await db.schema.alterTable('event_stats').dropColumn('reactions').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('nip46_tokens')
|
.createTable('nip46_tokens')
|
||||||
.addColumn('api_token', 'text', (col) => col.primaryKey().notNull())
|
.addColumn('api_token', 'text', (col) => col.primaryKey().notNull())
|
||||||
|
|
@ -12,6 +12,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('nip46_tokens').execute();
|
await db.schema.dropTable('nip46_tokens').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('event_stats')
|
.alterTable('event_stats')
|
||||||
.addColumn('quotes_count', 'integer', (col) => col.notNull().defaultTo(0))
|
.addColumn('quotes_count', 'integer', (col) => col.notNull().defaultTo(0))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('event_stats').dropColumn('quotes_count').execute();
|
await db.schema.alterTable('event_stats').dropColumn('quotes_count').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('event_stats')
|
.alterTable('event_stats')
|
||||||
.addColumn('zaps_amount', 'integer', (col) => col.notNull().defaultTo(0))
|
.addColumn('zaps_amount', 'integer', (col) => col.notNull().defaultTo(0))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('event_stats').dropColumn('zaps_amount').execute();
|
await db.schema.alterTable('event_stats').dropColumn('zaps_amount').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('idx_tags_name')
|
.createIndex('idx_tags_name')
|
||||||
.on('nostr_tags')
|
.on('nostr_tags')
|
||||||
|
|
@ -9,6 +9,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_tags_name').ifExists().execute();
|
await db.schema.dropIndex('idx_tags_name').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('event_zaps')
|
.createTable('event_zaps')
|
||||||
.addColumn('receipt_id', 'text', (col) => col.primaryKey())
|
.addColumn('receipt_id', 'text', (col) => col.primaryKey())
|
||||||
|
|
@ -25,7 +25,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_event_zaps_amount_millisats').ifExists().execute();
|
await db.schema.dropIndex('idx_event_zaps_amount_millisats').ifExists().execute();
|
||||||
await db.schema.dropIndex('idx_event_zaps_target_event_id').ifExists().execute();
|
await db.schema.dropIndex('idx_event_zaps_target_event_id').ifExists().execute();
|
||||||
await db.schema.dropTable('event_zaps').execute();
|
await db.schema.dropTable('event_zaps').execute();
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('nostr_events_created_at_kind')
|
.createIndex('nostr_events_created_at_kind')
|
||||||
.on('nostr_events')
|
.on('nostr_events')
|
||||||
|
|
@ -19,7 +19,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_events_kind_pubkey_created_at').execute();
|
await db.schema.dropIndex('idx_events_kind_pubkey_created_at').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('nostr_events_created_at_kind').execute();
|
await db.schema.dropIndex('nostr_events_created_at_kind').execute();
|
||||||
await db.schema.dropIndex('nostr_events_kind_pubkey_created_at').execute();
|
await db.schema.dropIndex('nostr_events_kind_pubkey_created_at').execute();
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('nostr_tags_new')
|
.createTable('nostr_tags_new')
|
||||||
.addColumn('event_id', 'text', (col) => col.notNull().references('nostr_events.id').onDelete('cascade'))
|
.addColumn('event_id', 'text', (col) => col.notNull().references('nostr_events.id').onDelete('cascade'))
|
||||||
|
|
@ -66,7 +66,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('nostr_tags_old')
|
.createTable('nostr_tags_old')
|
||||||
.addColumn('event_id', 'text', (col) => col.references('nostr_events.id').onDelete('cascade'))
|
.addColumn('event_id', 'text', (col) => col.references('nostr_events.id').onDelete('cascade'))
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
// Create new table and indexes.
|
// Create new table and indexes.
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('nostr_events_new')
|
.createTable('nostr_events_new')
|
||||||
|
|
@ -132,6 +132,6 @@ If you don't want to wait, you can create a fresh database and then import your
|
||||||
await db.schema.alterTable('nostr_events_new').renameTo('nostr_events').execute();
|
await db.schema.alterTable('nostr_events_new').renameTo('nostr_events').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function down(_db: Kysely<any>): Promise<void> {
|
export function down(_db: Kysely<unknown>): Promise<void> {
|
||||||
throw new Error("Sorry, you can't migrate back from here.");
|
throw new Error("Sorry, you can't migrate back from here.");
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('unattached_media').execute();
|
await db.schema.dropTable('unattached_media').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('unattached_media')
|
.createTable('unattached_media')
|
||||||
.addColumn('id', 'text', (c) => c.primaryKey())
|
.addColumn('id', 'text', (c) => c.primaryKey())
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('author_search')
|
.createTable('author_search')
|
||||||
.addColumn('pubkey', 'char(64)', (col) => col.primaryKey())
|
.addColumn('pubkey', 'char(64)', (col) => col.primaryKey())
|
||||||
|
|
@ -12,7 +12,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await sql`CREATE INDEX author_search_search_idx ON author_search USING GIN (search gin_trgm_ops)`.execute(db);
|
await sql`CREATE INDEX author_search_search_idx ON author_search USING GIN (search gin_trgm_ops)`.execute(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('author_search_search_idx').ifExists().execute();
|
await db.schema.dropIndex('author_search_search_idx').ifExists().execute();
|
||||||
await db.schema.dropTable('author_search').execute();
|
await db.schema.dropTable('author_search').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('nostr_events').addColumn('language', 'char(2)').execute();
|
await db.schema.alterTable('nostr_events').addColumn('language', 'char(2)').execute();
|
||||||
|
|
||||||
await db.schema.createIndex('nostr_events_language_created_idx')
|
await db.schema.createIndex('nostr_events_language_created_idx')
|
||||||
|
|
@ -9,7 +9,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('nostr_events').dropColumn('language').execute();
|
await db.schema.alterTable('nostr_events').dropColumn('language').execute();
|
||||||
await db.schema.dropIndex('nostr_events_language_created_idx').execute();
|
await db.schema.dropIndex('nostr_events_language_created_idx').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('author_stats')
|
.alterTable('author_stats')
|
||||||
|
|
@ -26,7 +27,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.dropTable('author_search').execute();
|
await db.schema.dropTable('author_search').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('author_stats_search_idx').ifExists().execute();
|
await db.schema.dropIndex('author_stats_search_idx').ifExists().execute();
|
||||||
await db.schema.alterTable('author_stats').dropColumn('search').execute();
|
await db.schema.alterTable('author_stats').dropColumn('search').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('author_stats_followers_count_idx')
|
.createIndex('author_stats_followers_count_idx')
|
||||||
.ifNotExists()
|
.ifNotExists()
|
||||||
|
|
@ -12,6 +12,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.dropIndex('idx_author_stats_pubkey').ifExists().execute();
|
await db.schema.dropIndex('idx_author_stats_pubkey').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropIndex('author_stats_followers_count_idx').ifExists().execute();
|
await db.schema.dropIndex('author_stats_followers_count_idx').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.deleteFrom('event_stats').where(sql<number>`length(event_id)`, '>', 64).execute();
|
await db.deleteFrom('event_stats').where(sql<number>`length(event_id)`, '>', 64).execute();
|
||||||
await db.deleteFrom('author_stats').where(sql<number>`length(pubkey)`, '>', 64).execute();
|
await db.deleteFrom('author_stats').where(sql<number>`length(pubkey)`, '>', 64).execute();
|
||||||
|
|
@ -8,7 +9,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.alterTable('author_stats').alterColumn('pubkey', (col) => col.setDataType('char(64)')).execute();
|
await db.schema.alterTable('author_stats').alterColumn('pubkey', (col) => col.setDataType('char(64)')).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.alterTable('event_stats').alterColumn('event_id', (col) => col.setDataType('text')).execute();
|
await db.schema.alterTable('event_stats').alterColumn('event_id', (col) => col.setDataType('text')).execute();
|
||||||
await db.schema.alterTable('author_stats').alterColumn('pubkey', (col) => col.setDataType('text')).execute();
|
await db.schema.alterTable('author_stats').alterColumn('pubkey', (col) => col.setDataType('text')).execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
|
||||||
import { aesEncrypt } from '@/utils/aes.ts';
|
|
||||||
import { getTokenHash } from '@/utils/auth.ts';
|
|
||||||
|
|
||||||
interface DB {
|
interface DB {
|
||||||
nip46_tokens: {
|
nip46_tokens: {
|
||||||
|
|
@ -32,19 +28,6 @@ export async function up(db: Kysely<DB>): Promise<void> {
|
||||||
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// There are probably not that many tokens in the database yet, so this should be fine.
|
|
||||||
const tokens = await db.selectFrom('nip46_tokens').selectAll().execute();
|
|
||||||
|
|
||||||
for (const token of tokens) {
|
|
||||||
await db.insertInto('auth_tokens').values({
|
|
||||||
token_hash: await getTokenHash(token.api_token),
|
|
||||||
pubkey: token.user_pubkey,
|
|
||||||
nip46_sk_enc: await aesEncrypt(Conf.seckey, token.server_seckey),
|
|
||||||
nip46_relays: JSON.parse(token.relays),
|
|
||||||
created_at: token.connected_at,
|
|
||||||
}).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.schema.dropTable('nip46_tokens').execute();
|
await db.schema.dropTable('nip46_tokens').execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.createTable('push_subscriptions')
|
.createTable('push_subscriptions')
|
||||||
.addColumn('id', 'bigserial', (c) => c.primaryKey())
|
.addColumn('id', 'bigserial', (c) => c.primaryKey())
|
||||||
|
|
@ -22,6 +22,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema.dropTable('push_subscriptions').execute();
|
await db.schema.dropTable('push_subscriptions').execute();
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await sql`
|
await sql`
|
||||||
CREATE OR REPLACE FUNCTION notify_nostr_event()
|
CREATE OR REPLACE FUNCTION notify_nostr_event()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
|
|
@ -31,7 +31,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await sql`DROP TRIGGER nostr_event_trigger ON nostr_events`.execute(db);
|
await sql`DROP TRIGGER nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
await sql`DROP FUNCTION notify_nostr_event()`.execute(db);
|
await sql`DROP FUNCTION notify_nostr_event()`.execute(db);
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Kysely } from 'kysely';
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('auth_tokens')
|
.alterTable('auth_tokens')
|
||||||
|
|
@ -14,7 +15,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('auth_tokens')
|
.alterTable('auth_tokens')
|
||||||
.dropColumn('bunker_pubkey')
|
.dropColumn('bunker_pubkey')
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);
|
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
|
|
||||||
await sql`
|
await sql`
|
||||||
|
|
@ -21,7 +21,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
await sql`DROP TRIGGER nostr_event_trigger ON nostr_events`.execute(db);
|
await sql`DROP TRIGGER nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
await sql`DROP FUNCTION notify_nostr_event()`.execute(db);
|
await sql`DROP FUNCTION notify_nostr_event()`.execute(db);
|
||||||
}
|
}
|
||||||
38
packages/db/migrations/042_add_search_ext.ts
Normal file
38
packages/db/migrations/042_add_search_ext.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('nostr_events')
|
||||||
|
.addColumn('search_ext', 'jsonb', (col) => col.notNull().defaultTo(sql`'{}'::jsonb`))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('nostr_events')
|
||||||
|
.addCheckConstraint('nostr_events_search_ext_chk', sql`jsonb_typeof(search_ext) = 'object'`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('nostr_events_search_ext_idx').using('gin')
|
||||||
|
.on('nostr_events')
|
||||||
|
.column('search_ext')
|
||||||
|
.ifNotExists()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.dropIndex('nostr_events_search_ext_idx')
|
||||||
|
.on('nostr_events')
|
||||||
|
.ifExists()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('nostr_events')
|
||||||
|
.dropConstraint('nostr_events_search_ext_chk')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('nostr_events')
|
||||||
|
.dropColumn('search_ext')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
14
packages/db/migrations/043_rm_language.ts
Normal file
14
packages/db/migrations/043_rm_language.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.alterTable('nostr_events').dropColumn('language').execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.alterTable('nostr_events').addColumn('language', 'char(2)').execute();
|
||||||
|
|
||||||
|
await db.schema.createIndex('nostr_events_language_created_idx')
|
||||||
|
.on('nostr_events')
|
||||||
|
.columns(['language', 'created_at desc', 'id asc', 'kind'])
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
12
packages/db/migrations/044_search_ext_drop_default.ts
Normal file
12
packages/db/migrations/044_search_ext_drop_default.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.alterTable('nostr_events').alterColumn('search_ext', (col) => col.dropDefault()).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('nostr_events')
|
||||||
|
.alterColumn('search_ext', (col) => col.setDefault("'{}'::jsonb"))
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
17
packages/db/migrations/045_streaks.ts
Normal file
17
packages/db/migrations/045_streaks.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addColumn('streak_start', 'integer')
|
||||||
|
.addColumn('streak_end', 'integer')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.dropColumn('streak_start')
|
||||||
|
.dropColumn('streak_end')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
48
packages/db/migrations/046_author_stats_nip05.ts
Normal file
48
packages/db/migrations/046_author_stats_nip05.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addColumn('nip05', 'varchar(320)')
|
||||||
|
.addColumn('nip05_domain', 'varchar(253)')
|
||||||
|
.addColumn('nip05_hostname', 'varchar(253)')
|
||||||
|
.addColumn('nip05_last_verified_at', 'integer')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addCheckConstraint('author_stats_nip05_domain_lowercase_chk', sql`nip05_domain = lower(nip05_domain)`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addCheckConstraint('author_stats_nip05_hostname_lowercase_chk', sql`nip05_hostname = lower(nip05_hostname)`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addCheckConstraint('author_stats_nip05_hostname_domain_chk', sql`nip05_hostname like '%' || nip05_domain`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('author_stats_nip05_domain_idx')
|
||||||
|
.on('author_stats')
|
||||||
|
.column('nip05_domain')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('author_stats_nip05_hostname_idx')
|
||||||
|
.on('author_stats')
|
||||||
|
.column('nip05_hostname')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.dropColumn('nip05')
|
||||||
|
.dropColumn('nip05_domain')
|
||||||
|
.dropColumn('nip05_hostname')
|
||||||
|
.dropColumn('nip05_last_verified_at')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
15
packages/db/migrations/047_add_domain_favicons.ts
Normal file
15
packages/db/migrations/047_add_domain_favicons.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable('domain_favicons')
|
||||||
|
.addColumn('domain', 'varchar(253)', (col) => col.primaryKey())
|
||||||
|
.addColumn('favicon', 'varchar(2048)', (col) => col.notNull())
|
||||||
|
.addColumn('last_updated_at', 'integer', (col) => col.notNull())
|
||||||
|
.addCheckConstraint('domain_favicons_https_chk', sql`favicon ~* '^https:\\/\\/'`)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.dropTable('domain_favicons').execute();
|
||||||
|
}
|
||||||
22
packages/db/migrations/048_rm_pubkey_domains.ts
Normal file
22
packages/db/migrations/048_rm_pubkey_domains.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.dropTable('pubkey_domains').execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable('pubkey_domains')
|
||||||
|
.ifNotExists()
|
||||||
|
.addColumn('pubkey', 'text', (col) => col.primaryKey())
|
||||||
|
.addColumn('domain', 'text', (col) => col.notNull())
|
||||||
|
.addColumn('last_updated_at', 'integer', (col) => col.notNull().defaultTo(0))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('pubkey_domains_domain_index')
|
||||||
|
.on('pubkey_domains')
|
||||||
|
.column('domain')
|
||||||
|
.ifNotExists()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
21
packages/db/migrations/049_author_stats_sorted.ts
Normal file
21
packages/db/migrations/049_author_stats_sorted.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createView('top_authors')
|
||||||
|
.materialized()
|
||||||
|
.as(db.selectFrom('author_stats').select(['pubkey', 'followers_count', 'search']).orderBy('followers_count desc'))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await sql`CREATE INDEX top_authors_search_idx ON top_authors USING GIN (search gin_trgm_ops)`.execute(db);
|
||||||
|
|
||||||
|
await db.schema.createIndex('top_authors_pubkey_idx').on('top_authors').column('pubkey').execute();
|
||||||
|
|
||||||
|
await db.schema.dropIndex('author_stats_search_idx').execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.dropView('top_authors').execute();
|
||||||
|
await sql`CREATE INDEX author_stats_search_idx ON author_stats USING GIN (search gin_trgm_ops)`.execute(db);
|
||||||
|
}
|
||||||
21
packages/db/migrations/050_notify_only_insert.ts
Normal file
21
packages/db/migrations/050_notify_only_insert.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
CREATE TRIGGER nostr_event_trigger
|
||||||
|
AFTER INSERT ON nostr_events
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_nostr_event()
|
||||||
|
`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
CREATE TRIGGER nostr_event_trigger
|
||||||
|
AFTER INSERT OR UPDATE ON nostr_events
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_nostr_event()
|
||||||
|
`.execute(db);
|
||||||
|
}
|
||||||
45
packages/db/migrations/051_notify_replaceable.ts
Normal file
45
packages/db/migrations/051_notify_replaceable.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await sql`
|
||||||
|
CREATE OR REPLACE FUNCTION notify_nostr_event()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF OLD.id IS DISTINCT FROM NEW.id THEN
|
||||||
|
PERFORM pg_notify('nostr_event', NEW.id::text);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`.execute(db);
|
||||||
|
|
||||||
|
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
CREATE TRIGGER nostr_event_trigger
|
||||||
|
AFTER INSERT OR UPDATE ON nostr_events
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_nostr_event()
|
||||||
|
`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await sql`
|
||||||
|
CREATE OR REPLACE FUNCTION notify_nostr_event()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify('nostr_event', NEW.id::text);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`.execute(db);
|
||||||
|
|
||||||
|
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
CREATE TRIGGER nostr_event_trigger
|
||||||
|
AFTER INSERT ON nostr_events
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION notify_nostr_event()
|
||||||
|
`.execute(db);
|
||||||
|
}
|
||||||
16
packages/db/migrations/052_rename_pkey.ts
Normal file
16
packages/db/migrations/052_rename_pkey.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
const result = await sql<{ count: number }>`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE indexname = 'nostr_events_new_pkey'
|
||||||
|
`.execute(db);
|
||||||
|
|
||||||
|
if (result.rows[0].count > 0) {
|
||||||
|
await sql`ALTER INDEX nostr_events_new_pkey RENAME TO nostr_events_pkey;`.execute(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(_db: Kysely<unknown>): Promise<void> {
|
||||||
|
}
|
||||||
7
packages/db/mod.ts
Normal file
7
packages/db/mod.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export { DittoPglite } from './adapters/DittoPglite.ts';
|
||||||
|
export { DittoPolyPg } from './adapters/DittoPolyPg.ts';
|
||||||
|
export { DittoPostgres } from './adapters/DittoPostgres.ts';
|
||||||
|
export { DummyDB } from './adapters/DummyDB.ts';
|
||||||
|
|
||||||
|
export type { DittoDB } from './DittoDB.ts';
|
||||||
|
export type { DittoTables } from './DittoTables.ts';
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { isWorker } from '@/utils/worker.ts';
|
import { isWorker } from './worker.ts';
|
||||||
|
|
||||||
Deno.test('isWorker from the main thread returns false', () => {
|
Deno.test('isWorker from the main thread returns false', () => {
|
||||||
assertEquals(isWorker(), false);
|
assertEquals(isWorker(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('isWorker from a worker thread returns true', async () => {
|
Deno.test('isWorker from a worker thread returns true', async () => {
|
||||||
|
const url = new URL('./worker.ts', import.meta.url);
|
||||||
|
|
||||||
const script = `
|
const script = `
|
||||||
import { isWorker } from '@/utils/worker.ts';
|
import { isWorker } from '${url.href}';
|
||||||
postMessage(isWorker());
|
postMessage(isWorker());
|
||||||
self.close();
|
self.close();
|
||||||
`;
|
`;
|
||||||
53
packages/ditto/DittoPush.ts
Normal file
53
packages/ditto/DittoPush.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
import { ApplicationServer, PushMessageOptions, PushSubscriber, PushSubscription } from '@negrel/webpush';
|
||||||
|
import { NStore } from '@nostrify/types';
|
||||||
|
import { logi } from '@soapbox/logi';
|
||||||
|
|
||||||
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
|
|
||||||
|
interface DittoPushOpts {
|
||||||
|
conf: DittoConf;
|
||||||
|
relay: NStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DittoPush {
|
||||||
|
private server: Promise<ApplicationServer | undefined>;
|
||||||
|
|
||||||
|
constructor(opts: DittoPushOpts) {
|
||||||
|
const { conf, relay } = opts;
|
||||||
|
|
||||||
|
this.server = (async () => {
|
||||||
|
const meta = await getInstanceMetadata(relay);
|
||||||
|
const keys = await conf.vapidKeys;
|
||||||
|
|
||||||
|
if (keys) {
|
||||||
|
return await ApplicationServer.new({
|
||||||
|
contactInformation: `mailto:${meta.email}`,
|
||||||
|
vapidKeys: keys,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logi({
|
||||||
|
level: 'warn',
|
||||||
|
ns: 'ditto.push',
|
||||||
|
msg: 'VAPID keys are not set. Push notifications will be disabled.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async push(
|
||||||
|
subscription: PushSubscription,
|
||||||
|
json: object,
|
||||||
|
opts: PushMessageOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const server = await this.server;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriber = new PushSubscriber(server, subscription);
|
||||||
|
const text = JSON.stringify(json);
|
||||||
|
return subscriber.pushTextMessage(text, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
586
packages/ditto/app.ts
Normal file
586
packages/ditto/app.ts
Normal file
|
|
@ -0,0 +1,586 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
import { DittoDB, DittoPolyPg } from '@ditto/db';
|
||||||
|
import { paginationMiddleware, tokenMiddleware, userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
|
import { DittoApp, type DittoEnv } from '@ditto/mastoapi/router';
|
||||||
|
import { relayPoolRelaysSizeGauge, relayPoolSubscriptionsSizeGauge } from '@ditto/metrics';
|
||||||
|
import { type DittoTranslator } from '@ditto/translators';
|
||||||
|
import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@hono/hono';
|
||||||
|
import { every } from '@hono/hono/combine';
|
||||||
|
import { cors } from '@hono/hono/cors';
|
||||||
|
import { serveStatic } from '@hono/hono/deno';
|
||||||
|
import { NostrEvent, NostrSigner, NRelay, NUploader } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
import { cron } from '@/cron.ts';
|
||||||
|
import { startFirehose } from '@/firehose.ts';
|
||||||
|
import { DittoAPIStore } from '@/storages/DittoAPIStore.ts';
|
||||||
|
import { DittoPgStore } from '@/storages/DittoPgStore.ts';
|
||||||
|
import { DittoPool } from '@/storages/DittoPool.ts';
|
||||||
|
import { Time } from '@/utils/time.ts';
|
||||||
|
import { seedZapSplits } from '@/utils/zap-split.ts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
accountController,
|
||||||
|
accountLookupController,
|
||||||
|
accountSearchController,
|
||||||
|
accountStatusesController,
|
||||||
|
blockController,
|
||||||
|
createAccountController,
|
||||||
|
familiarFollowersController,
|
||||||
|
favouritesController,
|
||||||
|
followController,
|
||||||
|
followersController,
|
||||||
|
followingController,
|
||||||
|
muteController,
|
||||||
|
relationshipsController,
|
||||||
|
unblockController,
|
||||||
|
unfollowController,
|
||||||
|
unmuteController,
|
||||||
|
updateCredentialsController,
|
||||||
|
verifyCredentialsController,
|
||||||
|
} from '@/controllers/api/accounts.ts';
|
||||||
|
import {
|
||||||
|
adminAccountsController,
|
||||||
|
adminActionController,
|
||||||
|
adminApproveController,
|
||||||
|
adminRejectController,
|
||||||
|
} from '@/controllers/api/admin.ts';
|
||||||
|
import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts';
|
||||||
|
import { blocksController } from '@/controllers/api/blocks.ts';
|
||||||
|
import { bookmarksController } from '@/controllers/api/bookmarks.ts';
|
||||||
|
import cashuApp from '@/controllers/api/cashu.ts';
|
||||||
|
import { captchaController, captchaVerifyController } from '@/controllers/api/captcha.ts';
|
||||||
|
import {
|
||||||
|
adminRelaysController,
|
||||||
|
adminSetRelaysController,
|
||||||
|
deleteZapSplitsController,
|
||||||
|
getZapSplitsController,
|
||||||
|
nameRequestController,
|
||||||
|
nameRequestsController,
|
||||||
|
statusZapSplitsController,
|
||||||
|
updateInstanceController,
|
||||||
|
updateZapSplitsController,
|
||||||
|
} from '@/controllers/api/ditto.ts';
|
||||||
|
import { emptyArrayController, notImplementedController } from '@/controllers/api/fallback.ts';
|
||||||
|
import {
|
||||||
|
instanceDescriptionController,
|
||||||
|
instanceV1Controller,
|
||||||
|
instanceV2Controller,
|
||||||
|
} from '@/controllers/api/instance.ts';
|
||||||
|
import { markersController, updateMarkersController } from '@/controllers/api/markers.ts';
|
||||||
|
import { mediaController, updateMediaController } from '@/controllers/api/media.ts';
|
||||||
|
import { mutesController } from '@/controllers/api/mutes.ts';
|
||||||
|
import { notificationController, notificationsController } from '@/controllers/api/notifications.ts';
|
||||||
|
import {
|
||||||
|
createTokenController,
|
||||||
|
oauthAuthorizeController,
|
||||||
|
oauthController,
|
||||||
|
revokeTokenController,
|
||||||
|
} from '@/controllers/api/oauth.ts';
|
||||||
|
import {
|
||||||
|
configController,
|
||||||
|
frontendConfigController,
|
||||||
|
pleromaAdminDeleteStatusController,
|
||||||
|
pleromaAdminSuggestController,
|
||||||
|
pleromaAdminTagController,
|
||||||
|
pleromaAdminUnsuggestController,
|
||||||
|
pleromaAdminUntagController,
|
||||||
|
updateConfigController,
|
||||||
|
} from '@/controllers/api/pleroma.ts';
|
||||||
|
import { preferencesController } from '@/controllers/api/preferences.ts';
|
||||||
|
import { getSubscriptionController, pushSubscribeController } from '@/controllers/api/push.ts';
|
||||||
|
import { deleteReactionController, reactionController, reactionsController } from '@/controllers/api/reactions.ts';
|
||||||
|
import { relayController } from '@/controllers/nostr/relay.ts';
|
||||||
|
import {
|
||||||
|
adminReportController,
|
||||||
|
adminReportReopenController,
|
||||||
|
adminReportResolveController,
|
||||||
|
adminReportsController,
|
||||||
|
reportController,
|
||||||
|
} from '@/controllers/api/reports.ts';
|
||||||
|
import { searchController } from '@/controllers/api/search.ts';
|
||||||
|
import {
|
||||||
|
bookmarkController,
|
||||||
|
contextController,
|
||||||
|
createStatusController,
|
||||||
|
deleteStatusController,
|
||||||
|
favouriteController,
|
||||||
|
favouritedByController,
|
||||||
|
pinController,
|
||||||
|
quotesController,
|
||||||
|
rebloggedByController,
|
||||||
|
reblogStatusController,
|
||||||
|
statusController,
|
||||||
|
unbookmarkController,
|
||||||
|
unpinController,
|
||||||
|
unreblogStatusController,
|
||||||
|
zapController,
|
||||||
|
zappedByController,
|
||||||
|
} from '@/controllers/api/statuses.ts';
|
||||||
|
import { streamingController } from '@/controllers/api/streaming.ts';
|
||||||
|
import {
|
||||||
|
localSuggestionsController,
|
||||||
|
suggestionsV1Controller,
|
||||||
|
suggestionsV2Controller,
|
||||||
|
} from '@/controllers/api/suggestions.ts';
|
||||||
|
import {
|
||||||
|
hashtagTimelineController,
|
||||||
|
homeTimelineController,
|
||||||
|
publicTimelineController,
|
||||||
|
suggestedTimelineController,
|
||||||
|
} from '@/controllers/api/timelines.ts';
|
||||||
|
import {
|
||||||
|
trendingLinksController,
|
||||||
|
trendingStatusesController,
|
||||||
|
trendingTagsController,
|
||||||
|
} from '@/controllers/api/trends.ts';
|
||||||
|
import { translateController } from '@/controllers/api/translate.ts';
|
||||||
|
import { errorHandler } from '@/controllers/error.ts';
|
||||||
|
import { frontendController } from '@/controllers/frontend.ts';
|
||||||
|
import { metricsController } from '@/controllers/metrics.ts';
|
||||||
|
import { manifestController } from '@/controllers/manifest.ts';
|
||||||
|
import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts';
|
||||||
|
import { nostrController } from '@/controllers/well-known/nostr.ts';
|
||||||
|
import { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts';
|
||||||
|
import { cspMiddleware } from '@/middleware/cspMiddleware.ts';
|
||||||
|
import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts';
|
||||||
|
import { notActivitypubMiddleware } from '@/middleware/notActivitypubMiddleware.ts';
|
||||||
|
import { rateLimitMiddleware } from '@/middleware/rateLimitMiddleware.ts';
|
||||||
|
import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts';
|
||||||
|
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
||||||
|
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
||||||
|
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
|
||||||
|
|
||||||
|
export interface AppEnv extends DittoEnv {
|
||||||
|
Variables: {
|
||||||
|
conf: DittoConf;
|
||||||
|
/** Uploader for the user to upload files. */
|
||||||
|
uploader?: NUploader;
|
||||||
|
/** NIP-98 signed event proving the pubkey is owned by the user. */
|
||||||
|
proof?: NostrEvent;
|
||||||
|
/** Kysely instance for the database. */
|
||||||
|
db: DittoDB;
|
||||||
|
/** Base database store. No content filtering. */
|
||||||
|
relay: NRelay;
|
||||||
|
/** Normalized pagination params. */
|
||||||
|
pagination: { since?: number; until?: number; limit: number };
|
||||||
|
/** Translation service. */
|
||||||
|
translator?: DittoTranslator;
|
||||||
|
signal: AbortSignal;
|
||||||
|
user?: {
|
||||||
|
/** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */
|
||||||
|
signer: NostrSigner;
|
||||||
|
/** User's relay. Might filter out unwanted content. */
|
||||||
|
relay: NRelay;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppContext = Context<AppEnv>;
|
||||||
|
type AppMiddleware = MiddlewareHandler<AppEnv>;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
type AppController<P extends string = any> = Handler<AppEnv, P, HonoInput, Response | Promise<Response>>;
|
||||||
|
|
||||||
|
const conf = new DittoConf(Deno.env);
|
||||||
|
|
||||||
|
const db = new DittoPolyPg(conf.databaseUrl, {
|
||||||
|
poolSize: conf.pg.poolSize,
|
||||||
|
debug: conf.pgliteDebug,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.migrate();
|
||||||
|
|
||||||
|
const pgstore = new DittoPgStore({
|
||||||
|
db,
|
||||||
|
pubkey: await conf.signer.getPublicKey(),
|
||||||
|
timeout: conf.db.timeouts.default,
|
||||||
|
notify: conf.notifyEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pool = new DittoPool({ conf, relay: pgstore });
|
||||||
|
const relay = new DittoRelayStore({ db, conf, relay: pgstore });
|
||||||
|
|
||||||
|
await seedZapSplits(relay);
|
||||||
|
|
||||||
|
if (conf.firehoseEnabled) {
|
||||||
|
startFirehose({
|
||||||
|
pool,
|
||||||
|
relay,
|
||||||
|
concurrency: conf.firehoseConcurrency,
|
||||||
|
kinds: conf.firehoseKinds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conf.cronEnabled) {
|
||||||
|
cron({ conf, db, relay });
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new DittoApp({ conf, db, relay }, { strict: false });
|
||||||
|
|
||||||
|
/** User-provided files in the gitignored `public/` directory. */
|
||||||
|
const publicFiles = serveStatic({ root: './public/' });
|
||||||
|
/** Static files provided by the Ditto repo, checked into git. */
|
||||||
|
const staticFiles = serveStatic({ root: new URL('./static/', import.meta.url).pathname });
|
||||||
|
|
||||||
|
app.use(cacheControlMiddleware({ noStore: true }));
|
||||||
|
|
||||||
|
const ratelimit = every(
|
||||||
|
rateLimitMiddleware(30, Time.seconds(5), false),
|
||||||
|
rateLimitMiddleware(300, Time.minutes(5), false),
|
||||||
|
);
|
||||||
|
|
||||||
|
const socketTokenMiddleware = tokenMiddleware((c) => {
|
||||||
|
const token = c.req.header('sec-websocket-protocol');
|
||||||
|
if (token) {
|
||||||
|
return `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
'/api/*',
|
||||||
|
(c, next) => {
|
||||||
|
c.set('relay', new DittoAPIStore({ relay, pool }));
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
metricsMiddleware,
|
||||||
|
ratelimit,
|
||||||
|
paginationMiddleware(),
|
||||||
|
logiMiddleware,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/.well-known/*', metricsMiddleware, ratelimit, logiMiddleware);
|
||||||
|
app.use('/nodeinfo/*', metricsMiddleware, ratelimit, logiMiddleware);
|
||||||
|
app.use('/oauth/*', metricsMiddleware, ratelimit, logiMiddleware);
|
||||||
|
|
||||||
|
app.get('/api/v1/streaming', socketTokenMiddleware, metricsMiddleware, ratelimit, streamingController);
|
||||||
|
app.get('/relay', metricsMiddleware, ratelimit, relayController);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cspMiddleware(),
|
||||||
|
cors({ origin: '*', exposeHeaders: ['link'] }),
|
||||||
|
tokenMiddleware(),
|
||||||
|
uploaderMiddleware,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/metrics', async (_c, next) => {
|
||||||
|
relayPoolRelaysSizeGauge.reset();
|
||||||
|
relayPoolSubscriptionsSizeGauge.reset();
|
||||||
|
|
||||||
|
for (const relay of pool.relays.values()) {
|
||||||
|
relayPoolRelaysSizeGauge.inc({ ready_state: relay.socket.readyState });
|
||||||
|
relayPoolSubscriptionsSizeGauge.inc(relay.subscriptions.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}, metricsController);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/.well-known/nodeinfo',
|
||||||
|
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||||
|
nodeInfoController,
|
||||||
|
);
|
||||||
|
app.get('/.well-known/nostr.json', nostrController);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/nodeinfo/:version',
|
||||||
|
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||||
|
nodeInfoSchemaController,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/manifest.webmanifest',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
manifestController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/api/v1/instance',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
instanceV1Controller,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v2/instance',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
instanceV2Controller,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/instance/extended_description',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
instanceDescriptionController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/api/v1/apps/verify_credentials', appCredentialsController);
|
||||||
|
app.post('/api/v1/apps', createAppController);
|
||||||
|
|
||||||
|
app.post('/oauth/token', createTokenController);
|
||||||
|
app.post('/oauth/revoke', revokeTokenController);
|
||||||
|
app.post('/oauth/authorize', oauthAuthorizeController);
|
||||||
|
app.get('/oauth/authorize', oauthController);
|
||||||
|
|
||||||
|
app.post('/api/v1/accounts', userMiddleware({ verify: true }), createAccountController);
|
||||||
|
app.get('/api/v1/accounts/verify_credentials', userMiddleware(), verifyCredentialsController);
|
||||||
|
app.patch('/api/v1/accounts/update_credentials', userMiddleware(), updateCredentialsController);
|
||||||
|
app.get('/api/v1/accounts/search', accountSearchController);
|
||||||
|
app.get('/api/v1/accounts/lookup', accountLookupController);
|
||||||
|
app.get('/api/v1/accounts/relationships', userMiddleware(), relationshipsController);
|
||||||
|
app.get('/api/v1/accounts/familiar_followers', userMiddleware(), familiarFollowersController);
|
||||||
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', userMiddleware(), blockController);
|
||||||
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', userMiddleware(), unblockController);
|
||||||
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', userMiddleware(), muteController);
|
||||||
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', userMiddleware(), unmuteController);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow',
|
||||||
|
rateLimitMiddleware(2, Time.seconds(1)),
|
||||||
|
userMiddleware(),
|
||||||
|
followController,
|
||||||
|
);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow',
|
||||||
|
rateLimitMiddleware(2, Time.seconds(1)),
|
||||||
|
userMiddleware(),
|
||||||
|
unfollowController,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/accounts/:pubkey{[0-9a-f]{64}}/followers',
|
||||||
|
rateLimitMiddleware(8, Time.seconds(30)),
|
||||||
|
followersController,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following',
|
||||||
|
rateLimitMiddleware(8, Time.seconds(30)),
|
||||||
|
followingController,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses',
|
||||||
|
rateLimitMiddleware(12, Time.seconds(30)),
|
||||||
|
accountStatusesController,
|
||||||
|
);
|
||||||
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}', accountController);
|
||||||
|
|
||||||
|
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/favourited_by', favouritedByController);
|
||||||
|
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/reblogged_by', rebloggedByController);
|
||||||
|
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/context', contextController);
|
||||||
|
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}', statusController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', userMiddleware(), favouriteController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', userMiddleware(), bookmarkController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', userMiddleware(), unbookmarkController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', userMiddleware(), pinController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', userMiddleware(), unpinController);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/statuses/:id{[0-9a-f]{64}}/translate',
|
||||||
|
userMiddleware(),
|
||||||
|
rateLimitMiddleware(15, Time.minutes(1)),
|
||||||
|
translatorMiddleware,
|
||||||
|
translateController,
|
||||||
|
);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', userMiddleware(), reblogStatusController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', userMiddleware(), unreblogStatusController);
|
||||||
|
app.post('/api/v1/statuses', userMiddleware(), createStatusController);
|
||||||
|
app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', userMiddleware(), deleteStatusController);
|
||||||
|
|
||||||
|
app.get('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/quotes', quotesController);
|
||||||
|
|
||||||
|
app.post('/api/v1/media', mediaController);
|
||||||
|
app.put(
|
||||||
|
'/api/v1/media/:id{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}',
|
||||||
|
updateMediaController,
|
||||||
|
);
|
||||||
|
app.post('/api/v2/media', mediaController);
|
||||||
|
|
||||||
|
app.get('/api/v1/timelines/home', rateLimitMiddleware(8, Time.seconds(30)), userMiddleware(), homeTimelineController);
|
||||||
|
app.get('/api/v1/timelines/public', rateLimitMiddleware(8, Time.seconds(30)), publicTimelineController);
|
||||||
|
app.get('/api/v1/timelines/tag/:hashtag', rateLimitMiddleware(8, Time.seconds(30)), hashtagTimelineController);
|
||||||
|
app.get('/api/v1/timelines/suggested', rateLimitMiddleware(8, Time.seconds(30)), suggestedTimelineController);
|
||||||
|
|
||||||
|
app.get('/api/v1/preferences', preferencesController);
|
||||||
|
app.get('/api/v1/search', searchController);
|
||||||
|
app.get('/api/v2/search', searchController);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/api/pleroma/frontend_configurations',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
frontendConfigController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/api/v1/trends/statuses', rateLimitMiddleware(8, Time.seconds(30)), trendingStatusesController);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/trends/links',
|
||||||
|
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||||
|
trendingLinksController,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/trends/tags',
|
||||||
|
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||||
|
trendingTagsController,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/trends',
|
||||||
|
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||||
|
trendingTagsController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/api/v1/suggestions', suggestionsV1Controller);
|
||||||
|
app.get('/api/v2/suggestions', suggestionsV2Controller);
|
||||||
|
app.get('/api/v2/ditto/suggestions/local', localSuggestionsController);
|
||||||
|
|
||||||
|
app.get('/api/v1/notifications', rateLimitMiddleware(8, Time.seconds(30)), userMiddleware(), notificationsController);
|
||||||
|
app.get('/api/v1/notifications/:id', userMiddleware(), notificationController);
|
||||||
|
|
||||||
|
app.get('/api/v1/favourites', userMiddleware(), favouritesController);
|
||||||
|
app.get('/api/v1/bookmarks', userMiddleware(), bookmarksController);
|
||||||
|
app.get('/api/v1/blocks', userMiddleware(), blocksController);
|
||||||
|
app.get('/api/v1/mutes', userMiddleware(), mutesController);
|
||||||
|
|
||||||
|
app.get('/api/v1/markers', userMiddleware({ verify: true }), markersController);
|
||||||
|
app.post('/api/v1/markers', userMiddleware({ verify: true }), updateMarkersController);
|
||||||
|
|
||||||
|
app.get('/api/v1/push/subscription', userMiddleware(), getSubscriptionController);
|
||||||
|
app.post('/api/v1/push/subscription', userMiddleware({ verify: true }), pushSubscribeController);
|
||||||
|
|
||||||
|
app.get('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions', reactionsController);
|
||||||
|
app.get('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', reactionsController);
|
||||||
|
app.put('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', userMiddleware(), reactionController);
|
||||||
|
app.delete('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', userMiddleware(), deleteReactionController);
|
||||||
|
|
||||||
|
app.get('/api/v1/pleroma/admin/config', userMiddleware({ role: 'admin' }), configController);
|
||||||
|
app.post('/api/v1/pleroma/admin/config', userMiddleware({ role: 'admin' }), updateConfigController);
|
||||||
|
app.delete('/api/v1/pleroma/admin/statuses/:id', userMiddleware({ role: 'admin' }), pleromaAdminDeleteStatusController);
|
||||||
|
|
||||||
|
app.get('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminRelaysController);
|
||||||
|
app.put('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminSetRelaysController);
|
||||||
|
|
||||||
|
app.put('/api/v1/admin/ditto/instance', userMiddleware({ role: 'admin' }), updateInstanceController);
|
||||||
|
|
||||||
|
app.post('/api/v1/ditto/names', userMiddleware(), nameRequestController);
|
||||||
|
app.get('/api/v1/ditto/names', userMiddleware(), nameRequestsController);
|
||||||
|
|
||||||
|
app.get('/api/v1/ditto/captcha', rateLimitMiddleware(3, Time.minutes(1)), captchaController);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/ditto/captcha/:id/verify',
|
||||||
|
rateLimitMiddleware(8, Time.minutes(1)),
|
||||||
|
userMiddleware({ verify: true }),
|
||||||
|
captchaVerifyController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/api/v1/ditto/zap_splits',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, public: true }),
|
||||||
|
getZapSplitsController,
|
||||||
|
);
|
||||||
|
app.get('/api/v1/ditto/:id{[0-9a-f]{64}}/zap_splits', statusZapSplitsController);
|
||||||
|
|
||||||
|
app.put('/api/v1/admin/ditto/zap_splits', userMiddleware({ role: 'admin' }), updateZapSplitsController);
|
||||||
|
app.delete('/api/v1/admin/ditto/zap_splits', userMiddleware({ role: 'admin' }), deleteZapSplitsController);
|
||||||
|
|
||||||
|
app.post('/api/v1/ditto/zap', userMiddleware(), zapController);
|
||||||
|
app.get('/api/v1/ditto/statuses/:id{[0-9a-f]{64}}/zapped_by', zappedByController);
|
||||||
|
|
||||||
|
app.route('/api/v1/ditto/cashu', cashuApp);
|
||||||
|
|
||||||
|
app.post('/api/v1/reports', userMiddleware(), reportController);
|
||||||
|
app.get('/api/v1/admin/reports', userMiddleware(), userMiddleware({ role: 'admin' }), adminReportsController);
|
||||||
|
app.get(
|
||||||
|
'/api/v1/admin/reports/:id{[0-9a-f]{64}}',
|
||||||
|
userMiddleware(),
|
||||||
|
userMiddleware({ role: 'admin' }),
|
||||||
|
adminReportController,
|
||||||
|
);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve',
|
||||||
|
userMiddleware(),
|
||||||
|
userMiddleware({ role: 'admin' }),
|
||||||
|
adminReportResolveController,
|
||||||
|
);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/admin/reports/:id{[0-9a-f]{64}}/reopen',
|
||||||
|
userMiddleware(),
|
||||||
|
userMiddleware({ role: 'admin' }),
|
||||||
|
adminReportReopenController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/api/v1/admin/accounts', userMiddleware({ role: 'admin' }), adminAccountsController);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action',
|
||||||
|
userMiddleware(),
|
||||||
|
userMiddleware({ role: 'admin' }),
|
||||||
|
adminActionController,
|
||||||
|
);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/admin/accounts/:id{[0-9a-f]{64}}/approve',
|
||||||
|
userMiddleware(),
|
||||||
|
userMiddleware({ role: 'admin' }),
|
||||||
|
adminApproveController,
|
||||||
|
);
|
||||||
|
app.post(
|
||||||
|
'/api/v1/admin/accounts/:id{[0-9a-f]{64}}/reject',
|
||||||
|
userMiddleware(),
|
||||||
|
userMiddleware({ role: 'admin' }),
|
||||||
|
adminRejectController,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.put('/api/v1/pleroma/admin/users/tag', userMiddleware({ role: 'admin' }), pleromaAdminTagController);
|
||||||
|
app.delete('/api/v1/pleroma/admin/users/tag', userMiddleware({ role: 'admin' }), pleromaAdminUntagController);
|
||||||
|
app.patch('/api/v1/pleroma/admin/users/suggest', userMiddleware({ role: 'admin' }), pleromaAdminSuggestController);
|
||||||
|
app.patch('/api/v1/pleroma/admin/users/unsuggest', userMiddleware({ role: 'admin' }), pleromaAdminUnsuggestController);
|
||||||
|
|
||||||
|
// Not (yet) implemented.
|
||||||
|
app.get('/api/v1/custom_emojis', emptyArrayController);
|
||||||
|
app.get('/api/v1/filters', emptyArrayController);
|
||||||
|
app.get('/api/v1/domain_blocks', emptyArrayController);
|
||||||
|
app.get('/api/v1/conversations', emptyArrayController);
|
||||||
|
app.get('/api/v1/lists', emptyArrayController);
|
||||||
|
|
||||||
|
app.use('/api/*', notImplementedController);
|
||||||
|
app.use('/.well-known/*', publicFiles, notImplementedController);
|
||||||
|
app.use('/nodeinfo/*', notImplementedController);
|
||||||
|
app.use('/oauth/*', notImplementedController);
|
||||||
|
|
||||||
|
// Known frontend routes
|
||||||
|
app.get('/:acct{@.*}', frontendController);
|
||||||
|
app.get('/:acct{@.*}/*', frontendController);
|
||||||
|
app.get('/:bech32{^[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}$}', frontendController);
|
||||||
|
app.get('/users/*', notActivitypubMiddleware, frontendController);
|
||||||
|
app.get('/tags/*', frontendController);
|
||||||
|
app.get('/statuses/*', frontendController);
|
||||||
|
app.get('/notice/*', frontendController);
|
||||||
|
app.get('/timeline/*', frontendController);
|
||||||
|
|
||||||
|
// Known static file routes
|
||||||
|
app.get('/sw.js', publicFiles);
|
||||||
|
app.get(
|
||||||
|
'/favicon.ico',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
publicFiles,
|
||||||
|
staticFiles,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/images/*',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
publicFiles,
|
||||||
|
staticFiles,
|
||||||
|
);
|
||||||
|
app.get(
|
||||||
|
'/instance/*',
|
||||||
|
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||||
|
publicFiles,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Packs contains immutable static files
|
||||||
|
app.get(
|
||||||
|
'/packs/*',
|
||||||
|
cacheControlMiddleware({
|
||||||
|
maxAge: 31536000,
|
||||||
|
staleWhileRevalidate: 86400,
|
||||||
|
staleIfError: 21600,
|
||||||
|
public: true,
|
||||||
|
immutable: true,
|
||||||
|
}),
|
||||||
|
publicFiles,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/', ratelimit, frontendController);
|
||||||
|
app.get('*', publicFiles, staticFiles, ratelimit, frontendController);
|
||||||
|
|
||||||
|
app.onError(errorHandler);
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
||||||
|
export type { AppContext, AppController, AppMiddleware };
|
||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue