+1
.idea/dictionaries/project.xml
+1
.idea/dictionaries/project.xml
+1
backend/drizzle.config.ts
+1
backend/drizzle.config.ts
+51
-50
backend/package.json
+51
-50
backend/package.json
···
1
1
{
2
-
"name": "@clipprjs/server",
3
-
"version": "0.1.0",
4
-
"repository": "https://tangled.sh/@hexmani.ac/clippr",
5
-
"license": "AGPL-3.0-only",
6
-
"scripts": {
7
-
"dev": "tsx watch src/main.ts",
8
-
"build": "tsc",
9
-
"start": "node dist/index.js",
10
-
"lint": "npx eslint .",
11
-
"lint-write": "npx eslint . --fix",
12
-
"fmt": "npx prettier --write .",
13
-
"db:push": "npx drizzle-kit push"
14
-
},
15
-
"type": "module",
16
-
"main": "src/main.ts",
17
-
"engines": {
18
-
"node": ">=24"
19
-
},
20
-
"dependencies": {
21
-
"@atcute/atproto": "^3.1.0",
22
-
"@atcute/lexicon-doc": "^1.0.3",
23
-
"@atcute/lexicons": "^1.1.0",
24
-
"@clipprjs/lexicons": "^0.1.1",
25
-
"@eslint/eslintrc": "^3.3.1",
26
-
"@hono/node-server": "^1.15.0",
27
-
"@libsql/client": "^0.15.9",
28
-
"@skyware/jetstream": "^0.2.2",
29
-
"drizzle-orm": "^0.44.2",
30
-
"hono": "^4.8.4",
31
-
"toml": "^3.0.0",
32
-
"winston": "^3.17.0"
33
-
},
34
-
"devDependencies": {
35
-
"@atcute/lex-cli": "^2.1.1",
36
-
"@eslint/js": "^9.30.1",
37
-
"@typescript-eslint/eslint-plugin": "^8.35.1",
38
-
"@typescript-eslint/parser": "^8.35.1",
39
-
"drizzle-kit": "^0.31.4",
40
-
"eslint": "^9.30.1",
41
-
"eslint-config-prettier": "^10.1.5",
42
-
"eslint-plugin-drizzle": "^0.2.3",
43
-
"eslint-plugin-import": "^2.32.0",
44
-
"eslint-plugin-prettier": "^5.5.1",
45
-
"globals": "^16.3.0",
46
-
"jiti": "^2.4.2",
47
-
"prettier": "^3.6.2",
48
-
"tsx": "^4.20.3",
49
-
"typescript": "^5.8.3",
50
-
"typescript-eslint": "^8.35.1"
51
-
}
2
+
"name": "@clipprjs/server",
3
+
"version": "0.1.0",
4
+
"repository": "https://tangled.sh/@hexmani.ac/clippr",
5
+
"license": "AGPL-3.0-only",
6
+
"scripts": {
7
+
"dev": "tsx watch src/main.ts",
8
+
"build": "rm -r dist/; tsc",
9
+
"start": "node dist/src/main.js",
10
+
"lint": "npx eslint .",
11
+
"lint-write": "npx eslint . --fix",
12
+
"fmt": "npx prettier --write .",
13
+
"db:push": "npx drizzle-kit push"
14
+
},
15
+
"type": "module",
16
+
"main": "src/main.ts",
17
+
"engines": {
18
+
"node": ">=24"
19
+
},
20
+
"dependencies": {
21
+
"@atcute/atproto": "^3.1.0",
22
+
"@atcute/lexicon-doc": "^1.0.3",
23
+
"@atcute/lexicons": "^1.1.0",
24
+
"@clipprjs/lexicons": "^0.1.3",
25
+
"@eslint/eslintrc": "^3.3.1",
26
+
"@hono/node-server": "^1.15.0",
27
+
"@libsql/client": "^0.15.9",
28
+
"@skyware/jetstream": "^0.2.2",
29
+
"drizzle-orm": "^0.44.2",
30
+
"hono": "^4.8.4",
31
+
"toml": "^3.0.0",
32
+
"winston": "^3.17.0",
33
+
"xxhash-wasm": "^1.1.0"
34
+
},
35
+
"devDependencies": {
36
+
"@atcute/lex-cli": "^2.1.1",
37
+
"@eslint/js": "^9.30.1",
38
+
"@typescript-eslint/eslint-plugin": "^8.35.1",
39
+
"@typescript-eslint/parser": "^8.35.1",
40
+
"drizzle-kit": "^0.31.4",
41
+
"eslint": "^9.30.1",
42
+
"eslint-config-prettier": "^10.1.5",
43
+
"eslint-plugin-drizzle": "^0.2.3",
44
+
"eslint-plugin-import": "^2.32.0",
45
+
"eslint-plugin-prettier": "^5.5.1",
46
+
"globals": "^16.3.0",
47
+
"jiti": "^2.4.2",
48
+
"prettier": "^3.6.2",
49
+
"tsx": "^4.20.3",
50
+
"typescript": "^5.8.3",
51
+
"typescript-eslint": "^8.35.1"
52
+
}
52
53
}
+13
-113
backend/pnpm-lock.yaml
+13
-113
backend/pnpm-lock.yaml
···
18
18
specifier: ^1.1.0
19
19
version: 1.1.0
20
20
'@clipprjs/lexicons':
21
-
specifier: ^0.1.1
22
-
version: 0.1.1
21
+
specifier: ^0.1.3
22
+
version: 0.1.3
23
23
'@eslint/eslintrc':
24
24
specifier: ^3.3.1
25
25
version: 3.3.1
···
38
38
hono:
39
39
specifier: ^4.8.4
40
40
version: 4.8.4
41
-
hono-pino:
42
-
specifier: ^0.9.1
43
-
version: 0.9.1(hono@4.8.4)(pino@9.7.0)
44
-
pino:
45
-
specifier: ^9.7.0
46
-
version: 9.7.0
47
41
toml:
48
42
specifier: ^3.0.0
49
43
version: 3.0.0
50
44
winston:
51
45
specifier: ^3.17.0
52
46
version: 3.17.0
47
+
xxhash-wasm:
48
+
specifier: ^1.1.0
49
+
version: 1.1.0
53
50
devDependencies:
54
51
'@atcute/lex-cli':
55
52
specifier: ^2.1.1
···
127
124
resolution: {integrity: sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ==}
128
125
engines: {node: '>= 18'}
129
126
130
-
'@clipprjs/lexicons@0.1.1':
131
-
resolution: {integrity: sha512-/TONuKi8ASHJe5ycg4wKj9m9paAQCcufEB+XJcnvSCLMM3MArAJpg4bmU1epRE3xuSLtkLnOTsmv8xJm8fjSpg==}
127
+
'@clipprjs/lexicons@0.1.3':
128
+
resolution: {integrity: sha512-jv52Ib/E4hhoD/rXntZgBmVcCqBJe1EY7+WOLBqCyVvkdajMzpYHDHMFxALU61z7Gc6ducYJK3S5Ki7EnBywCw==}
132
129
133
130
'@colors/colors@1.6.0':
134
131
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
···
717
714
async@3.2.6:
718
715
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
719
716
720
-
atomic-sleep@1.0.0:
721
-
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
722
-
engines: {node: '>=8.0.0'}
723
-
724
717
available-typed-arrays@1.0.7:
725
718
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
726
719
engines: {node: '>= 0.4'}
···
836
829
define-properties@1.2.1:
837
830
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
838
831
engines: {node: '>= 0.4'}
839
-
840
-
defu@6.1.4:
841
-
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
842
832
843
833
detect-libc@2.0.2:
844
834
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
···
1121
1111
fast-levenshtein@2.0.6:
1122
1112
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
1123
1113
1124
-
fast-redact@3.5.0:
1125
-
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
1126
-
engines: {node: '>=6'}
1127
-
1128
1114
fastq@1.19.1:
1129
1115
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
1130
1116
···
1249
1235
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
1250
1236
engines: {node: '>= 0.4'}
1251
1237
1252
-
hono-pino@0.9.1:
1253
-
resolution: {integrity: sha512-5HopJgf7FBAHw1NBXNSDMB9Iuxp6RD0IkXDqmA+MxNQk6s566B0a8GtdSkApbow9wbrhghtHE8aLri9RsvzG1A==}
1254
-
engines: {node: '>=18'}
1255
-
peerDependencies:
1256
-
hono: '>=4.0.0'
1257
-
pino: '>=7.1.0'
1258
-
1259
1238
hono@4.8.4:
1260
1239
resolution: {integrity: sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg==}
1261
1240
engines: {node: '>=16.9.0'}
···
1507
1486
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
1508
1487
engines: {node: '>= 0.4'}
1509
1488
1510
-
on-exit-leak-free@2.1.2:
1511
-
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
1512
-
engines: {node: '>=14.0.0'}
1513
-
1514
1489
one-time@1.0.0:
1515
1490
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
1516
1491
···
1554
1529
picomatch@2.3.1:
1555
1530
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
1556
1531
engines: {node: '>=8.6'}
1557
-
1558
-
pino-abstract-transport@2.0.0:
1559
-
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
1560
-
1561
-
pino-std-serializers@7.0.0:
1562
-
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
1563
-
1564
-
pino@9.7.0:
1565
-
resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==}
1566
-
hasBin: true
1567
1532
1568
1533
possible-typed-array-names@1.1.0:
1569
1534
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
···
1582
1547
engines: {node: '>=14'}
1583
1548
hasBin: true
1584
1549
1585
-
process-warning@5.0.0:
1586
-
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
1587
-
1588
1550
promise-limit@2.7.0:
1589
1551
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
1590
1552
···
1595
1557
queue-microtask@1.2.3:
1596
1558
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1597
1559
1598
-
quick-format-unescaped@4.0.4:
1599
-
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
1600
-
1601
1560
readable-stream@3.6.2:
1602
1561
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
1603
1562
engines: {node: '>= 6'}
1604
-
1605
-
real-require@0.2.0:
1606
-
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
1607
-
engines: {node: '>= 12.13.0'}
1608
1563
1609
1564
reflect.getprototypeof@1.0.10:
1610
1565
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
···
1700
1655
simple-swizzle@0.2.2:
1701
1656
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
1702
1657
1703
-
sonic-boom@4.2.0:
1704
-
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
1705
-
1706
1658
source-map-support@0.5.21:
1707
1659
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
1708
1660
···
1710
1662
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
1711
1663
engines: {node: '>=0.10.0'}
1712
1664
1713
-
split2@4.2.0:
1714
-
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
1715
-
engines: {node: '>= 10.x'}
1716
-
1717
1665
stack-trace@0.0.10:
1718
1666
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
1719
1667
···
1758
1706
1759
1707
text-hex@1.0.0:
1760
1708
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
1761
-
1762
-
thread-stream@3.1.0:
1763
-
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
1764
1709
1765
1710
to-regex-range@5.0.1:
1766
1711
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
···
1881
1826
utf-8-validate:
1882
1827
optional: true
1883
1828
1829
+
xxhash-wasm@1.1.0:
1830
+
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
1831
+
1884
1832
yocto-queue@0.1.0:
1885
1833
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
1886
1834
engines: {node: '>=10'}
···
1915
1863
1916
1864
'@badrap/valita@0.4.5': {}
1917
1865
1918
-
'@clipprjs/lexicons@0.1.1':
1866
+
'@clipprjs/lexicons@0.1.3':
1919
1867
dependencies:
1920
1868
'@atcute/atproto': 3.1.0
1921
1869
'@atcute/lexicons': 1.1.0
···
2418
2366
2419
2367
async@3.2.6: {}
2420
2368
2421
-
atomic-sleep@1.0.0: {}
2422
-
2423
2369
available-typed-arrays@1.0.7:
2424
2370
dependencies:
2425
2371
possible-typed-array-names: 1.1.0
···
2546
2492
define-data-property: 1.1.4
2547
2493
has-property-descriptors: 1.0.2
2548
2494
object-keys: 1.1.1
2549
-
2550
-
defu@6.1.4: {}
2551
2495
2552
2496
detect-libc@2.0.2: {}
2553
2497
···
2874
2818
2875
2819
fast-levenshtein@2.0.6: {}
2876
2820
2877
-
fast-redact@3.5.0: {}
2878
-
2879
2821
fastq@1.19.1:
2880
2822
dependencies:
2881
2823
reusify: 1.1.0
···
3003
2945
hasown@2.0.2:
3004
2946
dependencies:
3005
2947
function-bind: 1.1.2
3006
-
3007
-
hono-pino@0.9.1(hono@4.8.4)(pino@9.7.0):
3008
-
dependencies:
3009
-
defu: 6.1.4
3010
-
hono: 4.8.4
3011
-
pino: 9.7.0
3012
2948
3013
2949
hono@4.8.4: {}
3014
2950
···
3269
3205
define-properties: 1.2.1
3270
3206
es-object-atoms: 1.1.1
3271
3207
3272
-
on-exit-leak-free@2.1.2: {}
3273
-
3274
3208
one-time@1.0.0:
3275
3209
dependencies:
3276
3210
fn.name: 1.1.0
···
3316
3250
3317
3251
picomatch@2.3.1: {}
3318
3252
3319
-
pino-abstract-transport@2.0.0:
3320
-
dependencies:
3321
-
split2: 4.2.0
3322
-
3323
-
pino-std-serializers@7.0.0: {}
3324
-
3325
-
pino@9.7.0:
3326
-
dependencies:
3327
-
atomic-sleep: 1.0.0
3328
-
fast-redact: 3.5.0
3329
-
on-exit-leak-free: 2.1.2
3330
-
pino-abstract-transport: 2.0.0
3331
-
pino-std-serializers: 7.0.0
3332
-
process-warning: 5.0.0
3333
-
quick-format-unescaped: 4.0.4
3334
-
real-require: 0.2.0
3335
-
safe-stable-stringify: 2.5.0
3336
-
sonic-boom: 4.2.0
3337
-
thread-stream: 3.1.0
3338
-
3339
3253
possible-typed-array-names@1.1.0: {}
3340
3254
3341
3255
prelude-ls@1.2.1: {}
···
3346
3260
3347
3261
prettier@3.6.2: {}
3348
3262
3349
-
process-warning@5.0.0: {}
3350
-
3351
3263
promise-limit@2.7.0: {}
3352
3264
3353
3265
punycode@2.3.1: {}
3354
3266
3355
3267
queue-microtask@1.2.3: {}
3356
3268
3357
-
quick-format-unescaped@4.0.4: {}
3358
-
3359
3269
readable-stream@3.6.2:
3360
3270
dependencies:
3361
3271
inherits: 2.0.4
3362
3272
string_decoder: 1.3.0
3363
3273
util-deprecate: 1.0.2
3364
-
3365
-
real-require@0.2.0: {}
3366
3274
3367
3275
reflect.getprototypeof@1.0.10:
3368
3276
dependencies:
···
3487
3395
dependencies:
3488
3396
is-arrayish: 0.3.2
3489
3397
3490
-
sonic-boom@4.2.0:
3491
-
dependencies:
3492
-
atomic-sleep: 1.0.0
3493
-
3494
3398
source-map-support@0.5.21:
3495
3399
dependencies:
3496
3400
buffer-from: 1.1.2
···
3498
3402
3499
3403
source-map@0.6.1: {}
3500
3404
3501
-
split2@4.2.0: {}
3502
-
3503
3405
stack-trace@0.0.10: {}
3504
3406
3505
3407
stop-iteration-iterator@1.1.0:
···
3549
3451
'@pkgr/core': 0.2.7
3550
3452
3551
3453
text-hex@1.0.0: {}
3552
-
3553
-
thread-stream@3.1.0:
3554
-
dependencies:
3555
-
real-require: 0.2.0
3556
3454
3557
3455
to-regex-range@5.0.1:
3558
3456
dependencies:
···
3714
3612
word-wrap@1.2.5: {}
3715
3613
3716
3614
ws@8.18.3: {}
3615
+
3616
+
xxhash-wasm@1.1.0: {}
3717
3617
3718
3618
yocto-queue@0.1.0: {}
+1
-1
backend/src/db/database.ts
+1
-1
backend/src/db/database.ts
+17
-1
backend/src/db/schema.ts
+17
-1
backend/src/db/schema.ts
···
4
4
* SPDX-License-Identifier: AGPL-3.0-only
5
5
*/
6
6
7
+
// noinspection Annotator
8
+
7
9
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
8
10
import { sql } from "drizzle-orm";
9
11
10
12
// WebStorm keeps throwing errors with the default statements as it wants
11
-
// an actual SQLite query, despite being valid. Oh well.
13
+
// an actual SQLite query, despite being valid. Sucks.
12
14
export const clipsTable = sqliteTable("clips", {
13
15
id: int("id").primaryKey({ autoIncrement: true }),
14
16
timestamp: int("time_us", { mode: "timestamp_ms" })
···
46
48
.notNull()
47
49
.default(sql`(unixepoch() * 1000)`),
48
50
});
51
+
52
+
export const usersTable = sqliteTable("profiles", {
53
+
id: int("id").primaryKey({ autoIncrement: true }),
54
+
timestamp: int("time_us", { mode: "timestamp_ms" })
55
+
.notNull()
56
+
.default(sql`(unixepoch() * 1000)`),
57
+
did: text("did").notNull(),
58
+
displayName: text("displayName"),
59
+
description: text("description"),
60
+
avatar: text("avatar"),
61
+
createdAt: int("createdAt", { mode: "timestamp_ms" })
62
+
.notNull()
63
+
.default(sql`(unixepoch() * 1000)`),
64
+
});
+10
-10
backend/src/main.ts
+10
-10
backend/src/main.ts
···
5
5
*/
6
6
7
7
import { serve, type ServerType } from "@hono/node-server";
8
-
import { Config } from "./config.ts";
9
-
import { readFromFirehose, startFirehose, stopFirehose } from "./network/jetstream.ts";
10
-
import app from "./server.ts";
8
+
import { Config } from "./config.js";
9
+
import { readFromFirehose, startFirehose, stopFirehose } from "./network/jetstream.js";
10
+
import app from "./server.js";
11
11
import { Database } from "./db/database.js";
12
12
import Logger from "./logger.js";
13
13
14
14
async function main() {
15
15
const logger = Logger;
16
-
logger.info("clippr-BE starting...");
16
+
logger.info("Clippr-BE starting...");
17
17
18
-
logger.info("initializing config");
18
+
logger.verbose("Reading configuration...");
19
19
const config = Config.getInstance();
20
20
21
-
logger.info("initializing database");
21
+
logger.verbose("Initializing database...");
22
22
Database.getInstance();
23
23
24
-
logger.info("initializing firehose connection");
24
+
logger.verbose("Initializing Jetstream connection...");
25
25
startFirehose();
26
26
readFromFirehose();
27
27
···
32
32
});
33
33
34
34
logger.info(
35
-
`server started at http://${config.get("hostname")}:${config.get("port")}`,
35
+
`XRPC server launched at http://${config.get("hostname")}:${config.get("port")}`,
36
36
);
37
37
38
38
process.removeAllListeners("SIGINT");
···
42
42
process.once("SIGTERM", () => gracefulShutdown("SIGTERM"));
43
43
44
44
function gracefulShutdown(signal: string) {
45
-
logger.info(`received ${signal}, shutting down...`);
45
+
logger.info(`Received ${signal}, shutting down...`);
46
46
stopFirehose();
47
47
server.close();
48
-
logger.info("server shut down, bye!");
48
+
logger.info("Bye!");
49
49
process.exit(0);
50
50
}
51
51
}
+161
-9
backend/src/network/commit.ts
+161
-9
backend/src/network/commit.ts
···
5
5
*/
6
6
7
7
import type { CommitEvent } from "@skyware/jetstream";
8
-
import { Database } from "../db/database.ts";
9
-
import { clipsTable, tagsTable } from "../db/schema.js";
8
+
import { Database } from "../db/database.js";
9
+
import { clipsTable, tagsTable, usersTable } from "../db/schema.js";
10
10
import { is } from "@atcute/lexicons";
11
-
import { SocialClipprFeedClip, SocialClipprFeedTag } from "@clipprjs/lexicons";
11
+
import { SocialClipprActorProfile, SocialClipprFeedClip, SocialClipprFeedTag } from "@clipprjs/lexicons";
12
12
import type { At } from "@atcute/client/lexicons";
13
13
import Logger from "../logger.js";
14
+
import { isBlob } from "@atcute/lexicons/interfaces";
15
+
import { validateClip, validateProfile, validateTag } from "./validator.js";
16
+
import xxhash from "xxhash-wasm";
14
17
15
18
const db = Database.getInstance().getDb();
16
19
20
+
/// Converts an ``At.DID`` type to a proper string, for type reasons.
17
21
function convertDidToString(did: At.DID): string {
18
22
return did.toString();
19
23
}
20
24
25
+
/// Converts a microsecond Unix date to a Date object, for type reasons.
21
26
function convertMicroToDate(micro: number): Date {
22
27
return new Date(micro / 1000);
23
28
}
24
29
25
30
export async function handleClip(
26
31
event: CommitEvent<`social.clippr.${string}`>,
27
-
) {
28
-
if (event.commit.operation !== "create") return; // We currently do not handle these.
32
+
): Promise<void> {
33
+
if (event.commit.operation !== "create") {
34
+
Logger.warn(
35
+
`Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
36
+
);
37
+
return;
38
+
} // We currently do not handle these.
39
+
40
+
if (event.commit.record.$type !== "social.clippr.feed.clip") {
41
+
Logger.verbose(
42
+
"Invalid type for incoming clip record",
43
+
event.commit.record,
44
+
);
45
+
}
29
46
30
47
if (!is(SocialClipprFeedClip.mainSchema, event.commit.record)) {
31
-
Logger.verbose("Invalid clip", event.commit.record);
48
+
Logger.verbose(
49
+
"Invalid schema for incoming clip record",
50
+
event.commit.record,
51
+
);
32
52
return;
33
53
}
34
54
···
45
65
url: event.commit.record.url,
46
66
};
47
67
68
+
// xxh64, NOT xxh3 learned that the hard way
69
+
const { h64 } = await xxhash();
70
+
const urlHash = h64(record.url).toString(16);
71
+
72
+
if (urlHash !== event.commit.rkey) {
73
+
Logger.verbose(
74
+
`Record key hash (${event.commit.rkey}) does not match hash of URL (${urlHash}) in incoming clip record`,
75
+
event.commit.record,
76
+
);
77
+
return;
78
+
}
79
+
80
+
if (!(await validateClip(record))) {
81
+
return;
82
+
}
83
+
48
84
await db.insert(clipsTable).values({
49
85
// @ts-expect-error Weird type error despite being a normal string.
50
86
did: convertDidToString(event.did),
···
64
100
Logger.verbose("Indexed new clip:", event.did, event.commit.rkey);
65
101
}
66
102
67
-
export async function handleTag(event: CommitEvent<`social.clippr.${string}`>) {
68
-
if (event.commit.operation !== "create") return; // We currently do not handle these.
103
+
export async function handleTag(
104
+
event: CommitEvent<`social.clippr.${string}`>,
105
+
): Promise<void> {
106
+
if (event.commit.operation !== "create") {
107
+
Logger.warn(
108
+
`Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
109
+
);
110
+
return;
111
+
} // We currently do not handle these.
112
+
113
+
if (event.commit.record.$type !== "social.clippr.feed.tag") {
114
+
Logger.verbose("Invalid type for incoming tag record", event.commit.record);
115
+
return;
116
+
}
69
117
70
118
if (!is(SocialClipprFeedTag.mainSchema, event.commit.record)) {
71
-
Logger.verbose("Invalid tag", event.commit.record);
119
+
Logger.verbose(
120
+
"Invalid schema for incoming tag record",
121
+
event.commit.record,
122
+
);
72
123
return;
73
124
}
74
125
···
79
130
color: event.commit.record.color,
80
131
};
81
132
133
+
if (record.name !== event.commit.rkey) {
134
+
Logger.verbose(
135
+
"Record key does not match name of incoming tag record",
136
+
event.commit.record,
137
+
);
138
+
return;
139
+
}
140
+
141
+
// Independent validations
142
+
if (!(await validateTag(record))) {
143
+
return;
144
+
}
145
+
82
146
await db.insert(tagsTable).values({
83
147
did: convertDidToString(event.did),
84
148
timestamp: convertMicroToDate(event.time_us),
···
90
154
91
155
Logger.verbose("Indexed new tag:", event.did, event.commit.rkey);
92
156
}
157
+
158
+
export async function handleProfile(
159
+
event: CommitEvent<`social.clippr.${string}`>,
160
+
): Promise<void> {
161
+
if (event.commit.operation !== "create") {
162
+
Logger.warn(
163
+
`Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
164
+
);
165
+
return;
166
+
} // We currently do not handle these.
167
+
168
+
if (event.commit.record.$type !== "social.clippr.actor.profile") {
169
+
Logger.verbose(
170
+
"Invalid type for incoming profile record",
171
+
event.commit.record,
172
+
);
173
+
return;
174
+
}
175
+
176
+
if (!is(SocialClipprActorProfile.mainSchema, event.commit.record)) {
177
+
Logger.verbose(
178
+
"Invalid schema for incoming profile record",
179
+
event.commit.record,
180
+
);
181
+
return;
182
+
}
183
+
184
+
const record: SocialClipprActorProfile.Main = {
185
+
$type: "social.clippr.actor.profile",
186
+
createdAt: event.commit.record.createdAt,
187
+
displayName: event.commit.record.displayName,
188
+
description: event.commit.record.description || undefined,
189
+
avatar: event.commit.record.avatar || undefined,
190
+
};
191
+
192
+
if (event.commit.rkey !== "self") {
193
+
Logger.verbose(
194
+
"Record key of incoming profile record does not match 'self'",
195
+
event.commit.record,
196
+
);
197
+
return;
198
+
}
199
+
200
+
// This needs to be here so the avatar can be recognized as a proper blob.
201
+
if (record.avatar) {
202
+
if (!isBlob(record.avatar)) {
203
+
Logger.verbose("Avatar in incoming profile record is not a blob", record);
204
+
return;
205
+
}
206
+
207
+
if (record.avatar.mimeType.match(/^image\/(png|jpeg)$/i) === null) {
208
+
Logger.verbose(
209
+
"Avatar in incoming profile record is not a PNG or JPEG",
210
+
record,
211
+
);
212
+
return;
213
+
}
214
+
215
+
if (record.avatar.ref?.$link === undefined) {
216
+
Logger.verbose(
217
+
"Avatar in incoming profile record has no link to blob",
218
+
record,
219
+
);
220
+
return;
221
+
}
222
+
223
+
if (record.avatar.size > 1000000) {
224
+
Logger.verbose("Avatar in incoming profile record is too large", record);
225
+
return;
226
+
}
227
+
}
228
+
229
+
// Independent validations
230
+
if (!(await validateProfile(record))) {
231
+
return;
232
+
}
233
+
234
+
await db.insert(usersTable).values({
235
+
did: convertDidToString(event.did),
236
+
timestamp: convertMicroToDate(event.time_us),
237
+
createdAt: new Date(record.createdAt),
238
+
displayName: record.displayName,
239
+
avatar: record.avatar?.ref.$link,
240
+
description: record.description,
241
+
});
242
+
243
+
Logger.verbose("Indexed new profile for:", convertDidToString(event.did));
244
+
}
+8
-5
backend/src/network/jetstream.ts
+8
-5
backend/src/network/jetstream.ts
···
5
5
*/
6
6
7
7
import { Jetstream } from "@skyware/jetstream";
8
-
import { Config } from "../config.ts";
9
-
import { handleClip, handleTag } from "./commit.js";
8
+
import { Config } from "../config.js";
9
+
import { handleClip, handleProfile, handleTag } from "./commit.js";
10
10
import Logger from "../logger.js";
11
11
12
12
const config = Config.getInstance();
···
34
34
case "social.clippr.feed.tag":
35
35
handleTag(e);
36
36
break;
37
+
case "social.clippr.actor.profile":
38
+
handleProfile(e);
39
+
break;
37
40
default:
38
-
Logger.debug(
39
-
`commit for ${e.commit.collection} is not relevant, dropping`,
41
+
Logger.verbose(
42
+
`Commit for ${e.commit.collection} is not relevant, dropping`,
40
43
);
41
44
break;
42
45
}
···
51
54
});
52
55
53
56
jetstream.on("error", (e) => {
54
-
Logger.warn(e);
57
+
Logger.warn(`Error while reading from firehose: ${e.message}`);
55
58
});
56
59
}
+148
backend/src/network/validator.ts
+148
backend/src/network/validator.ts
···
1
+
/*
2
+
* clippr: a social bookmarking service for the AT Protocol
3
+
* Copyright (c) 2025 clippr contributors.
4
+
* SPDX-License-Identifier: AGPL-3.0-only
5
+
*/
6
+
7
+
import {SocialClipprActorProfile, SocialClipprFeedClip, SocialClipprFeedTag,} from "@clipprjs/lexicons";
8
+
import {isDatetime, isLanguageCode} from "@atcute/lexicons/syntax";
9
+
import Logger from "../logger.js";
10
+
11
+
export async function validateProfile(
12
+
record: SocialClipprActorProfile.Main,
13
+
): Promise<boolean> {
14
+
if (!isDatetime(record.createdAt)) {
15
+
Logger.verbose(
16
+
"Invalid createdAt timestamp for incoming profile record",
17
+
record,
18
+
);
19
+
return false;
20
+
}
21
+
22
+
if (record.displayName) {
23
+
if (record.displayName.length > 64) {
24
+
Logger.verbose(
25
+
"Too long displayName from incoming profile record",
26
+
record,
27
+
);
28
+
return false;
29
+
}
30
+
} else {
31
+
Logger.verbose("No displayName from incoming profile record", record);
32
+
return false;
33
+
}
34
+
35
+
if (record.description) {
36
+
if (record.description.length > 500) {
37
+
Logger.verbose(
38
+
"Too long description from incoming profile record",
39
+
record,
40
+
);
41
+
return false;
42
+
}
43
+
}
44
+
45
+
return true;
46
+
}
47
+
48
+
export async function validateTag(
49
+
record: SocialClipprFeedTag.Main,
50
+
): Promise<boolean> {
51
+
if (!isDatetime(record.createdAt)) {
52
+
Logger.verbose(
53
+
"Invalid createdAt timestamp for incoming tag record",
54
+
record,
55
+
);
56
+
return false;
57
+
}
58
+
59
+
if (record.name.length > 64) {
60
+
Logger.verbose("Invalid name length for incoming tag record", record);
61
+
return false;
62
+
}
63
+
64
+
if (record.color) {
65
+
if (record.color.length > 7) {
66
+
Logger.verbose("Invalid color length for incoming tag record", record);
67
+
return false;
68
+
}
69
+
70
+
if (!record.color.match("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")) {
71
+
Logger.verbose(
72
+
"Invalid hexadecimal color for incoming tag record",
73
+
record,
74
+
);
75
+
return false;
76
+
}
77
+
}
78
+
79
+
return true;
80
+
}
81
+
82
+
export async function validateClip(
83
+
record: SocialClipprFeedClip.Main,
84
+
): Promise<boolean> {
85
+
if (record.url.length > 2000) {
86
+
Logger.verbose("Too long url from incoming clip record", record);
87
+
return false;
88
+
}
89
+
90
+
if (record.title.length > 2048) {
91
+
Logger.verbose("Too long title from incoming clip record", record);
92
+
return false;
93
+
}
94
+
95
+
if (record.description.length > 4096) {
96
+
Logger.verbose("Too long description from incoming clip record", record);
97
+
return false;
98
+
}
99
+
100
+
if (record.notes) {
101
+
if (record.notes.length > 10000) {
102
+
Logger.verbose("Too long notes from incoming clip record", record);
103
+
return false;
104
+
}
105
+
}
106
+
107
+
if (record.tags) {
108
+
if (record.tags.some((tag) => tag.$type !== "com.atproto.repo.strongRef")) {
109
+
Logger.verbose(
110
+
"A tag ref from incoming clip record is not a strongRef",
111
+
record,
112
+
);
113
+
return false;
114
+
}
115
+
116
+
// There should be more tests here, but I'm not exactly sure what to add...
117
+
}
118
+
119
+
if (typeof record.unlisted !== "boolean") {
120
+
Logger.verbose(
121
+
"Unlisted value from incoming clip record is not a boolean",
122
+
record,
123
+
);
124
+
return false;
125
+
}
126
+
127
+
// Same with "unread" but it's not required so
128
+
129
+
if (record.languages) {
130
+
if (record.languages.some((lang) => !isLanguageCode(lang))) {
131
+
Logger.verbose(
132
+
"An item in the incoming clip record's languages array is not a valid language code",
133
+
record,
134
+
);
135
+
return false;
136
+
}
137
+
}
138
+
139
+
if (!isDatetime(record.createdAt)) {
140
+
Logger.verbose(
141
+
"Invalid createdAt timestamp for incoming clip record",
142
+
record,
143
+
);
144
+
return false;
145
+
}
146
+
147
+
return true;
148
+
}
+1
-1
backend/src/server.ts
+1
-1
backend/src/server.ts
-1
backend/tsconfig.json
-1
backend/tsconfig.json