283 Commits

Author SHA1 Message Date
acelinkio
a99f29074c scanner: adding the probes back (#1197) 2025-12-01 18:30:23 -08:00
Arlan Lloyd
f449a0878a adding the probes back 2025-12-02 02:26:34 +00:00
acelinkio
097985ab6d scanner: refactor otel integration (#1194) 2025-12-01 23:50:28 +01:00
11c300ecf7 Add status api to get scanner's status (#1195) 2025-12-01 20:18:21 +01:00
1e975ce238 Set null not distinct in scanner request constraint 2025-12-01 20:15:57 +01:00
b39fa4262d Add status api to get scanner's status 2025-12-01 20:04:16 +01:00
d7699389bc Fix missing kid in apikeys jwt 2025-12-01 20:04:16 +01:00
renovate[bot]
1036e9f3f3 chore(deps): update dependency @biomejs/biome to v2.3.7 (#1189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 10:36:11 +01:00
renovate[bot]
b4749f3ed3 fix(deps): update aws-sdk-go-v2 monorepo (#1191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 10:34:25 +01:00
renovate[bot]
a20c61206f fix(deps): update module github.com/labstack/echo-jwt/v4 to v4.4.0 (#1192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 10:34:14 +01:00
renovate[bot]
0644a43cb1 chore(deps): update actions/checkout action to v6 (#1193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 10:34:05 +01:00
af4742ae0b Fix sqlarr of api (#1188) 2025-11-30 19:29:46 +00:00
acelinkio
e401ca98c0 downgrade cloudpirates postgres 0.12.0 (#1187) 2025-11-28 13:15:55 -08:00
Arlan Lloyd
a756c875fd 0.12.1 has a bug with rendering nested values 2025-11-28 21:13:10 +00:00
acelinkio
2ef26e5d02 add devspace (#1173) 2025-11-28 20:52:10 +01:00
acelinkio
e7d9002156 kyoo_api logs redact password & other sensitive fields (#1182) 2025-11-28 16:42:27 +00:00
acelinkio
28d2e193aa kyoo_api extension install specify schema (#1183) 2025-11-28 16:39:47 +00:00
ce5bee11c0 Use unnest in insertion methods (#1185) 2025-11-28 17:28:11 +01:00
60d59d7f7b Wrap every insert with a trace 2025-11-28 17:25:29 +01:00
464d720ef9 Fix unnest issues 2025-11-28 17:11:43 +01:00
8fc279d2ed Use unnest everywhere 2025-11-28 17:11:43 +01:00
a45e992339 Properly type unnestValues return 2025-11-28 17:11:43 +01:00
5f8ddd435a Use unnest for entries 2025-11-28 17:11:43 +01:00
d822463fe0 Add a trace for api migrations 2025-11-28 17:11:43 +01:00
acelinkio
3a0cbf786d fix(deps): update aws-sdk-go-v2 monorepo (#1159) 2025-11-24 13:12:25 -08:00
renovate[bot]
dfb4777a5d fix(deps): update aws-sdk-go-v2 monorepo 2025-11-24 20:29:44 +00:00
acelinkio
eea32c47e9 chore(deps): update postgres docker tag to v0.12.1 (#1181) 2025-11-24 09:49:58 -08:00
renovate[bot]
6bcd03b18e chore(deps): update postgres docker tag to v0.12.1 2025-11-24 13:46:46 +00:00
acelinkio
87a3df6897 chore(deps): update dependency @biomejs/biome to v2.3.6 (#1179) 2025-11-23 18:38:41 -08:00
acelinkio
7f7a16e9b5 chore(deps): update postgres docker tag to v0.12.0 (#1180) 2025-11-23 18:38:17 -08:00
renovate[bot]
b95dd9056b chore(deps): update postgres docker tag to v0.12.0 2025-11-24 02:22:25 +00:00
renovate[bot]
5044f941b1 chore(deps): update dependency @biomejs/biome to v2.3.6 2025-11-24 02:08:06 +00:00
c56f9ea791 Remove identify traces (#1178) 2025-11-23 23:55:23 +01:00
eb56dd70d6 Batch images task insertion and add priority (#1177) 2025-11-23 23:22:01 +01:00
a4f5ef33ff Fix deadlock on image downloading 2025-11-23 23:20:40 +01:00
20ab1dae6c Force tests to run on kyoo_test database 2025-11-23 23:16:58 +01:00
7ebc0fe504 Fix type issues 2025-11-23 22:44:59 +01:00
019aceb8d9 Batch images task insertion and add priority 2025-11-23 22:44:59 +01:00
f59cb5d671 Properly handle spans of image downloading (#1176) 2025-11-23 19:00:54 +01:00
d4deafe1dc Forward X-Forward-Host & proto headers in traefik 2025-11-23 18:58:19 +01:00
7b2f1c7a82 Fix image test in github 2025-11-23 18:06:55 +01:00
c5fa3ecb01 Cleanup scanner processing span 2025-11-23 17:49:23 +01:00
3602905e86 Properly handle spans of image downloading 2025-11-23 17:49:23 +01:00
1f7844b8a5 Fix api images path (#1175) 2025-11-23 14:57:07 +01:00
Jory Irving
3b76fb2647 Allow manual helm chart publishing for specific tags (#1174) 2025-11-23 13:24:14 +01:00
acelinkio
9a00d5036f Bump golang.org/x/crypto from 0.44.0 to 0.45.0 in /transcoder (#1171) 2025-11-21 09:34:30 -08:00
dependabot[bot]
7c315602cd Bump golang.org/x/crypto from 0.44.0 to 0.45.0 in /transcoder
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 17:03:04 +00:00
acelinkio
19e0e402da Bump golang.org/x/crypto from 0.43.0 to 0.45.0 in /auth (#1168) 2025-11-21 09:02:35 -08:00
acelinkio
ef38468178 chore(deps): update dependency @biomejs/biome to v2.3.5 (#1157) 2025-11-21 09:02:17 -08:00
2cbbb450c2 Lock scanner processing to a single runner (#1170) 2025-11-20 12:13:34 +01:00
9f466ff702 Update elysia otel plugin 2025-11-20 12:12:01 +01:00
05f7fabb3c Lock scanner processing to a single runner 2025-11-20 12:12:01 +01:00
5bc6a06b91 Chunk identify scans (#1169) 2025-11-20 09:46:49 +01:00
f7e801e574 Chunk identify scans 2025-11-20 09:44:00 +01:00
dependabot[bot]
c663189df1 Bump golang.org/x/crypto from 0.43.0 to 0.45.0 in /auth
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.43.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 02:45:53 +00:00
37ec32b52d Name migrate span of scanner 2025-11-20 00:03:24 +01:00
188ce3f67d Log to stdout & otel for scanner 2025-11-19 23:59:08 +01:00
18b2ae2c5f Remove name prefix in apikeys (#1167) 2025-11-19 23:29:31 +01:00
a115c83cba Make scanner ready check a noop (#1166) 2025-11-19 20:49:42 +01:00
27d25f4829 Fix transcoder service name in otel (#1165) 2025-11-19 19:27:50 +00:00
acelinkio
d1d6fa9556 docs: change logo to use google source (#1164) 2025-11-19 10:09:03 -08:00
Arlan Lloyd
7878673d8d change logo to use google source 2025-11-19 18:07:09 +00:00
34761b43ae Fix need rendering compute on videos slug (#1163) 2025-11-19 16:28:47 +00:00
acelinkio
536b03b1ef chart: update docs (#1162) 2025-11-18 14:22:53 -08:00
acelinkio
6f37c128ef Merge branch 'master' into chart/update_notes 2025-11-18 14:21:51 -08:00
Arlan Lloyd
8e3a582f75 update docs 2025-11-18 22:20:32 +00:00
acelinkio
b5720dca06 update diagrams for v5 (#1156) 2025-11-18 10:11:51 -08:00
Arlan Lloyd
df26dbca63 update SSR notes + api downloads media images 2025-11-18 18:10:13 +00:00
acelinkio
851ae9eb9d Merge branch 'master' into update_diagrams 2025-11-18 09:37:45 -08:00
Arlan Lloyd
8aaf786400 update metadata storage + scanner docs 2025-11-18 16:04:18 +00:00
3e40842c84 Setup otel & export traces for all services (#1161) 2025-11-17 23:38:30 +01:00
8ab4146241 Fix format 2025-11-17 23:20:09 +01:00
0fa25eaeca Use my work of drizzle-otel to properly instrument db errors 2025-11-17 23:07:59 +01:00
2194831d86 Manually instrument scanner 2025-11-17 23:07:59 +01:00
7124a3d3c6 Instrument scanner 2025-11-17 23:07:59 +01:00
55a22f1c9e Instrument pgx 2025-11-17 23:07:54 +01:00
6d58164a6d Instrument auth's echo 2025-11-17 23:03:49 +01:00
fea9c16515 Instrument drizzle 2025-11-17 23:03:49 +01:00
01883d08cc Instrument elyzia 2025-11-17 23:03:49 +01:00
acelinkio
03792487c3 chore(deps): update postgres docker tag to v0.11.6 (#1158) 2025-11-17 07:50:27 -08:00
renovate[bot]
87a0fa39f7 chore(deps): update postgres docker tag to v0.11.6 2025-11-17 15:03:19 +00:00
renovate[bot]
64dae6ddce chore(deps): update dependency @biomejs/biome to v2.3.5 2025-11-17 01:48:59 +00:00
Arlan Lloyd
efec489c96 update diagrams for v5 2025-11-15 20:11:24 +00:00
Arlan Lloyd
5837b9875d update project structure 2025-11-14 16:46:51 +00:00
1e1a6a1159 chart: v5 update (#884) 2025-11-14 09:41:09 +01:00
5e63b57440 Fix pagination URLs when behind SSL-terminating reverse proxy 2025-11-13 14:04:05 +01:00
Arlan Lloyd
befc0fc84f update sslmode default for kyoo_api 2025-11-12 23:41:40 +00:00
Arlan Lloyd
9bbdb3d7f0 add way to specify shared database 2025-11-12 06:50:52 +00:00
Arlan Lloyd
58690eb428 copy default claims from .env 2025-11-11 15:29:32 +00:00
Arlan Lloyd
7d47b7642c add sslmode to api/scanner & fix default for transcoder 2025-11-10 19:11:33 +00:00
Arlan Lloyd
2f3682c226 remove unneeded comment 2025-11-10 18:49:32 +00:00
Arlan Lloyd
d952444919 fix scanner claims 2025-11-10 17:13:45 +00:00
Arlan Lloyd
86cb391425 add interpolation to handle apikey concatination 2025-11-10 16:01:08 +00:00
Arlan Lloyd
ed6623293b fix probe locations 2025-11-10 15:27:58 +00:00
Arlan Lloyd
7a756dd67c remove test manifest 2025-11-10 14:28:16 +00:00
Arlan Lloyd
e71640a636 remove test values 2025-11-10 14:26:47 +00:00
renovate[bot]
f4b1ab5fa0 Update dependency @biomejs/biome to v2.3.4 (#1152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 11:11:34 +01:00
renovate[bot]
90475e47b1 Update aws-sdk-go-v2 monorepo (#1137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 09:49:56 +00:00
renovate[bot]
c32b58e974 Update dependency @biomejs/biome to v2.3.3 (#1148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 10:38:15 +01:00
renovate[bot]
211f75f71a Update dependency go to v1.25.4 (#1149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 10:38:07 +01:00
renovate[bot]
f0b9f3afdc Update module github.com/asticode/go-astisub to v0.38.0 (#1124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 10:37:51 +01:00
renovate[bot]
6bc041723e Update module golang.org/x/sync to v0.18.0 (#1151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 10:37:39 +01:00
Arlan Lloyd
82ea4fbe0b move oidc vars 2025-11-10 04:44:19 +00:00
Arlan Lloyd
1749dc814b add postgres extension + config 2025-11-10 04:35:50 +00:00
Arlan Lloyd
dfc886127a update schema configs 2025-11-10 04:30:12 +00:00
Arlan Lloyd
4943c94182 remove swagger middleware 2025-11-10 04:21:09 +00:00
Arlan Lloyd
238702f81c add probes 2025-11-10 03:59:53 +00:00
Antoine Labarussias
c60abb26f9 fix(chart): send cookie header to middleware 2025-11-09 20:13:20 +01:00
Antoine Labarussias
2fb393bb45 fix(chart): update transcoder pg env vars 2025-11-09 20:13:20 +01:00
Antoine Labarussias
6bab124331 fix(chart): set existingSecret to null 2025-11-09 20:13:20 +01:00
Antoine Labarussias
9c57e01426 feat(chart): add auth private key 2025-11-09 20:13:20 +01:00
Arlan Lloyd
66dedaee29 update apikey format to $name-$key 2025-11-09 20:13:20 +01:00
Arlan Lloyd
eb31c0d8e6 add in scanner & extra apikey support 2025-11-09 20:13:20 +01:00
Arlan Lloyd
cd65632527 allow for specifying middleware proxy root url 2025-11-09 20:13:20 +01:00
Arlan Lloyd
a563d8f8ba update docs 2025-11-09 20:13:20 +01:00
Arlan Lloyd
60082ee799 remove apikey reference 2025-11-09 20:13:20 +01:00
Arlan Lloyd
6065b73d13 fix port metadata 2025-11-09 20:13:20 +01:00
Arlan Lloyd
32a9dfc11c fix middleware 2025-11-09 20:13:20 +01:00
Arlan Lloyd
57c135c86b set traefik args via extraArgs 2025-11-09 20:13:20 +01:00
Arlan Lloyd
5064111a93 update traefik configs 2025-11-09 20:13:20 +01:00
Arlan Lloyd
de718b6a46 update scanner settings 2025-11-09 20:13:20 +01:00
Arlan Lloyd
d7748eb83e remove specifying auth schema and rsa 2025-11-09 20:13:20 +01:00
Arlan Lloyd
d730b5f3f4 specify traefik repo/version 2025-11-09 20:13:20 +01:00
Arlan Lloyd
5d17f8f0f9 fix subchart settings/notes 2025-11-09 20:13:20 +01:00
Arlan Lloyd
864ee3efa2 add explicit namespace 2025-11-09 20:13:20 +01:00
Arlan Lloyd
a14a0145a9 update transcoder env 2025-11-09 20:13:20 +01:00
Arlan Lloyd
a86c361825 fix api port 2025-11-09 20:13:20 +01:00
Arlan Lloyd
e17e969bfe fix scanner envs 2025-11-09 20:13:20 +01:00
Arlan Lloyd
4708186f5c update api env vars 2025-11-09 20:13:20 +01:00
Arlan Lloyd
6238c8d9a0 fix default address 2025-11-09 20:13:20 +01:00
Arlan Lloyd
7a34bbedae fix readme 2025-11-09 20:13:20 +01:00
Arlan Lloyd
e59477eddd finish renaming kyoo_back to kyoo_api 2025-11-09 20:13:20 +01:00
Arlan Lloyd
7368af7266 rename back to api 2025-11-09 20:13:20 +01:00
Arlan Lloyd
9c03f99524 move to cloudpirates postgres 2025-11-09 20:13:19 +01:00
Arlan Lloyd
54d4965a9a purge old infra var 2025-11-09 20:13:18 +01:00
Arlan Lloyd
dc7d6919da purge kyoo_migrations, no longer needed 2025-11-09 20:13:18 +01:00
Arlan Lloyd
aa180bfcea purge autosync! 2025-11-09 20:13:18 +01:00
Arlan Lloyd
5ab28622d9 purge rabbitmq 2025-11-09 20:13:17 +01:00
Arlan Lloyd
7f97ea6e90 purge meilisearch from chart 2025-11-09 20:13:14 +01:00
Arlan Lloyd
869b0206b8 purge matcher specific settings 2025-11-09 20:13:14 +01:00
Arlan Lloyd
159a4cc77a prepare ingress to traefikproxy 2025-11-09 20:13:14 +01:00
Arlan Lloyd
571590a40d update variables 2025-11-09 20:13:13 +01:00
Arlan Lloyd
0943401d03 initial add of auth 2025-11-09 20:13:13 +01:00
Arlan Lloyd
ababb67b1a update traefik configs 2025-11-09 20:13:13 +01:00
Arlan Lloyd
5bf4d70623 traefik update services configs 2025-11-09 20:13:13 +01:00
Arlan Lloyd
c495589927 use short names 2025-11-09 20:13:13 +01:00
Arlan Lloyd
5e20257202 fix middleware name 2025-11-09 20:13:13 +01:00
Arlan Lloyd
e8154c31ce update ForwardAuth docs 2025-11-09 20:13:13 +01:00
Arlan Lloyd
b1d8d00a9b update 2025-11-09 20:13:13 +01:00
Arlan Lloyd
c5c0de5493 configure defaultConfigmap 2025-11-09 20:13:13 +01:00
Arlan Lloyd
cfafe12c82 add conditional enable 2025-11-09 20:13:13 +01:00
Arlan Lloyd
594e0233ca fix template 2025-11-09 20:13:13 +01:00
Arlan Lloyd
63c5b40123 initial add Traefik
Signed-off-by: Arlan Lloyd <arlanlloyd@gmail.com>
2025-11-09 20:13:13 +01:00
1caff13adc Add /health & /ready for every service (#1147) 2025-11-09 20:12:36 +01:00
b4c85f3f28 Add /health and /ready for scanner 2025-11-09 19:58:41 +01:00
a95bbcb6eb Fix last_seen update on auth 2025-11-09 19:58:41 +01:00
61b38d5b03 Add /ready for api 2025-11-09 19:25:08 +01:00
563ae85db1 Add /health and /ready for transcoder 2025-11-09 19:21:29 +01:00
8f0fb42b47 Add /ready api to auth 2025-11-09 19:21:29 +01:00
40c13e7ddf Cleanup casing in api extension setup 2025-11-09 19:21:29 +01:00
0a862c3782 Remove phantom token middleware for swagger 2025-11-09 19:21:29 +01:00
b1723c2f2c Transcoder misc fixes (#1144)
Co-authored-by: Fred Heinecke <fred.heinecke@yahoo.com>
2025-11-09 18:07:20 +00:00
Weblate (bot)
84fcbbbb42 Translations update from Hosted Weblate (#1146) 2025-11-09 18:53:50 +01:00
Weblate (bot)
93b5b50ba1 Translations update from Hosted Weblate (#1145) 2025-11-09 18:52:22 +01:00
4f9d340ef4 Add settings page (#1143) 2025-11-09 18:49:42 +01:00
5142e2bc25 Fix expo build 2025-11-09 18:27:03 +01:00
8f7f388403 Fix claim edit always editing permissions 2025-11-09 18:27:03 +01:00
5a37327e63 Fix change password api 2025-11-09 18:27:03 +01:00
d42062679a Handle logout and account deletion 2025-11-09 16:38:10 +01:00
6bb905b388 Rework settings page 2025-11-09 16:38:10 +01:00
39cfd501ac Add automatic language detection and language setting 2025-11-09 16:38:10 +01:00
079cc6b4f9 Fix translations keys 2025-11-09 16:38:10 +01:00
951ae955ed Use a postinstall script to generate translation list 2025-11-09 16:38:10 +01:00
61708857af Rework useMutation optimistic updates & start settings page 2025-11-09 16:38:10 +01:00
d3c69876d4 Add back search logic (#1142) 2025-11-08 15:03:14 +01:00
ebaf6d2177 Delete old front routing 2025-11-08 14:40:45 +01:00
9bc30ab62d Add back search logic 2025-11-08 14:40:45 +01:00
Antoine
f71a65d134 feat(auth): update forward auth endpoint (#1141) 2025-11-06 01:25:33 +01:00
renovate[bot]
ca0722b55c chore(deps): update helm release postgresql to v18.1.3 (#1118)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:42:27 +01:00
renovate[bot]
22cf24fd8c fix(deps): update module github.com/lestrrat-go/jwx/v3 to v3.0.12 (#1088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:42:15 +01:00
Antoine
19e3619a42 docs(transcoder): update env examples (#1140) 2025-11-05 09:26:20 +01:00
1db4dea56f Handle cookies in keibi + fix database/env stuff (#1135) 2025-11-04 10:09:39 +01:00
03bb51661a Format stuff 2025-11-04 09:48:21 +01:00
01177c2489 Proprely remove cookies on logout 2025-11-04 09:48:21 +01:00
a86cd969a3 Properly handle rate limits in the scanner 2025-11-04 09:48:21 +01:00
bc6c93c9c7 Fix bearer cookie being base64ed 2025-11-04 09:48:21 +01:00
572ddc69ad Fix video controller permissions 2025-11-04 09:48:21 +01:00
c1b243df9c Fix image downloading error handling 2025-11-04 09:48:21 +01:00
31500dc3c5 Use an api key for the scanner 2025-11-04 09:48:21 +01:00
509e7c08cd Switch transcoder to pgx 2025-11-04 09:48:21 +01:00
165d9e8f31 Update .env.example 2025-11-04 09:48:21 +01:00
04171af3e3 Require core.play to play videos in gocoder 2025-11-04 09:48:21 +01:00
f1ddc7e7b9 Hardcode gocoder db schema 2025-11-04 09:48:21 +01:00
5827cc32e8 Hard code keibi postgres schema 2025-11-04 09:48:21 +01:00
4dc34641ec Handle cookies in keibi 2025-11-04 09:48:21 +01:00
Antoine
14c8f25499 fix(front): small fixes (#1134) 2025-11-03 18:37:18 +00:00
Antoine
d98ac3b452 fix(api): don't crash if not superuser (#1133) 2025-11-03 12:34:30 +01:00
3e7b27342c chore(deps): update actions/upload-artifact action to v5 (#1138) 2025-11-03 09:32:57 +01:00
dd9e611bef chore(deps): update dependency node to v24 (#1139) 2025-11-03 09:32:48 +01:00
renovate[bot]
0905e4424e chore(deps): update dependency node to v24 2025-11-03 01:54:30 +00:00
renovate[bot]
64c43a7833 chore(deps): update actions/upload-artifact action to v5 2025-11-03 01:54:26 +00:00
648a03e3ea Translations update from Hosted Weblate (#1136) 2025-11-02 22:40:29 +01:00
Максим Горпиніч
eb6887d189 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (211 of 211 strings)

Translation: Kyoo/Kyoo
Translate-URL: https://hosted.weblate.org/projects/kyoo/kyoo/uk/
2025-11-02 18:51:11 +00:00
acelinkio
c6133d85ff fix: auth's publish location (#1131) 2025-11-02 12:22:07 +01:00
Antoine
8a1b90f035 fix(front): update initial state api url (#1132) 2025-11-02 12:21:44 +01:00
renovate[bot]
8348e185fc chore(deps): update dependency @biomejs/biome to v2.3.2 (#1128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 14:06:12 +01:00
renovate[bot]
1d78b26a37 fix(deps): update aws-sdk-go-v2 monorepo (#1129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 09:20:39 +01:00
a8b2e575d5 Ignore front in renovate (#1127) 2025-10-27 10:57:03 +01:00
c132afe163 Delete v4 backend + docker fixes (#1116) 2025-10-26 22:55:56 +01:00
ebad8f32a4 Fix details page query 2025-10-26 22:32:40 +01:00
fb74ed30f8 Fix dockerfile of api 2025-10-26 22:31:40 +01:00
a0be0555e0 Update api packages 2025-10-26 22:31:40 +01:00
002b9e4b35 Add front dockerfile 2025-10-26 22:31:40 +01:00
996ad52159 Update docker compose 2025-10-26 11:20:51 +01:00
3b8c21c20f Misc cleanups 2025-10-26 11:20:51 +01:00
b3b58f7a1e Delete old back 2025-10-26 11:20:51 +01:00
7739908a19 Add back info page (#1115) 2025-10-26 09:48:38 +00:00
a3f29c73ec Player rewrite (#1020) 2025-10-24 16:33:49 +02:00
5fdc96db64 Format stuff 2025-10-24 16:18:35 +02:00
9206a30182 Fix player on mobile 2025-10-24 12:33:31 +02:00
1d81981c3f Fix expo install with bun 1.3 2025-10-22 13:41:37 +02:00
a15f28e541 Update bun-types in api 2025-10-22 09:18:44 +02:00
23832929e9 Add libass support 2025-10-22 09:17:56 +02:00
3590963206 Add subtitle conversion (from srt to vtt for example) 2025-10-22 09:17:54 +02:00
5ca1ae938f wip: Add subtitles handling 2025-10-22 09:16:45 +02:00
dfdeca35f3 Add keyboard bindings for player 2025-10-22 09:16:45 +02:00
70ff2285d5 Add video & quality selector 2025-10-22 09:16:45 +02:00
c5f237771c Add audio track selector 2025-10-22 09:16:45 +02:00
8fea8b1fe7 Add player error handling and hls fallback 2025-10-22 09:16:45 +02:00
816ee8de14 Web player fixes & fullscreen 2025-10-22 09:16:45 +02:00
a7d5f94dfb Allow clientId to be specified in query params 2025-10-22 09:16:45 +02:00
b7c6ba1e85 Fix subtitle extraction race condition 2025-10-22 09:16:45 +02:00
e348464261 Use my build of rnv to fix events 2025-10-22 09:16:45 +02:00
5031cc7163 Fix package versions 2025-10-22 09:16:45 +02:00
ebfb486363 Fix select for web 2025-10-22 09:16:44 +02:00
6cb1ae5fa1 Use on-failure instead of unless-stopped in dev docker compose 2025-10-22 09:16:44 +02:00
ad41c09055 Fix some typescript issues 2025-10-22 09:16:44 +02:00
5b320974df Update packages 2025-10-22 09:16:44 +02:00
e01c67b1ef Fix player controls style 2025-10-22 09:16:44 +02:00
da5823deb2 Fix /videos/:id?with=show date formatting 2025-10-22 09:16:44 +02:00
7085a68733 fixup! Add mime_codec to subtitles in the transcoder 2025-10-22 09:16:44 +02:00
84a855602e Implement middle controls 2025-10-22 09:16:44 +02:00
57779f02b1 Keep controls open in menues or hover 2025-10-22 09:16:44 +02:00
becc550add Implement bottom controls 2025-10-22 09:16:44 +02:00
9343bb524c Remake touch controls 2025-10-22 09:16:44 +02:00
fc9695a2dc Rewrite player's controls compenents 2025-10-22 09:16:44 +02:00
a310ceaed5 Implement player's enEnd & loading indicator 2025-10-22 09:16:44 +02:00
4d8806fc7f Make history's time non nullable (it was never null anyways) 2025-10-22 09:16:44 +02:00
d70b45d1fd Add external subtitles handling 2025-10-22 09:16:44 +02:00
5eb067639b Add mime_codec to subtitles in the transcoder 2025-10-22 09:16:44 +02:00
c364d3a67e Fix rnv startup 2025-10-22 09:16:44 +02:00
666ad9279f Fix dockerignore of auth & transcoder 2025-10-22 09:16:44 +02:00
02c77d2f32 Use rnv v7 for the rework 2025-10-22 09:16:44 +02:00
ebbed77650 Add /videos/:id/direct & /videos/:id/master.m3u8 routes 2025-10-22 09:16:44 +02:00
578dc4bbc9 Delete old models package 2025-10-22 09:16:44 +02:00
e3ae961b68 Add progress to /videos/:id 2025-10-22 09:16:44 +02:00
c33ba01e54 Add with=show to /videos/:id 2025-10-22 09:16:44 +02:00
787bfc1151 Type video in the front 2025-10-22 09:16:44 +02:00
f12718d67f Cleanup video info type 2025-10-22 09:16:43 +02:00
df3e0d1ed7 Remove some transcoder's deprecated fields 2025-10-22 09:16:43 +02:00
8ffed25580 Add /api/videos/:id/info route to redirect to the transcoder 2025-10-22 09:16:43 +02:00
a0739e57f2 Init player rework 2025-10-22 09:16:43 +02:00
renovate[bot]
da79d235be fix(deps): update aws-sdk-go-v2 monorepo (#1105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 20:36:43 +00:00
renovate[bot]
ae20682c92 chore(deps): update aws-sdk-net monorepo (#1110)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 09:42:34 +02:00
renovate[bot]
99d4a21c12 chore(deps): update dependency system.componentmodel.composition to 9.0.10 (#1111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 09:42:28 +02:00
renovate[bot]
17040e9775 chore(deps): update dotnet monorepo to 8.0.21 (#1112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 09:42:22 +02:00
renovate[bot]
f92f93f960 chore(deps): update helm release postgresql to v18.0.15 (#1113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 09:42:15 +02:00
renovate[bot]
1eb832f00d chore(deps): update actions/setup-node action to v6 (#1114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 09:42:09 +02:00
renovate[bot]
b14ada8f48 chore(deps): update dependency @biomejs/biome to v2.2.6 (#1103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-19 17:04:13 +02:00
renovate[bot]
f5f7a187c4 chore(deps): update dependency go to v1.25.3 (#1104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 11:32:20 +02:00
renovate[bot]
938f7ca047 fix(deps): update module golang.org/x/text to v0.30.0 (#1109)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 22:53:09 +02:00
renovate[bot]
b0c1df2827 fix(deps): update module github.com/go-playground/validator/v10 to v10.28.0 (#1106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 09:29:16 +02:00
renovate[bot]
be0b5d1523 chore(deps): update helm release postgresql to v18 (#1107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 09:29:09 +02:00
renovate[bot]
fbee956876 chore(deps): update aws-sdk-net monorepo (#1102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 09:30:43 +02:00
renovate[bot]
db1c1a5e92 chore(deps): update aws-sdk-net monorepo (#1099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 09:32:48 +02:00
renovate[bot]
b4f9552048 chore(deps): update dependency nswag.aspnetcore to 14.6.1 (#1100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 09:32:42 +02:00
renovate[bot]
1a5b93e5aa fix(deps): update aws-sdk-go-v2 monorepo (#1101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 09:32:34 +02:00
acelinkio
216a58af24 Add runtime class in transcoder chart (#1093) 2025-09-30 07:24:41 -07:00
renovate[bot]
eff7a32b09 chore(deps): update aws-sdk-net monorepo (#1094)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 10:03:15 +02:00
renovate[bot]
bd041c1a4e chore(deps): update dependency meilisearch to 0.17.1 (#1095)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:37:47 +02:00
renovate[bot]
48876494f7 chore(deps): update skiasharp monorepo to 3.119.1 (#1096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:37:36 +02:00
renovate[bot]
e4d92ff364 fix(deps): update aws-sdk-go-v2 monorepo (#1097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:37:28 +02:00
renovate[bot]
d35ab8c894 chore(deps): update dependency nswag.aspnetcore to 14.6.0 (#1098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:37:21 +02:00
579 changed files with 13427 additions and 51917 deletions

View File

@@ -10,6 +10,8 @@ LIBRARY_ROOT=./video
# You should set this to a path where kyoo can write large amount of data, this is used as a cache by the transcoder.
# It will automatically be cleaned up on kyoo's startup/shutdown/runtime.
CACHE_ROOT=/tmp/kyoo_cache
# Where to store downloaded images of the shows
IMAGES_PATH="./images";
# A pattern (regex) to ignore files.
LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*"
@@ -21,8 +23,8 @@ GOCODER_PRESET=fast
# Keep those empty to use kyoo's default api key. You can also specify a custom API key if you want.
# go to https://www.themoviedb.org/settings/api and copy the api key (not the read access token, the api key)
THEMOVIEDB_APIKEY=
# go to https://www.themoviedb.org/settings/api and copy the read access token (not the api key)
THEMOVIEDB_API_ACCESS_TOKEN=""
# go to https://thetvdb.com/api-information/signup and copy the api key
TVDB_APIKEY=
# you can also input your subscriber's pin to support TVDB
@@ -32,40 +34,48 @@ TVDB_PIN=
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
PUBLIC_URL=http://localhost:8901
# Use a builtin oidc service (google, discord, trakt, or simkl):
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
OIDC_DISCORD_CLIENTID=
OIDC_DISCORD_SECRET=
# Or add your custom one:
OIDC_SERVICE_NAME=YourPrettyName
OIDC_SERVICE_LOGO=https://url-of-your-logo.com
OIDC_SERVICE_CLIENTID=
OIDC_SERVICE_SECRET=
OIDC_SERVICE_AUTHORIZATION=https://url-of-the-authorization-endpoint-of-the-oidc-service.com/auth
OIDC_SERVICE_TOKEN=https://url-of-the-token-endpoint-of-the-oidc-service.com/token
OIDC_SERVICE_PROFILE=https://url-of-the-profile-endpoint-of-the-oidc-service.com/userinfo
OIDC_SERVICE_SCOPE="the list of scopes space separeted like email identity"
# Token authentication method as seen in https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
# Supported values: ClientSecretBasic (default) or ClientSecretPost
# If in doubt, leave this empty.
OIDC_SERVICE_AUTHMETHOD=ClientSecretBasic
# on the previous list, service is the internal name of your service, you can add as many as you want.
# Default permissions of new users. They are able to browse & play videos.
# Set `verified` to true if you don't wanna manually verify users.
EXTRA_CLAIMS='{"permissions": ["core.read", "core.play"], "verified": false}'
# This is the permissions of the first user (aka the first user is admin)
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete", "apikeys.read", "apikeys.write", "core.read", "core.write", "core.play", "scanner.trigger"], "verified": true}'
# Guest (meaning unlogged in users) can be:
# unauthorized (they need to connect before doing anything)
# GUEST_CLAIMS=""
# able to browse & see what you have but not able to play
GUEST_CLAIMS='{"permissions": ["core.read"], "verified": true}'
# or have browse & play permissions
GUEST_CLAIMS='{"permissions": ["core.read", "core.play"], "verified": true}'
# DO NOT change this.
PROTECTED_CLAIMS="permissions,verified"
# Following options are optional and only useful for debugging.
# You can create apikeys at runtime via POST /keys but you can also have some defined in the env.
# Replace $YOURNAME with the name of the key you want (only alpha are valid)
# The value will be the apikey (max 128 bytes)
KEIBI_APIKEY_SCANNER=EJqUB8robwKwLNt37SuHqdcsNGrtwpfYxeExfiAbokpxZVd4WctWr7gnSZ
KEIBI_APIKEY_SCANNER_CLAIMS='{"permissions": ["core.read", "core.write"]}'
# To debug the front end, you can set the following to an external backend
KYOO_URL=
# Database things
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
# It is recommended to use the below PG environment variables when possible.
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
# The behavior of the below variables match what is documented here:
# https://www.postgresql.org/docs/current/libpq-envars.html
PGUSER=kyoo
PGPASSWORD=password
PGDATABASE=kyoo
PGHOST=postgres
PGPORT=5432
# v5 stuff, does absolutely nothing on master (aka: you can delete this)
EXTRA_CLAIMS='{"permissions": ["core.read"], "verified": false}'
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write", "scanner.trigger"], "verified": true}'
GUEST_CLAIMS='{"permissions": ["core.read"]}'
PROTECTED_CLAIMS="permissions,verified"
# PGOPTIONS=-c search_path=kyoo,public
# PGPASSFILE=/my/password # Takes precedence over PGPASSWORD. New line characters are not trimmed.
# PGSSLMODE=verify-full
# PGSSLROOTCERT=/my/serving.crt
# PGSSLCERT=/my/client.crt
# PGSSLKEY=/my/client.key=password

View File

@@ -1,3 +0,0 @@
7e6e56a366babe17e7891a5897180efbf93c00c5
a5638203a6ecb9f372a5a61e1c8fd443bf3a17fe
18e301f26acd7f2e97eac26c5f48377fa13956f5

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*.Designer.cs linguist-generated=true

View File

@@ -15,17 +15,18 @@ jobs:
postgres:
image: postgres:15
ports:
- "5432:5432"
- "5432:5432"
env:
POSTGRES_USER: kyoo
POSTGRES_PASSWORD: password
POSTGRES_DB: kyoo_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
@@ -37,3 +38,4 @@ jobs:
run: bun test
env:
PGHOST: localhost
IMAGES_PATH: ./images

View File

@@ -25,7 +25,7 @@ jobs:
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: gacts/install-hurl@v1

View File

@@ -2,20 +2,6 @@ name: Coding Style
on: [pull_request, workflow_dispatch]
jobs:
back:
name: "Lint Back"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./back
steps:
- uses: actions/checkout@v5
- name: Check coding style
run: |
dotnet tool restore
dotnet csharpier . --check
api:
name: "Lint api"
runs-on: ubuntu-latest
@@ -23,7 +9,7 @@ jobs:
run:
working-directory: ./api
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Biome
uses: biomejs/setup-biome@v2
@@ -40,7 +26,7 @@ jobs:
run:
working-directory: ./front
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Biome
uses: biomejs/setup-biome@v2
@@ -54,7 +40,7 @@ jobs:
name: "Lint scanner/autosync"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: chartboost/ruff-action@v1
with:
@@ -67,7 +53,7 @@ jobs:
run:
working-directory: ./transcoder
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Run go fmt
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
@@ -79,7 +65,7 @@ jobs:
run:
working-directory: ./auth
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Run go fmt
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi

View File

@@ -19,16 +19,6 @@ jobs:
fail-fast: false
matrix:
include:
- context: ./back
dockerfile: Dockerfile
label: back
image: ${{ github.repository_owner }}/kyoo_back
- context: ./back
dockerfile: Dockerfile.migrations
label: migrations
image: ${{ github.repository_owner }}/kyoo_migrations
- context: ./api
dockerfile: Dockerfile
label: api
@@ -57,12 +47,12 @@ jobs:
- context: ./auth
dockerfile: Dockerfile
label: auth
image: ${{ github.repository_owner }}/keibi
image: ${{ github.repository_owner }}/kyoo_auth
env:
DOCKERHUB_ENABLED: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_PASSWORD && 'true' || 'false' }}
name: Build ${{matrix.label}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter

View File

@@ -2,41 +2,53 @@ name: Release Helm Chart
on:
push:
tags:
- v*
- v*
workflow_dispatch:
inputs:
channel:
description: 'Release channel (master, edge, or leave blank for tag-based)'
required: false
default: 'master'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Update Helm Dependencies
run: |
helm dependency update ./chart
- name: Update Helm Dependencies
run: helm dependency update ./chart
- name: Package Helm Chart
run: |
export tag=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
helm package ./chart --version $tag --app-version $tag
- name: Build Helm-safe repo name
run: |
REPO_NAME="$(echo "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/helm-charts" | tr '[:upper:]' '[:lower:]')"
echo "REPO_NAME=${REPO_NAME}" >> "${GITHUB_ENV}"
- name: Determine Chart Version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.channel }}"
else
TAG=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
fi
echo "TAG=$TAG" >> "${GITHUB_ENV}"
echo "Using chart version: $TAG"
- name: Push Helm Chart to GHCR
run: |
helm push kyoo-*.tgz "${REPO_NAME}"
- name: Package Helm Chart
run: helm package ./chart --version $TAG --app-version $TAG
- name: Build Helm-safe repo name
run: |
REPO_NAME="$(echo "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/helm-charts" | tr '[:upper:]' '[:lower:]')"
echo "REPO_NAME=${REPO_NAME}" >> "${GITHUB_ENV}"
- name: Push Helm Chart to GHCR
run: helm push kyoo-*.tgz "${REPO_NAME}"

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Helm
uses: azure/setup-helm@v4

View File

@@ -13,7 +13,7 @@ jobs:
working-directory: ./front
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# This is required because GHA doesn't support secrets in the `if` condition
- name: Check if Expo build is enabled
@@ -27,10 +27,10 @@ jobs:
echo "Expo build is disabled for forks. To enable it, add an EXPO_TOKEN secret to this repository. See https://docs.expo.dev/eas-update/github-actions/ for more information."
- name: Setup Node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
if: env.IS_EXPO_ENABLED == 'true'
with:
node-version: 22.x
node-version: 24.x
cache: yarn
cache-dependency-path: front/yarn.lock
@@ -62,7 +62,7 @@ jobs:
if: env.IS_EXPO_ENABLED == 'true'
run: wget -O kyoo.apk ${{ steps.url.outputs.assetUrl }}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
if: env.IS_EXPO_ENABLED == 'true'
with:
name: kyoo.apk

View File

@@ -13,7 +13,7 @@ jobs:
working-directory: ./front
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# This is required because GHA doesn't support secrets in the `if` condition
- name: Check if Expo build is enabled
@@ -27,10 +27,10 @@ jobs:
echo "Expo build is disabled for forks. To enable it, add an EXPO_TOKEN secret to this repository. See https://docs.expo.dev/eas-update/github-actions/ for more information."
- name: Setup Node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
if: env.IS_EXPO_ENABLED == 'true'
with:
node-version: 22.x
node-version: 24.x
cache: yarn
cache-dependency-path: front/yarn.lock

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set correct versions
run: |

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/video
.devspace/
.env
.venv
.idea

View File

@@ -1,4 +0,0 @@
# Authors
Ordered by the date of the first commit.
* Zoe Roux ([@zoriya](http://github.com/zoriya))

View File

@@ -5,27 +5,26 @@ These diagrams are created with Mermaid and rendered locally. For the best expe
Kyoo is a monorepo that consists of several projects each in their own directory. Diagram below shows an outline of kyoo, projects, and artifacts.
```mermaid
block-beta
block
columns 1
block:proj1:1
proj_name["Kyoo"]:1
end
block:proj2:1
dir_1["autosync/"]
dir_2["back/"]
dir_1["api/"]
dir_2["auth/"]
dir_3["front/"]
dir_4["transcoder/"]
dir_5["scanner/"]
end
block:proj3:1
%% columns auto (default)
block:autosync_b:1
autosync_i1("kyoo_autosync")
block:api_b:1
autosync_i1("kyoo_api")
end
block:back_b:1
block:auth_b:1
columns 1
back_i1("kyoo_back")
back_i2("kyoo_migrations")
back_i1("kyoo_auth")
end
block:front_b:1
front_i1("kyoo_front")
@@ -36,7 +35,6 @@ block-beta
block:scanner_b:1
columns 1
scanner_i1("kyoo_scanner")
scanner_i2("kyoo_scanner*")
end
end
@@ -51,19 +49,17 @@ block-beta
style dir_4 fill:#438dd5,stroke-width:0px
style dir_5 fill:#438dd5,stroke-width:0px
style autosync_b fill:#438dd5,stroke-width:0px
style back_b fill:#438dd5,stroke-width:0px
style api_b fill:#438dd5,stroke-width:0px
style auth_b fill:#438dd5,stroke-width:0px
style front_b fill:#438dd5,stroke-width:0px
style transcoder_b fill:#438dd5,stroke-width:0px
style scanner_b fill:#438dd5,stroke-width:0px
style autosync_i1 fill:#85bbf0,stroke-width:0px
style back_i1 fill:#85bbf0,stroke-width:0px
style back_i2 fill:#85bbf0,stroke-width:0px
style front_i1 fill:#85bbf0,stroke-width:0px
style transcoder_i1 fill:#85bbf0,stroke-width:0px
style scanner_i1 fill:#85bbf0,stroke-width:0px
style scanner_i2 fill:#85bbf0,stroke-width:0px
```
# C4 Diagrams
@@ -89,212 +85,165 @@ C4Context
```
## Container
Messaging is middleware. EnterpriseMessageBus is for any messaging handled between different projects.
Kyoo leverages the [API Gateway](https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway) approach to microservices and [offloads](https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-offloading) authentication at the gateway.
```mermaid
C4Container
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="3")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
title Container diagram for Kyoo System
Person(user, "User")
System_Boundary(internal, "Kyoo") {
Container(frontend, "front/")
Container(backend, "back/")
ContainerQueue(emb, "emb", "", "EnterpriseMessageBus")
Container(transcoder, "transcoder/")
Container(scanner, "scanner/")
Container(autosync, "autosync/")
}
System_Boundary(external, "") {
System_Ext(content, "ContentDatabase", "")
}
System_Boundary(external2, "") {
System_Ext(tracker, "ActivityTracker", "")
}
System_Boundary(external3, "") {
System_Ext(media, "MediaLibrary", "")
}
Container(apigateway, "API Gateway")
Container(auth, "auth")
Container(transcoder, "transcoder")
Container(scanner, "scanner")
Container(frontend, "front")
System_Ext(media, "MediaLibrary", "")
System_Ext(content, "ContentDatabase", "")
Container(api, "api")
System_Ext(tracker, "ActivityTracker", "")
Rel(user, frontend, "")
Rel(user, backend, "")
Rel(frontend, backend, "")
Rel(backend, emb, "")
Rel(backend, transcoder, "")
Rel_Back(autosync, emb, "")
Rel(autosync, tracker, "")
Rel_Back(scanner, emb, "")
Rel(scanner, backend, "")
Rel(user, apigateway, "")
Rel(apigateway, frontend, "")
Rel(apigateway, scanner, "")
Rel(apigateway, transcoder, "")
Rel(apigateway, api, "")
Rel(apigateway, auth, "")
Rel(frontend, api, "")
Rel(api, tracker, "")
Rel(api, content, "")
Rel(scanner, api, "")
Rel(scanner, media, "")
Rel(scanner, content, "")
Rel(transcoder, media, "")
```
## Component
### Autosync
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="2")
title Component Diagram for Autosync
Container_Boundary(autosync, "autosync") {
Component(autosync_c1, "kyoo_autosync", "python, python3.12", "")
}
Container_Boundary(emb, "emb") {
ComponentQueue(emb_q1, "autosync", "RabbitMQ, Queue", "")
ComponentQueue(emb_e1, "events.watched", "RabbitMQ, Exchange", "")
}
Container_Boundary(tracker, "ActivityTracker") {
Component_Ext(tracker_c1, "TrackerProvider", "API", "simkl")
}
Container_Boundary(backend, "back") {
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
}
#### Auth
Auth microservice is implicitly used by each other microservice for both end user authentication and microservice to microservice communications. Auth microservice will not be directly represented in the other component diagrams. Instead in their relationsihp will specify "auth via middleware".
Rel(emb_e1, emb_q1, "bound")
Rel_Back(autosync_c1, emb_q1, "consumes")
Rel(backend_c2, emb_e1, "produces")
Rel(autosync_c1, tracker_c1, "updates")
```
### Back
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
title Component Diagram for Back
title Auth Component Diagram
Person(user, "User")
Container_Boundary(frontend, "front") {
Component(frontend_c1, "kyoo_front", "typescript, node.js", "Static Content")
Container_Boundary(auth, "auth") {
Component(auth_c1, "kyoo_auth", "Go", "")
ComponentDb(auth_db1, "kelbi", "Postgres", "")
}
Container_Boundary(backend, "back") {
ComponentDb(backend_db2, "search", "Meilisearch", "search resource")
Component(backend_c3, "BackendMetadata", "Volume", "Persistent. Distributed Metadata")
ComponentDb(backend_db1, "backend", "Postgres", "user data and session state")
Component(backend_c1, "kyoo_migrations", "C#, .NET 8.0", "Postgres Migration")
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
}
Container_Boundary(emb, "emb") {
ComponentQueue(emb_e1, "events.watched", "RabbitMQ, Exchange", "")
ComponentQueue(emb_q1, "autosync", "RabbitMQ, Queue", "")
ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
ComponentQueue(emb_e2, "events.resource", "RabbitMQ, Exchange", "unused")
}
Container_Boundary(scanner, "scanner") {
Component(scanner_c2, "kyoo_scanner", "python, python3.12", "matcher")
Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
}
Container_Boundary(autosync, "autosync") {
Component(autosync_c1, "kyoo_autosync", "python, python3.12", "")
}
Container_Boundary(transcoder, "transcoder") {
Component(transcoder_c1, "kyoo_transcoder", "go, go", "Video Transcoder")
}
Rel(user, backend_c2, "")
Rel(backend_c1, backend_db1, "")
Rel(backend_c2, backend_db1, "")
Rel(backend_c2, backend_db2, "")
Rel(backend_c2, transcoder_c1, "")
Rel(backend_c2, backend_c3, "")
Rel(backend_c2, emb_q2, "produces")
Rel(backend_c2, emb_e1, "produces")
Rel(backend_c2, emb_e2, "produces")
Rel(emb_e1, emb_q1, "bound")
Rel_Back(autosync_c1, emb_q1, "consumes")
Rel_Back(scanner_c1, emb_q2, "consumes")
Rel(scanner_c1, backend_c2, "")
Rel(scanner_c2, backend_c2, "")
Rel(frontend_c1, backend_c2, "")
Rel(auth_c1, auth_db1, "")
```
### Front
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="2")
title Component Diagram for Front
Person(user, "User")
Container_Boundary(frontend, "front") {
Component(frontend_c1, "kyoo_front", "typescript, node.js", "Static Content")
}
Container_Boundary(backend, "back") {
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
}
Rel(frontend_c1, backend_c2, "ssr")
Rel(user, frontend_c1, "")
```
### Scanner
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="3")
title Component Diagram for Scanner
Container_Boundary(media, "MediaLibrary") {
Component_Ext(media_c1, "MediaShare", "Volume", "Read Only")
}
Container_Boundary(content, "ContentDatabase") {
Component_Ext(content_c1, "ContentProvider", "API", "tmdb or tvdb")
}
Container_Boundary(scanner, "scanner") {
Component(scanner_c2, "kyoo_scanner", "python, python3.12", "matcher")
ComponentQueue(scanner_q1, "scanner", "RabbitMQ, Queue", "")
Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
}
Container_Boundary(backend, "back") {
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
}
Container_Boundary(emb, "emb") {
ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
}
Rel(scanner_c1, scanner_q1, "produces")
Rel(scanner_c1, media_c1, "watches")
Rel(scanner_c1, backend_c2, "Fetch existing scans")
Rel(scanner_c2, content_c1, "Fetch media data")
Rel(scanner_c2, backend_c2, "Pushes media data")
Rel_Back(scanner_c2, scanner_q1, "consumes")
Rel_Back(scanner_c1, emb_q2, "consumes")
Rel(backend_c2, emb_q2, "produces")
```
### Transcoder
#### Api
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2")
title Component Diagram for Transcoder
title Api Component Diagram
Person(user, "User")
Container_Boundary(api, "api") {
ComponentDb(api_db1, "kyoo", "Postgres", "")
Component(api_c1, "kyoo_api", "TypeScript", "")
Component(api_c2, "ApiMetadata", "S3/Volume", "Persistent. Distributed Metadata")
}
Container_Boundary(scanner, "scanner") {
Component(scanner_c1, "kyoo_scanner", "Python", "")
}
System_Boundary(external, "") {
System_Ext(tracker, "ActivityTracker", "")
System_Ext(content, "ContentDatabase", "")
}
Container_Boundary(front, "front") {
Component(front_c1, "kyoo_front", "TypeScript", "")
}
Rel(user, api_c1, "auth via middleware")
Rel(api_c1, api_db1, "")
Rel(api_c1, api_c2, "")
Rel(api_c1, content, "http(s) <br/> retries media images")
Rel(api_c1, tracker, "")
Rel(scanner_c1, api_c1, "auth via middleware")
Rel(front_c1, api_c1, "http(s) SSR <br/> auth via middleware")
```
#### Front
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
title Front Component Diagram
Person(user, "User")
Container_Boundary(front, "front") {
Component(front_c1, "kyoo_front", "TypeScript", "")
}
Container_Boundary(api, "api") {
Component(api_c1, "kyoo_api", "TypeScript", "")
}
Rel(user, front_c1, "")
Rel(front_c1, api_c1, "http(s) SSR <br/> auth via middleware")
```
#### Transcoder
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")
title Transcoder Component Diagram
Person(user, "User")
Container_Boundary(transcoder, "transcoder") {
Component(transcoder_c2, "TranscodeMetadata", "Volume", "Persistent. Distributed Metadata")
Component(transcoder_c1, "kyoo_transcoder", "go, go", "Video Transcoder")
ComponentDb(transcoder_db1, "gocoder", "Postgres", "")
Component(transcoder_c1, "kyoo_transcoder", "Go", "")
Component(transcoder_c2, "TranscodeMetadata", "S3/Volume", "Persistent. Distributed Metadata")
Component(transcoder_c3, "TranscodeCache", "Volume", "Volatile. Local cache")
}
Container_Boundary(media, "MediaLibrary") {
Component_Ext(media_c1, "MediaShare", "Volume", "Read Only")
}
Container_Boundary(backend, "back") {
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
System_Boundary(external, "") {
System_Ext(media, "MediaLibrary", "")
}
Rel(transcoder_c1, media_c1, "mounts")
Rel(user, transcoder_c1, "auth via middleware")
Rel(transcoder_c1, media, "mounted to filesystem <br/> reads")
Rel(transcoder_c1, transcoder_db1, "")
Rel(transcoder_c1, transcoder_c2, "")
Rel(transcoder_c1, transcoder_c3, "")
Rel(backend_c2, transcoder_c1, "")
```
#### Scanner
```mermaid
C4Component
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
title Scanner Component Diagram
Person(user, "User")
Container_Boundary(api, "api") {
Component(api_c1, "kyoo_api", "TypeScript", "")
}
Container_Boundary(scanner, "scanner") {
Component(scanner_c1, "kyoo_scanner", "Python", "")
ComponentDb(scanner_db1, "scanner", "Postgres", "")
}
System_Boundary(external, "") {
System_Ext(content, "ContentDatabase", "")
System_Ext(media, "MediaLibrary", "")
}
Rel(user, scanner_c1, "http(s) <br/> auth via middleware")
Rel(scanner_c1, api_c1, "http(s) <br/> auth via middleware")
Rel(scanner_c1, scanner_db1, "")
Rel(scanner_c1, content, "http(s) <br/> gathers media metadata")
Rel(scanner_c1, media, "mounted to filesystem <br/> watches")
```

View File

@@ -45,7 +45,7 @@ Then, fill the following environment variables in your `.env` file:
```env
PUBLIC_URL=https://your-kyoo-instance.com
OIDC_GOOGLE_NAME=Google
OIDC_GOOGLE_LOGO=https://logo.clearbit.com/google.com
OIDC_GOOGLE_LOGO=https://www.gstatic.com/marketing-cms/assets/images/d5/dc/cfe9ce8b4425b410b49b7f2dd3f3/g.webp=s200
OIDC_GOOGLE_CLIENTID=<client-id> # the client ID you got from Google
OIDC_GOOGLE_SECRET=<client-secret> # the client secret you got from Google
OIDC_GOOGLE_AUTHORIZATION=https://accounts.google.com/o/oauth2/auth

View File

@@ -39,10 +39,7 @@ Kyoo does not have a plugin system and aim to have every features built-in (see
## 📺 Clients
Kyoo currently supports Web and Android clients, with additional platforms being thought about. Rough estimates:
* Today: Web & Android
* Spring 2025: Chromecast
* Summer 2025: Android-TV
Kyoo currently supports Web and Android clients, with additional platforms being thought about.
Don't see your client? Kyoo is focused on adding features, but welcomes contributors! The frontend is built with React-Native and Expo. Come hang and develop with us on Discord.

View File

@@ -11,6 +11,9 @@ AUTH_SERVER=http://auth:4568
IMAGES_PATH=./images
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
# It is recommended to use the below PG environment variables when possible.
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
# The behavior of the below variables match what is documented here:

View File

@@ -1,4 +1,4 @@
FROM oven/bun AS builder
FROM oven/bun:debian AS builder
WORKDIR /app
COPY package.json bun.lock .
@@ -18,11 +18,13 @@ RUN bun build \
--outfile server \
./src/index.ts
FROM gcr.io/distroless/base
FROM debian
WORKDIR /app
COPY --from=builder /app/server server
COPY --from=builder /app/node_modules/@img /app/node_modules/@img
COPY ./drizzle ./drizzle
ENV NODE_ENV=production
EXPOSE 3567
CMD ["./server"]
CMD ["/app/server"]

View File

@@ -1,150 +1,245 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "api",
"dependencies": {
"@elysiajs/opentelemetry": "^1.4.8",
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
"@kubiks/otel-drizzle": "zoriya/drizzle-otel#build",
"@types/bun": "^1.3.1",
"blurhash": "^2.0.5",
"drizzle-kit": "^0.31.1",
"drizzle-orm": "0.43.1",
"elysia": "^1.3.1",
"jose": "^6.0.11",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "0.44.7",
"elysia": "^1.4.13",
"jose": "^6.1.0",
"node-addon-api": "^8.5.0",
"parjs": "^1.3.9",
"pg": "^8.16.0",
"sharp": "^0.34.2",
"pg": "^8.16.3",
"sharp": "^0.34.4",
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@types/pg": "^8.15.2",
"bun-types": "^1.2.14",
"node-addon-api": "^8.3.1",
"@biomejs/biome": "2.3.7",
"@types/pg": "^8.15.5",
},
},
},
"patchedDependencies": {
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch",
"drizzle-orm@0.44.7": "patches/drizzle-orm@0.44.7.patch",
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
"@biomejs/biome": ["@biomejs/biome@2.3.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.7", "@biomejs/cli-darwin-x64": "2.3.7", "@biomejs/cli-linux-arm64": "2.3.7", "@biomejs/cli-linux-arm64-musl": "2.3.7", "@biomejs/cli-linux-x64": "2.3.7", "@biomejs/cli-linux-x64-musl": "2.3.7", "@biomejs/cli-win32-arm64": "2.3.7", "@biomejs/cli-win32-x64": "2.3.7" }, "bin": { "biome": "bin/biome" } }, "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.8", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-c9unbcdXfehExCv1GsiTCfos5SyIAyDwP7apcMeXmUMBaJZiAYMfiEH8RFFFIfIHJHC/xlNJzUPodkcUaaoJJQ=="],
"@elysiajs/swagger": ["@elysiajs/swagger@github:zoriya/elysia-swagger#f88fbc7", { "dependencies": { "@scalar/themes": "^0.9.81", "@scalar/types": "^0.1.3", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "zoriya-elysia-swagger-f88fbc7"],
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.3", "", { "os": "android", "cpu": "arm" }, "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.3", "", { "os": "android", "cpu": "arm64" }, "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.3", "", { "os": "android", "cpu": "x64" }, "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.3", "", { "os": "linux", "cpu": "arm" }, "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64" }, "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.3", "", { "os": "none", "cpu": "arm64" }, "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.3", "", { "os": "none", "cpu": "x64" }, "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="],
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@kubiks/otel-drizzle": ["@kubiks/otel-drizzle@github:zoriya/drizzle-otel#cc1d59b", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "drizzle-orm": ">=0.28.0" } }, "zoriya-drizzle-otel-cc1d59b"],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="],
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
"@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="],
"@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g=="],
"@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA=="],
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="],
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="],
"@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw=="],
"@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q=="],
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA=="],
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg=="],
"@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg=="],
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
"@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="],
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw=="],
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
@@ -158,45 +253,65 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
"@types/pg": ["@types/pg@8.15.2", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg=="],
"@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"blurhash": ["blurhash@2.0.5", "", {}, "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"char-info": ["char-info@0.3.5", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-gRslEBFEcuLMGLNO1EFIrdN1MMUfO+aqa7y8iWzNyAzB3mYKnTIvP+ioW3jpyeEvqA5WapVLIPINGtFjEIH4cQ=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"drizzle-kit": ["drizzle-kit@0.31.1", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q=="],
"drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
"drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="],
"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
"elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="],
"elysia": ["elysia@1.4.13", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-6QaWQEm7QN1UCo1TPpEjaRJPHUmnM7R29y6LY224frDGk5PrpAnWmdHkoZxkcv+JRWp1j2ROr2IHbxHbG/jRjw=="],
"esbuild": ["esbuild@0.25.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.3", "@esbuild/android-arm": "0.25.3", "@esbuild/android-arm64": "0.25.3", "@esbuild/android-x64": "0.25.3", "@esbuild/darwin-arm64": "0.25.3", "@esbuild/darwin-x64": "0.25.3", "@esbuild/freebsd-arm64": "0.25.3", "@esbuild/freebsd-x64": "0.25.3", "@esbuild/linux-arm": "0.25.3", "@esbuild/linux-arm64": "0.25.3", "@esbuild/linux-ia32": "0.25.3", "@esbuild/linux-loong64": "0.25.3", "@esbuild/linux-mips64el": "0.25.3", "@esbuild/linux-ppc64": "0.25.3", "@esbuild/linux-riscv64": "0.25.3", "@esbuild/linux-s390x": "0.25.3", "@esbuild/linux-x64": "0.25.3", "@esbuild/netbsd-arm64": "0.25.3", "@esbuild/netbsd-x64": "0.25.3", "@esbuild/openbsd-arm64": "0.25.3", "@esbuild/openbsd-x64": "0.25.3", "@esbuild/sunos-x64": "0.25.3", "@esbuild/win32-arm64": "0.25.3", "@esbuild/win32-ia32": "0.25.3", "@esbuild/win32-x64": "0.25.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
@@ -204,59 +319,81 @@
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="],
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-addon-api": ["node-addon-api@8.3.1", "", {}, "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA=="],
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
"node-interval-tree": ["node-interval-tree@1.3.3", "", { "dependencies": { "shallowequal": "^1.0.2" } }, "sha512-K9vk96HdTK5fEipJwxSvIIqwTqr4e3HRJeJrNxBSeVMNSC/JWARRaX7etOLOuTmrRMeOI/K5TCJu3aWIwZiNTw=="],
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"parjs": ["parjs@1.3.9", "", { "dependencies": { "char-info": "0.3.*" } }, "sha512-zmQhbzWM3M391tjwTGvNvvtoT8rRE/bBTjw6+54g8ANaPpnyekDF1d8q5tzN4kxmVud82cNj8zSd+uxSL4LE0A=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
"pg": ["pg@8.16.0", "", { "dependencies": { "pg-connection-string": "^2.9.0", "pg-pool": "^3.10.0", "pg-protocol": "^1.10.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.5" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg=="],
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
"pg-cloudflare": ["pg-cloudflare@1.2.5", "", {}, "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg=="],
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
"pg-connection-string": ["pg-connection-string@2.9.0", "", {}, "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ=="],
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
"pg-pool": ["pg-pool@3.10.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA=="],
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
"pg-protocol": ["pg-protocol@1.10.0", "", {}, "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q=="],
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
@@ -264,9 +401,9 @@
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
"sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="],
"sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -274,27 +411,39 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pg/pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
@@ -339,13 +488,5 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
}
}

23
api/devspace.yaml Normal file
View File

@@ -0,0 +1,23 @@
version: v2beta1
name: api
dev:
api:
imageSelector: ghcr.io/zoriya/kyoo_api
devImage: docker.io/oven/bun:latest
workingDir: /app
sync:
- path: .:/app
excludePaths:
- node_modules
startContainer: true
onUpload:
exec:
- command: bun install --frozen-lockfile
onChange:
- "./bun.lock"
command:
- bash
- -c
- "bun install && bun dev"
ports:
- port: "3567"

View File

@@ -0,0 +1,3 @@
ALTER TABLE "kyoo"."history" ALTER COLUMN "time" SET DEFAULT 0;--> statement-breakpoint
ALTER TABLE "kyoo"."history" ALTER COLUMN "time" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "kyoo"."mqueue" ADD COLUMN "priority" integer DEFAULT 0 NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "kyoo"."seasons" ALTER COLUMN "entries_count" SET DEFAULT 0;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -162,6 +162,20 @@
"when": 1752446736231,
"tag": "0022_seasons-count",
"breakpoints": true
},
{
"idx": 23,
"version": "7",
"when": 1763924097229,
"tag": "0023_mqueue-priority",
"breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1763932730557,
"tag": "0024_fix-season-count",
"breakpoints": true
}
]
}

View File

@@ -9,24 +9,26 @@
"format": "biome check --write ."
},
"dependencies": {
"@elysiajs/opentelemetry": "^1.4.8",
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
"@kubiks/otel-drizzle": "zoriya/drizzle-otel#build",
"@types/bun": "^1.3.1",
"blurhash": "^2.0.5",
"drizzle-kit": "^0.31.1",
"drizzle-orm": "0.43.1",
"elysia": "^1.3.1",
"jose": "^6.0.11",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "0.44.7",
"elysia": "^1.4.13",
"jose": "^6.1.0",
"node-addon-api": "^8.5.0",
"parjs": "^1.3.9",
"pg": "^8.16.0",
"sharp": "^0.34.2"
"pg": "^8.16.3",
"sharp": "^0.34.4"
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@types/pg": "^8.15.2",
"bun-types": "^1.2.14",
"node-addon-api": "^8.3.1"
"@biomejs/biome": "2.3.7",
"@types/pg": "^8.15.5"
},
"module": "src/index.js",
"patchedDependencies": {
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch"
"drizzle-orm@0.44.7": "patches/drizzle-orm@0.44.7.patch"
}
}

View File

@@ -1,8 +1,8 @@
diff --git a/pg-core/dialect.cjs b/pg-core/dialect.cjs
index a0ef03142f21d319376bc50070ff7fdcd4d18132..45fc94e5a7c3fa4c201e636dd227122164e1bd02 100644
index d776a1fb503f35b5e53c6d3c1c086efa8230ea94..86541bf408e4955029c65be59d7b8ec98bb6e914 100644
--- a/pg-core/dialect.cjs
+++ b/pg-core/dialect.cjs
@@ -348,7 +348,14 @@ class PgDialect {
@@ -347,7 +347,14 @@ class PgDialect {
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
const valuesSqlList = [];
const columns = table[import_table2.Table.Symbol.Columns];
@@ -19,30 +19,30 @@ index a0ef03142f21d319376bc50070ff7fdcd4d18132..45fc94e5a7c3fa4c201e636dd2271221
([, column]) => import_sql2.sql.identifier(this.casing.getColumnCasing(column))
);
diff --git a/pg-core/dialect.js b/pg-core/dialect.js
index 120aaed9c3e4ae0a24653893379b98506c866f6f..48df463c0a6d5864fe2c324c8f86432860e50e00 100644
index 74a16c9e86fe3a89ced32af44af0a72f1cf43cf6..08f820d46a040c315fed40b8b36d94d094174425 100644
--- a/pg-core/dialect.js
+++ b/pg-core/dialect.js
@@ -346,7 +346,14 @@ class PgDialect {
@@ -345,7 +345,14 @@ class PgDialect {
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
const valuesSqlList = [];
const columns = table[Table.Symbol.Columns];
- const colEntries = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert());
+ let colEntries = Object.entries(columns);
+ colEntries = select && !is(valuesOrSelect, SQL)
+ ? Object
+ .keys(valuesOrSelect._.selectedFields)
+ .map((key) => [key, columns[key]])
+ : overridingSystemValue_
+ ? colEntries
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
+ let colEntries = Object.entries(columns);
+ colEntries = select && !is(valuesOrSelect, SQL)
+ ? Object
+ .keys(valuesOrSelect._.selectedFields)
+ .map((key) => [key, columns[key]])
+ : overridingSystemValue_
+ ? colEntries
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
const insertOrder = colEntries.map(
([, column]) => sql.identifier(this.casing.getColumnCasing(column))
);
diff --git a/pg-core/query-builders/insert.cjs b/pg-core/query-builders/insert.cjs
index 08bb0d7485ebf997e3f081e2254ea8fd8bc20f65..20c8036374a1f25f7c5880c40e8d3c42c05f3eee 100644
index 22e0a4b4ad7ac64b065fc540416c6ed15ff4336b..fd590a6a3feb48c9f1894a0619b09ca6a5d22a5c 100644
--- a/pg-core/query-builders/insert.cjs
+++ b/pg-core/query-builders/insert.cjs
@@ -75,11 +75,6 @@ class PgInsertBuilder {
@@ -76,11 +76,6 @@ class PgInsertBuilder {
}
select(selectQuery) {
const select = typeof selectQuery === "function" ? selectQuery(new import_query_builder.QueryBuilder()) : selectQuery;
@@ -55,10 +55,10 @@ index 08bb0d7485ebf997e3f081e2254ea8fd8bc20f65..20c8036374a1f25f7c5880c40e8d3c42
}
}
diff --git a/pg-core/query-builders/insert.js b/pg-core/query-builders/insert.js
index 0fc8eeb80f4a5512f6c84f3d596832623a33b748..998e2ab0bfe3f322bf268a01f71ebd06c57d4d07 100644
index 60a8bb0d1c22b890bd8fbf4c85d5df41ca42444c..8754d0f2923f905816016c42f339c3e9097b4128 100644
--- a/pg-core/query-builders/insert.js
+++ b/pg-core/query-builders/insert.js
@@ -51,11 +51,6 @@ class PgInsertBuilder {
@@ -52,11 +52,6 @@ class PgInsertBuilder {
}
select(selectQuery) {
const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery;

View File

@@ -4,7 +4,7 @@ pkgs.mkShell {
bun
biome
# for psql to debug from the cli
postgresql_15
postgresql_18
# to build libvips (for sharp)
nodejs
node-gyp

View File

@@ -73,7 +73,7 @@ export const auth = new Elysia({ name: "auth" })
.macro({
permissions(perms: string[]) {
return {
beforeHandle: ({ jwt, status }) => {
beforeHandle: function permissionCheck({ jwt, status }) {
for (const perm of perms) {
if (!jwt!.permissions.includes(perm)) {
return status(403, {

View File

@@ -13,11 +13,23 @@ import { series } from "./controllers/shows/series";
import { showsH } from "./controllers/shows/shows";
import { staffH } from "./controllers/staff";
import { studiosH } from "./controllers/studios";
import { videosH } from "./controllers/videos";
import { videosReadH, videosWriteH } from "./controllers/videos";
import { db } from "./db";
import type { KError } from "./models/error";
import { otel } from "./otel";
export const base = new Elysia({ name: "base" })
.onError(({ code, error }) => {
// sometimes elysia as an unknown code when throwing errors
if (code === "UNKNOWN") {
try {
const details = JSON.parse(error.message);
if (details?.code === "KError") {
const { code, ...ret } = details;
return ret;
}
} catch {}
}
if (code === "VALIDATION") {
const details = JSON.parse(error.message);
if (details.code === "KError") {
@@ -34,30 +46,54 @@ export const base = new Elysia({ name: "base" })
details: details,
} as KError;
}
if (code === "INTERNAL_SERVER_ERROR") {
console.error(error);
return {
status: 500,
message: error.message,
details: error,
} as KError;
}
if (code === "NOT_FOUND") {
return error;
}
console.error(code, error);
return error;
return {
status: 500,
message: "message" in error ? (error?.message ?? code) : code,
details: error,
} as KError;
})
.get("/health", () => ({ status: "healthy" }) as const, {
detail: { description: "Check if the api is healthy." },
response: { 200: t.Object({ status: t.Literal("healthy") }) },
})
.as("scoped");
.get(
"/ready",
async ({ status }) => {
try {
await db.execute("select 1");
return { status: "healthy", database: "healthy" } as const;
} catch (e) {
return status(500, {
status: "unhealthy",
database: e,
});
}
},
{
detail: { description: "Check if the api is healthy." },
response: {
200: t.Object({
status: t.Literal("healthy"),
database: t.Literal("healthy"),
}),
500: t.Object({
status: t.Literal("unhealthy"),
database: t.Any(),
}),
},
},
)
.as("global");
export const prefix = "/api";
export const handlers = new Elysia({ prefix })
.use(base)
.use(auth)
.use(otel)
.guard(
{
// Those are not applied for now. See https://github.com/elysiajs/elysia/issues/1139
@@ -84,7 +120,8 @@ export const handlers = new Elysia({ prefix })
.use(imagesH)
.use(watchlistH)
.use(historyH)
.use(nextup),
.use(nextup)
.use(videosReadH),
)
.guard(
{
@@ -98,5 +135,5 @@ export const handlers = new Elysia({ prefix })
// },
permissions: ["core.write"],
},
(app) => app.use(videosH).use(seed),
(app) => app.use(videosWriteH).use(seed),
);

View File

@@ -54,7 +54,7 @@ export const entryProgressQ = db
})
.from(history)
.leftJoin(videos, eq(history.videoPk, videos.pk))
.leftJoin(profiles, eq(history.profilePk, profiles.pk))
.innerJoin(profiles, eq(history.profilePk, profiles.pk))
.where(eq(profiles.id, sql.placeholder("userId")))
.orderBy(history.entryPk, desc(history.playedDate))
.as("progress");
@@ -157,7 +157,7 @@ export const mapProgress = ({ aliased }: { aliased: boolean }) => {
const ret = {
time: coalesce(time, sql<number>`0`),
percent: coalesce(percent, sql<number>`0`),
playedDate: sql`to_char(${playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
playedDate: sql<string>`to_char(${playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
videoId: sql<string>`${videoId}`,
};
if (!aliased) return ret;
@@ -256,7 +256,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
async ({
params: { id },
query: { limit, after, query, sort, filter },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub },
status,
@@ -294,7 +294,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
userId: sub,
})) as Entry[];
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get entries of a serie" },
@@ -338,6 +338,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
params: { id },
query: { limit, after, query, sort, filter },
request: { url },
headers,
jwt: { sub },
status,
}) => {
@@ -373,7 +374,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
userId: sub,
})) as Extra[];
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get extras of a serie" },
@@ -410,6 +411,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
async ({
query: { limit, after, query, filter },
request: { url },
headers,
jwt: { sub },
}) => {
const sort = newsSort;
@@ -427,7 +429,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
userId: sub,
})) as Entry[];
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get new movies/episodes added recently." },

View File

@@ -27,9 +27,9 @@ function getRedirectToImageHandler({ filter }: { filter?: SQL }) {
status,
redirect,
}: {
params: { id: string; image: "poster" | "thumbnail" | "banner" | "logo" };
params: { id?: string; image: "poster" | "thumbnail" | "banner" | "logo" };
headers: { "accept-language": string };
query: { quality: "high" | "medium" | "low" };
query: { quality?: "high" | "medium" | "low" };
set: Context["set"];
status: Context["status"];
redirect: Context["redirect"];
@@ -212,12 +212,9 @@ export const imagesH = new Elysia({ tags: ["images"] })
},
)
.guard({
headers: t.Object(
{
"accept-language": AcceptLanguage(),
},
{ additionalProperties: true },
),
headers: t.Object({
"accept-language": AcceptLanguage(),
}),
})
.get(
"/studios/:id/logo",
@@ -307,6 +304,9 @@ export const imagesH = new Elysia({ tags: ["images"] })
description: "The type of image to retrive.",
}),
}),
headers: t.Object({
"accept-language": AcceptLanguage(),
}),
})
.get(
"/movies/:id/:image",

View File

@@ -67,7 +67,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
"/profiles/me/history",
async ({
query: { sort, filter, query, limit, after },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub },
}) => {
@@ -87,7 +87,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
progressQ: historyProgressQ,
})) as Entry[];
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {
@@ -109,7 +109,11 @@ export const historyH = new Elysia({ tags: ["profiles"] })
async ({
params: { id },
query: { sort, filter, query, limit, after },
headers: { "accept-language": languages, authorization },
headers: {
"accept-language": languages,
authorization,
...headers
},
request: { url },
status,
}) => {
@@ -132,7 +136,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
progressQ: historyProgressQ,
})) as Entry[];
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {

View File

@@ -69,7 +69,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
"/profiles/me/nextup",
async ({
query: { sort, filter, query, limit, after },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub },
}) => {
@@ -124,7 +124,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
.limit(limit)
.execute({ userId: sub });
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {

View File

@@ -142,7 +142,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
"/profiles/me/watchlist",
async ({
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub, settings },
}) => {
@@ -162,7 +162,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
relations: ["nextEntry"],
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all movies/series in your watchlist" },
@@ -195,7 +195,11 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
jwt: { settings },
headers: { "accept-language": languages, authorization },
headers: {
"accept-language": languages,
authorization,
...headers
},
request: { url },
status,
}) => {
@@ -218,7 +222,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
relations: ["nextEntry"],
userId: uInfo.id,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {

View File

@@ -53,7 +53,7 @@ export const seasonsH = new Elysia({ tags: ["series"] })
async ({
params: { id },
query: { limit, after, query, sort, filter },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
status,
}) => {
@@ -110,7 +110,7 @@ export const seasonsH = new Elysia({ tags: ["series"] })
)
.limit(limit);
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get seasons of a serie" },

View File

@@ -1,4 +1,6 @@
import path from "node:path";
import { getCurrentSpan, setAttributes } from "@elysiajs/opentelemetry";
import { SpanStatusCode } from "@opentelemetry/api";
import { encode } from "blurhash";
import { and, eq, is, lt, type SQL, sql } from "drizzle-orm";
import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
@@ -7,13 +9,15 @@ import type { PoolClient } from "pg";
import sharp from "sharp";
import { db, type Transaction } from "~/db";
import { mqueue } from "~/db/schema/mqueue";
import { unnestValues } from "~/db/utils";
import type { Image } from "~/models/utils";
import { record } from "~/otel";
import { getFile } from "~/utils";
export const imageDir = process.env.IMAGES_PATH ?? "./images";
export const imageDir = process.env.IMAGES_PATH ?? "/images";
export const defaultBlurhash = "000000";
type ImageTask = {
export type ImageTask = {
id: string;
url: string;
table: string;
@@ -23,12 +27,12 @@ type ImageTask = {
// this will only push a task to the image downloader service and not download it instantly.
// this is both done to prevent too many requests to be sent at once and to make sure POST
// requests are not blocked by image downloading or blurhash calculation
export const enqueueOptImage = async (
tx: Transaction,
export const enqueueOptImage = (
imgQueue: ImageTask[],
img:
| { url: string | null; column: PgColumn }
| { url: string | null; table: PgTable; column: SQL },
): Promise<Image | null> => {
): Image | null => {
if (!img.url) return null;
const hasher = new Bun.CryptoHasher("sha256");
@@ -64,11 +68,8 @@ export const enqueueOptImage = async (
table: db.dialect.sqlToQuery(sql`${img.column.table}`).sql,
column: sql.identifier(img.column.name).value,
};
await tx.insert(mqueue).values({
kind: "image",
message,
});
await tx.execute(sql`notify kyoo_image`);
imgQueue.push(message);
return {
id,
@@ -77,43 +78,21 @@ export const enqueueOptImage = async (
};
};
export const processImages = async () => {
async function processOne() {
return await db.transaction(async (tx) => {
const [item] = await tx
.select()
.from(mqueue)
.for("update", { skipLocked: true })
.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
.orderBy(mqueue.attempt, mqueue.createdAt)
.limit(1);
if (!item) return false;
const img = item.message as ImageTask;
try {
const blurhash = await downloadImage(img.id, img.url);
const ret: Image = { id: img.id, source: img.url, blurhash };
const table = sql.raw(img.table);
const column = sql.raw(img.column);
await tx.execute(sql`
update ${table} set ${column} = ${ret} where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
`);
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
} catch (err: any) {
console.error("Failed to download image", img.url, err.message);
await tx
.update(mqueue)
.set({ attempt: sql`${mqueue.attempt}+1` })
.where(eq(mqueue.id, item.id));
}
return true;
});
}
export const flushImageQueue = record(
"enqueueImages",
async (tx: Transaction, imgQueue: ImageTask[], priority: number) => {
if (!imgQueue.length) return;
await tx.insert(mqueue).select(
unnestValues(
imgQueue.map((x) => ({ kind: "image", message: x, priority })),
mqueue,
),
);
await tx.execute(sql`notify kyoo_image`);
},
);
export const processImages = record("processImages", async () => {
let running = false;
async function processAll() {
if (running) return;
@@ -136,7 +115,50 @@ export const processImages = async () => {
// start processing old tasks
await processAll();
return () => client.release(true);
};
});
const processOne = record("download", async () => {
return await db.transaction(async (tx) => {
const [item] = await tx
.select()
.from(mqueue)
.for("update", { skipLocked: true })
.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
.orderBy(mqueue.priority, mqueue.attempt, mqueue.createdAt)
.limit(1);
if (!item) return false;
const img = item.message as ImageTask;
setAttributes({ "item.url": img.url });
try {
const blurhash = await downloadImage(img.id, img.url);
const ret: Image = { id: img.id, source: img.url, blurhash };
const table = sql.raw(img.table);
const column = sql.raw(img.column);
await tx.execute(sql`
update ${table} set ${column} = ${ret}
where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
`);
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
} catch (err: any) {
const span = getCurrentSpan();
if (span) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR });
}
console.error("Failed to download image", img.url, err.message);
await tx
.update(mqueue)
.set({ attempt: sql`${mqueue.attempt}+1` })
.where(eq(mqueue.id, item.id));
}
return true;
});
});
async function downloadImage(id: string, url: string): Promise<string> {
const low = await getFile(path.join(imageDir, `${id}.low.jpg`))

View File

@@ -5,79 +5,89 @@ import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie";
import type { SeedSerie } from "~/models/serie";
import { enqueueOptImage } from "../images";
import { record } from "~/otel";
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
type ShowTrans = typeof showTranslations.$inferInsert;
export const insertCollection = async (
collection: SeedCollection | undefined,
show: (({ kind: "movie" } & SeedMovie) | ({ kind: "serie" } & SeedSerie)) & {
nextRefresh: string;
export const insertCollection = record(
"insertCollection",
async (
collection: SeedCollection | undefined,
show: (
| ({ kind: "movie" } & SeedMovie)
| ({ kind: "serie" } & SeedSerie)
) & {
nextRefresh: string;
},
) => {
if (!collection) return null;
const { translations, ...col } = collection;
return await db.transaction(async (tx) => {
const imgQueue: ImageTask[] = [];
const [ret] = await tx
.insert(shows)
.values({
kind: "collection",
status: "unknown",
startAir: show.kind === "movie" ? show.airDate : show.startAir,
endAir: show.kind === "movie" ? show.airDate : show.endAir,
nextRefresh: show.nextRefresh,
entriesCount: 0,
original: {} as any,
...col,
})
.onConflictDoUpdate({
target: shows.slug,
set: {
...conflictUpdateAllExcept(shows, [
"pk",
"id",
"slug",
"createdAt",
"startAir",
"endAir",
]),
startAir: sql`least(${shows.startAir}, excluded.start_air)`,
endAir: sql`greatest(${shows.endAir}, excluded.end_air)`,
},
})
.returning({ pk: shows.pk, id: shows.id, slug: shows.slug });
const trans: ShowTrans[] = Object.entries(translations).map(
([lang, tr]) => ({
pk: ret.pk,
language: lang,
...tr,
poster: enqueueOptImage(imgQueue, {
url: tr.poster,
column: showTranslations.poster,
}),
thumbnail: enqueueOptImage(imgQueue, {
url: tr.thumbnail,
column: showTranslations.thumbnail,
}),
logo: enqueueOptImage(imgQueue, {
url: tr.logo,
column: showTranslations.logo,
}),
banner: enqueueOptImage(imgQueue, {
url: tr.banner,
column: showTranslations.banner,
}),
}),
);
await flushImageQueue(tx, imgQueue, 100);
// we can't unnest values here because show translations contains arrays.
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
return ret;
});
},
) => {
if (!collection) return null;
const { translations, ...col } = collection;
return await db.transaction(async (tx) => {
const [ret] = await tx
.insert(shows)
.values({
kind: "collection",
status: "unknown",
startAir: show.kind === "movie" ? show.airDate : show.startAir,
endAir: show.kind === "movie" ? show.airDate : show.endAir,
nextRefresh: show.nextRefresh,
entriesCount: 0,
original: {} as any,
...col,
})
.onConflictDoUpdate({
target: shows.slug,
set: {
...conflictUpdateAllExcept(shows, [
"pk",
"id",
"slug",
"createdAt",
"startAir",
"endAir",
]),
startAir: sql`least(${shows.startAir}, excluded.start_air)`,
endAir: sql`greatest(${shows.endAir}, excluded.end_air)`,
},
})
.returning({ pk: shows.pk, id: shows.id, slug: shows.slug });
const trans: ShowTrans[] = await Promise.all(
Object.entries(translations).map(async ([lang, tr]) => ({
pk: ret.pk,
language: lang,
...tr,
poster: await enqueueOptImage(tx, {
url: tr.poster,
column: showTranslations.poster,
}),
thumbnail: await enqueueOptImage(tx, {
url: tr.thumbnail,
column: showTranslations.thumbnail,
}),
logo: await enqueueOptImage(tx, {
url: tr.logo,
column: showTranslations.logo,
}),
banner: await enqueueOptImage(tx, {
url: tr.banner,
column: showTranslations.banner,
}),
})),
);
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
return ret;
});
};
);

View File

@@ -6,9 +6,10 @@ import {
entryVideoJoin,
videos,
} from "~/db/schema";
import { conflictUpdateAllExcept, values } from "~/db/utils";
import { conflictUpdateAllExcept, unnest, unnestValues } from "~/db/utils";
import type { SeedEntry as SEntry, SeedExtra as SExtra } from "~/models/entry";
import { enqueueOptImage } from "../images";
import { record } from "~/otel";
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
import { guessNextRefresh } from "../refresh";
import { updateAvailableCount, updateAvailableSince } from "./shows";
@@ -42,22 +43,24 @@ const generateSlug = (
}
};
export const insertEntries = async (
show: { pk: number; slug: string; kind: "movie" | "serie" | "collection" },
items: (SeedEntry | SeedExtra)[],
onlyExtras = false,
) => {
if (!items.length) return [];
export const insertEntries = record(
"insertEntries",
async (
show: { pk: number; slug: string; kind: "movie" | "serie" | "collection" },
items: (SeedEntry | SeedExtra)[],
onlyExtras = false,
) => {
if (!items.length) return [];
const retEntries = await db.transaction(async (tx) => {
const vals: EntryI[] = await Promise.all(
items.map(async (seed) => {
const retEntries = await db.transaction(async (tx) => {
const imgQueue: ImageTask[] = [];
const vals: EntryI[] = items.map((seed) => {
const { translations, videos, video, ...entry } = seed;
return {
...entry,
showPk: show.pk,
slug: generateSlug(show.slug, seed),
thumbnail: await enqueueOptImage(tx, {
thumbnail: enqueueOptImage(imgQueue, {
url: seed.thumbnail,
column: entries.thumbnail,
}),
@@ -72,136 +75,132 @@ export const insertEntries = async (
? entry.number
: undefined,
};
}),
);
const ret = await tx
.insert(entries)
.values(vals)
.onConflictDoUpdate({
target: entries.slug,
set: conflictUpdateAllExcept(entries, [
"pk",
"showPk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: entries.pk, id: entries.id, slug: entries.slug });
const trans: EntryTransI[] = (
await Promise.all(
items.map(async (seed, i) => {
if (seed.kind === "extra") {
return [
{
pk: ret[i].pk,
// yeah we hardcode the language to extra because if we want to support
// translations one day it won't be awkward
language: "extra",
name: seed.name,
description: null,
poster: undefined,
},
];
}
return await Promise.all(
Object.entries(seed.translations).map(async ([lang, tr]) => ({
// assumes ret is ordered like items.
pk: ret[i].pk,
language: lang,
...tr,
poster:
seed.kind === "movie"
? await enqueueOptImage(tx, {
url: (tr as any).poster,
column: entryTranslations.poster,
})
: undefined,
})),
);
}),
)
).flat();
await tx
.insert(entryTranslations)
.values(trans)
.onConflictDoUpdate({
target: [entryTranslations.pk, entryTranslations.language],
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
});
const ret = await tx
.insert(entries)
.select(unnestValues(vals, entries))
.onConflictDoUpdate({
target: entries.slug,
set: conflictUpdateAllExcept(entries, [
"pk",
"showPk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: entries.pk, id: entries.id, slug: entries.slug });
return ret;
});
const trans: EntryTransI[] = items.flatMap((seed, i) => {
if (seed.kind === "extra") {
return [
{
pk: ret[i].pk,
// yeah we hardcode the language to extra because if we want to support
// translations one day it won't be awkward
language: "extra",
name: seed.name,
description: null,
poster: undefined,
},
];
}
const vids = items.flatMap((seed, i) => {
if (seed.kind === "extra") {
return {
videoId: seed.video,
return Object.entries(seed.translations).map(([lang, tr]) => ({
// assumes ret is ordered like items.
pk: ret[i].pk,
language: lang,
...tr,
poster:
seed.kind === "movie"
? enqueueOptImage(imgQueue, {
url: (tr as any).poster,
column: entryTranslations.poster,
})
: undefined,
}));
});
await flushImageQueue(tx, imgQueue, 0);
await tx
.insert(entryTranslations)
.select(unnestValues(trans, entryTranslations))
.onConflictDoUpdate({
target: [entryTranslations.pk, entryTranslations.language],
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
});
return ret;
});
const vids = items.flatMap((seed, i) => {
if (seed.kind === "extra") {
return {
videoId: seed.video,
entryPk: retEntries[i].pk,
entrySlug: retEntries[i].slug,
needRendering: false,
};
}
if (!seed.videos) return [];
return seed.videos.map((x, j) => ({
videoId: x,
entryPk: retEntries[i].pk,
entrySlug: retEntries[i].slug,
needRendering: false,
};
// The first video should not have a rendering.
needRendering: j !== 0 && seed.videos!.length > 1,
}));
});
if (vids.length === 0) {
// we have not added videos but we need to update the `entriesCount`
if (show.kind === "serie" && !onlyExtras)
await updateAvailableCount(db, [show.pk], true);
return retEntries.map((x) => ({ id: x.id, slug: x.slug, videos: [] }));
}
if (!seed.videos) return [];
return seed.videos.map((x, j) => ({
videoId: x,
entryPk: retEntries[i].pk,
entrySlug: retEntries[i].slug,
// The first video should not have a rendering.
needRendering: j !== 0 && seed.videos!.length > 1,
const retVideos = await db.transaction(async (tx) => {
const ret = await tx
.insert(entryVideoJoin)
.select(
db
.select({
entryPk: sql<number>`vids."entryPk"`.as("entry"),
videoPk: videos.pk,
slug: computeVideoSlug(
sql`vids."entrySlug"`,
sql`vids."needRendering"`,
),
})
.from(
unnest(vids, "vids", {
entryPk: "integer",
entrySlug: "varchar(255)",
needRendering: "boolean",
videoId: "uuid",
}),
)
.innerJoin(videos, eq(videos.id, sql`vids."videoId"`)),
)
.onConflictDoNothing()
.returning({
slug: entryVideoJoin.slug,
entryPk: entryVideoJoin.entryPk,
});
if (!onlyExtras)
await updateAvailableCount(tx, [show.pk], show.kind === "serie");
await updateAvailableSince(tx, [...new Set(vids.map((x) => x.entryPk))]);
return ret;
});
return retEntries.map((entry) => ({
id: entry.id,
slug: entry.slug,
videos: retVideos.filter((x) => x.entryPk === entry.pk),
}));
});
if (vids.length === 0) {
// we have not added videos but we need to update the `entriesCount`
if (show.kind === "serie" && !onlyExtras)
await updateAvailableCount(db, [show.pk], true);
return retEntries.map((x) => ({ id: x.id, slug: x.slug, videos: [] }));
}
const retVideos = await db.transaction(async (tx) => {
const ret = await tx
.insert(entryVideoJoin)
.select(
db
.select({
entryPk: sql<number>`vids.entryPk`.as("entry"),
videoPk: videos.pk,
slug: computeVideoSlug(
sql`vids.entrySlug`,
sql`vids.needRendering`,
),
})
.from(
values(vids, {
entryPk: "integer",
needRendering: "boolean",
videoId: "uuid",
}).as("vids"),
)
.innerJoin(videos, eq(videos.id, sql`vids.videoId`)),
)
.onConflictDoNothing()
.returning({
slug: entryVideoJoin.slug,
entryPk: entryVideoJoin.entryPk,
});
if (!onlyExtras)
await updateAvailableCount(tx, [show.pk], show.kind === "serie");
await updateAvailableSince(tx, [...new Set(vids.map((x) => x.entryPk))]);
return ret;
});
return retEntries.map((entry) => ({
id: entry.id,
slug: entry.slug,
videos: retVideos.filter((x) => x.entryPk === entry.pk),
}));
};
},
);
export function computeVideoSlug(entrySlug: SQL | Column, needsRendering: SQL) {
return sql<string>`

View File

@@ -1,82 +1,78 @@
import { db } from "~/db";
import { seasons, seasonTranslations } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import { conflictUpdateAllExcept, unnestValues } from "~/db/utils";
import type { SeedSeason } from "~/models/season";
import { enqueueOptImage } from "../images";
import { record } from "~/otel";
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
import { guessNextRefresh } from "../refresh";
type SeasonI = typeof seasons.$inferInsert;
type SeasonTransI = typeof seasonTranslations.$inferInsert;
export const insertSeasons = async (
show: { pk: number; slug: string },
items: SeedSeason[],
) => {
if (!items.length) return [];
export const insertSeasons = record(
"insertSeasons",
async (show: { pk: number; slug: string }, items: SeedSeason[]) => {
if (!items.length) return [];
return db.transaction(async (tx) => {
const vals: SeasonI[] = items.map((x) => {
const { translations, ...season } = x;
return {
...season,
showPk: show.pk,
slug:
season.seasonNumber === 0
? `${show.slug}-specials`
: `${show.slug}-s${season.seasonNumber}`,
nextRefresh: guessNextRefresh(season.startAir ?? new Date()),
};
});
const ret = await tx
.insert(seasons)
.values(vals)
.onConflictDoUpdate({
target: seasons.slug,
set: conflictUpdateAllExcept(seasons, [
"pk",
"showPk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: seasons.pk, id: seasons.id, slug: seasons.slug });
const trans: SeasonTransI[] = (
await Promise.all(
items.map(
async (seed, i) =>
await Promise.all(
Object.entries(seed.translations).map(async ([lang, tr]) => ({
// assumes ret is ordered like items.
pk: ret[i].pk,
language: lang,
...tr,
poster: await enqueueOptImage(tx, {
url: tr.poster,
column: seasonTranslations.poster,
}),
thumbnail: await enqueueOptImage(tx, {
url: tr.thumbnail,
column: seasonTranslations.thumbnail,
}),
banner: await enqueueOptImage(tx, {
url: tr.banner,
column: seasonTranslations.banner,
}),
})),
),
),
)
).flat();
await tx
.insert(seasonTranslations)
.values(trans)
.onConflictDoUpdate({
target: [seasonTranslations.pk, seasonTranslations.language],
set: conflictUpdateAllExcept(seasonTranslations, ["pk", "language"]),
return db.transaction(async (tx) => {
const imgQueue: ImageTask[] = [];
const vals: SeasonI[] = items.map((x) => {
const { translations, ...season } = x;
return {
...season,
showPk: show.pk,
slug:
season.seasonNumber === 0
? `${show.slug}-specials`
: `${show.slug}-s${season.seasonNumber}`,
nextRefresh: guessNextRefresh(season.startAir ?? new Date()),
};
});
const ret = await tx
.insert(seasons)
.select(unnestValues(vals, seasons))
.onConflictDoUpdate({
target: seasons.slug,
set: conflictUpdateAllExcept(seasons, [
"pk",
"showPk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: seasons.pk, id: seasons.id, slug: seasons.slug });
return ret;
});
};
const trans: SeasonTransI[] = items.flatMap((seed, i) =>
Object.entries(seed.translations).map(([lang, tr]) => ({
// assumes ret is ordered like items.
pk: ret[i].pk,
language: lang,
...tr,
poster: enqueueOptImage(imgQueue, {
url: tr.poster,
column: seasonTranslations.poster,
}),
thumbnail: enqueueOptImage(imgQueue, {
url: tr.thumbnail,
column: seasonTranslations.thumbnail,
}),
banner: enqueueOptImage(imgQueue, {
url: tr.banner,
column: seasonTranslations.banner,
}),
})),
);
await flushImageQueue(tx, imgQueue, -10);
await tx
.insert(seasonTranslations)
.select(unnestValues(trans, seasonTranslations))
.onConflictDoUpdate({
target: [seasonTranslations.pk, seasonTranslations.language],
set: conflictUpdateAllExcept(seasonTranslations, ["pk", "language"]),
});
return ret;
});
},
);

View File

@@ -21,86 +21,93 @@ import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie";
import type { SeedSerie } from "~/models/serie";
import type { Original } from "~/models/utils";
import { record } from "~/otel";
import { getYear } from "~/utils";
import { enqueueOptImage } from "../images";
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
type Show = typeof shows.$inferInsert;
type ShowTrans = typeof showTranslations.$inferInsert;
export const insertShow = async (
show: Omit<Show, "original">,
original: Original & {
poster: string | null;
thumbnail: string | null;
banner: string | null;
logo: string | null;
},
translations:
| SeedMovie["translations"]
| SeedSerie["translations"]
| SeedCollection["translations"],
) => {
return await db.transaction(async (tx) => {
const orig = {
...original,
poster: await enqueueOptImage(tx, {
url: original.poster,
table: shows,
column: sql`${shows.original}['poster']`,
}),
thumbnail: await enqueueOptImage(tx, {
url: original.thumbnail,
table: shows,
column: sql`${shows.original}['thumbnail']`,
}),
banner: await enqueueOptImage(tx, {
url: original.banner,
table: shows,
column: sql`${shows.original}['banner']`,
}),
logo: await enqueueOptImage(tx, {
url: original.logo,
table: shows,
column: sql`${shows.original}['logo']`,
}),
};
const ret = await insertBaseShow(tx, { ...show, original: orig });
if ("status" in ret) return ret;
export const insertShow = record(
"insertShow",
async (
show: Omit<Show, "original">,
original: Original & {
poster: string | null;
thumbnail: string | null;
banner: string | null;
logo: string | null;
},
translations:
| SeedMovie["translations"]
| SeedSerie["translations"]
| SeedCollection["translations"],
) => {
return await db.transaction(async (tx) => {
const imgQueue: ImageTask[] = [];
const orig = {
...original,
poster: enqueueOptImage(imgQueue, {
url: original.poster,
table: shows,
column: sql`${shows.original}['poster']`,
}),
thumbnail: enqueueOptImage(imgQueue, {
url: original.thumbnail,
table: shows,
column: sql`${shows.original}['thumbnail']`,
}),
banner: enqueueOptImage(imgQueue, {
url: original.banner,
table: shows,
column: sql`${shows.original}['banner']`,
}),
logo: enqueueOptImage(imgQueue, {
url: original.logo,
table: shows,
column: sql`${shows.original}['logo']`,
}),
};
const ret = await insertBaseShow(tx, { ...show, original: orig });
if ("status" in ret) return ret;
const trans: ShowTrans[] = await Promise.all(
Object.entries(translations).map(async ([lang, tr]) => ({
pk: ret.pk,
language: lang,
...tr,
latinName: tr.latinName ?? null,
poster: await enqueueOptImage(tx, {
url: tr.poster,
column: showTranslations.poster,
const trans: ShowTrans[] = Object.entries(translations).map(
([lang, tr]) => ({
pk: ret.pk,
language: lang,
...tr,
latinName: tr.latinName ?? null,
poster: enqueueOptImage(imgQueue, {
url: tr.poster,
column: showTranslations.poster,
}),
thumbnail: enqueueOptImage(imgQueue, {
url: tr.thumbnail,
column: showTranslations.thumbnail,
}),
logo: enqueueOptImage(imgQueue, {
url: tr.logo,
column: showTranslations.logo,
}),
banner: enqueueOptImage(imgQueue, {
url: tr.banner,
column: showTranslations.banner,
}),
}),
thumbnail: await enqueueOptImage(tx, {
url: tr.thumbnail,
column: showTranslations.thumbnail,
}),
logo: await enqueueOptImage(tx, {
url: tr.logo,
column: showTranslations.logo,
}),
banner: await enqueueOptImage(tx, {
url: tr.banner,
column: showTranslations.banner,
}),
})),
);
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
return ret;
});
};
);
await flushImageQueue(tx, imgQueue, 200);
// we can't unnest values here because show translations contains arrays.
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
return ret;
});
},
);
async function insertBaseShow(tx: Transaction, show: Show) {
function insert() {

View File

@@ -1,58 +1,63 @@
import { eq, sql } from "drizzle-orm";
import { db } from "~/db";
import { roles, staff } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import { conflictUpdateAllExcept, unnestValues } from "~/db/utils";
import type { SeedStaff } from "~/models/staff";
import { enqueueOptImage } from "../images";
import { record } from "~/otel";
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
export const insertStaff = async (
seed: SeedStaff[] | undefined,
showPk: number,
) => {
if (!seed?.length) return [];
export const insertStaff = record(
"insertStaff",
async (seed: SeedStaff[] | undefined, showPk: number) => {
if (!seed?.length) return [];
return await db.transaction(async (tx) => {
const people = await Promise.all(
seed.map(async (x) => ({
return await db.transaction(async (tx) => {
const imgQueue: ImageTask[] = [];
const people = seed.map((x) => ({
...x.staff,
image: await enqueueOptImage(tx, {
image: enqueueOptImage(imgQueue, {
url: x.staff.image,
column: staff.image,
}),
})),
);
const ret = await tx
.insert(staff)
.values(people)
.onConflictDoUpdate({
target: staff.slug,
set: conflictUpdateAllExcept(staff, ["pk", "id", "slug", "createdAt"]),
})
.returning({ pk: staff.pk, id: staff.id, slug: staff.slug });
}));
const ret = await tx
.insert(staff)
.select(unnestValues(people, staff))
.onConflictDoUpdate({
target: staff.slug,
set: conflictUpdateAllExcept(staff, [
"pk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: staff.pk, id: staff.id, slug: staff.slug });
const rval = await Promise.all(
seed.map(async (x, i) => ({
const rval = seed.map((x, i) => ({
showPk,
staffPk: ret[i].pk,
kind: x.kind,
order: i,
character: {
...x.character,
image: await enqueueOptImage(tx, {
image: enqueueOptImage(imgQueue, {
url: x.character.image,
table: roles,
column: sql`${roles.character}['image']`,
}),
},
})),
);
}));
// always replace all roles. this is because:
// - we want `order` to stay in sync (& without duplicates)
// - we don't have ways to identify a role so we can't onConflict
await tx.delete(roles).where(eq(roles.showPk, showPk));
await tx.insert(roles).values(rval);
await flushImageQueue(tx, imgQueue, -200);
return ret;
});
};
// always replace all roles. this is because:
// - we want `order` to stay in sync (& without duplicates)
// - we don't have ways to identify a role so we can't onConflict
await tx.delete(roles).where(eq(roles.showPk, showPk));
await tx.insert(roles).select(unnestValues(rval, roles));
return ret;
});
},
);

View File

@@ -1,68 +1,74 @@
import { sql } from "drizzle-orm";
import { db } from "~/db";
import { showStudioJoin, studios, studioTranslations } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import { conflictUpdateAllExcept, sqlarr, unnestValues } from "~/db/utils";
import type { SeedStudio } from "~/models/studio";
import { enqueueOptImage } from "../images";
import { record } from "~/otel";
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
type StudioI = typeof studios.$inferInsert;
type StudioTransI = typeof studioTranslations.$inferInsert;
export const insertStudios = async (
seed: SeedStudio[] | undefined,
showPk: number,
) => {
if (!seed?.length) return [];
export const insertStudios = record(
"insertStudios",
async (seed: SeedStudio[] | undefined, showPk: number) => {
if (!seed?.length) return [];
return await db.transaction(async (tx) => {
const vals: StudioI[] = seed.map((x) => {
const { translations, ...item } = x;
return item;
});
const ret = await tx
.insert(studios)
.values(vals)
.onConflictDoUpdate({
target: studios.slug,
set: conflictUpdateAllExcept(studios, [
"pk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: studios.pk, id: studios.id, slug: studios.slug });
const trans: StudioTransI[] = (
await Promise.all(
seed.map(
async (x, i) =>
await Promise.all(
Object.entries(x.translations).map(async ([lang, tr]) => ({
pk: ret[i].pk,
language: lang,
name: tr.name,
logo: await enqueueOptImage(tx, {
url: tr.logo,
column: studioTranslations.logo,
}),
})),
),
),
)
).flat();
await tx
.insert(studioTranslations)
.values(trans)
.onConflictDoUpdate({
target: [studioTranslations.pk, studioTranslations.language],
set: conflictUpdateAllExcept(studioTranslations, ["pk", "language"]),
return await db.transaction(async (tx) => {
const vals: StudioI[] = seed.map((x) => {
const { translations, ...item } = x;
return item;
});
await tx
.insert(showStudioJoin)
.values(ret.map((studio) => ({ showPk: showPk, studioPk: studio.pk })))
.onConflictDoNothing();
return ret;
});
};
const ret = await tx
.insert(studios)
.select(unnestValues(vals, studios))
.onConflictDoUpdate({
target: studios.slug,
set: conflictUpdateAllExcept(studios, [
"pk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: studios.pk, id: studios.id, slug: studios.slug });
const imgQueue: ImageTask[] = [];
const trans: StudioTransI[] = seed.flatMap((x, i) =>
Object.entries(x.translations).map(([lang, tr]) => ({
pk: ret[i].pk,
language: lang,
name: tr.name,
logo: enqueueOptImage(imgQueue, {
url: tr.logo,
column: studioTranslations.logo,
}),
})),
);
await flushImageQueue(tx, imgQueue, -100);
await tx
.insert(studioTranslations)
.select(unnestValues(trans, studioTranslations))
.onConflictDoUpdate({
target: [studioTranslations.pk, studioTranslations.language],
set: conflictUpdateAllExcept(studioTranslations, ["pk", "language"]),
});
await tx
.insert(showStudioJoin)
.select(
db
.select({
showPk: sql`${showPk}`.as("showPk"),
studioPk: sql`v."studioPk"`.as("studioPk"),
})
.from(
sql`unnest(${sqlarr(ret.map((x) => x.pk))}::integer[]) as v("studioPk")`,
),
)
.onConflictDoNothing();
return ret;
});
},
);

View File

@@ -143,7 +143,7 @@ export const collections = new Elysia({
"",
async ({
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
}) => {
@@ -158,7 +158,7 @@ export const collections = new Elysia({
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all collections" },
@@ -227,7 +227,7 @@ export const collections = new Elysia({
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
status,
@@ -265,7 +265,7 @@ export const collections = new Elysia({
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all movies in a collection" },
@@ -284,7 +284,7 @@ export const collections = new Elysia({
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
status,
@@ -322,7 +322,7 @@ export const collections = new Elysia({
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all series in a collection" },
@@ -341,7 +341,7 @@ export const collections = new Elysia({
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
status,
@@ -375,7 +375,7 @@ export const collections = new Elysia({
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all series & movies in a collection" },

View File

@@ -133,7 +133,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
"",
async ({
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub, settings },
}) => {
@@ -148,7 +148,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all movies" },

View File

@@ -136,7 +136,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
"",
async ({
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub, settings },
}) => {
@@ -151,7 +151,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all series" },

View File

@@ -63,7 +63,7 @@ export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] })
preferOriginal,
ignoreInCollection,
},
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub, settings },
}) => {
@@ -81,7 +81,7 @@ export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] })
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all movies/series/collections" },

View File

@@ -189,7 +189,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub, settings },
status,
@@ -227,7 +227,6 @@ export const staffH = new Elysia({ tags: ["staff"] })
.from(watchlist)
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
.where(and(eq(profiles.id, sub), eq(watchlist.showPk, shows.pk)))
.limit(1)
.as("watchstatus");
const items = await db
@@ -270,7 +269,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
roles.showPk,
)
.limit(limit);
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {
@@ -317,7 +316,11 @@ export const staffH = new Elysia({ tags: ["staff"] })
)
.get(
"/staff",
async ({ query: { limit, after, sort, query }, request: { url } }) => {
async ({
query: { limit, after, sort, query },
request: { url },
headers,
}) => {
const items = await db
.select()
.from(staff)
@@ -334,7 +337,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
staff.pk,
)
.limit(limit);
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {
@@ -363,6 +366,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
params: { id },
query: { limit, after, query, sort, filter },
request: { url },
headers,
status,
}) => {
const [movie] = await db
@@ -390,7 +394,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
sort,
filter: and(eq(roles.showPk, movie.pk), filter),
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {
@@ -430,6 +434,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
params: { id },
query: { limit, after, query, sort, filter },
request: { url },
headers,
status,
}) => {
const [serie] = await db
@@ -457,7 +462,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
sort,
filter: and(eq(roles.showPk, serie.pk), filter),
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: {

View File

@@ -228,7 +228,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
"",
async ({
query: { limit, after, query, sort },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
request: { url },
}) => {
const langs = processLanguages(languages);
@@ -239,7 +239,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
sort,
languages: langs,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all studios" },
@@ -302,7 +302,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
status,
@@ -344,7 +344,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all series & movies made by a studio." },
@@ -363,7 +363,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
status,
@@ -406,7 +406,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all movies made by a studio." },
@@ -425,7 +425,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
headers: { "accept-language": languages, ...headers },
jwt: { sub, settings },
request: { url },
status,
@@ -468,7 +468,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
preferOriginal: preferOriginal ?? settings.preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
return createPage(items, { url, sort, limit, headers });
},
{
detail: { description: "Get all series made by a studio." },

View File

@@ -7,6 +7,7 @@ import {
lt,
max,
min,
ne,
notExists,
or,
sql,
@@ -15,7 +16,16 @@ import { alias } from "drizzle-orm/pg-core";
import { Elysia, t } from "elysia";
import { auth } from "~/auth";
import { db, type Transaction } from "~/db";
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
import {
entries,
entryVideoJoin,
history,
profiles,
shows,
showTranslations,
videos,
} from "~/db/schema";
import { watchlist } from "~/db/schema/watchlist";
import {
coalesce,
conflictUpdateAllExcept,
@@ -25,15 +35,20 @@ import {
jsonbBuildObject,
jsonbObjectAgg,
sqlarr,
values,
unnest,
unnestValues,
} from "~/db/utils";
import { Entry } from "~/models/entry";
import { KError } from "~/models/error";
import { bubbleVideo } from "~/models/examples";
import { Progress } from "~/models/history";
import { Movie, type MovieStatus } from "~/models/movie";
import { Serie } from "~/models/serie";
import {
AcceptLanguage,
buildRelations,
createPage,
type Image,
isUuid,
keysetPaginate,
Page,
@@ -44,6 +59,7 @@ import {
} from "~/models/utils";
import { desc as description } from "~/models/utils/descriptions";
import { Guess, Guesses, SeedVideo, Video } from "~/models/video";
import type { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist";
import { comment } from "~/utils";
import {
entryProgressQ,
@@ -86,10 +102,23 @@ async function linkVideos(
.innerJoin(shows, eq(entries.showPk, shows.pk))
.as("entriesQ");
const hasRenderingQ = tx
.select()
.from(entryVideoJoin)
.where(eq(entryVideoJoin.entryPk, entriesQ.pk));
const renderVid = alias(videos, "renderVid");
const hasRenderingQ = or(
gt(
sql`dense_rank() over (partition by ${entriesQ.pk} order by ${videos.rendering})`,
1,
),
sql`exists(${tx
.select()
.from(entryVideoJoin)
.innerJoin(renderVid, eq(renderVid.pk, entryVideoJoin.videoPk))
.where(
and(
eq(entryVideoJoin.entryPk, entriesQ.pk),
ne(renderVid.rendering, videos.rendering),
),
)})`,
)!;
const ret = await tx
.insert(entryVideoJoin)
@@ -98,13 +127,13 @@ async function linkVideos(
.selectDistinctOn([entriesQ.pk, videos.pk], {
entryPk: entriesQ.pk,
videoPk: videos.pk,
slug: computeVideoSlug(entriesQ.slug, sql`exists(${hasRenderingQ})`),
slug: computeVideoSlug(entriesQ.slug, hasRenderingQ),
})
.from(
values(links, {
unnest(links, "j", {
video: "integer",
entry: "jsonb",
}).as("j"),
}),
)
.innerJoin(videos, eq(videos.pk, sql`j.video`))
.innerJoin(
@@ -206,14 +235,44 @@ const videoRelations = {
slugs: () => {
return db
.select({
slugs: coalesce(jsonbAgg(entryVideoJoin.slug), sql`'[]'::jsonb`).as(
"slugs",
),
slugs: coalesce<string[]>(
jsonbAgg(entryVideoJoin.slug),
sql`'[]'::jsonb`,
).as("slugs"),
})
.from(entryVideoJoin)
.where(eq(entryVideoJoin.videoPk, videos.pk))
.as("slugs");
},
progress: () => {
const query = db
.select({
json: jsonbBuildObject<Progress>({
percent: history.percent,
time: history.time,
playedDate: sql`to_char(${history.playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
videoId: videos.id,
}),
})
.from(history)
.innerJoin(profiles, eq(history.profilePk, profiles.pk))
.where(
and(
eq(profiles.id, sql.placeholder("userId")),
eq(history.videoPk, videos.pk),
),
)
.orderBy(desc(history.playedDate))
.limit(1);
return sql`
(
select coalesce(
${query},
'{"percent": 0, "time": 0, "playedDate": null, "videoId": null}'::jsonb
)
as "progress"
)` as any;
},
entries: ({ languages }: { languages: string[] }) => {
const transQ = getEntryTransQ(languages);
@@ -229,6 +288,7 @@ const videoRelations = {
progress: mapProgress({ aliased: false }),
createdAt: sql`to_char(${entries.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
updatedAt: sql`to_char(${entries.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
availableSince: sql`to_char(${entries.availableSince}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
}),
),
sql`'[]'::jsonb`,
@@ -242,6 +302,74 @@ const videoRelations = {
.where(eq(entryVideoJoin.videoPk, videos.pk))
.as("entries");
},
show: ({
languages,
preferOriginal,
}: {
languages: string[];
preferOriginal: boolean;
}) => {
const transQ = db
.selectDistinctOn([showTranslations.pk])
.from(showTranslations)
.orderBy(
showTranslations.pk,
sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
)
.as("t");
const watchStatusQ = db
.select({
watchStatus: jsonbBuildObject<MovieWatchStatus & SerieWatchStatus>({
...getColumns(watchlist),
percent: watchlist.seenCount,
}).as("watchStatus"),
})
.from(watchlist)
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
.where(
and(
eq(profiles.id, sql.placeholder("userId")),
eq(watchlist.showPk, shows.pk),
),
);
return db
.select({
json: jsonbBuildObject<Serie | Movie>({
...getColumns(shows),
...getColumns(transQ),
// movie columns (status is only a typescript hint)
status: sql<MovieStatus>`${shows.status}`,
airDate: shows.startAir,
kind: sql<any>`${shows.kind}`,
isAvailable: sql<boolean>`${shows.availableCount} != 0`,
createdAt: sql`to_char(${shows.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
updatedAt: sql`to_char(${shows.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
...(preferOriginal && {
poster: sql<Image>`coalesce(nullif(${shows.original}->'poster', 'null'::jsonb), ${transQ.poster})`,
thumbnail: sql<Image>`coalesce(nullif(${shows.original}->'thumbnail', 'null'::jsonb), ${transQ.thumbnail})`,
banner: sql<Image>`coalesce(nullif(${shows.original}->'banner', 'null'::jsonb), ${transQ.banner})`,
logo: sql<Image>`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`,
}),
watchStatus: sql`${watchStatusQ}`,
}).as("json"),
})
.from(shows)
.innerJoin(transQ, eq(shows.pk, transQ.pk))
.where(
eq(
shows.pk,
db
.select({ pk: entries.showPk })
.from(entries)
.innerJoin(entryVideoJoin, eq(entryVideoJoin.entryPk, entries.pk))
.where(eq(videos.pk, entryVideoJoin.videoPk)),
),
)
.as("show");
},
previous: ({ languages }: { languages: string[] }) => {
return getNextVideoEntry({ languages, prev: true });
},
@@ -263,7 +391,7 @@ function getNextVideoEntry({
const evj = alias(entryVideoJoin, `evj_${prev ? "prev" : "next"}`);
return db
.select({
json: jsonbBuildObject<Entry>({
json: jsonbBuildObject<{ video: string; entry: Entry }>({
video: entryVideoJoin.slug,
entry: {
...getColumns(entries),
@@ -274,7 +402,7 @@ function getNextVideoEntry({
createdAt: sql`to_char(${entries.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
updatedAt: sql`to_char(${entries.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
},
}),
}).as("json"),
})
.from(entries)
.innerJoin(transQ, eq(entries.pk, transQ.pk))
@@ -326,10 +454,9 @@ function getNextVideoEntry({
.as("next");
}
export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
export const videosReadH = new Elysia({ prefix: "/videos", tags: ["videos"] })
.model({
video: Video,
"created-videos": t.Array(CreatedVideo),
error: t.Object({}),
})
.use(auth)
@@ -337,9 +464,9 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
":id",
async ({
params: { id },
query: { with: relations },
query: { with: relations, preferOriginal },
headers: { "accept-language": langs },
jwt: { sub },
jwt: { sub, settings },
status,
}) => {
const languages = processLanguages(langs);
@@ -351,10 +478,11 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
.select({
...getColumns(videos),
...buildRelations(
["slugs", "entries", ...relations],
["slugs", "progress", "entries", ...relations],
videoRelations,
{
languages,
preferOriginal: preferOriginal ?? settings.preferOriginal,
},
),
})
@@ -369,7 +497,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
message: `No video found with id or slug '${id}'`,
});
}
return video;
return video as any;
},
{
detail: {
@@ -382,10 +510,15 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
}),
}),
query: t.Object({
with: t.Array(t.UnionEnum(["previous", "next"]), {
with: t.Array(t.UnionEnum(["previous", "next", "show"]), {
default: [],
description: "Include related entries in the response.",
}),
preferOriginal: t.Optional(
t.Boolean({
description: description.preferOriginal,
}),
),
}),
headers: t.Object(
{
@@ -400,6 +533,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
slugs: t.Array(
t.String({ format: "slug", examples: ["made-in-abyss-s1e13"] }),
),
progress: Progress,
entries: t.Array(Entry),
previous: t.Optional(
t.Nullable(
@@ -423,6 +557,12 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
}),
),
),
show: t.Optional(
t.Union([
t.Composite([t.Object({ kind: t.Literal("movie") }), Movie]),
t.Composite([t.Object({ kind: t.Literal("serie") }), Serie]),
]),
),
}),
]),
404: {
@@ -433,6 +573,133 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
},
},
)
.get(
":id/info",
async ({ params: { id }, status, redirect }) => {
const [video] = await db
.select({
path: videos.path,
})
.from(videos)
.leftJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
.where(isUuid(id) ? eq(videos.id, id) : eq(entryVideoJoin.slug, id))
.limit(1);
if (!video) {
return status(404, {
status: 404,
message: `No video found with id or slug '${id}'`,
});
}
const path = Buffer.from(video.path, "utf8").toString("base64url");
return redirect(`/video/${path}/info`);
},
{
detail: { description: "Get a video's metadata informations" },
params: t.Object({
id: t.String({
description: "The id or slug of the video to retrieve.",
example: "made-in-abyss-s1e13",
}),
}),
response: {
302: t.Void({
description:
"Redirected to the [/video/{path}/info](?api=transcoder#tag/metadata/get/:path/info) route (of the transcoder)",
}),
404: {
...KError,
description: "No video found with the given id or slug.",
},
},
},
)
.get(
":id/direct",
async ({ params: { id }, status, redirect }) => {
const [video] = await db
.select({
path: videos.path,
})
.from(videos)
.leftJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
.where(isUuid(id) ? eq(videos.id, id) : eq(entryVideoJoin.slug, id))
.limit(1);
if (!video) {
return status(404, {
status: 404,
message: `No video found with id or slug '${id}'`,
});
}
const path = Buffer.from(video.path, "utf8").toString("base64url");
const filename = path.substring(path.lastIndexOf("/") + 1);
return redirect(`/video/${path}/direct/${filename}`);
},
{
detail: {
description: "Get redirected to the direct stream of the video",
},
params: t.Object({
id: t.String({
description: "The id or slug of the video to watch.",
example: "made-in-abyss-s1e13",
}),
}),
response: {
302: t.Void({
description:
"Redirected to the [/video/{path}/direct](?api=transcoder#tag/metadata/get/:path/direct) route (of the transcoder)",
}),
404: {
...KError,
description: "No video found with the given id or slug.",
},
},
},
)
.get(
":id/master.m3u8",
async ({ params: { id }, request, status, redirect }) => {
const [video] = await db
.select({
path: videos.path,
})
.from(videos)
.leftJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
.where(isUuid(id) ? eq(videos.id, id) : eq(entryVideoJoin.slug, id))
.limit(1);
if (!video) {
return status(404, {
status: 404,
message: `No video found with id or slug '${id}'`,
});
}
const path = Buffer.from(video.path, "utf8").toString("base64url");
const query = request.url.substring(request.url.indexOf("?"));
return redirect(`/video/${path}/master.m3u8${query}`);
},
{
detail: { description: "Get redirected to the master.m3u8 of the video" },
params: t.Object({
id: t.String({
description: "The id or slug of the video to watch.",
example: "made-in-abyss-s1e13",
}),
}),
response: {
302: t.Void({
description:
"Redirected to the [/video/{path}/master.m3u8](?api=transcoder#tag/metadata/get/:path/master.m3u8) route (of the transcoder)",
}),
404: {
...KError,
description: "No video found with the given id or slug.",
},
},
},
)
.get(
"",
async () => {
@@ -552,7 +819,15 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
422: KError,
},
},
)
);
export const videosWriteH = new Elysia({ prefix: "/videos", tags: ["videos"] })
.model({
video: Video,
"created-videos": t.Array(CreatedVideo),
error: t.Object({}),
})
.use(auth)
.post(
"",
async ({ body, status }) => {
@@ -561,7 +836,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
try {
vids = await tx
.insert(videos)
.values(body)
.select(unnestValues(body, videos))
.onConflictDoUpdate({
target: [videos.path],
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),

View File

@@ -1,28 +1,32 @@
import os from "node:os";
import path from "node:path";
import tls, { type ConnectionOptions } from "node:tls";
import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate as migrateDb } from "drizzle-orm/node-postgres/migrator";
import type { PoolConfig } from "pg";
import { record } from "~/otel";
import * as schema from "./schema";
async function getPostgresConfig(): Promise<PoolConfig> {
const config: PoolConfig = {
connectionString: process.env.POSTGRES_URL,
host: process.env.PGHOST ?? "postgres",
port: Number(process.env.PGPORT) || 5432,
database: process.env.PGDATABASE ?? "kyoo",
user: process.env.PGUSER ?? "kyoo",
password: process.env.PGPASSWORD ?? "password",
options: process.env.PGOPTIONS,
application_name: process.env.PGAPPNAME ?? "kyoo",
};
const config: PoolConfig = {
connectionString: process.env.POSTGRES_URL,
host: process.env.PGHOST ?? "postgres",
port: Number(process.env.PGPORT) || 5432,
database: process.env.PGDATABASE ?? "kyoo",
user: process.env.PGUSER ?? "kyoo",
password: process.env.PGPASSWORD ?? "password",
options: process.env.PGOPTIONS,
application_name: process.env.PGAPPNAME ?? "kyoo",
};
async function parseSslConfig(): Promise<PoolConfig> {
// Due to an upstream bug, if `ssl` is not falsey, an SSL connection will always be attempted. This means
// that non-SSL connection options under `ssl` (which is incorrectly named) cannot be set unless SSL is enabled.
if (!process.env.PGSSLMODE || process.env.PGSSLMODE === "disable")
if (!process.env.PGSSLMODE || process.env.PGSSLMODE === "disable") {
config.ssl = false;
return config;
}
// Despite this field's name, it is used to configure everything below the application layer.
const ssl: ConnectionOptions = {};
@@ -107,28 +111,52 @@ async function getPostgresConfig(): Promise<PoolConfig> {
return config;
}
const postgresConfig = await getPostgresConfig();
const postgresConfig = await parseSslConfig();
// use this when using drizzle-kit since it can't parse await statements
// const postgresConfig = config;
console.log("Connecting to postgres with config", {
...postgresConfig,
password: postgresConfig.password ? "<redacted>" : undefined,
ssl:
postgresConfig.ssl && typeof postgresConfig.ssl === "object"
? {
...postgresConfig.ssl,
key: "<redacted>",
cert: "<redacted>",
ca: "<redacted>",
}
: postgresConfig.ssl,
});
export const db = drizzle({
schema,
connection: postgresConfig,
casing: "snake_case",
});
instrumentDrizzleClient(db, {
maxQueryTextLength: 100_000_000,
});
export const migrate = async () => {
await db.execute(
sql.raw(`
create extension if not exists pg_trgm;
SET pg_trgm.word_similarity_threshold = 0.4;
ALTER DATABASE "${postgresConfig.database}" SET pg_trgm.word_similarity_threshold = 0.4;
`),
);
export const migrate = record("migrate", async () => {
const APP_SCHEMA = "kyoo";
try {
await db.execute(
sql.raw(`
create schema if not exists ${APP_SCHEMA};
create extension if not exists pg_trgm schema ${APP_SCHEMA};
set pg_trgm.word_similarity_threshold = 0.4;
alter database "${postgresConfig.database}" set pg_trgm.word_similarity_threshold = 0.4;
`),
);
} catch (err: any) {
console.error("Error while updating pg_trgm", err.message);
}
await migrateDb(db, {
migrationsSchema: "kyoo",
migrationsSchema: APP_SCHEMA,
migrationsFolder: "./drizzle",
});
console.log(`Database ${postgresConfig.database} migrated!`);
};
});
export type Transaction =
| typeof db

View File

@@ -12,9 +12,8 @@ import {
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { timestamp } from "../utils";
import { shows } from "./shows";
import { image, language, schema } from "./utils";
import { image, language, schema, timestamp } from "./utils";
import { entryVideoJoin } from "./videos";
export const entryType = schema.enum("entry_type", [

View File

@@ -1,9 +1,8 @@
import { sql } from "drizzle-orm";
import { check, index, integer } from "drizzle-orm/pg-core";
import { timestamp } from "../utils";
import { entries } from "./entries";
import { profiles } from "./profiles";
import { schema } from "./utils";
import { schema, timestamp } from "./utils";
import { videos } from "./videos";
export const history = schema.table(
@@ -18,7 +17,7 @@ export const history = schema.table(
.references(() => entries.pk, { onDelete: "cascade" }),
videoPk: integer().references(() => videos.pk, { onDelete: "set null" }),
percent: integer().notNull().default(0),
time: integer(),
time: integer().notNull().default(0),
playedDate: timestamp({ withTimezone: true, mode: "iso" })
.notNull()
.default(sql`now()`),

View File

@@ -1,7 +1,6 @@
import { sql } from "drizzle-orm";
import { index, integer, jsonb, uuid, varchar } from "drizzle-orm/pg-core";
import { timestamp } from "../utils";
import { schema } from "./utils";
import { schema, timestamp } from "./utils";
export const mqueue = schema.table(
"mqueue",
@@ -9,6 +8,7 @@ export const mqueue = schema.table(
id: uuid().notNull().primaryKey().defaultRandom(),
kind: varchar({ length: 255 }).notNull(),
message: jsonb().notNull(),
priority: integer().notNull().default(0),
attempt: integer().notNull().default(0),
createdAt: timestamp({ withTimezone: true, mode: "iso" })
.notNull()

View File

@@ -10,9 +10,8 @@ import {
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { timestamp } from "../utils";
import { shows } from "./shows";
import { image, language, schema } from "./utils";
import { image, language, schema, timestamp } from "./utils";
export const season_extid = () =>
jsonb()
@@ -40,7 +39,7 @@ export const seasons = schema.table(
startAir: date(),
endAir: date(),
entriesCount: integer().notNull(),
entriesCount: integer().notNull().default(0),
availableCount: integer().notNull().default(0),
externalId: season_extid(),

View File

@@ -13,12 +13,11 @@ import {
varchar,
} from "drizzle-orm/pg-core";
import type { Image, Original } from "~/models/utils";
import { timestamp } from "../utils";
import { entries } from "./entries";
import { seasons } from "./seasons";
import { roles } from "./staff";
import { showStudioJoin } from "./studios";
import { externalid, image, language, schema } from "./utils";
import { externalid, image, language, schema, timestamp } from "./utils";
export const showKind = schema.enum("show_kind", [
"serie",

View File

@@ -8,9 +8,8 @@ import {
varchar,
} from "drizzle-orm/pg-core";
import type { Character } from "~/models/staff";
import { timestamp } from "../utils";
import { shows } from "./shows";
import { externalid, image, schema } from "./utils";
import { externalid, image, schema, timestamp } from "./utils";
export const roleKind = schema.enum("role_kind", [
"actor",

View File

@@ -7,9 +7,8 @@ import {
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { timestamp } from "../utils";
import { shows } from "./shows";
import { externalid, image, language, schema } from "./utils";
import { externalid, image, language, schema, timestamp } from "./utils";
export const studios = schema.table("studios", {
pk: integer().primaryKey().generatedAlwaysAsIdentity(),

View File

@@ -1,4 +1,4 @@
import { jsonb, pgSchema, varchar } from "drizzle-orm/pg-core";
import { customType, jsonb, pgSchema, varchar } from "drizzle-orm/pg-core";
import type { Image } from "~/models/utils";
export const schema = pgSchema("kyoo");
@@ -20,3 +20,19 @@ export const externalid = () =>
>()
.notNull()
.default({});
export const timestamp = customType<{
data: string;
driverData: string;
config: { withTimezone: boolean; precision?: number; mode: "iso" };
}>({
dataType(config) {
const precision = config?.precision ? ` (${config.precision})` : "";
return `timestamp${precision}${config?.withTimezone ? " with time zone" : ""}`;
},
fromDriver(value: string): string {
// postgres format: 2025-06-22 16:13:37.489301+00
// what we want: 2025-06-22T16:13:37Z
return `${value.substring(0, 10)}T${value.substring(11, 19)}Z`;
},
});

View File

@@ -10,9 +10,8 @@ import {
varchar,
} from "drizzle-orm/pg-core";
import type { Guess } from "~/models/video";
import { timestamp } from "../utils";
import { entries } from "./entries";
import { schema } from "./utils";
import { schema, timestamp } from "./utils";
export const videos = schema.table(
"videos",

View File

@@ -1,10 +1,9 @@
import { sql } from "drizzle-orm";
import { check, integer, primaryKey } from "drizzle-orm/pg-core";
import { timestamp } from "../utils";
import { entries } from "./entries";
import { profiles } from "./profiles";
import { shows } from "./shows";
import { schema } from "./utils";
import { schema, timestamp } from "./utils";
export const watchlistStatus = schema.enum("watchlist_status", [
"watching",

View File

@@ -8,15 +8,16 @@ import {
type Subquery,
sql,
Table,
type TableConfig,
View,
ViewBaseConfig,
} from "drizzle-orm";
import type { CasingCache } from "drizzle-orm/casing";
import type { AnyMySqlSelect } from "drizzle-orm/mysql-core";
import {
type AnyPgSelect,
customType,
type SelectedFieldsFlat,
import type {
AnyPgSelect,
PgTableWithColumns,
SelectedFieldsFlat,
} from "drizzle-orm/pg-core";
import type { AnySQLiteSelect } from "drizzle-orm/sqlite-core";
import type { WithSubquery } from "drizzle-orm/subquery";
@@ -73,8 +74,18 @@ export function conflictUpdateAllExcept<
}
// drizzle is bugged and doesn't allow js arrays to be used in raw sql.
export function sqlarr(array: unknown[]) {
return `{${array.map((item) => `"${item}"`).join(",")}}`;
export function sqlarr(array: unknown[]): string {
return `{${array
.map((item) =>
item === "null" || item === null || item === undefined
? "null"
: Array.isArray(item)
? sqlarr(item)
: typeof item === "object"
? `"${JSON.stringify(item).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
: `"${item?.toString().replaceAll('"', '\\"')}"`,
)
.join(", ")}}`;
}
// See https://github.com/drizzle-team/drizzle-orm/issues/4044
@@ -107,7 +118,103 @@ export function values<K extends string>(
};
}
export const coalesce = <T>(val: SQL<T> | Column, def: SQL<T> | Column) => {
/* goal:
* unnestValues([{a: 1, b: 2}, {a: 3, b: 4}], tbl)
*
* ```sql
* select a, b, now() as updated_at from unnest($1::integer[], $2::integer[]);
* ```
* params:
* $1: [1, 2]
* $2: [3, 4]
*
* select
*/
export const unnestValues = <
T extends Record<string, unknown>,
C extends TableConfig = never,
>(
values: T[],
typeInfo: PgTableWithColumns<C>,
) => {
if (values[0] === undefined)
throw new Error("Invalid values, expecting at least one items");
const columns = getTableColumns(typeInfo);
const keys = Object.keys(values[0]).filter((x) => x in columns);
// @ts-expect-error: drizzle internal
const casing = db.dialect.casing as CasingCache;
const dbNames = Object.fromEntries(
Object.entries(columns).map(([k, v]) => [k, casing.getColumnCasing(v)]),
);
const vals = values.reduce(
(acc, cur, i) => {
for (const k of keys) {
if (k in cur) acc[k].push(cur[k]);
else acc[k].push(null);
}
for (const k of Object.keys(cur)) {
if (k in acc) continue;
if (!(k in columns)) continue;
keys.push(k);
acc[k] = new Array(i).fill(null);
acc[k].push(cur[k]);
}
return acc;
},
Object.fromEntries(keys.map((x) => [x, [] as unknown[]])),
);
const computed = Object.entries(columns)
.filter(([k, v]) => (v.defaultFn || v.onUpdateFn) && !keys.includes(k))
.map(([k]) => k);
return db
.select(
Object.fromEntries([
...keys.map((x) => [x, sql.raw(`"${dbNames[x]}"`)]),
...computed.map((x) => [
x,
(columns[x].defaultFn?.() ?? columns[x].onUpdateFn!()).as(dbNames[x]),
]),
]) as {
[k in keyof typeof typeInfo.$inferInsert]-?: SQL.Aliased<
(typeof typeInfo.$inferInsert)[k]
>;
},
)
.from(
sql`unnest(${sql.join(
keys.map(
(k) =>
sql`${sqlarr(vals[k])}${sql.raw(`::${columns[k].getSQLType()}[]`)}`,
),
sql.raw(", "),
)}) as v(${sql.raw(keys.map((x) => `"${dbNames[x]}"`).join(", "))})`,
);
};
export const unnest = <T extends Record<string, unknown>>(
values: T[],
name: string,
typeInfo: Record<keyof T, string>,
) => {
const keys = Object.keys(typeInfo);
const vals = values.reduce(
(acc, cur) => {
for (const k of keys) {
if (k in cur) acc[k].push(cur[k]);
else acc[k].push(null);
}
return acc;
},
Object.fromEntries(keys.map((x) => [x, [] as unknown[]])),
);
return sql`unnest(${sql.join(
keys.map((k) => sql`${sqlarr(vals[k])}${sql.raw(`::${typeInfo[k]}[]`)}`),
sql.raw(", "),
)}) as ${sql.raw(name)}(${sql.raw(keys.map((x) => `"${x}"`).join(", "))})`;
};
export const coalesce = <T>(val: SQL<T> | SQLWrapper, def: SQL<T> | Column) => {
return sql<T>`coalesce(${val}, ${def})`;
};
@@ -148,23 +255,12 @@ export const jsonbBuildObject = <T>(select: JsonFields) => {
};
export const isUniqueConstraint = (e: unknown): boolean => {
if (typeof e !== "object" || !e || !("cause" in e)) return false;
const cause = e.cause;
return (
typeof e === "object" && e != null && "code" in e && e.code === "23505"
typeof cause === "object" &&
cause != null &&
"code" in cause &&
cause.code === "23505"
);
};
export const timestamp = customType<{
data: string;
driverData: string;
config: { withTimezone: boolean; precision?: number; mode: "iso" };
}>({
dataType(config) {
const precision = config?.precision ? ` (${config.precision})` : "";
return `timestamp${precision}${config?.withTimezone ? " with time zone" : ""}`;
},
fromDriver(value: string): string {
// postgres format: 2025-06-22 16:13:37.489301+00
// what we want: 2025-06-22T16:13:37Z
return `${value.substring(0, 10)}T${value.substring(11, 19)}Z`;
},
});

View File

@@ -3,15 +3,13 @@ import { comment } from "~/utils";
export const Progress = t.Object({
percent: t.Integer({ minimum: 0, maximum: 100 }),
time: t.Nullable(
t.Integer({
minimum: 0,
description: comment`
time: t.Integer({
minimum: 0,
description: comment`
When this episode was stopped (in seconds since the start).
This value is null if the entry was never watched or is finished.
`,
}),
),
}),
playedDate: t.Nullable(t.String({ format: "date-time" })),
videoId: t.Nullable(
t.String({

View File

@@ -18,11 +18,31 @@ export const Page = <T extends TSchema>(schema: T, options?: ObjectOptions) =>
export const createPage = <T>(
items: T[],
{ url, sort, limit }: { url: string; sort: Sort; limit: number },
{
url,
sort,
limit,
headers,
}: {
url: string;
sort: Sort;
limit: number;
headers?: Record<string, string | undefined>;
},
) => {
let next: string | null = null;
const uri = new URL(url);
const forwardedProto = headers?.["x-forwarded-proto"];
if (forwardedProto) {
uri.protocol = forwardedProto;
}
const forwardedHost = headers?.["x-forwarded-host"];
if (forwardedHost) {
uri.host = forwardedHost;
}
const current = uri.toString();
let next: string | null = null;
if (sort.random) {
uri.searchParams.set("sort", `random:${sort.random.seed}`);
url = uri.toString();
@@ -35,5 +55,5 @@ export const createPage = <T>(
uri.searchParams.set("after", generateAfter(items[items.length - 1], sort));
next = uri.toString();
}
return { items, this: url, next };
return { items, this: current, next };
};

View File

@@ -55,10 +55,11 @@ export const Sort = (
),
)
.Decode((sort: string[]): Sort => {
if (!Array.isArray(sort)) sort = [sort];
const random = sort.find((x) => x.startsWith("random"));
if (random) {
const seed = random.includes(":")
? Number.parseInt(random.substring("random:".length))
? Number.parseInt(random.substring("random:".length), 10)
: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
return { tablePk, random: { seed }, sort: [] };
}

43
api/src/otel.ts Normal file
View File

@@ -0,0 +1,43 @@
import { record as elysiaRecord, opentelemetry } from "@elysiajs/opentelemetry";
import { OTLPMetricExporter as GrpcMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
import { OTLPMetricExporter as HttpMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
import { OTLPTraceExporter as GrpcTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
import { OTLPTraceExporter as HttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
import Elysia from "elysia";
const protocol =
process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ??
process.env.OTEL_EXPORTER_OTLP_PROTOCOL ??
"http/protobuf";
export const otel = new Elysia()
.use(
opentelemetry({
serviceName: "kyoo.api",
spanProcessors: [
new BatchSpanProcessor(
protocol === "grpc"
? new GrpcTraceExporter()
: new HttpTraceExporter(),
),
],
metricReader: new PeriodicExportingMetricReader({
exporter:
protocol === "grpc"
? new GrpcMetricExporter()
: new HttpMetricExporter(),
}),
}),
)
.as("global");
export function record<T extends (...args: any) => any>(
spanName: string,
fn: T,
): T {
const wrapped = (...args: Parameters<T>) =>
elysiaRecord(spanName, () => fn(...args));
return wrapped as T;
}

View File

@@ -0,0 +1,48 @@
import { buildUrl } from "tests/utils";
import { handlers } from "~/base";
import { getJwtHeaders } from "./jwt";
export const getCollection = async (
id: string,
{
langs,
...query
}: { langs?: string; preferOriginal?: boolean; with?: string[] },
) => {
const resp = await handlers.handle(
new Request(buildUrl(`collections/${id}`, query), {
method: "GET",
headers: langs
? {
"Accept-Language": langs,
...(await getJwtHeaders()),
}
: await getJwtHeaders(),
}),
);
const body = await resp.json();
return [resp, body] as const;
};
export const getCollections = async ({
langs,
...query
}: {
langs?: string;
preferOriginal?: boolean;
with?: string[];
}) => {
const resp = await handlers.handle(
new Request(buildUrl("collections", query), {
method: "GET",
headers: langs
? {
"Accept-Language": langs,
...(await getJwtHeaders()),
}
: await getJwtHeaders(),
}),
);
const body = await resp.json();
return [resp, body] as const;
};

View File

@@ -1,4 +1,5 @@
export * from "~/base";
export * from "./collections-helper";
export * from "./movies-helper";
export * from "./series-helper";
export * from "./shows-helper";

View File

@@ -20,6 +20,7 @@ const [resp, body] = await createVideo([
title: "mia",
episodes: [{ season: 1, episode: 13 }],
from: "test",
history: [],
},
part: null,
path: "/video/mia s1e13.mkv",
@@ -33,6 +34,7 @@ const [resp, body] = await createVideo([
episodes: [{ season: 2, episode: 1 }],
years: [2017],
from: "test",
history: [],
},
part: null,
path: "/video/mia 2017 s2e1.mkv",
@@ -41,7 +43,7 @@ const [resp, body] = await createVideo([
for: [{ slug: `${madeInAbyss.slug}-s2e1` }],
},
{
guess: { title: "bubble", from: "test" },
guess: { title: "bubble", from: "test", history: [] },
part: null,
path: "/video/bubble.mkv",
rendering: "sha5",

View File

@@ -1,19 +1,23 @@
import { describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import { beforeAll, describe, expect, it } from "bun:test";
import { and, eq, sql } from "drizzle-orm";
import { createMovie, createSerie } from "tests/helpers";
import { expectStatus } from "tests/utils";
import { defaultBlurhash, processImages } from "~/controllers/seed/images";
import { db } from "~/db";
import { mqueue, shows, staff, studios, videos } from "~/db/schema";
import { madeInAbyss } from "~/models/examples";
import { createSerie } from "../helpers";
import { dune, madeInAbyss } from "~/models/examples";
describe("images", () => {
it("Create a serie download images", async () => {
beforeAll(async () => {
await db.delete(shows);
await db.delete(studios);
await db.delete(staff);
await db.delete(videos);
await db.delete(mqueue);
});
it("Create a serie download images", async () => {
await db.delete(mqueue);
await createSerie(madeInAbyss);
const release = await processImages();
// remove notifications to prevent other images to be downloaded (do not curl 20000 images for nothing)
@@ -26,4 +30,34 @@ describe("images", () => {
expect(ret!.original.poster!.blurhash).toBeString();
expect(ret!.original.poster!.blurhash).not.toBe(defaultBlurhash);
});
it("Download 404 image", async () => {
await db.delete(mqueue);
const url404 = "https://mockhttp.org/status/404";
const [ret, body] = await createMovie({
...dune,
translations: {
en: {
...dune.translations.en,
poster: url404,
thumbnail: null,
banner: null,
logo: null,
},
},
});
expectStatus(ret, body).toBe(201);
const release = await processImages();
// remove notifications to prevent other images to be downloaded (do not curl 20000 images for nothing)
release();
const failed = await db.query.mqueue.findFirst({
where: and(
eq(mqueue.kind, "image"),
eq(sql`${mqueue.message}->>'url'`, url404),
),
});
expect(failed!.attempt).toBe(5);
});
});

View File

@@ -0,0 +1,106 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { getJwtHeaders } from "tests/helpers/jwt";
import { expectStatus } from "tests/utils";
import { db } from "~/db";
import { shows } from "~/db/schema";
import { bubble } from "~/models/examples";
import { dune1984 } from "~/models/examples/dune-1984";
import { dune } from "~/models/examples/dune-2021";
import { createMovie, getMovies, handlers } from "../helpers";
beforeAll(async () => {
await db.delete(shows);
for (const movie of [bubble, dune1984, dune]) {
const [ret, _] = await createMovie(movie);
expect(ret.status).toBe(201);
}
});
describe("X-Forwarded-Proto header support", () => {
it("Pagination URLs use HTTPS when X-Forwarded-Proto is https", async () => {
const resp = await handlers.handle(
new Request("http://localhost/api/movies?limit=2", {
headers: {
...(await getJwtHeaders()),
"x-forwarded-proto": "https",
},
}),
);
const body = await resp.json();
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
items: expect.any(Array),
this: "https://localhost/api/movies?limit=2",
next: expect.stringContaining("https://localhost/api/movies?limit=2"),
});
});
it("Pagination URLs use HTTP when no X-Forwarded-Proto header", async () => {
const [resp, body] = await getMovies({
limit: 2,
langs: "en",
});
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
items: expect.any(Array),
this: "http://localhost/api/movies?limit=2",
next: expect.stringContaining("http://localhost/api/movies?limit=2"),
});
});
it("X-Forwarded-Host header changes the host in pagination URLs", async () => {
const resp = await handlers.handle(
new Request("http://localhost/api/movies?limit=2", {
headers: {
...(await getJwtHeaders()),
"x-forwarded-proto": "https",
"x-forwarded-host": "kyoo.example.com",
},
}),
);
const body = await resp.json();
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
items: expect.any(Array),
this: "https://kyoo.example.com/api/movies?limit=2",
next: expect.stringContaining(
"https://kyoo.example.com/api/movies?limit=2",
),
});
});
it("Second page of pagination respects X-Forwarded headers", async () => {
let resp = await handlers.handle(
new Request("http://localhost/api/movies?limit=2", {
headers: {
...(await getJwtHeaders()),
"x-forwarded-proto": "https",
"x-forwarded-host": "kyoo.example.com",
},
}),
);
let body = await resp.json();
expectStatus(resp, body).toBe(200);
expect(body.next).toBeTruthy();
expect(body.next).toContain("https://kyoo.example.com");
// Follow the next link with the same headers
resp = await handlers.handle(
new Request(body.next, {
headers: {
...(await getJwtHeaders()),
"x-forwarded-proto": "https",
"x-forwarded-host": "kyoo.example.com",
},
}),
);
body = await resp.json();
expectStatus(resp, body).toBe(200);
expect(body.this).toContain("https://kyoo.example.com");
});
});

View File

@@ -1,9 +1,12 @@
import { beforeAll } from "bun:test";
import { migrate } from "~/db";
process.env.PGDATABASE = "kyoo_test";
process.env.JWT_SECRET = "this is a secret";
process.env.JWT_ISSUER = "https://kyoo.zoriya.dev";
process.env.IMAGES_PATH = "./images";
beforeAll(async () => {
// lazy load this so env set before actually applies
const { migrate } = await import("~/db");
await migrate();
});

View File

@@ -591,4 +591,127 @@ describe("Video seeding", () => {
expect(vid!.evj[1].slug).toBe("made-in-abyss-s2e1");
expect(vid!.evj[1].entry.slug).toBe("made-in-abyss-s2e1");
});
it("work with duplicated episodes", async () => {
await db.delete(videos);
const [resp, body] = await createVideo({
guess: {
title: "mia",
episodes: [
{ season: 1, episode: 13 },
{ season: 1, episode: 13 },
],
from: "test",
history: [],
},
part: null,
path: "/video/mia s1e13.mkv",
rendering: "duptest",
version: 1,
for: [
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
],
});
expectStatus(resp, body).toBe(201);
expect(body).toBeArrayOfSize(1);
expect(body[0].id).toBeString();
const vid = await db.query.videos.findFirst({
where: eq(videos.id, body[0].id),
with: {
evj: { with: { entry: true } },
},
});
expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/mia s1e13.mkv");
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
expect(body[0].entries).toBeArrayOfSize(1);
expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13");
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13");
});
it("work with duplicated two episodes", async () => {
await db.delete(videos);
const [resp, body] = await createVideo([
{
guess: {
title: "mia",
episodes: [
{ season: 1, episode: 13 },
{ season: 1, episode: 13 },
],
from: "test",
history: [],
},
part: null,
path: "/video/mia s1e13.mkv",
rendering: "duptest-two",
version: 1,
for: [
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
],
},
{
guess: {
title: "mia",
episodes: [
{ season: 1, episode: 13 },
{ season: 1, episode: 13 },
],
from: "test",
history: [],
},
part: null,
path: "/video/mia s1e13 bis.mkv",
rendering: "duptest-two-bis",
version: 1,
for: [
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
],
},
]);
expectStatus(resp, body).toBe(201);
expect(body).toBeArrayOfSize(2);
expect(body[0].id).toBeString();
expect(body[1].id).toBeString();
expect(body[0].entries).toBeArrayOfSize(1);
expect(body[0].entries[0].slug).toBe("made-in-abyss-s1e13");
expect(body[1].entries).toBeArrayOfSize(1);
expect(body[1].entries[0].slug).toBe("made-in-abyss-s1e13-duptest-two-bis");
let vid = await db.query.videos.findFirst({
where: eq(videos.id, body[0].id),
with: {
evj: { with: { entry: true } },
},
});
expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/mia s1e13.mkv");
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13");
vid = await db.query.videos.findFirst({
where: eq(videos.id, body[1].id),
with: {
evj: { with: { entry: true } },
},
});
expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/mia s1e13 bis.mkv");
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13-duptest-two-bis");
});
});

View File

@@ -3,7 +3,6 @@
"target": "ES2021",
"module": "ES2022",
"moduleResolution": "node",
"types": ["bun-types"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,

View File

@@ -1,7 +1,7 @@
**
!/go.mod
!/go.sum
!/**.go
!/**/*.go
# generated via sqlc
!/sql
!/dbc

View File

@@ -19,12 +19,15 @@ PROTECTED_CLAIMS="permissions"
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
PUBLIC_URL=http://localhost:8901
# You can create apikeys at runtime via POST /apikey but you can also have some defined in the env.
# You can create apikeys at runtime via POST /key but you can also have some defined in the env.
# Replace $YOURNAME with the name of the key you want (only alpha are valid)
# The value will be the apikey (max 128 bytes)
# KEIBI_APIKEY_$YOURNAME=oaeushtaoesunthoaensuth
# KEIBI_APIKEY_$YOURNAME_CLAIMS='{"permissions": ["users.read"]}'
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
# Database things
# It is recommended to use the below PG environment variables when possible.
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
@@ -43,8 +46,3 @@ PGPORT=5432
# PGSSLROOTCERT=/my/serving.crt
# PGSSLCERT=/my/client.crt
# PGSSLKEY=/my/client.key
# Default is keibi, you can specify "disabled" to use the default search_path of the user.
# If this is not "disabled", the schema will be created (if it does not exists) and
# the search_path of the user will be ignored (only the schema specified will be used).
POSTGRES_SCHEMA=keibi

View File

@@ -60,8 +60,8 @@ GET `/users/$id/sessions` can be used by admins to list others session
### Api keys
```
Get `/apikeys`
Post `/apikeys` {...claims} Create a new api keys with given claims
Get `/keys`
Post `/keys` {...claims} Create a new api keys with given claims
```
An api key can be used like an opaque token, calling /jwt with it will return a valid jwt with the claims you specified during the post request to create it.

View File

@@ -4,10 +4,9 @@ import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"maps"
"net/http"
"strings"
"slices"
"time"
"github.com/golang-jwt/jwt/v5"
@@ -45,7 +44,7 @@ func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
CreatedAt: key.CreatedAt,
LastUsed: key.LastUsed,
},
Token: fmt.Sprintf("%s-%s", key.Name, key.Token),
Token: key.Token,
}
}
@@ -75,7 +74,10 @@ func (h *Handler) CreateApiKey(c echo.Context) error {
return err
}
if _, conflict := h.config.EnvApiKeys[req.Name]; conflict {
conflict := slices.ContainsFunc(h.config.EnvApiKeys, func(k ApiKeyWToken) bool {
return k.Name == req.Name
})
if conflict {
return echo.NewHTTPError(409, "An env apikey is already defined with the same name")
}
@@ -174,17 +176,15 @@ func (h *Handler) ListApiKey(c echo.Context) error {
}
func (h *Handler) createApiJwt(apikey string) (string, error) {
info := strings.SplitN(apikey, "-", 2)
if len(info) != 2 {
return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key format")
var key *ApiKeyWToken
for _, k := range h.config.EnvApiKeys {
if k.Token == apikey {
key = &k
break
}
}
key, fromEnv := h.config.EnvApiKeys[info[0]]
if !fromEnv {
dbKey, err := h.db.GetApiKey(context.Background(), dbc.GetApiKeyParams{
Name: info[0],
Token: info[1],
})
if key == nil {
dbKey, err := h.db.GetApiKey(context.Background(), apikey)
if err == pgx.ErrNoRows {
return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key")
} else if err != nil {
@@ -195,7 +195,8 @@ func (h *Handler) createApiJwt(apikey string) (string, error) {
h.db.TouchApiKey(context.Background(), dbKey.Pk)
}()
key = MapDbKey(&dbKey)
found := MapDbKey(&dbKey)
key = &found
}
claims := maps.Clone(key.Claims)
@@ -210,6 +211,7 @@ func (h *Handler) createApiJwt(apikey string) (string, error) {
Time: time.Now().UTC().Add(time.Hour),
}
jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
jwt.Header["kid"] = h.config.JwtKid
t, err := jwt.SignedString(h.config.JwtPrivateKey)
if err != nil {
return "", err

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"maps"
"os"
"slices"
"strings"
"time"
@@ -31,7 +32,7 @@ type Configuration struct {
GuestClaims jwt.MapClaims
ProtectedClaims []string
ExpirationDelay time.Duration
EnvApiKeys map[string]ApiKeyWToken
EnvApiKeys []ApiKeyWToken
}
var DefaultConfig = Configuration{
@@ -39,7 +40,7 @@ var DefaultConfig = Configuration{
FirstUserClaims: make(jwt.MapClaims),
ProtectedClaims: []string{"permissions"},
ExpirationDelay: 30 * 24 * time.Hour,
EnvApiKeys: make(map[string]ApiKeyWToken),
EnvApiKeys: make([]ApiKeyWToken, 0),
}
func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
@@ -137,14 +138,14 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
}
name = strings.ToLower(name)
ret.EnvApiKeys[name] = ApiKeyWToken{
ret.EnvApiKeys = append(ret.EnvApiKeys, ApiKeyWToken{
ApiKey: ApiKey{
Id: uuid.New(),
Name: name,
Claims: claims,
},
Token: v[1],
}
})
}
apikeys, err := db.ListApiKeys(context.Background())
@@ -152,7 +153,10 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
return nil, err
}
for _, key := range apikeys {
if _, defined := ret.EnvApiKeys[key.Name]; defined {
dup := slices.ContainsFunc(ret.EnvApiKeys, func(k ApiKeyWToken) bool {
return k.Name == key.Name
})
if dup {
return nil, fmt.Errorf(
"an api key with the name %s is already defined in database. Can't specify a new one via env var",
key.Name,

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.30.0
// source: apikeys.sql
package dbc
@@ -13,7 +13,7 @@ import (
)
const createApiKey = `-- name: CreateApiKey :one
insert into apikeys(name, token, claims, created_by)
insert into keibi.apikeys(name, token, claims, created_by)
values ($1, $2, $3, $4)
returning
pk, id, name, token, claims, created_by, created_at, last_used
@@ -48,7 +48,7 @@ func (q *Queries) CreateApiKey(ctx context.Context, arg CreateApiKeyParams) (Api
}
const deleteApiKey = `-- name: DeleteApiKey :one
delete from apikeys
delete from keibi.apikeys
where id = $1
returning
pk, id, name, token, claims, created_by, created_at, last_used
@@ -74,19 +74,13 @@ const getApiKey = `-- name: GetApiKey :one
select
pk, id, name, token, claims, created_by, created_at, last_used
from
apikeys
keibi.apikeys
where
name = $1
and token = $2
token = $1
`
type GetApiKeyParams struct {
Name string `json:"name"`
Token string `json:"token"`
}
func (q *Queries) GetApiKey(ctx context.Context, arg GetApiKeyParams) (Apikey, error) {
row := q.db.QueryRow(ctx, getApiKey, arg.Name, arg.Token)
func (q *Queries) GetApiKey(ctx context.Context, token string) (Apikey, error) {
row := q.db.QueryRow(ctx, getApiKey, token)
var i Apikey
err := row.Scan(
&i.Pk,
@@ -105,7 +99,7 @@ const listApiKeys = `-- name: ListApiKeys :many
select
pk, id, name, token, claims, created_by, created_at, last_used
from
apikeys
keibi.apikeys
order by
last_used
`
@@ -141,7 +135,7 @@ func (q *Queries) ListApiKeys(ctx context.Context) ([]Apikey, error) {
const touchApiKey = `-- name: TouchApiKey :exec
update
apikeys
keibi.apikeys
set
last_used = now()::timestamptz
where

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.30.0
package dbc

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.30.0
package dbc

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.30.0
// source: sessions.sql
package dbc
@@ -13,7 +13,7 @@ import (
)
const clearOtherSessions = `-- name: ClearOtherSessions :exec
delete from sessions as s using users as u
delete from keibi.sessions as s using keibi.users as u
where s.user_pk = u.pk
and s.id != $1
and u.id = $2
@@ -30,7 +30,7 @@ func (q *Queries) ClearOtherSessions(ctx context.Context, arg ClearOtherSessions
}
const createSession = `-- name: CreateSession :one
insert into sessions(token, user_pk, device)
insert into keibi.sessions(token, user_pk, device)
values ($1, $2, $3)
returning
pk, id, token, user_pk, created_date, last_used, device
@@ -58,7 +58,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
}
const deleteSession = `-- name: DeleteSession :one
delete from sessions as s using users as u
delete from keibi.sessions as s using keibi.users as u
where s.user_pk = u.pk
and s.id = $1
and u.id = $2
@@ -93,8 +93,8 @@ select
s.last_used,
u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen
from
users as u
inner join sessions as s on u.pk = s.user_pk
keibi.users as u
inner join keibi.sessions as s on u.pk = s.user_pk
where
s.token = $1
limit 1
@@ -130,8 +130,8 @@ const getUserSessions = `-- name: GetUserSessions :many
select
s.pk, s.id, s.token, s.user_pk, s.created_date, s.last_used, s.device
from
sessions as s
inner join users as u on u.pk = s.user_pk
keibi.sessions as s
inner join keibi.users as u on u.pk = s.user_pk
where
u.pk = $1
order by
@@ -168,7 +168,7 @@ func (q *Queries) GetUserSessions(ctx context.Context, pk int32) ([]Session, err
const touchSession = `-- name: TouchSession :exec
update
sessions
keibi.sessions
set
last_used = now()::timestamptz
where

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.30.0
// source: users.sql
package dbc
@@ -13,12 +13,12 @@ import (
)
const createUser = `-- name: CreateUser :one
insert into users(username, email, password, claims)
insert into keibi.users(username, email, password, claims)
values ($1, $2, $3, case when not exists (
select
pk, id, username, email, password, claims, created_date, last_seen
from
users) then
keibi.users) then
$4::jsonb
else
$5::jsonb
@@ -58,7 +58,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
}
const deleteUser = `-- name: DeleteUser :one
delete from users
delete from keibi.users
where id = $1
returning
pk, id, username, email, password, claims, created_date, last_seen
@@ -84,7 +84,7 @@ const getAllUsers = `-- name: GetAllUsers :many
select
pk, id, username, email, password, claims, created_date, last_seen
from
users
keibi.users
order by
id
limit $1
@@ -123,7 +123,7 @@ const getAllUsersAfter = `-- name: GetAllUsersAfter :many
select
pk, id, username, email, password, claims, created_date, last_seen
from
users
keibi.users
where
id >= $2
order by
@@ -173,8 +173,8 @@ select
h.username,
h.profile_url
from
users as u
left join oidc_handle as h on u.pk = h.user_pk
keibi.users as u
left join keibi.oidc_handle as h on u.pk = h.user_pk
where ($1::boolean
and u.id = $2)
or (not $1
@@ -232,7 +232,7 @@ const getUserByLogin = `-- name: GetUserByLogin :one
select
pk, id, username, email, password, claims, created_date, last_seen
from
users
keibi.users
where
email = $1
or username = $1
@@ -257,9 +257,9 @@ func (q *Queries) GetUserByLogin(ctx context.Context, login string) (User, error
const touchUser = `-- name: TouchUser :exec
update
users
keibi.users
set
last_used = now()::timestamptz
last_seen = now()::timestamptz
where
pk = $1
`
@@ -271,12 +271,12 @@ func (q *Queries) TouchUser(ctx context.Context, pk int32) error {
const updateUser = `-- name: UpdateUser :one
update
users
keibi.users
set
username = coalesce($2, username),
email = coalesce($3, email),
password = coalesce($4, password),
claims = coalesce($5, claims)
claims = claims || coalesce($5, '{}'::jsonb)
where
id = $1
returning

18
auth/devspace.yaml Normal file
View File

@@ -0,0 +1,18 @@
version: v2beta1
name: auth
dev:
auth:
imageSelector: ghcr.io/zoriya/kyoo_auth
devImage: docker.io/golang:1.25
workingDir: /app
sync:
- path: .:/app
startContainer: true
onUpload:
restartContainer: true
command:
- bash
- -c
- "go mod download; go run -race ."
ports:
- port: "4568"

View File

@@ -1,36 +1,59 @@
module github.com/zoriya/kyoo/keibi
go 1.23.3
go 1.24.0
toolchain go1.25.1
toolchain go1.25.4
require (
github.com/alexedwards/argon2id v1.0.0
github.com/exaring/otelpgx v0.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6
github.com/labstack/echo-jwt/v4 v4.3.1
github.com/labstack/echo-jwt/v4 v4.4.0
github.com/labstack/echo/v4 v4.13.4
github.com/lestrrat-go/jwx/v3 v3.0.10
github.com/lestrrat-go/jwx/v3 v3.0.12
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.6
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/log v0.14.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
)
require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.0 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
golang.org/x/mod v0.25.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
golang.org/x/mod v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
@@ -38,7 +61,7 @@ require (
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0
github.com/go-playground/validator/v10 v10.28.0
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -55,13 +78,16 @@ require (
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -25,12 +27,15 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/exaring/otelpgx v0.9.3 h1:4yO02tXC7ZJZ+hcqcUkfxblYNCIFGVhpUWI0iw1TzPU=
github.com/exaring/otelpgx v0.9.3/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -49,8 +54,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -59,10 +64,14 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -84,8 +93,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-jwt/v4 v4.3.1 h1:d8+/qf8nx7RxeL46LtoIwHJsH2PNN8xXCQ/jDianycE=
github.com/labstack/echo-jwt/v4 v4.3.1/go.mod h1:yJi83kN8S/5vePVPd+7ID75P4PqPNVRs2HVeuvYJH00=
github.com/labstack/echo-jwt/v4 v4.4.0 h1:nrXaEnJupfc2R4XChcLRDyghhMZup77F8nIzHnBK19U=
github.com/labstack/echo-jwt/v4 v4.4.0/go.mod h1:kYXWgWms9iFqI3ldR+HAEj/Zfg5rZtR7ePOgktG4Hjg=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@@ -94,12 +103,16 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
@@ -126,17 +139,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
@@ -152,35 +165,67 @@ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 h1:6YeICKmGrvgJ5th4+OMNpcuoB6q/Xs8gt0YCO7MUv1k=
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0/go.mod h1:ZEA7j2B35siNV0T00aapacNzjz4tvOlNoHp0ncCfwNQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo=
go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -190,8 +235,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -203,17 +248,27 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -34,19 +34,30 @@ func (h *Handler) CreateJwt(c echo.Context) error {
if err != nil {
return err
}
c.Response().Header().Add("Authorization", fmt.Sprintf("Bearer %s", token))
return c.JSON(http.StatusOK, Jwt{
Token: &token,
})
}
auth := c.Request().Header.Get("Authorization")
var jwt *string
var token string
if !strings.HasPrefix(auth, "Bearer ") {
if auth == "" {
c, _ := c.Request().Cookie("X-Bearer")
if c != nil {
token = c.Value
}
} else if strings.HasPrefix(auth, "Bearer ") {
token = auth[len("Bearer "):]
} else if auth != "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid bearer format.")
}
var jwt *string
if token == "" {
jwt = h.createGuestJwt()
} else {
token := auth[len("Bearer "):]
tkn, err := h.createJwt(token)
if err != nil {
return err

View File

@@ -19,10 +19,12 @@ import (
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/labstack/echo-jwt/v4"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/swaggo/echo-swagger"
echoSwagger "github.com/swaggo/echo-swagger"
"github.com/exaring/otelpgx"
)
func ErrorHandler(err error, c echo.Context) {
@@ -59,7 +61,27 @@ func (v *Validator) Validate(i any) error {
}
func (h *Handler) CheckHealth(c echo.Context) error {
return c.JSON(200, struct{ Status string }{Status: "healthy"})
return c.JSON(200, struct {
Status string `json:"status"`
}{Status: "healthy"})
}
func (h *Handler) CheckReady(c echo.Context) error {
_, err := h.rawDb.Exec(c.Request().Context(), "select 1")
status := "healthy"
db := "healthy"
ret := 200
if err != nil {
status = "unhealthy"
db = err.Error()
ret = 500
}
return c.JSON(ret, struct {
Status string `json:"status"`
Database string `json:"database"`
}{Status: status, Database: db})
}
func GetenvOr(env string, def string) string {
@@ -106,10 +128,11 @@ func OpenDatabase() (*pgxpool.Pool, error) {
config.ConnConfig.RuntimeParams["application_name"] = "keibi"
}
schema := GetenvOr("POSTGRES_SCHEMA", "keibi")
if _, ok := config.ConnConfig.RuntimeParams["search_path"]; !ok {
config.ConnConfig.RuntimeParams["search_path"] = schema
}
config.ConnConfig.Tracer = otelpgx.NewTracer(
otelpgx.WithSpanNameFunc(dbGetSpanName),
otelpgx.WithDisableQuerySpanNamePrefix(),
otelpgx.WithIncludeQueryParameters(),
)
db, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
@@ -117,18 +140,14 @@ func OpenDatabase() (*pgxpool.Pool, error) {
return nil, err
}
if schema != "disabled" {
_, err = db.Exec(ctx, fmt.Sprintf("create schema if not exists %s", schema))
if err != nil {
return nil, err
}
}
fmt.Println("Migrating database")
dbi := stdlib.OpenDBFromPool(db)
defer dbi.Close()
driver, err := pgxd.WithInstance(dbi, &pgxd.Config{})
dbi.Exec("create schema if not exists keibi")
driver, err := pgxd.WithInstance(dbi, &pgxd.Config{
SchemaName: "keibi",
})
if err != nil {
return nil, err
}
@@ -144,6 +163,7 @@ func OpenDatabase() (*pgxpool.Pool, error) {
type Handler struct {
db *dbc.Queries
rawDb *pgxpool.Pool
config *Configuration
}
@@ -212,6 +232,13 @@ func main() {
e.Validator = &Validator{validator: validator.New(validator.WithRequiredStructEnabled())}
e.HTTPErrorHandler = ErrorHandler
cleanup, err := setupOtel(e)
if err != nil {
e.Logger.Fatal("Failed to setup otel: ", err)
return
}
defer cleanup()
db, err := OpenDatabase()
if err != nil {
e.Logger.Fatal("Could not open database: ", err)
@@ -219,7 +246,8 @@ func main() {
}
h := Handler{
db: dbc.New(db),
db: dbc.New(db),
rawDb: db,
}
conf, err := LoadConfiguration(h.db)
if err != nil {
@@ -237,6 +265,7 @@ func main() {
}))
g.GET("/health", h.CheckHealth)
g.GET("/ready", h.CheckReady)
r.GET("/users", h.ListUsers)
r.GET("/users/:id", h.GetUser)
@@ -257,6 +286,7 @@ func main() {
r.DELETE("/keys/:id", h.DeleteApiKey)
g.GET("/jwt", h.CreateJwt)
g.Any("/jwt/*", h.CreateJwt)
e.GET("/.well-known/jwks.json", h.GetJwks)
e.GET("/.well-known/openid-configuration", h.GetOidcConfig)

140
auth/otel.go Normal file
View File

@@ -0,0 +1,140 @@
package main
import (
"context"
"strings"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func setupOtel(e *echo.Echo) (func(), error) {
ctx := context.Background()
proto := GetenvOr("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
res, err := resource.New(
ctx,
resource.WithAttributes(semconv.ServiceNameKey.String("kyoo.auth")),
resource.WithFromEnv(),
resource.WithTelemetrySDK(),
resource.WithProcess(),
resource.WithOS(),
resource.WithContainer(),
resource.WithHost(),
)
if err != nil {
return nil, err
}
var le log.Exporter
if proto == "http/protobuf" {
le, err = otlploghttp.New(ctx)
} else {
le, err = otlploggrpc.New(ctx)
}
if err != nil {
return nil, err
}
lp := log.NewLoggerProvider(
log.WithProcessor(log.NewBatchProcessor(le)),
log.WithResource(res),
)
global.SetLoggerProvider(lp)
var me metric.Exporter
if proto == "http/protobuf" {
me, err = otlpmetrichttp.New(ctx)
} else {
me, err = otlpmetricgrpc.New(ctx)
}
if err != nil {
return func() {}, err
}
mp := metric.NewMeterProvider(
metric.WithReader(
metric.NewPeriodicReader(me),
),
metric.WithResource(res),
)
otel.SetMeterProvider(mp)
var te *otlptrace.Exporter
if proto == "http/protobuf" {
te, err = otlptracehttp.New(ctx)
} else {
te, err = otlptracegrpc.New(ctx)
}
if err != nil {
return func() {}, err
}
tp := trace.NewTracerProvider(
trace.WithBatcher(te),
trace.WithResource(res),
)
otel.SetTracerProvider(tp)
e.Use(otelecho.Middleware("kyoo.auth", otelecho.WithSkipper(func(c echo.Context) bool {
return c.Path() == "/auth/health" || c.Path() == "/auth/ready"
})))
return func() {
lp.Shutdown(ctx)
mp.Shutdown(ctx)
tp.Shutdown(ctx)
}, nil
}
// stolen from https://github.com/exaring/otelpgx/issues/47
func dbGetSpanName(sql string) string {
if len(sql) >= 10 && sql[0:9] == "-- name: " {
// -- name: {name} :{type}
if index := strings.Index(sql[9:], ":"); index != -1 {
// remove leading space before :
// optimised assuming this comment has been generated by SQLC
return sql[9 : 9+(index-1)]
}
// -- name: {name}
if index := strings.Index(sql[9:], "\n"); index != -1 {
if sql[len(sql[9:9+index])-1] != ' ' {
return sql[9 : 9+index]
}
return strings.TrimSpace(sql[9 : 9+index])
}
}
if len(sql) >= 9 && sql[0:8] == "-- name:" {
// -- name:{name}
if index := strings.Index(sql[8:], "\n"); index != -1 {
if sql[len(sql[8:8+index])-1] != ' ' {
return sql[8 : 8+index]
}
return strings.TrimSpace(sql[8 : 8+index])
}
}
if len(sql) >= 8 && sql[0:7] == "--name:" {
// -- name:{name}
if index := strings.Index(sql[7:], "\n"); index != -1 {
if sql[len(sql[7:7+index])-1] != ' ' {
return sql[7 : 7+index]
}
return strings.TrimSpace(sql[7 : 7+index])
}
}
return sql
}

View File

@@ -7,9 +7,9 @@ pkgs.mkShell {
sqlc
go-swag
# for psql in cli (+ pgformatter for sql files)
postgresql_15
postgresql_18
pgformatter
# to run tests
# hurl
hurl
];
}

View File

@@ -1,6 +1,6 @@
begin;
drop table oidc_handle;
drop table users;
drop table keibi.oidc_handle;
drop table keibi.users;
commit;

View File

@@ -1,6 +1,8 @@
begin;
create table users(
create schema if not exists keibi;
create table keibi.users(
pk serial primary key,
id uuid not null default gen_random_uuid(),
username varchar(256) not null unique,
@@ -12,8 +14,8 @@ create table users(
last_seen timestamptz not null default now()::timestamptz
);
create table oidc_handle(
user_pk integer not null references users(pk) on delete cascade,
create table keibi.oidc_handle(
user_pk integer not null references keibi.users(pk) on delete cascade,
provider varchar(256) not null,
id text not null,

View File

@@ -1,5 +1,5 @@
begin;
drop table sessions;
drop table keibi.sessions;
commit;

View File

@@ -1,10 +1,10 @@
begin;
create table sessions(
create table keibi.sessions(
pk serial primary key,
id uuid not null default gen_random_uuid(),
token varchar(128) not null unique,
user_pk integer not null references users(pk) on delete cascade,
user_pk integer not null references keibi.users(pk) on delete cascade,
created_date timestamptz not null default now()::timestamptz,
last_used timestamptz not null default now()::timestamptz,
device varchar(1024)

View File

@@ -1,5 +1,5 @@
begin;
drop table apikeys;
drop table keibi.apikeys;
commit;

View File

@@ -1,13 +1,13 @@
begin;
create table apikeys(
create table keibi.apikeys(
pk serial primary key,
id uuid not null default gen_random_uuid(),
name varchar(256) not null unique,
token varchar(128) not null unique,
claims jsonb not null,
created_by integer references users(pk) on delete cascade,
created_by integer references keibi.users(pk) on delete cascade,
created_at timestamptz not null default now()::timestamptz,
last_used timestamptz not null default now()::timestamptz
);

View File

@@ -2,14 +2,13 @@
select
*
from
apikeys
keibi.apikeys
where
name = $1
and token = $2;
token = $1;
-- name: TouchApiKey :exec
update
apikeys
keibi.apikeys
set
last_used = now()::timestamptz
where
@@ -19,18 +18,18 @@ where
select
*
from
apikeys
keibi.apikeys
order by
last_used;
-- name: CreateApiKey :one
insert into apikeys(name, token, claims, created_by)
insert into keibi.apikeys(name, token, claims, created_by)
values ($1, $2, $3, $4)
returning
*;
-- name: DeleteApiKey :one
delete from apikeys
delete from keibi.apikeys
where id = $1
returning
*;

View File

@@ -5,15 +5,15 @@ select
s.last_used,
sqlc.embed(u)
from
users as u
inner join sessions as s on u.pk = s.user_pk
keibi.users as u
inner join keibi.sessions as s on u.pk = s.user_pk
where
s.token = $1
limit 1;
-- name: TouchSession :exec
update
sessions
keibi.sessions
set
last_used = now()::timestamptz
where
@@ -23,21 +23,21 @@ where
select
s.*
from
sessions as s
inner join users as u on u.pk = s.user_pk
keibi.sessions as s
inner join keibi.users as u on u.pk = s.user_pk
where
u.pk = $1
order by
last_used;
-- name: CreateSession :one
insert into sessions(token, user_pk, device)
insert into keibi.sessions(token, user_pk, device)
values ($1, $2, $3)
returning
*;
-- name: DeleteSession :one
delete from sessions as s using users as u
delete from keibi.sessions as s using keibi.users as u
where s.user_pk = u.pk
and s.id = $1
and u.id = sqlc.arg(user_id)
@@ -45,7 +45,7 @@ returning
s.*;
-- name: ClearOtherSessions :exec
delete from sessions as s using users as u
delete from keibi.sessions as s using keibi.users as u
where s.user_pk = u.pk
and s.id != @session_id
and u.id = @user_id;

Some files were not shown because too many files have changed in this diff Show More