+96
.github/workflows/docker-publish.yml
+96
.github/workflows/docker-publish.yml
···
1
+
name: Docker
2
+
3
+
# This workflow uses actions that are not certified by GitHub.
4
+
# They are provided by a third-party and are governed by
5
+
# separate terms of service, privacy policy, and support
6
+
# documentation.
7
+
8
+
on:
9
+
push:
10
+
branches: [ "master" ]
11
+
# Publish semver tags as releases.
12
+
tags: [ 'v*.*.*' ]
13
+
pull_request:
14
+
branches: [ "master" ]
15
+
16
+
env:
17
+
# Use docker.io for Docker Hub if empty
18
+
REGISTRY: ghcr.io
19
+
# github.repository as <account>/<repo>
20
+
IMAGE_NAME: ${{ github.repository }}
21
+
22
+
23
+
jobs:
24
+
build:
25
+
26
+
runs-on: ubuntu-latest
27
+
permissions:
28
+
contents: read
29
+
packages: write
30
+
# This is used to complete the identity challenge
31
+
# with sigstore/fulcio when running outside of PRs.
32
+
id-token: write
33
+
34
+
steps:
35
+
- name: Checkout repository
36
+
uses: actions/checkout@v4
37
+
38
+
# Install the cosign tool except on PR
39
+
# https://github.com/sigstore/cosign-installer
40
+
- name: Install cosign
41
+
if: github.event_name != 'pull_request'
42
+
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
43
+
with:
44
+
cosign-release: 'v2.2.4'
45
+
46
+
# Set up BuildKit Docker container builder to be able to build
47
+
# multi-platform images and export cache
48
+
# https://github.com/docker/setup-buildx-action
49
+
- name: Set up Docker Buildx
50
+
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
51
+
52
+
# Login against a Docker registry except on PR
53
+
# https://github.com/docker/login-action
54
+
- name: Log into registry ${{ env.REGISTRY }}
55
+
if: github.event_name != 'pull_request'
56
+
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
57
+
with:
58
+
registry: ${{ env.REGISTRY }}
59
+
username: ${{ github.actor }}
60
+
password: ${{ secrets.GITHUB_TOKEN }}
61
+
62
+
# Extract metadata (tags, labels) for Docker
63
+
# https://github.com/docker/metadata-action
64
+
- name: Extract Docker metadata
65
+
id: meta
66
+
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
67
+
with:
68
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
69
+
70
+
# Build and push Docker image with Buildx (don't push on PR)
71
+
# https://github.com/docker/build-push-action
72
+
- name: Build and push Docker image
73
+
id: build-and-push
74
+
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
75
+
with:
76
+
context: .
77
+
push: ${{ github.event_name != 'pull_request' }}
78
+
tags: ${{ steps.meta.outputs.tags }}
79
+
labels: ${{ steps.meta.outputs.labels }}
80
+
cache-from: type=gha
81
+
cache-to: type=gha,mode=max
82
+
83
+
# Sign the resulting Docker image digest except on PRs.
84
+
# This will only write to the public Rekor transparency log when the Docker
85
+
# repository is public to avoid leaking data. If you would like to publish
86
+
# transparency data even for private images, pass --force to cosign below.
87
+
# https://github.com/sigstore/cosign
88
+
- name: Sign the published Docker image
89
+
if: ${{ github.event_name != 'pull_request' }}
90
+
env:
91
+
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
92
+
TAGS: ${{ steps.meta.outputs.tags }}
93
+
DIGEST: ${{ steps.build-and-push.outputs.digest }}
94
+
# This step uses the identity token to provision an ephemeral certificate
95
+
# against the sigstore community Fulcio instance.
96
+
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
+13
.tangled/workflows/build.yml
+13
.tangled/workflows/build.yml
+333
-14
Cargo.lock
+333
-14
Cargo.lock
···
30
30
]
31
31
32
32
[[package]]
33
+
name = "aho-corasick"
34
+
version = "1.1.3"
35
+
source = "registry+https://github.com/rust-lang/crates.io-index"
36
+
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
37
+
dependencies = [
38
+
"memchr",
39
+
]
40
+
41
+
[[package]]
33
42
name = "anyhow"
34
-
version = "1.0.99"
43
+
version = "1.0.100"
35
44
source = "registry+https://github.com/rust-lang/crates.io-index"
36
-
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
45
+
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
46
+
47
+
[[package]]
48
+
name = "async-lock"
49
+
version = "3.4.1"
50
+
source = "registry+https://github.com/rust-lang/crates.io-index"
51
+
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
52
+
dependencies = [
53
+
"event-listener",
54
+
"event-listener-strategy",
55
+
"pin-project-lite",
56
+
]
37
57
38
58
[[package]]
39
59
name = "async-trait"
···
145
165
146
166
[[package]]
147
167
name = "cc"
148
-
version = "1.2.37"
168
+
version = "1.2.38"
149
169
source = "registry+https://github.com/rust-lang/crates.io-index"
150
-
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
170
+
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
151
171
dependencies = [
152
172
"find-msvc-tools",
153
173
"jobserver",
···
162
182
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
163
183
164
184
[[package]]
185
+
name = "concurrent-queue"
186
+
version = "2.5.0"
187
+
source = "registry+https://github.com/rust-lang/crates.io-index"
188
+
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
189
+
dependencies = [
190
+
"crossbeam-utils",
191
+
]
192
+
193
+
[[package]]
165
194
name = "core-foundation"
166
195
version = "0.9.4"
167
196
source = "registry+https://github.com/rust-lang/crates.io-index"
···
187
216
]
188
217
189
218
[[package]]
219
+
name = "crossbeam-channel"
220
+
version = "0.5.15"
221
+
source = "registry+https://github.com/rust-lang/crates.io-index"
222
+
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
223
+
dependencies = [
224
+
"crossbeam-utils",
225
+
]
226
+
227
+
[[package]]
228
+
name = "crossbeam-epoch"
229
+
version = "0.9.18"
230
+
source = "registry+https://github.com/rust-lang/crates.io-index"
231
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
232
+
dependencies = [
233
+
"crossbeam-utils",
234
+
]
235
+
236
+
[[package]]
237
+
name = "crossbeam-utils"
238
+
version = "0.8.21"
239
+
source = "registry+https://github.com/rust-lang/crates.io-index"
240
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
241
+
242
+
[[package]]
190
243
name = "crypto-common"
191
244
version = "0.1.6"
192
245
source = "registry+https://github.com/rust-lang/crates.io-index"
···
320
373
"anyhow",
321
374
"async-trait",
322
375
"dotenv",
376
+
"lazy_static",
377
+
"moka",
323
378
"reqwest",
324
379
"rocketman",
380
+
"serde",
325
381
"serde_json",
326
382
"tokio",
327
383
]
···
369
425
]
370
426
371
427
[[package]]
428
+
name = "event-listener"
429
+
version = "5.4.1"
430
+
source = "registry+https://github.com/rust-lang/crates.io-index"
431
+
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
432
+
dependencies = [
433
+
"concurrent-queue",
434
+
"parking",
435
+
"pin-project-lite",
436
+
]
437
+
438
+
[[package]]
439
+
name = "event-listener-strategy"
440
+
version = "0.5.4"
441
+
source = "registry+https://github.com/rust-lang/crates.io-index"
442
+
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
443
+
dependencies = [
444
+
"event-listener",
445
+
"pin-project-lite",
446
+
]
447
+
448
+
[[package]]
372
449
name = "fastrand"
373
450
version = "2.3.0"
374
451
source = "registry+https://github.com/rust-lang/crates.io-index"
···
376
453
377
454
[[package]]
378
455
name = "find-msvc-tools"
379
-
version = "0.1.1"
456
+
version = "0.1.2"
380
457
source = "registry+https://github.com/rust-lang/crates.io-index"
381
-
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
458
+
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
382
459
383
460
[[package]]
384
461
name = "flume"
···
476
553
]
477
554
478
555
[[package]]
556
+
name = "generator"
557
+
version = "0.8.7"
558
+
source = "registry+https://github.com/rust-lang/crates.io-index"
559
+
checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
560
+
dependencies = [
561
+
"cc",
562
+
"cfg-if",
563
+
"libc",
564
+
"log",
565
+
"rustversion",
566
+
"windows",
567
+
]
568
+
569
+
[[package]]
479
570
name = "generic-array"
480
571
version = "0.14.7"
481
572
source = "registry+https://github.com/rust-lang/crates.io-index"
···
537
628
538
629
[[package]]
539
630
name = "hashbrown"
540
-
version = "0.15.5"
631
+
version = "0.16.0"
541
632
source = "registry+https://github.com/rust-lang/crates.io-index"
542
-
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
633
+
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
543
634
544
635
[[package]]
545
636
name = "http"
···
623
714
"http 1.3.1",
624
715
"hyper",
625
716
"hyper-util",
626
-
"rustls 0.23.31",
717
+
"rustls 0.23.32",
627
718
"rustls-pki-types",
628
719
"tokio",
629
720
"tokio-rustls 0.26.3",
···
787
878
788
879
[[package]]
789
880
name = "indexmap"
790
-
version = "2.11.3"
881
+
version = "2.11.4"
791
882
source = "registry+https://github.com/rust-lang/crates.io-index"
792
-
checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
883
+
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
793
884
dependencies = [
794
885
"equivalent",
795
886
"hashbrown",
···
889
980
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
890
981
891
982
[[package]]
983
+
name = "loom"
984
+
version = "0.7.2"
985
+
source = "registry+https://github.com/rust-lang/crates.io-index"
986
+
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
987
+
dependencies = [
988
+
"cfg-if",
989
+
"generator",
990
+
"scoped-tls",
991
+
"tracing",
992
+
"tracing-subscriber",
993
+
]
994
+
995
+
[[package]]
996
+
name = "matchers"
997
+
version = "0.2.0"
998
+
source = "registry+https://github.com/rust-lang/crates.io-index"
999
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
1000
+
dependencies = [
1001
+
"regex-automata",
1002
+
]
1003
+
1004
+
[[package]]
892
1005
name = "memchr"
893
1006
version = "2.7.5"
894
1007
source = "registry+https://github.com/rust-lang/crates.io-index"
···
928
1041
"libc",
929
1042
"wasi 0.11.1+wasi-snapshot-preview1",
930
1043
"windows-sys 0.59.0",
1044
+
]
1045
+
1046
+
[[package]]
1047
+
name = "moka"
1048
+
version = "0.12.10"
1049
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1050
+
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
1051
+
dependencies = [
1052
+
"async-lock",
1053
+
"crossbeam-channel",
1054
+
"crossbeam-epoch",
1055
+
"crossbeam-utils",
1056
+
"event-listener",
1057
+
"futures-util",
1058
+
"loom",
1059
+
"parking_lot",
1060
+
"portable-atomic",
1061
+
"rustc_version",
1062
+
"smallvec",
1063
+
"tagptr",
1064
+
"thiserror",
1065
+
"uuid",
931
1066
]
932
1067
933
1068
[[package]]
···
1025
1160
]
1026
1161
1027
1162
[[package]]
1163
+
name = "parking"
1164
+
version = "2.2.1"
1165
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1166
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
1167
+
1168
+
[[package]]
1169
+
name = "parking_lot"
1170
+
version = "0.12.4"
1171
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1172
+
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
1173
+
dependencies = [
1174
+
"lock_api",
1175
+
"parking_lot_core",
1176
+
]
1177
+
1178
+
[[package]]
1179
+
name = "parking_lot_core"
1180
+
version = "0.9.11"
1181
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1182
+
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
1183
+
dependencies = [
1184
+
"cfg-if",
1185
+
"libc",
1186
+
"redox_syscall",
1187
+
"smallvec",
1188
+
"windows-targets",
1189
+
]
1190
+
1191
+
[[package]]
1028
1192
name = "percent-encoding"
1029
1193
version = "2.3.2"
1030
1194
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1137
1301
]
1138
1302
1139
1303
[[package]]
1304
+
name = "redox_syscall"
1305
+
version = "0.5.17"
1306
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1307
+
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
1308
+
dependencies = [
1309
+
"bitflags",
1310
+
]
1311
+
1312
+
[[package]]
1313
+
name = "regex-automata"
1314
+
version = "0.4.10"
1315
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1316
+
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
1317
+
dependencies = [
1318
+
"aho-corasick",
1319
+
"memchr",
1320
+
"regex-syntax",
1321
+
]
1322
+
1323
+
[[package]]
1324
+
name = "regex-syntax"
1325
+
version = "0.8.6"
1326
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1327
+
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1328
+
1329
+
[[package]]
1140
1330
name = "reqwest"
1141
1331
version = "0.12.23"
1142
1332
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1221
1411
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
1222
1412
1223
1413
[[package]]
1414
+
name = "rustc_version"
1415
+
version = "0.4.1"
1416
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1417
+
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1418
+
dependencies = [
1419
+
"semver",
1420
+
]
1421
+
1422
+
[[package]]
1224
1423
name = "rustix"
1225
1424
version = "1.1.2"
1226
1425
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1247
1446
1248
1447
[[package]]
1249
1448
name = "rustls"
1250
-
version = "0.23.31"
1449
+
version = "0.23.32"
1251
1450
source = "registry+https://github.com/rust-lang/crates.io-index"
1252
-
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
1451
+
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
1253
1452
dependencies = [
1254
1453
"once_cell",
1255
1454
"rustls-pki-types",
···
1331
1530
]
1332
1531
1333
1532
[[package]]
1533
+
name = "scoped-tls"
1534
+
version = "1.0.1"
1535
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1536
+
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
1537
+
1538
+
[[package]]
1334
1539
name = "scopeguard"
1335
1540
version = "1.2.0"
1336
1541
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1368
1573
"core-foundation-sys",
1369
1574
"libc",
1370
1575
]
1576
+
1577
+
[[package]]
1578
+
name = "semver"
1579
+
version = "1.0.27"
1580
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1581
+
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
1371
1582
1372
1583
[[package]]
1373
1584
name = "serde"
···
1552
1763
]
1553
1764
1554
1765
[[package]]
1766
+
name = "tagptr"
1767
+
version = "0.2.0"
1768
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1769
+
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
1770
+
1771
+
[[package]]
1555
1772
name = "tempfile"
1556
1773
version = "3.22.0"
1557
1774
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1658
1875
source = "registry+https://github.com/rust-lang/crates.io-index"
1659
1876
checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd"
1660
1877
dependencies = [
1661
-
"rustls 0.23.31",
1878
+
"rustls 0.23.32",
1662
1879
"tokio",
1663
1880
]
1664
1881
···
1785
2002
source = "registry+https://github.com/rust-lang/crates.io-index"
1786
2003
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
1787
2004
dependencies = [
2005
+
"matchers",
1788
2006
"nu-ansi-term",
2007
+
"once_cell",
2008
+
"regex-automata",
1789
2009
"sharded-slab",
1790
2010
"smallvec",
1791
2011
"thread_local",
2012
+
"tracing",
1792
2013
"tracing-core",
1793
2014
"tracing-log",
1794
2015
]
···
1862
2083
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1863
2084
1864
2085
[[package]]
2086
+
name = "uuid"
2087
+
version = "1.18.1"
2088
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2089
+
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
2090
+
dependencies = [
2091
+
"getrandom 0.3.3",
2092
+
"js-sys",
2093
+
"wasm-bindgen",
2094
+
]
2095
+
2096
+
[[package]]
1865
2097
name = "valuable"
1866
2098
version = "0.1.1"
1867
2099
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2001
2233
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
2002
2234
2003
2235
[[package]]
2236
+
name = "windows"
2237
+
version = "0.61.3"
2238
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2239
+
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
2240
+
dependencies = [
2241
+
"windows-collections",
2242
+
"windows-core",
2243
+
"windows-future",
2244
+
"windows-link 0.1.3",
2245
+
"windows-numerics",
2246
+
]
2247
+
2248
+
[[package]]
2249
+
name = "windows-collections"
2250
+
version = "0.2.0"
2251
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2252
+
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
2253
+
dependencies = [
2254
+
"windows-core",
2255
+
]
2256
+
2257
+
[[package]]
2258
+
name = "windows-core"
2259
+
version = "0.61.2"
2260
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2261
+
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
2262
+
dependencies = [
2263
+
"windows-implement",
2264
+
"windows-interface",
2265
+
"windows-link 0.1.3",
2266
+
"windows-result",
2267
+
"windows-strings",
2268
+
]
2269
+
2270
+
[[package]]
2271
+
name = "windows-future"
2272
+
version = "0.2.1"
2273
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2274
+
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
2275
+
dependencies = [
2276
+
"windows-core",
2277
+
"windows-link 0.1.3",
2278
+
"windows-threading",
2279
+
]
2280
+
2281
+
[[package]]
2282
+
name = "windows-implement"
2283
+
version = "0.60.0"
2284
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2285
+
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
2286
+
dependencies = [
2287
+
"proc-macro2",
2288
+
"quote",
2289
+
"syn",
2290
+
]
2291
+
2292
+
[[package]]
2293
+
name = "windows-interface"
2294
+
version = "0.59.1"
2295
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2296
+
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
2297
+
dependencies = [
2298
+
"proc-macro2",
2299
+
"quote",
2300
+
"syn",
2301
+
]
2302
+
2303
+
[[package]]
2004
2304
name = "windows-link"
2005
2305
version = "0.1.3"
2006
2306
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2011
2311
version = "0.2.0"
2012
2312
source = "registry+https://github.com/rust-lang/crates.io-index"
2013
2313
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
2314
+
2315
+
[[package]]
2316
+
name = "windows-numerics"
2317
+
version = "0.2.0"
2318
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2319
+
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
2320
+
dependencies = [
2321
+
"windows-core",
2322
+
"windows-link 0.1.3",
2323
+
]
2014
2324
2015
2325
[[package]]
2016
2326
name = "windows-registry"
···
2082
2392
"windows_x86_64_gnu",
2083
2393
"windows_x86_64_gnullvm",
2084
2394
"windows_x86_64_msvc",
2395
+
]
2396
+
2397
+
[[package]]
2398
+
name = "windows-threading"
2399
+
version = "0.1.0"
2400
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2401
+
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
2402
+
dependencies = [
2403
+
"windows-link 0.1.3",
2085
2404
]
2086
2405
2087
2406
[[package]]
+4
-1
Cargo.toml
+4
-1
Cargo.toml
···
6
6
[dependencies]
7
7
anyhow = "1.0.99"
8
8
async-trait = "0.1.89"
9
+
lazy_static = "1.4.0"
10
+
moka = { version = "0.12", features = ["future"] }
9
11
reqwest = { version = "0.12.23", features = ["json"] }
10
-
rocketman = "0.2.3"
12
+
rocketman = "0.2.5"
13
+
serde = { version = "1.0", features = ["derive"] }
11
14
serde_json = "1.0.145"
12
15
dotenv = "0.15.0"
13
16
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+21
LICENSE
+21
LICENSE
···
1
+
MIT License
2
+
3
+
Copyright (c) 2025 teal computing, LLC
4
+
5
+
Permission is hereby granted, free of charge, to any person obtaining a copy
6
+
of this software and associated documentation files (the "Software"), to deal
7
+
in the Software without restriction, including without limitation the rights
8
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+
copies of the Software, and to permit persons to whom the Software is
10
+
furnished to do so, subject to the following conditions:
11
+
12
+
The above copyright notice and this permission notice shall be included in all
13
+
copies or substantial portions of the Software.
14
+
15
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+
SOFTWARE.
+29
-2
src/main.rs
+29
-2
src/main.rs
···
5
5
connection::JetstreamConnection, handler, ingestion::LexiconIngestor,
6
6
options::JetstreamOptions, types::event::Event,
7
7
};
8
-
use serde_json::{Value, json};
8
+
use serde_json::{json, Value};
9
9
use std::{
10
10
collections::HashMap,
11
11
sync::{Arc, Mutex},
12
12
};
13
+
14
+
mod resolve;
13
15
14
16
#[tokio::main]
15
17
async fn main() {
···
20
22
let opts = JetstreamOptions::builder()
21
23
// your EXACT nsids
22
24
.wanted_collections(vec!["fm.teal.alpha.feed.play".to_string()])
25
+
.ws_url(rocketman::endpoints::JetstreamEndpoints::Custom(
26
+
"wss://jetstream1.us-east.fire.hose.cam/subscribe".to_string(),
27
+
))
23
28
.build();
24
29
// create the jetstream connector
25
30
let jetstream = JetstreamConnection::new(opts);
···
79
84
let client = Client::new();
80
85
let url = std::env::var("DISCORD_WEBHOOK_URL")
81
86
.expect("DISCORD_WEBHOOK_URL environment variable must be set");
87
+
88
+
// Get resolver app view URL from environment
89
+
let resolver_app_view = std::env::var("RESOLVER_APP_VIEW")
90
+
.unwrap_or_else(|_| "https://bsky.social".to_string());
91
+
82
92
// Safely extract track name and artist from the record
83
93
let track_info = message
84
94
.commit
···
92
102
})
93
103
.unwrap_or_else(|| "unknown track".to_string());
94
104
105
+
let submission_client_agent = message
106
+
.commit
107
+
.as_ref()
108
+
.and_then(|commit| commit.record.as_ref())
109
+
.and_then(|record| record.get("submissionClientAgent")?.as_str());
110
+
111
+
// Resolve the handle from the DID
112
+
let handle = match resolve::resolve_identity(&message.did, &resolver_app_view).await {
113
+
Ok(resolved) => resolved.identity,
114
+
Err(e) => {
115
+
eprintln!("Failed to resolve handle for DID {}: {}", message.did, e);
116
+
// Fallback to showing the DID if resolution fails
117
+
message.did.clone()
118
+
}
119
+
};
120
+
95
121
let payload = json!({
96
-
"content": format!("{} is listening to {}", message.did, track_info)
122
+
"content": format!("{} is listening to {} via `{}`", handle, track_info, submission_client_agent.unwrap_or("unknown client")),
123
+
"allowed_mentions": { "parse": [] },
97
124
});
98
125
let response = client.post(url).json(&payload).send().await?;
99
126
+295
src/resolve.rs
+295
src/resolve.rs
···
1
+
// parts rewritten from https://github.com/mary-ext/atcute/blob/trunk/packages/oauth/browser-client/
2
+
// from https://github.com/espeon/geranium/blob/main/src/resolve.rs
3
+
// MIT License
4
+
5
+
use lazy_static::lazy_static;
6
+
use moka::future::Cache;
7
+
use serde::{Deserialize, Serialize};
8
+
use std::time::Duration;
9
+
10
+
// Cache for handle resolution - maps handle to DID
11
+
type HandleCache = Cache<String, String>;
12
+
13
+
// Cache for DID documents - maps DID to DidDocument
14
+
type DidDocumentCache = Cache<String, DidDocument>;
15
+
16
+
// Global cache instances
17
+
lazy_static::lazy_static! {
18
+
static ref HANDLE_CACHE: HandleCache = Cache::builder()
19
+
.time_to_live(Duration::from_secs(3600)) // 1 hour TTL
20
+
.max_capacity(10000)
21
+
.build();
22
+
23
+
static ref DID_DOCUMENT_CACHE: DidDocumentCache = Cache::builder()
24
+
.time_to_live(Duration::from_secs(3600)) // 1 hour TTL
25
+
.max_capacity(10000)
26
+
.build();
27
+
}
28
+
29
+
// should be same as regex /^did:[a-z]+:[\S\s]+/
30
+
fn is_did(did: &str) -> bool {
31
+
let parts: Vec<&str> = did.split(':').collect();
32
+
33
+
if parts.len() != 3 {
34
+
// must have exactly 3 parts: "did", method, and identifier
35
+
return false;
36
+
}
37
+
38
+
if parts[0] != "did" {
39
+
// first part must be "did"
40
+
return false;
41
+
}
42
+
43
+
if !parts[1].chars().all(|c| c.is_ascii_lowercase()) {
44
+
// method must be all lowercase
45
+
return false;
46
+
}
47
+
48
+
if parts[2].is_empty() {
49
+
// identifier can't be empty
50
+
return false;
51
+
}
52
+
53
+
true
54
+
}
55
+
56
+
fn is_valid_domain(domain: &str) -> bool {
57
+
// Check if empty or too long
58
+
if domain.is_empty() || domain.len() > 253 {
59
+
return false;
60
+
}
61
+
62
+
// Split into labels
63
+
let labels: Vec<&str> = domain.split('.').collect();
64
+
65
+
// Must have at least 2 labels
66
+
if labels.len() < 2 {
67
+
return false;
68
+
}
69
+
70
+
// Check each label
71
+
for label in labels {
72
+
// Label length check
73
+
if label.is_empty() || label.len() > 63 {
74
+
return false;
75
+
}
76
+
77
+
// Must not start or end with hyphen
78
+
if label.starts_with('-') || label.ends_with('-') {
79
+
return false;
80
+
}
81
+
82
+
// Check characters
83
+
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
84
+
return false;
85
+
}
86
+
}
87
+
88
+
true
89
+
}
90
+
91
+
async fn resolve_handle(handle: &str, resolver_app_view: &str) -> Result<String, reqwest::Error> {
92
+
// Check cache first
93
+
if let Some(cached_did) = HANDLE_CACHE.get(handle).await {
94
+
println!("๐ฏ Cache HIT for handle: {} -> {}", handle, cached_did);
95
+
return Ok(cached_did);
96
+
}
97
+
98
+
println!("โ Cache MISS for handle: {}, resolving from API", handle);
99
+
100
+
// If not in cache, resolve from API
101
+
let res = reqwest::get(format!(
102
+
"{}/xrpc/com.atproto.identity.resolveHandle?handle={}",
103
+
resolver_app_view, handle
104
+
))
105
+
.await?
106
+
.json::<ResolvedHandle>()
107
+
.await?;
108
+
109
+
let did = res.did;
110
+
111
+
// Cache the result
112
+
HANDLE_CACHE.insert(handle.to_string(), did.clone()).await;
113
+
println!("๐พ Cached handle resolution: {} -> {}", handle, did);
114
+
115
+
Ok(did)
116
+
}
117
+
118
+
async fn get_did_doc(did: &str) -> Result<DidDocument, reqwest::Error> {
119
+
// Check cache first
120
+
if let Some(cached_doc) = DID_DOCUMENT_CACHE.get(did).await {
121
+
println!("๐ฏ Cache HIT for DID document: {}", did);
122
+
return Ok(cached_doc);
123
+
}
124
+
125
+
println!("โ Cache MISS for DID document: {}, resolving from API", did);
126
+
127
+
// If not in cache, resolve from API
128
+
// get the specific did spec
129
+
// did:plc:abcd1e -> plc
130
+
let parts: Vec<&str> = did.split(':').collect();
131
+
let spec = parts[1];
132
+
let doc = match spec {
133
+
"plc" => {
134
+
println!("๐ก Fetching DID document from PLC directory for: {}", did);
135
+
let res: DidDocument = reqwest::get(format!("https://plc.directory/{}", did))
136
+
.await?
137
+
.error_for_status()?
138
+
.json()
139
+
.await?;
140
+
res
141
+
}
142
+
"web" => {
143
+
if !is_valid_domain(parts[2]) {
144
+
todo!("Error for domain in did:web is not valid");
145
+
};
146
+
let ident = parts[2];
147
+
println!("๐ก Fetching DID document from web domain: {}", ident);
148
+
let res = reqwest::get(format!("https://{}/.well-known/did.json", ident))
149
+
.await?
150
+
.error_for_status()?
151
+
.json()
152
+
.await?;
153
+
res
154
+
}
155
+
_ => todo!("Identifier not supported"),
156
+
};
157
+
158
+
// Cache the result
159
+
DID_DOCUMENT_CACHE.insert(did.to_string(), doc.clone()).await;
160
+
println!("๐พ Cached DID document: {}", did);
161
+
162
+
Ok(doc)
163
+
}
164
+
165
+
fn get_pds_endpoint(doc: &DidDocument) -> Option<DidDocumentService> {
166
+
get_service_endpoint(doc, "#atproto_pds", "AtprotoPersonalDataServer")
167
+
}
168
+
169
+
fn get_service_endpoint(
170
+
doc: &DidDocument,
171
+
svc_id: &str,
172
+
svc_type: &str,
173
+
) -> Option<DidDocumentService> {
174
+
doc.service
175
+
.iter()
176
+
.find(|svc| svc.id == svc_id && svc._type == svc_type)
177
+
.cloned()
178
+
}
179
+
180
+
fn extract_handle_from_doc(doc: &DidDocument) -> Option<String> {
181
+
// Look through alsoKnownAs list for at:// URLs
182
+
for also_known_as in &doc.also_known_as {
183
+
if also_known_as.starts_with("at://") {
184
+
// Extract handle from "at://handle.domain" format
185
+
let handle = also_known_as.strip_prefix("at://")?;
186
+
println!("๐ฏ Found handle in alsoKnownAs: {} -> {}", also_known_as, handle);
187
+
return Some(handle.to_string());
188
+
}
189
+
}
190
+
None
191
+
}
192
+
193
+
pub async fn resolve_identity(
194
+
id: &str,
195
+
resolver_app_view: &str,
196
+
) -> Result<ResolvedIdentity, reqwest::Error> {
197
+
println!("๐ Resolving identity: {}", id);
198
+
199
+
// is our identifier a did
200
+
let did = if is_did(id) {
201
+
println!("โ
Input is already a DID: {}", id);
202
+
id
203
+
} else {
204
+
println!("๐ Input is a handle, resolving to DID: {}", id);
205
+
// our id must be either invalid or a handle
206
+
if let Ok(res) = resolve_handle(id, resolver_app_view).await {
207
+
&res.clone()
208
+
} else {
209
+
todo!("Error type for could not resolve handle")
210
+
}
211
+
};
212
+
213
+
let doc = get_did_doc(did).await?;
214
+
let pds = get_pds_endpoint(&doc);
215
+
216
+
if pds.is_none() {
217
+
todo!("Error for could not find PDS")
218
+
}
219
+
220
+
// Extract handle from alsoKnownAs list
221
+
let handle = extract_handle_from_doc(&doc).unwrap_or_else(|| {
222
+
println!("โ ๏ธ No handle found in alsoKnownAs, using original input: {}", id);
223
+
id.to_string()
224
+
});
225
+
226
+
println!("โ
Successfully resolved identity: {} -> {} (handle: {}) (PDS: {})",
227
+
id, did, handle, pds.as_ref().unwrap().service_endpoint);
228
+
229
+
return Ok(ResolvedIdentity {
230
+
did: did.to_owned(),
231
+
doc,
232
+
identity: handle,
233
+
pds: pds.unwrap().service_endpoint,
234
+
});
235
+
}
236
+
237
+
/// Clear all cached handle resolutions and DID documents
238
+
pub async fn clear_cache() {
239
+
HANDLE_CACHE.invalidate_all();
240
+
DID_DOCUMENT_CACHE.invalidate_all();
241
+
}
242
+
243
+
/// Get cache statistics for monitoring
244
+
pub async fn get_cache_stats() -> (u64, u64) {
245
+
let handle_count = HANDLE_CACHE.entry_count();
246
+
let did_doc_count = DID_DOCUMENT_CACHE.entry_count();
247
+
(handle_count, did_doc_count)
248
+
}
249
+
250
+
// want this to be reusable on case of scope expansion :(
251
+
#[allow(dead_code)]
252
+
#[derive(Serialize, Deserialize, Debug)]
253
+
pub struct ResolvedIdentity {
254
+
pub did: String,
255
+
pub doc: DidDocument,
256
+
pub identity: String,
257
+
// should prob be url type but not really needed rn
258
+
pub pds: String,
259
+
}
260
+
261
+
#[derive(Serialize, Deserialize, Debug)]
262
+
struct ResolvedHandle {
263
+
did: String,
264
+
}
265
+
266
+
#[derive(Serialize, Deserialize, Debug, Clone)]
267
+
pub struct DidDocument {
268
+
#[serde(alias = "@context")]
269
+
pub _context: Vec<String>,
270
+
pub id: String,
271
+
#[serde(alias = "alsoKnownAs")]
272
+
pub also_known_as: Vec<String>,
273
+
#[serde(alias = "verificationMethod")]
274
+
pub verification_method: Vec<DidDocumentVerificationMethod>,
275
+
pub service: Vec<DidDocumentService>,
276
+
}
277
+
278
+
#[derive(Serialize, Deserialize, Debug, Clone)]
279
+
pub struct DidDocumentVerificationMethod {
280
+
pub id: String,
281
+
#[serde(alias = "type")]
282
+
pub _type: String,
283
+
pub controller: String,
284
+
#[serde(alias = "publicKeyMultibase")]
285
+
pub public_key_multibase: String,
286
+
}
287
+
288
+
#[derive(Serialize, Deserialize, Debug, Clone)]
289
+
pub struct DidDocumentService {
290
+
pub id: String,
291
+
#[serde(alias = "type")]
292
+
pub _type: String,
293
+
#[serde(alias = "serviceEndpoint")]
294
+
pub service_endpoint: String,
295
+
}