-13
README.md
-13
README.md
···
1
-
# ATmosphereConf
2
-
3
-
Main website: https://atmosphereconf.org> (currently redirects to our ticketing landing page)
4
-
News & Updates: <https://news.atprotocol.org> (powered by Leaflet!)
5
-
Save the Date with an RSVP on [Smoke Signal](https://smokesignal.events/did:plc:lehcqqkwzcwvjvw66uthu5oq/3lte3c7x43l2e)
6
-
7
-
We will be building out a conference website with a handful of ATProto specific features.
8
-
9
-
For open discussion, and for those who are joining as active volunteers, please visit the [community forum](https://discourse.atprotocol.community/c/atmosphereconf/25/none).
10
-
11
-
## Conference Profile
12
-
For starters, we're going to have a custom conference profile. Attendees and speakers (and anyone else!) can login and create an extended profile. Inspired by [Discover Toronto](https://discover.toronto.inc/), we had an [initial discussion in the forum](https://discourse.atprotocol.community/t/conference-profiles/186) and are going to work on fleshing this out here with detailed issues.
13
-
14
1
# Astro ATProto OAuth Starter
15
2
16
3
A minimal [Astro](https://astro.build) starter template demonstrating OAuth authentication with AT Protocol (ATProto), the decentralized social networking protocol used by Bluesky and other services.
+156
lexicons/defs.json
+156
lexicons/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.defs",
4
+
"defs": {
5
+
"label": {
6
+
"type": "object",
7
+
"description": "Metadata tag on an atproto resource (eg, repo or record).",
8
+
"required": ["src", "uri", "val", "cts"],
9
+
"properties": {
10
+
"ver": {
11
+
"type": "integer",
12
+
"description": "The AT Protocol version of the label object."
13
+
},
14
+
"src": {
15
+
"type": "string",
16
+
"format": "did",
17
+
"description": "DID of the actor who created this label."
18
+
},
19
+
"uri": {
20
+
"type": "string",
21
+
"format": "uri",
22
+
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
23
+
},
24
+
"cid": {
25
+
"type": "string",
26
+
"format": "cid",
27
+
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
28
+
},
29
+
"val": {
30
+
"type": "string",
31
+
"maxLength": 128,
32
+
"description": "The short string name of the value or type of this label."
33
+
},
34
+
"neg": {
35
+
"type": "boolean",
36
+
"description": "If true, this is a negation label, overwriting a previous label."
37
+
},
38
+
"cts": {
39
+
"type": "string",
40
+
"format": "datetime",
41
+
"description": "Timestamp when this label was created."
42
+
},
43
+
"exp": {
44
+
"type": "string",
45
+
"format": "datetime",
46
+
"description": "Timestamp at which this label expires (no longer applies)."
47
+
},
48
+
"sig": {
49
+
"type": "bytes",
50
+
"description": "Signature of dag-cbor encoded label."
51
+
}
52
+
}
53
+
},
54
+
"selfLabels": {
55
+
"type": "object",
56
+
"description": "Metadata tags on an atproto record, published by the author within the record.",
57
+
"required": ["values"],
58
+
"properties": {
59
+
"values": {
60
+
"type": "array",
61
+
"items": { "type": "ref", "ref": "#selfLabel" },
62
+
"maxLength": 10
63
+
}
64
+
}
65
+
},
66
+
"selfLabel": {
67
+
"type": "object",
68
+
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
69
+
"required": ["val"],
70
+
"properties": {
71
+
"val": {
72
+
"type": "string",
73
+
"maxLength": 128,
74
+
"description": "The short string name of the value or type of this label."
75
+
}
76
+
}
77
+
},
78
+
"labelValueDefinition": {
79
+
"type": "object",
80
+
"description": "Declares a label value and its expected interpretations and behaviors.",
81
+
"required": ["identifier", "severity", "blurs", "locales"],
82
+
"properties": {
83
+
"identifier": {
84
+
"type": "string",
85
+
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
86
+
"maxLength": 100,
87
+
"maxGraphemes": 100
88
+
},
89
+
"severity": {
90
+
"type": "string",
91
+
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
92
+
"knownValues": ["inform", "alert", "none"]
93
+
},
94
+
"blurs": {
95
+
"type": "string",
96
+
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
97
+
"knownValues": ["content", "media", "none"]
98
+
},
99
+
"defaultSetting": {
100
+
"type": "string",
101
+
"description": "The default setting for this label.",
102
+
"knownValues": ["ignore", "warn", "hide"],
103
+
"default": "warn"
104
+
},
105
+
"adultOnly": {
106
+
"type": "boolean",
107
+
"description": "Does the user need to have adult content enabled in order to configure this label?"
108
+
},
109
+
"locales": {
110
+
"type": "array",
111
+
"items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
112
+
}
113
+
}
114
+
},
115
+
"labelValueDefinitionStrings": {
116
+
"type": "object",
117
+
"description": "Strings which describe the label in the UI, localized into a specific language.",
118
+
"required": ["lang", "name", "description"],
119
+
"properties": {
120
+
"lang": {
121
+
"type": "string",
122
+
"description": "The code of the language these strings are written in.",
123
+
"format": "language"
124
+
},
125
+
"name": {
126
+
"type": "string",
127
+
"description": "A short human-readable name for the label.",
128
+
"maxGraphemes": 64,
129
+
"maxLength": 640
130
+
},
131
+
"description": {
132
+
"type": "string",
133
+
"description": "A longer description of what the label means and why it might be applied.",
134
+
"maxGraphemes": 10000,
135
+
"maxLength": 100000
136
+
}
137
+
}
138
+
},
139
+
"labelValue": {
140
+
"type": "string",
141
+
"knownValues": [
142
+
"!hide",
143
+
"!no-promote",
144
+
"!warn",
145
+
"!no-unauthenticated",
146
+
"dmca-violation",
147
+
"doxxing",
148
+
"porn",
149
+
"sexual",
150
+
"nudity",
151
+
"nsfl",
152
+
"gore"
153
+
]
154
+
}
155
+
}
156
+
}
+45
lexicons/profile.json
+45
lexicons/profile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "org.atmosphereconf.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A conference attendee profile.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"properties": {
12
+
"displayName": {
13
+
"type": "string",
14
+
"maxGraphemes": 64,
15
+
"maxLength": 640
16
+
},
17
+
"description": {
18
+
"type": "string",
19
+
"description": "Free-form profile description text.",
20
+
"maxGraphemes": 256,
21
+
"maxLength": 2560
22
+
},
23
+
"avatar": {
24
+
"type": "blob",
25
+
"description": "Profile picture for conference attendee",
26
+
"accept": ["image/png", "image/jpeg"],
27
+
"maxSize": 1000000
28
+
},
29
+
"banner": {
30
+
"type": "blob",
31
+
"description": "Larger horizontal image to display behind profile view.",
32
+
"accept": ["image/png", "image/jpeg"],
33
+
"maxSize": 1000000
34
+
},
35
+
"labels": {
36
+
"type": "union",
37
+
"description": "Self-label values for the conference profile.",
38
+
"refs": ["com.atproto.label.defs#selfLabels"]
39
+
},
40
+
"createdAt": { "type": "string", "format": "datetime" }
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+15
lexicons/strongRef.json
+15
lexicons/strongRef.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.repo.strongRef",
4
+
"description": "A URI with a content-hash fingerprint.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": ["uri", "cid"],
9
+
"properties": {
10
+
"uri": { "type": "string", "format": "at-uri" },
11
+
"cid": { "type": "string", "format": "cid" }
12
+
}
13
+
}
14
+
}
15
+
}
+344
-303
package-lock.json
+344
-303
package-lock.json
···
9
9
"version": "0.0.1",
10
10
"dependencies": {
11
11
"@astrojs/node": "^9.5.0",
12
-
"@astrojs/tailwind": "^6.0.2",
13
12
"@atproto/api": "^0.17.4",
14
13
"@atproto/oauth-client-node": "^0.3.10",
15
14
"@tailwindcss/vite": "^4.1.16",
···
17
16
"daisyui": "^5.3.9",
18
17
"dotenv": "^17.2.3",
19
18
"tailwindcss": "^4.1.16"
19
+
},
20
+
"devDependencies": {
21
+
"@atproto/lex-cli": "^0.9.6"
20
22
}
21
23
},
22
24
"node_modules/@astrojs/compiler": {
···
84
86
},
85
87
"engines": {
86
88
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
87
-
}
88
-
},
89
-
"node_modules/@astrojs/tailwind": {
90
-
"version": "6.0.2",
91
-
"resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz",
92
-
"integrity": "sha512-j3mhLNeugZq6A8dMNXVarUa8K6X9AW+QHU9u3lKNrPLMHhOQ0S7VeWhHwEeJFpEK1BTKEUY1U78VQv2gN6hNGg==",
93
-
"license": "MIT",
94
-
"dependencies": {
95
-
"autoprefixer": "^10.4.21",
96
-
"postcss": "^8.5.3",
97
-
"postcss-load-config": "^4.0.2"
98
-
},
99
-
"peerDependencies": {
100
-
"astro": "^3.0.0 || ^4.0.0 || ^5.0.0",
101
-
"tailwindcss": "^3.0.24"
102
89
}
103
90
},
104
91
"node_modules/@astrojs/telemetry": {
···
216
203
}
217
204
},
218
205
"node_modules/@atproto/api": {
219
-
"version": "0.17.4",
220
-
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.17.4.tgz",
221
-
"integrity": "sha512-MRa0WdxyDiGF7fVKd/2ldvonsHQjsaLUOGw/PHrZ7J01lqlw/jaXLS25FNNYzjPGmGpnIyDCIg4Uucd/OblI9w==",
206
+
"version": "0.17.6",
207
+
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.17.6.tgz",
208
+
"integrity": "sha512-0iYCD8+LOsHjHjwJcqGPfJN/h4b+IpU3GjOV0TSLk0XdCaxpHBKNu3wgCJVst4DhVjXcgsr2qQoRZ3Jja2LupA==",
222
209
"license": "MIT",
223
210
"dependencies": {
224
211
"@atproto/common-web": "^0.4.3",
···
281
268
"@atproto/jwk": "0.6.0",
282
269
"@atproto/jwk-jose": "0.1.11",
283
270
"zod": "^3.23.8"
271
+
}
272
+
},
273
+
"node_modules/@atproto/lex-cli": {
274
+
"version": "0.9.6",
275
+
"resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.6.tgz",
276
+
"integrity": "sha512-EedEKmURoSP735YwSDHsFrLOhZ4P2it8goCHv5ApWi/R9DFpOKOpmYfIXJ9MAprK8cw+yBnjDJbzpLJy7UXlTg==",
277
+
"dev": true,
278
+
"license": "MIT",
279
+
"dependencies": {
280
+
"@atproto/lexicon": "^0.5.1",
281
+
"@atproto/syntax": "^0.4.1",
282
+
"chalk": "^4.1.2",
283
+
"commander": "^9.4.0",
284
+
"prettier": "^3.2.5",
285
+
"ts-morph": "^24.0.0",
286
+
"yesno": "^0.4.0",
287
+
"zod": "^3.23.8"
288
+
},
289
+
"bin": {
290
+
"lex": "dist/index.js"
291
+
},
292
+
"engines": {
293
+
"node": ">=18.7.0"
294
+
}
295
+
},
296
+
"node_modules/@atproto/lex-cli/node_modules/ansi-styles": {
297
+
"version": "4.3.0",
298
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
299
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
300
+
"dev": true,
301
+
"license": "MIT",
302
+
"dependencies": {
303
+
"color-convert": "^2.0.1"
304
+
},
305
+
"engines": {
306
+
"node": ">=8"
307
+
},
308
+
"funding": {
309
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
310
+
}
311
+
},
312
+
"node_modules/@atproto/lex-cli/node_modules/chalk": {
313
+
"version": "4.1.2",
314
+
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
315
+
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
316
+
"dev": true,
317
+
"license": "MIT",
318
+
"dependencies": {
319
+
"ansi-styles": "^4.1.0",
320
+
"supports-color": "^7.1.0"
321
+
},
322
+
"engines": {
323
+
"node": ">=10"
324
+
},
325
+
"funding": {
326
+
"url": "https://github.com/chalk/chalk?sponsor=1"
284
327
}
285
328
},
286
329
"node_modules/@atproto/lexicon": {
···
1642
1685
]
1643
1686
},
1644
1687
"node_modules/@shikijs/core": {
1645
-
"version": "3.13.0",
1646
-
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.13.0.tgz",
1647
-
"integrity": "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==",
1688
+
"version": "3.14.0",
1689
+
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.14.0.tgz",
1690
+
"integrity": "sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==",
1648
1691
"license": "MIT",
1649
1692
"dependencies": {
1650
-
"@shikijs/types": "3.13.0",
1693
+
"@shikijs/types": "3.14.0",
1651
1694
"@shikijs/vscode-textmate": "^10.0.2",
1652
1695
"@types/hast": "^3.0.4",
1653
1696
"hast-util-to-html": "^9.0.5"
1654
1697
}
1655
1698
},
1656
1699
"node_modules/@shikijs/engine-javascript": {
1657
-
"version": "3.13.0",
1658
-
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.13.0.tgz",
1659
-
"integrity": "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==",
1700
+
"version": "3.14.0",
1701
+
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.14.0.tgz",
1702
+
"integrity": "sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==",
1660
1703
"license": "MIT",
1661
1704
"dependencies": {
1662
-
"@shikijs/types": "3.13.0",
1705
+
"@shikijs/types": "3.14.0",
1663
1706
"@shikijs/vscode-textmate": "^10.0.2",
1664
1707
"oniguruma-to-es": "^4.3.3"
1665
1708
}
1666
1709
},
1667
1710
"node_modules/@shikijs/engine-oniguruma": {
1668
-
"version": "3.13.0",
1669
-
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.13.0.tgz",
1670
-
"integrity": "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==",
1711
+
"version": "3.14.0",
1712
+
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz",
1713
+
"integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==",
1671
1714
"license": "MIT",
1672
1715
"dependencies": {
1673
-
"@shikijs/types": "3.13.0",
1716
+
"@shikijs/types": "3.14.0",
1674
1717
"@shikijs/vscode-textmate": "^10.0.2"
1675
1718
}
1676
1719
},
1677
1720
"node_modules/@shikijs/langs": {
1678
-
"version": "3.13.0",
1679
-
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.13.0.tgz",
1680
-
"integrity": "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==",
1721
+
"version": "3.14.0",
1722
+
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.14.0.tgz",
1723
+
"integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==",
1681
1724
"license": "MIT",
1682
1725
"dependencies": {
1683
-
"@shikijs/types": "3.13.0"
1726
+
"@shikijs/types": "3.14.0"
1684
1727
}
1685
1728
},
1686
1729
"node_modules/@shikijs/themes": {
1687
-
"version": "3.13.0",
1688
-
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.13.0.tgz",
1689
-
"integrity": "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==",
1730
+
"version": "3.14.0",
1731
+
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.14.0.tgz",
1732
+
"integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==",
1690
1733
"license": "MIT",
1691
1734
"dependencies": {
1692
-
"@shikijs/types": "3.13.0"
1735
+
"@shikijs/types": "3.14.0"
1693
1736
}
1694
1737
},
1695
1738
"node_modules/@shikijs/types": {
1696
-
"version": "3.13.0",
1697
-
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz",
1698
-
"integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==",
1739
+
"version": "3.14.0",
1740
+
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.14.0.tgz",
1741
+
"integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==",
1699
1742
"license": "MIT",
1700
1743
"dependencies": {
1701
1744
"@shikijs/vscode-textmate": "^10.0.2",
···
1974
2017
"vite": "^5.2.0 || ^6 || ^7"
1975
2018
}
1976
2019
},
2020
+
"node_modules/@ts-morph/common": {
2021
+
"version": "0.25.0",
2022
+
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz",
2023
+
"integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==",
2024
+
"dev": true,
2025
+
"license": "MIT",
2026
+
"dependencies": {
2027
+
"minimatch": "^9.0.4",
2028
+
"path-browserify": "^1.0.1",
2029
+
"tinyglobby": "^0.2.9"
2030
+
}
2031
+
},
1977
2032
"node_modules/@types/debug": {
1978
2033
"version": "4.1.12",
1979
2034
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
···
2032
2087
}
2033
2088
},
2034
2089
"node_modules/@types/node": {
2035
-
"version": "24.9.1",
2036
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
2037
-
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
2090
+
"version": "24.9.2",
2091
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
2092
+
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
2038
2093
"license": "MIT",
2039
2094
"dependencies": {
2040
2095
"undici-types": "~7.16.0"
···
2189
2244
}
2190
2245
},
2191
2246
"node_modules/astro": {
2192
-
"version": "5.15.1",
2193
-
"resolved": "https://registry.npmjs.org/astro/-/astro-5.15.1.tgz",
2194
-
"integrity": "sha512-VM679M1qxOjGo6q3vKYDNDddkALGgMopG93IwbEXd3Buc2xVLuuPj4HNziNugSbPQx5S6UReMp5uzw10EJN81A==",
2247
+
"version": "5.15.2",
2248
+
"resolved": "https://registry.npmjs.org/astro/-/astro-5.15.2.tgz",
2249
+
"integrity": "sha512-xQQ+PiYJ7WpUJrHJpAb52TQAUCFmSR8lAtQr3tFfSIZoTQiEMFx3HITJ01t3eDUpHjja8J6JcYqgAhr9xygKQg==",
2195
2250
"license": "MIT",
2196
2251
"dependencies": {
2197
2252
"@astrojs/compiler": "^2.12.2",
···
2248
2303
"unist-util-visit": "^5.0.0",
2249
2304
"unstorage": "^1.17.0",
2250
2305
"vfile": "^6.0.3",
2251
-
"vite": "^6.3.6",
2306
+
"vite": "^6.4.1",
2252
2307
"vitefu": "^1.1.1",
2253
2308
"xxhash-wasm": "^1.1.0",
2254
2309
"yargs-parser": "^21.1.1",
···
2273
2328
"sharp": "^0.34.0"
2274
2329
}
2275
2330
},
2276
-
"node_modules/autoprefixer": {
2277
-
"version": "10.4.21",
2278
-
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
2279
-
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
2280
-
"funding": [
2281
-
{
2282
-
"type": "opencollective",
2283
-
"url": "https://opencollective.com/postcss/"
2284
-
},
2285
-
{
2286
-
"type": "tidelift",
2287
-
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
2288
-
},
2289
-
{
2290
-
"type": "github",
2291
-
"url": "https://github.com/sponsors/ai"
2292
-
}
2293
-
],
2331
+
"node_modules/astro/node_modules/vite": {
2332
+
"version": "6.4.1",
2333
+
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
2334
+
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
2294
2335
"license": "MIT",
2295
2336
"dependencies": {
2296
-
"browserslist": "^4.24.4",
2297
-
"caniuse-lite": "^1.0.30001702",
2298
-
"fraction.js": "^4.3.7",
2299
-
"normalize-range": "^0.1.2",
2300
-
"picocolors": "^1.1.1",
2301
-
"postcss-value-parser": "^4.2.0"
2337
+
"esbuild": "^0.25.0",
2338
+
"fdir": "^6.4.4",
2339
+
"picomatch": "^4.0.2",
2340
+
"postcss": "^8.5.3",
2341
+
"rollup": "^4.34.9",
2342
+
"tinyglobby": "^0.2.13"
2302
2343
},
2303
2344
"bin": {
2304
-
"autoprefixer": "bin/autoprefixer"
2345
+
"vite": "bin/vite.js"
2305
2346
},
2306
2347
"engines": {
2307
-
"node": "^10 || ^12 || >=14"
2348
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2349
+
},
2350
+
"funding": {
2351
+
"url": "https://github.com/vitejs/vite?sponsor=1"
2352
+
},
2353
+
"optionalDependencies": {
2354
+
"fsevents": "~2.3.3"
2308
2355
},
2309
2356
"peerDependencies": {
2310
-
"postcss": "^8.1.0"
2357
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2358
+
"jiti": ">=1.21.0",
2359
+
"less": "*",
2360
+
"lightningcss": "^1.21.0",
2361
+
"sass": "*",
2362
+
"sass-embedded": "*",
2363
+
"stylus": "*",
2364
+
"sugarss": "*",
2365
+
"terser": "^5.16.0",
2366
+
"tsx": "^4.8.1",
2367
+
"yaml": "^2.4.2"
2368
+
},
2369
+
"peerDependenciesMeta": {
2370
+
"@types/node": {
2371
+
"optional": true
2372
+
},
2373
+
"jiti": {
2374
+
"optional": true
2375
+
},
2376
+
"less": {
2377
+
"optional": true
2378
+
},
2379
+
"lightningcss": {
2380
+
"optional": true
2381
+
},
2382
+
"sass": {
2383
+
"optional": true
2384
+
},
2385
+
"sass-embedded": {
2386
+
"optional": true
2387
+
},
2388
+
"stylus": {
2389
+
"optional": true
2390
+
},
2391
+
"sugarss": {
2392
+
"optional": true
2393
+
},
2394
+
"terser": {
2395
+
"optional": true
2396
+
},
2397
+
"tsx": {
2398
+
"optional": true
2399
+
},
2400
+
"yaml": {
2401
+
"optional": true
2402
+
}
2311
2403
}
2312
2404
},
2313
2405
"node_modules/await-lock": {
···
2335
2427
"url": "https://github.com/sponsors/wooorm"
2336
2428
}
2337
2429
},
2430
+
"node_modules/balanced-match": {
2431
+
"version": "1.0.2",
2432
+
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
2433
+
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
2434
+
"dev": true,
2435
+
"license": "MIT"
2436
+
},
2338
2437
"node_modules/base-64": {
2339
2438
"version": "1.0.0",
2340
2439
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
···
2361
2460
],
2362
2461
"license": "MIT"
2363
2462
},
2364
-
"node_modules/baseline-browser-mapping": {
2365
-
"version": "2.8.20",
2366
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz",
2367
-
"integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==",
2368
-
"license": "Apache-2.0",
2369
-
"bin": {
2370
-
"baseline-browser-mapping": "dist/cli.js"
2371
-
}
2372
-
},
2373
2463
"node_modules/boxen": {
2374
2464
"version": "8.0.1",
2375
2465
"resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
···
2392
2482
"url": "https://github.com/sponsors/sindresorhus"
2393
2483
}
2394
2484
},
2485
+
"node_modules/brace-expansion": {
2486
+
"version": "2.0.2",
2487
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
2488
+
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
2489
+
"dev": true,
2490
+
"license": "MIT",
2491
+
"dependencies": {
2492
+
"balanced-match": "^1.0.0"
2493
+
}
2494
+
},
2395
2495
"node_modules/brotli": {
2396
2496
"version": "1.3.3",
2397
2497
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
···
2401
2501
"base64-js": "^1.1.2"
2402
2502
}
2403
2503
},
2404
-
"node_modules/browserslist": {
2405
-
"version": "4.27.0",
2406
-
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
2407
-
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
2408
-
"funding": [
2409
-
{
2410
-
"type": "opencollective",
2411
-
"url": "https://opencollective.com/browserslist"
2412
-
},
2413
-
{
2414
-
"type": "tidelift",
2415
-
"url": "https://tidelift.com/funding/github/npm/browserslist"
2416
-
},
2417
-
{
2418
-
"type": "github",
2419
-
"url": "https://github.com/sponsors/ai"
2420
-
}
2421
-
],
2422
-
"license": "MIT",
2423
-
"dependencies": {
2424
-
"baseline-browser-mapping": "^2.8.19",
2425
-
"caniuse-lite": "^1.0.30001751",
2426
-
"electron-to-chromium": "^1.5.238",
2427
-
"node-releases": "^2.0.26",
2428
-
"update-browserslist-db": "^1.1.4"
2429
-
},
2430
-
"bin": {
2431
-
"browserslist": "cli.js"
2432
-
},
2433
-
"engines": {
2434
-
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
2435
-
}
2436
-
},
2437
2504
"node_modules/camelcase": {
2438
2505
"version": "8.0.0",
2439
2506
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
···
2445
2512
"funding": {
2446
2513
"url": "https://github.com/sponsors/sindresorhus"
2447
2514
}
2448
-
},
2449
-
"node_modules/caniuse-lite": {
2450
-
"version": "1.0.30001751",
2451
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
2452
-
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
2453
-
"funding": [
2454
-
{
2455
-
"type": "opencollective",
2456
-
"url": "https://opencollective.com/browserslist"
2457
-
},
2458
-
{
2459
-
"type": "tidelift",
2460
-
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
2461
-
},
2462
-
{
2463
-
"type": "github",
2464
-
"url": "https://github.com/sponsors/ai"
2465
-
}
2466
-
],
2467
-
"license": "CC-BY-4.0"
2468
2515
},
2469
2516
"node_modules/ccount": {
2470
2517
"version": "2.0.1",
···
2578
2625
"node": ">=6"
2579
2626
}
2580
2627
},
2628
+
"node_modules/code-block-writer": {
2629
+
"version": "13.0.3",
2630
+
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
2631
+
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
2632
+
"dev": true,
2633
+
"license": "MIT"
2634
+
},
2635
+
"node_modules/color-convert": {
2636
+
"version": "2.0.1",
2637
+
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
2638
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
2639
+
"dev": true,
2640
+
"license": "MIT",
2641
+
"dependencies": {
2642
+
"color-name": "~1.1.4"
2643
+
},
2644
+
"engines": {
2645
+
"node": ">=7.0.0"
2646
+
}
2647
+
},
2648
+
"node_modules/color-name": {
2649
+
"version": "1.1.4",
2650
+
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
2651
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
2652
+
"dev": true,
2653
+
"license": "MIT"
2654
+
},
2581
2655
"node_modules/comma-separated-tokens": {
2582
2656
"version": "2.0.3",
2583
2657
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
···
2586
2660
"funding": {
2587
2661
"type": "github",
2588
2662
"url": "https://github.com/sponsors/wooorm"
2663
+
}
2664
+
},
2665
+
"node_modules/commander": {
2666
+
"version": "9.5.0",
2667
+
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
2668
+
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
2669
+
"dev": true,
2670
+
"license": "MIT",
2671
+
"engines": {
2672
+
"node": "^12.20.0 || >=14"
2589
2673
}
2590
2674
},
2591
2675
"node_modules/common-ancestor-path": {
···
2655
2739
}
2656
2740
},
2657
2741
"node_modules/daisyui": {
2658
-
"version": "5.3.9",
2659
-
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.9.tgz",
2660
-
"integrity": "sha512-741x1pGGSGHcrBYtdE7iKbqW1OoiijYdAZ8oJPZR9MhSKLcMBlHjKfN3YlM2/K7t5jd7O0sg4SqkVNPylalRFw==",
2742
+
"version": "5.3.10",
2743
+
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz",
2744
+
"integrity": "sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==",
2661
2745
"license": "MIT",
2662
2746
"funding": {
2663
2747
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
···
2811
2895
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
2812
2896
"license": "MIT"
2813
2897
},
2814
-
"node_modules/electron-to-chromium": {
2815
-
"version": "1.5.240",
2816
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz",
2817
-
"integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==",
2818
-
"license": "ISC"
2819
-
},
2820
2898
"node_modules/emoji-regex": {
2821
2899
"version": "10.6.0",
2822
2900
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
···
2904
2982
"@esbuild/win32-x64": "0.25.11"
2905
2983
}
2906
2984
},
2907
-
"node_modules/escalade": {
2908
-
"version": "3.2.0",
2909
-
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
2910
-
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
2911
-
"license": "MIT",
2912
-
"engines": {
2913
-
"node": ">=6"
2914
-
}
2915
-
},
2916
2985
"node_modules/escape-html": {
2917
2986
"version": "1.0.3",
2918
2987
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
···
3020
3089
"unicode-trie": "^2.0.0"
3021
3090
}
3022
3091
},
3023
-
"node_modules/fraction.js": {
3024
-
"version": "4.3.7",
3025
-
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
3026
-
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
3027
-
"license": "MIT",
3028
-
"engines": {
3029
-
"node": "*"
3030
-
},
3031
-
"funding": {
3032
-
"type": "patreon",
3033
-
"url": "https://github.com/sponsors/rawify"
3034
-
}
3035
-
},
3036
3092
"node_modules/fresh": {
3037
3093
"version": "2.0.0",
3038
3094
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
···
3101
3157
"radix3": "^1.1.2",
3102
3158
"ufo": "^1.6.1",
3103
3159
"uncrypto": "^0.1.3"
3160
+
}
3161
+
},
3162
+
"node_modules/has-flag": {
3163
+
"version": "4.0.0",
3164
+
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
3165
+
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
3166
+
"dev": true,
3167
+
"license": "MIT",
3168
+
"engines": {
3169
+
"node": ">=8"
3104
3170
}
3105
3171
},
3106
3172
"node_modules/hast-util-from-html": {
···
3734
3800
"url": "https://opencollective.com/parcel"
3735
3801
}
3736
3802
},
3737
-
"node_modules/lilconfig": {
3738
-
"version": "3.1.3",
3739
-
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
3740
-
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
3741
-
"license": "MIT",
3742
-
"engines": {
3743
-
"node": ">=14"
3744
-
},
3745
-
"funding": {
3746
-
"url": "https://github.com/sponsors/antonk52"
3747
-
}
3748
-
},
3749
3803
"node_modules/longest-streak": {
3750
3804
"version": "3.1.0",
3751
3805
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
···
4607
4661
"node": ">= 0.6"
4608
4662
}
4609
4663
},
4664
+
"node_modules/minimatch": {
4665
+
"version": "9.0.5",
4666
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
4667
+
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
4668
+
"dev": true,
4669
+
"license": "ISC",
4670
+
"dependencies": {
4671
+
"brace-expansion": "^2.0.1"
4672
+
},
4673
+
"engines": {
4674
+
"node": ">=16 || 14 >=14.17"
4675
+
},
4676
+
"funding": {
4677
+
"url": "https://github.com/sponsors/isaacs"
4678
+
}
4679
+
},
4610
4680
"node_modules/mrmime": {
4611
4681
"version": "2.0.1",
4612
4682
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
···
4680
4750
"integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==",
4681
4751
"license": "MIT"
4682
4752
},
4683
-
"node_modules/node-releases": {
4684
-
"version": "2.0.26",
4685
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
4686
-
"integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
4687
-
"license": "MIT"
4688
-
},
4689
4753
"node_modules/normalize-path": {
4690
4754
"version": "3.0.0",
4691
4755
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
···
4695
4759
"node": ">=0.10.0"
4696
4760
}
4697
4761
},
4698
-
"node_modules/normalize-range": {
4699
-
"version": "0.1.2",
4700
-
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
4701
-
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
4702
-
"license": "MIT",
4703
-
"engines": {
4704
-
"node": ">=0.10.0"
4705
-
}
4706
-
},
4707
4762
"node_modules/ofetch": {
4708
-
"version": "1.4.1",
4709
-
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz",
4710
-
"integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==",
4763
+
"version": "1.5.0",
4764
+
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.0.tgz",
4765
+
"integrity": "sha512-A7llJ7eZyziA5xq9//3ZurA8OhFqtS99K5/V1sLBJ5j137CM/OAjlbA/TEJXBuOWwOfLqih+oH5U3ran4za1FQ==",
4711
4766
"license": "MIT",
4712
4767
"dependencies": {
4713
-
"destr": "^2.0.3",
4714
-
"node-fetch-native": "^1.6.4",
4715
-
"ufo": "^1.5.4"
4768
+
"destr": "^2.0.5",
4769
+
"node-fetch-native": "^1.6.7",
4770
+
"ufo": "^1.6.1"
4716
4771
}
4717
4772
},
4718
4773
"node_modules/ohash": {
···
4835
4890
"url": "https://github.com/inikulin/parse5?sponsor=1"
4836
4891
}
4837
4892
},
4893
+
"node_modules/path-browserify": {
4894
+
"version": "1.0.1",
4895
+
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
4896
+
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
4897
+
"dev": true,
4898
+
"license": "MIT"
4899
+
},
4838
4900
"node_modules/picocolors": {
4839
4901
"version": "1.1.1",
4840
4902
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
···
4881
4943
"node": "^10 || ^12 || >=14"
4882
4944
}
4883
4945
},
4884
-
"node_modules/postcss-load-config": {
4885
-
"version": "4.0.2",
4886
-
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
4887
-
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
4888
-
"funding": [
4889
-
{
4890
-
"type": "opencollective",
4891
-
"url": "https://opencollective.com/postcss/"
4892
-
},
4893
-
{
4894
-
"type": "github",
4895
-
"url": "https://github.com/sponsors/ai"
4896
-
}
4897
-
],
4946
+
"node_modules/prettier": {
4947
+
"version": "3.6.2",
4948
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
4949
+
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
4950
+
"dev": true,
4898
4951
"license": "MIT",
4899
-
"dependencies": {
4900
-
"lilconfig": "^3.0.0",
4901
-
"yaml": "^2.3.4"
4952
+
"bin": {
4953
+
"prettier": "bin/prettier.cjs"
4902
4954
},
4903
4955
"engines": {
4904
-
"node": ">= 14"
4956
+
"node": ">=14"
4905
4957
},
4906
-
"peerDependencies": {
4907
-
"postcss": ">=8.0.9",
4908
-
"ts-node": ">=9.0.0"
4909
-
},
4910
-
"peerDependenciesMeta": {
4911
-
"postcss": {
4912
-
"optional": true
4913
-
},
4914
-
"ts-node": {
4915
-
"optional": true
4916
-
}
4958
+
"funding": {
4959
+
"url": "https://github.com/prettier/prettier?sponsor=1"
4917
4960
}
4918
-
},
4919
-
"node_modules/postcss-value-parser": {
4920
-
"version": "4.2.0",
4921
-
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
4922
-
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
4923
-
"license": "MIT"
4924
4961
},
4925
4962
"node_modules/prismjs": {
4926
4963
"version": "1.30.0",
···
5346
5383
}
5347
5384
},
5348
5385
"node_modules/shiki": {
5349
-
"version": "3.13.0",
5350
-
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.13.0.tgz",
5351
-
"integrity": "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==",
5386
+
"version": "3.14.0",
5387
+
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.14.0.tgz",
5388
+
"integrity": "sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==",
5352
5389
"license": "MIT",
5353
5390
"dependencies": {
5354
-
"@shikijs/core": "3.13.0",
5355
-
"@shikijs/engine-javascript": "3.13.0",
5356
-
"@shikijs/engine-oniguruma": "3.13.0",
5357
-
"@shikijs/langs": "3.13.0",
5358
-
"@shikijs/themes": "3.13.0",
5359
-
"@shikijs/types": "3.13.0",
5391
+
"@shikijs/core": "3.14.0",
5392
+
"@shikijs/engine-javascript": "3.14.0",
5393
+
"@shikijs/engine-oniguruma": "3.14.0",
5394
+
"@shikijs/langs": "3.14.0",
5395
+
"@shikijs/themes": "3.14.0",
5396
+
"@shikijs/types": "3.14.0",
5360
5397
"@shikijs/vscode-textmate": "^10.0.2",
5361
5398
"@types/hast": "^3.0.4"
5362
5399
}
···
5453
5490
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
5454
5491
}
5455
5492
},
5493
+
"node_modules/supports-color": {
5494
+
"version": "7.2.0",
5495
+
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
5496
+
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
5497
+
"dev": true,
5498
+
"license": "MIT",
5499
+
"dependencies": {
5500
+
"has-flag": "^4.0.0"
5501
+
},
5502
+
"engines": {
5503
+
"node": ">=8"
5504
+
}
5505
+
},
5456
5506
"node_modules/tailwindcss": {
5457
5507
"version": "4.1.16",
5458
5508
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
···
5538
5588
"url": "https://github.com/sponsors/wooorm"
5539
5589
}
5540
5590
},
5591
+
"node_modules/ts-morph": {
5592
+
"version": "24.0.0",
5593
+
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz",
5594
+
"integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==",
5595
+
"dev": true,
5596
+
"license": "MIT",
5597
+
"dependencies": {
5598
+
"@ts-morph/common": "~0.25.0",
5599
+
"code-block-writer": "^13.0.3"
5600
+
}
5601
+
},
5541
5602
"node_modules/tsconfck": {
5542
5603
"version": "3.1.6",
5543
5604
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
···
5574
5635
},
5575
5636
"funding": {
5576
5637
"url": "https://github.com/sponsors/sindresorhus"
5638
+
}
5639
+
},
5640
+
"node_modules/typescript": {
5641
+
"version": "5.9.3",
5642
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
5643
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
5644
+
"license": "Apache-2.0",
5645
+
"peer": true,
5646
+
"bin": {
5647
+
"tsc": "bin/tsc",
5648
+
"tsserver": "bin/tsserver"
5649
+
},
5650
+
"engines": {
5651
+
"node": ">=14.17"
5577
5652
}
5578
5653
},
5579
5654
"node_modules/ufo": {
···
5887
5962
}
5888
5963
}
5889
5964
},
5890
-
"node_modules/update-browserslist-db": {
5891
-
"version": "1.1.4",
5892
-
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
5893
-
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
5894
-
"funding": [
5895
-
{
5896
-
"type": "opencollective",
5897
-
"url": "https://opencollective.com/browserslist"
5898
-
},
5899
-
{
5900
-
"type": "tidelift",
5901
-
"url": "https://tidelift.com/funding/github/npm/browserslist"
5902
-
},
5903
-
{
5904
-
"type": "github",
5905
-
"url": "https://github.com/sponsors/ai"
5906
-
}
5907
-
],
5908
-
"license": "MIT",
5909
-
"dependencies": {
5910
-
"escalade": "^3.2.0",
5911
-
"picocolors": "^1.1.1"
5912
-
},
5913
-
"bin": {
5914
-
"update-browserslist-db": "cli.js"
5915
-
},
5916
-
"peerDependencies": {
5917
-
"browserslist": ">= 4.21.0"
5918
-
}
5919
-
},
5920
5965
"node_modules/vfile": {
5921
5966
"version": "6.0.3",
5922
5967
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
···
5960
6005
}
5961
6006
},
5962
6007
"node_modules/vite": {
5963
-
"version": "6.4.1",
5964
-
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
5965
-
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
6008
+
"version": "7.1.12",
6009
+
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
6010
+
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
5966
6011
"license": "MIT",
6012
+
"peer": true,
5967
6013
"dependencies": {
5968
6014
"esbuild": "^0.25.0",
5969
-
"fdir": "^6.4.4",
5970
-
"picomatch": "^4.0.2",
5971
-
"postcss": "^8.5.3",
5972
-
"rollup": "^4.34.9",
5973
-
"tinyglobby": "^0.2.13"
6015
+
"fdir": "^6.5.0",
6016
+
"picomatch": "^4.0.3",
6017
+
"postcss": "^8.5.6",
6018
+
"rollup": "^4.43.0",
6019
+
"tinyglobby": "^0.2.15"
5974
6020
},
5975
6021
"bin": {
5976
6022
"vite": "bin/vite.js"
5977
6023
},
5978
6024
"engines": {
5979
-
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
6025
+
"node": "^20.19.0 || >=22.12.0"
5980
6026
},
5981
6027
"funding": {
5982
6028
"url": "https://github.com/vitejs/vite?sponsor=1"
···
5985
6031
"fsevents": "~2.3.3"
5986
6032
},
5987
6033
"peerDependencies": {
5988
-
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
6034
+
"@types/node": "^20.19.0 || >=22.12.0",
5989
6035
"jiti": ">=1.21.0",
5990
-
"less": "*",
6036
+
"less": "^4.0.0",
5991
6037
"lightningcss": "^1.21.0",
5992
-
"sass": "*",
5993
-
"sass-embedded": "*",
5994
-
"stylus": "*",
5995
-
"sugarss": "*",
6038
+
"sass": "^1.70.0",
6039
+
"sass-embedded": "^1.70.0",
6040
+
"stylus": ">=0.54.8",
6041
+
"sugarss": "^5.0.0",
5996
6042
"terser": "^5.16.0",
5997
6043
"tsx": "^4.8.1",
5998
6044
"yaml": "^2.4.2"
···
6109
6155
"integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==",
6110
6156
"license": "MIT"
6111
6157
},
6112
-
"node_modules/yaml": {
6113
-
"version": "2.8.1",
6114
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
6115
-
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
6116
-
"license": "ISC",
6117
-
"bin": {
6118
-
"yaml": "bin.mjs"
6119
-
},
6120
-
"engines": {
6121
-
"node": ">= 14.6"
6122
-
}
6123
-
},
6124
6158
"node_modules/yargs-parser": {
6125
6159
"version": "21.1.1",
6126
6160
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
···
6129
6163
"engines": {
6130
6164
"node": ">=12"
6131
6165
}
6166
+
},
6167
+
"node_modules/yesno": {
6168
+
"version": "0.4.0",
6169
+
"resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz",
6170
+
"integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==",
6171
+
"dev": true,
6172
+
"license": "BSD"
6132
6173
},
6133
6174
"node_modules/yocto-queue": {
6134
6175
"version": "1.2.1",
+5
-2
package.json
+5
-2
package.json
···
6
6
"dev": "astro dev",
7
7
"build": "astro build",
8
8
"preview": "astro preview",
9
-
"astro": "astro"
9
+
"astro": "astro",
10
+
"lexgen": "lex gen-server ./src/lexicon ./lexicons/*"
10
11
},
11
12
"dependencies": {
12
13
"@astrojs/node": "^9.5.0",
13
-
"@astrojs/tailwind": "^6.0.2",
14
14
"@atproto/api": "^0.17.4",
15
15
"@atproto/oauth-client-node": "^0.3.10",
16
16
"@tailwindcss/vite": "^4.1.16",
···
18
18
"daisyui": "^5.3.9",
19
19
"dotenv": "^17.2.3",
20
20
"tailwindcss": "^4.1.16"
21
+
},
22
+
"devDependencies": {
23
+
"@atproto/lex-cli": "^0.9.6"
21
24
}
22
25
}
+124
src/components/ProfileForm.astro
+124
src/components/ProfileForm.astro
···
1
+
---
2
+
interface Props {
3
+
displayName?: string
4
+
description?: string
5
+
avatar?: string
6
+
banner?: string
7
+
submitLabel?: string
8
+
action?: string
9
+
}
10
+
11
+
const {
12
+
displayName = '',
13
+
description = '',
14
+
avatar = '',
15
+
banner = '',
16
+
submitLabel = 'Create Profile',
17
+
action = '/api/profile/create'
18
+
} = Astro.props
19
+
---
20
+
21
+
<form
22
+
method="POST"
23
+
action={action}
24
+
enctype="multipart/form-data"
25
+
class="space-y-4"
26
+
>
27
+
<div class="form-control">
28
+
<label class="label">
29
+
<span class="label-text">Display Name</span>
30
+
<span class="label-text-alt">Max 64 characters</span>
31
+
</label>
32
+
<input
33
+
type="text"
34
+
name="displayName"
35
+
placeholder="Enter your display name"
36
+
class="input input-bordered w-full"
37
+
value={displayName}
38
+
maxlength="64"
39
+
required
40
+
/>
41
+
</div>
42
+
43
+
<div class="form-control">
44
+
<label class="label">
45
+
<span class="label-text">Description</span>
46
+
<span class="label-text-alt">Max 256 characters</span>
47
+
</label>
48
+
<textarea
49
+
name="description"
50
+
placeholder="Tell us about yourself"
51
+
class="textarea textarea-bordered h-24"
52
+
maxlength="256"
53
+
>{description}</textarea>
54
+
</div>
55
+
56
+
<div class="form-control">
57
+
<label class="label">
58
+
<span class="label-text">Avatar</span>
59
+
<span class="label-text-alt">PNG or JPEG, max 1MB</span>
60
+
</label>
61
+
{avatar && (
62
+
<div class="avatar mb-2">
63
+
<div class="w-24 rounded-full">
64
+
<img src={avatar} alt="Current avatar" />
65
+
</div>
66
+
</div>
67
+
)}
68
+
<input
69
+
type="file"
70
+
name="avatar"
71
+
accept="image/png,image/jpeg"
72
+
class="file-input file-input-bordered w-full"
73
+
/>
74
+
</div>
75
+
76
+
<div class="form-control">
77
+
<label class="label">
78
+
<span class="label-text">Banner</span>
79
+
<span class="label-text-alt">PNG or JPEG, max 1MB</span>
80
+
</label>
81
+
{banner && (
82
+
<div class="mb-2">
83
+
<img src={banner} alt="Current banner" class="w-full h-32 object-cover rounded-lg" />
84
+
</div>
85
+
)}
86
+
<input
87
+
type="file"
88
+
name="banner"
89
+
accept="image/png,image/jpeg"
90
+
class="file-input file-input-bordered w-full"
91
+
/>
92
+
</div>
93
+
94
+
<div class="form-control mt-6">
95
+
<button type="submit" class="btn btn-primary w-full">
96
+
{submitLabel}
97
+
</button>
98
+
</div>
99
+
</form>
100
+
101
+
<script>
102
+
// Client-side validation for file sizes
103
+
const form = document.querySelector('form')
104
+
if (form) {
105
+
form.addEventListener('submit', (e) => {
106
+
const avatarInput = form.querySelector('input[name="avatar"]') as HTMLInputElement
107
+
const bannerInput = form.querySelector('input[name="banner"]') as HTMLInputElement
108
+
109
+
const maxSize = 1000000 // 1MB
110
+
111
+
if (avatarInput?.files?.[0] && avatarInput.files[0].size > maxSize) {
112
+
e.preventDefault()
113
+
alert('Avatar file size must be less than 1MB')
114
+
return
115
+
}
116
+
117
+
if (bannerInput?.files?.[0] && bannerInput.files[0].size > maxSize) {
118
+
e.preventDefault()
119
+
alert('Banner file size must be less than 1MB')
120
+
return
121
+
}
122
+
})
123
+
}
124
+
</script>
+74
src/lexicon/index.ts
+74
src/lexicon/index.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import {
5
+
type Auth,
6
+
type Options as XrpcOptions,
7
+
Server as XrpcServer,
8
+
type StreamConfigOrHandler,
9
+
type MethodConfigOrHandler,
10
+
createServer as createXrpcServer,
11
+
} from '@atproto/xrpc-server'
12
+
import { schemas } from './lexicons.js'
13
+
14
+
export function createServer(options?: XrpcOptions): Server {
15
+
return new Server(options)
16
+
}
17
+
18
+
export class Server {
19
+
xrpc: XrpcServer
20
+
org: OrgNS
21
+
com: ComNS
22
+
23
+
constructor(options?: XrpcOptions) {
24
+
this.xrpc = createXrpcServer(schemas, options)
25
+
this.org = new OrgNS(this)
26
+
this.com = new ComNS(this)
27
+
}
28
+
}
29
+
30
+
export class OrgNS {
31
+
_server: Server
32
+
atmosphereconf: OrgAtmosphereconfNS
33
+
34
+
constructor(server: Server) {
35
+
this._server = server
36
+
this.atmosphereconf = new OrgAtmosphereconfNS(server)
37
+
}
38
+
}
39
+
40
+
export class OrgAtmosphereconfNS {
41
+
_server: Server
42
+
43
+
constructor(server: Server) {
44
+
this._server = server
45
+
}
46
+
}
47
+
48
+
export class ComNS {
49
+
_server: Server
50
+
atproto: ComAtprotoNS
51
+
52
+
constructor(server: Server) {
53
+
this._server = server
54
+
this.atproto = new ComAtprotoNS(server)
55
+
}
56
+
}
57
+
58
+
export class ComAtprotoNS {
59
+
_server: Server
60
+
repo: ComAtprotoRepoNS
61
+
62
+
constructor(server: Server) {
63
+
this._server = server
64
+
this.repo = new ComAtprotoRepoNS(server)
65
+
}
66
+
}
67
+
68
+
export class ComAtprotoRepoNS {
69
+
_server: Server
70
+
71
+
constructor(server: Server) {
72
+
this._server = server
73
+
}
74
+
}
+298
src/lexicon/lexicons.ts
+298
src/lexicon/lexicons.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import {
5
+
type LexiconDoc,
6
+
Lexicons,
7
+
ValidationError,
8
+
type ValidationResult,
9
+
} from '@atproto/lexicon'
10
+
import { type $Typed, is$typed, maybe$typed } from './util.js'
11
+
12
+
export const schemaDict = {
13
+
ComAtprotoLabelDefs: {
14
+
lexicon: 1,
15
+
id: 'com.atproto.label.defs',
16
+
defs: {
17
+
label: {
18
+
type: 'object',
19
+
description:
20
+
'Metadata tag on an atproto resource (eg, repo or record).',
21
+
required: ['src', 'uri', 'val', 'cts'],
22
+
properties: {
23
+
ver: {
24
+
type: 'integer',
25
+
description: 'The AT Protocol version of the label object.',
26
+
},
27
+
src: {
28
+
type: 'string',
29
+
format: 'did',
30
+
description: 'DID of the actor who created this label.',
31
+
},
32
+
uri: {
33
+
type: 'string',
34
+
format: 'uri',
35
+
description:
36
+
'AT URI of the record, repository (account), or other resource that this label applies to.',
37
+
},
38
+
cid: {
39
+
type: 'string',
40
+
format: 'cid',
41
+
description:
42
+
"Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
43
+
},
44
+
val: {
45
+
type: 'string',
46
+
maxLength: 128,
47
+
description:
48
+
'The short string name of the value or type of this label.',
49
+
},
50
+
neg: {
51
+
type: 'boolean',
52
+
description:
53
+
'If true, this is a negation label, overwriting a previous label.',
54
+
},
55
+
cts: {
56
+
type: 'string',
57
+
format: 'datetime',
58
+
description: 'Timestamp when this label was created.',
59
+
},
60
+
exp: {
61
+
type: 'string',
62
+
format: 'datetime',
63
+
description:
64
+
'Timestamp at which this label expires (no longer applies).',
65
+
},
66
+
sig: {
67
+
type: 'bytes',
68
+
description: 'Signature of dag-cbor encoded label.',
69
+
},
70
+
},
71
+
},
72
+
selfLabels: {
73
+
type: 'object',
74
+
description:
75
+
'Metadata tags on an atproto record, published by the author within the record.',
76
+
required: ['values'],
77
+
properties: {
78
+
values: {
79
+
type: 'array',
80
+
items: {
81
+
type: 'ref',
82
+
ref: 'lex:com.atproto.label.defs#selfLabel',
83
+
},
84
+
maxLength: 10,
85
+
},
86
+
},
87
+
},
88
+
selfLabel: {
89
+
type: 'object',
90
+
description:
91
+
'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',
92
+
required: ['val'],
93
+
properties: {
94
+
val: {
95
+
type: 'string',
96
+
maxLength: 128,
97
+
description:
98
+
'The short string name of the value or type of this label.',
99
+
},
100
+
},
101
+
},
102
+
labelValueDefinition: {
103
+
type: 'object',
104
+
description:
105
+
'Declares a label value and its expected interpretations and behaviors.',
106
+
required: ['identifier', 'severity', 'blurs', 'locales'],
107
+
properties: {
108
+
identifier: {
109
+
type: 'string',
110
+
description:
111
+
"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
112
+
maxLength: 100,
113
+
maxGraphemes: 100,
114
+
},
115
+
severity: {
116
+
type: 'string',
117
+
description:
118
+
"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
119
+
knownValues: ['inform', 'alert', 'none'],
120
+
},
121
+
blurs: {
122
+
type: 'string',
123
+
description:
124
+
"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
125
+
knownValues: ['content', 'media', 'none'],
126
+
},
127
+
defaultSetting: {
128
+
type: 'string',
129
+
description: 'The default setting for this label.',
130
+
knownValues: ['ignore', 'warn', 'hide'],
131
+
default: 'warn',
132
+
},
133
+
adultOnly: {
134
+
type: 'boolean',
135
+
description:
136
+
'Does the user need to have adult content enabled in order to configure this label?',
137
+
},
138
+
locales: {
139
+
type: 'array',
140
+
items: {
141
+
type: 'ref',
142
+
ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',
143
+
},
144
+
},
145
+
},
146
+
},
147
+
labelValueDefinitionStrings: {
148
+
type: 'object',
149
+
description:
150
+
'Strings which describe the label in the UI, localized into a specific language.',
151
+
required: ['lang', 'name', 'description'],
152
+
properties: {
153
+
lang: {
154
+
type: 'string',
155
+
description:
156
+
'The code of the language these strings are written in.',
157
+
format: 'language',
158
+
},
159
+
name: {
160
+
type: 'string',
161
+
description: 'A short human-readable name for the label.',
162
+
maxGraphemes: 64,
163
+
maxLength: 640,
164
+
},
165
+
description: {
166
+
type: 'string',
167
+
description:
168
+
'A longer description of what the label means and why it might be applied.',
169
+
maxGraphemes: 10000,
170
+
maxLength: 100000,
171
+
},
172
+
},
173
+
},
174
+
labelValue: {
175
+
type: 'string',
176
+
knownValues: [
177
+
'!hide',
178
+
'!no-promote',
179
+
'!warn',
180
+
'!no-unauthenticated',
181
+
'dmca-violation',
182
+
'doxxing',
183
+
'porn',
184
+
'sexual',
185
+
'nudity',
186
+
'nsfl',
187
+
'gore',
188
+
],
189
+
},
190
+
},
191
+
},
192
+
OrgAtmosphereconfProfile: {
193
+
lexicon: 1,
194
+
id: 'org.atmosphereconf.profile',
195
+
defs: {
196
+
main: {
197
+
type: 'record',
198
+
description: 'A conference attendee profile.',
199
+
key: 'literal:self',
200
+
record: {
201
+
type: 'object',
202
+
properties: {
203
+
displayName: {
204
+
type: 'string',
205
+
maxGraphemes: 64,
206
+
maxLength: 640,
207
+
},
208
+
description: {
209
+
type: 'string',
210
+
description: 'Free-form profile description text.',
211
+
maxGraphemes: 256,
212
+
maxLength: 2560,
213
+
},
214
+
avatar: {
215
+
type: 'blob',
216
+
description: 'Profile picture for conference attendee',
217
+
accept: ['image/png', 'image/jpeg'],
218
+
maxSize: 1000000,
219
+
},
220
+
banner: {
221
+
type: 'blob',
222
+
description:
223
+
'Larger horizontal image to display behind profile view.',
224
+
accept: ['image/png', 'image/jpeg'],
225
+
maxSize: 1000000,
226
+
},
227
+
labels: {
228
+
type: 'union',
229
+
description: 'Self-label values for the conference profile.',
230
+
refs: ['lex:com.atproto.label.defs#selfLabels'],
231
+
},
232
+
createdAt: {
233
+
type: 'string',
234
+
format: 'datetime',
235
+
},
236
+
},
237
+
},
238
+
},
239
+
},
240
+
},
241
+
ComAtprotoRepoStrongRef: {
242
+
lexicon: 1,
243
+
id: 'com.atproto.repo.strongRef',
244
+
description: 'A URI with a content-hash fingerprint.',
245
+
defs: {
246
+
main: {
247
+
type: 'object',
248
+
required: ['uri', 'cid'],
249
+
properties: {
250
+
uri: {
251
+
type: 'string',
252
+
format: 'at-uri',
253
+
},
254
+
cid: {
255
+
type: 'string',
256
+
format: 'cid',
257
+
},
258
+
},
259
+
},
260
+
},
261
+
},
262
+
} as const satisfies Record<string, LexiconDoc>
263
+
export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]
264
+
export const lexicons: Lexicons = new Lexicons(schemas)
265
+
266
+
export function validate<T extends { $type: string }>(
267
+
v: unknown,
268
+
id: string,
269
+
hash: string,
270
+
requiredType: true,
271
+
): ValidationResult<T>
272
+
export function validate<T extends { $type?: string }>(
273
+
v: unknown,
274
+
id: string,
275
+
hash: string,
276
+
requiredType?: false,
277
+
): ValidationResult<T>
278
+
export function validate(
279
+
v: unknown,
280
+
id: string,
281
+
hash: string,
282
+
requiredType?: boolean,
283
+
): ValidationResult {
284
+
return (requiredType ? is$typed : maybe$typed)(v, id, hash)
285
+
? lexicons.validate(`${id}#${hash}`, v)
286
+
: {
287
+
success: false,
288
+
error: new ValidationError(
289
+
`Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`,
290
+
),
291
+
}
292
+
}
293
+
294
+
export const ids = {
295
+
ComAtprotoLabelDefs: 'com.atproto.label.defs',
296
+
OrgAtmosphereconfProfile: 'org.atmosphereconf.profile',
297
+
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
298
+
} as const
+146
src/lexicon/types/com/atproto/label/defs.ts
+146
src/lexicon/types/com/atproto/label/defs.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+
import { CID } from 'multiformats/cid'
6
+
import { validate as _validate } from '../../../../lexicons'
7
+
import {
8
+
type $Typed,
9
+
is$typed as _is$typed,
10
+
type OmitKey,
11
+
} from '../../../../util'
12
+
13
+
const is$typed = _is$typed,
14
+
validate = _validate
15
+
const id = 'com.atproto.label.defs'
16
+
17
+
/** Metadata tag on an atproto resource (eg, repo or record). */
18
+
export interface Label {
19
+
$type?: 'com.atproto.label.defs#label'
20
+
/** The AT Protocol version of the label object. */
21
+
ver?: number
22
+
/** DID of the actor who created this label. */
23
+
src: string
24
+
/** AT URI of the record, repository (account), or other resource that this label applies to. */
25
+
uri: string
26
+
/** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */
27
+
cid?: string
28
+
/** The short string name of the value or type of this label. */
29
+
val: string
30
+
/** If true, this is a negation label, overwriting a previous label. */
31
+
neg?: boolean
32
+
/** Timestamp when this label was created. */
33
+
cts: string
34
+
/** Timestamp at which this label expires (no longer applies). */
35
+
exp?: string
36
+
/** Signature of dag-cbor encoded label. */
37
+
sig?: Uint8Array
38
+
}
39
+
40
+
const hashLabel = 'label'
41
+
42
+
export function isLabel<V>(v: V) {
43
+
return is$typed(v, id, hashLabel)
44
+
}
45
+
46
+
export function validateLabel<V>(v: V) {
47
+
return validate<Label & V>(v, id, hashLabel)
48
+
}
49
+
50
+
/** Metadata tags on an atproto record, published by the author within the record. */
51
+
export interface SelfLabels {
52
+
$type?: 'com.atproto.label.defs#selfLabels'
53
+
values: SelfLabel[]
54
+
}
55
+
56
+
const hashSelfLabels = 'selfLabels'
57
+
58
+
export function isSelfLabels<V>(v: V) {
59
+
return is$typed(v, id, hashSelfLabels)
60
+
}
61
+
62
+
export function validateSelfLabels<V>(v: V) {
63
+
return validate<SelfLabels & V>(v, id, hashSelfLabels)
64
+
}
65
+
66
+
/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
67
+
export interface SelfLabel {
68
+
$type?: 'com.atproto.label.defs#selfLabel'
69
+
/** The short string name of the value or type of this label. */
70
+
val: string
71
+
}
72
+
73
+
const hashSelfLabel = 'selfLabel'
74
+
75
+
export function isSelfLabel<V>(v: V) {
76
+
return is$typed(v, id, hashSelfLabel)
77
+
}
78
+
79
+
export function validateSelfLabel<V>(v: V) {
80
+
return validate<SelfLabel & V>(v, id, hashSelfLabel)
81
+
}
82
+
83
+
/** Declares a label value and its expected interpretations and behaviors. */
84
+
export interface LabelValueDefinition {
85
+
$type?: 'com.atproto.label.defs#labelValueDefinition'
86
+
/** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */
87
+
identifier: string
88
+
/** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */
89
+
severity: 'inform' | 'alert' | 'none' | (string & {})
90
+
/** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
91
+
blurs: 'content' | 'media' | 'none' | (string & {})
92
+
/** The default setting for this label. */
93
+
defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})
94
+
/** Does the user need to have adult content enabled in order to configure this label? */
95
+
adultOnly?: boolean
96
+
locales: LabelValueDefinitionStrings[]
97
+
}
98
+
99
+
const hashLabelValueDefinition = 'labelValueDefinition'
100
+
101
+
export function isLabelValueDefinition<V>(v: V) {
102
+
return is$typed(v, id, hashLabelValueDefinition)
103
+
}
104
+
105
+
export function validateLabelValueDefinition<V>(v: V) {
106
+
return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)
107
+
}
108
+
109
+
/** Strings which describe the label in the UI, localized into a specific language. */
110
+
export interface LabelValueDefinitionStrings {
111
+
$type?: 'com.atproto.label.defs#labelValueDefinitionStrings'
112
+
/** The code of the language these strings are written in. */
113
+
lang: string
114
+
/** A short human-readable name for the label. */
115
+
name: string
116
+
/** A longer description of what the label means and why it might be applied. */
117
+
description: string
118
+
}
119
+
120
+
const hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'
121
+
122
+
export function isLabelValueDefinitionStrings<V>(v: V) {
123
+
return is$typed(v, id, hashLabelValueDefinitionStrings)
124
+
}
125
+
126
+
export function validateLabelValueDefinitionStrings<V>(v: V) {
127
+
return validate<LabelValueDefinitionStrings & V>(
128
+
v,
129
+
id,
130
+
hashLabelValueDefinitionStrings,
131
+
)
132
+
}
133
+
134
+
export type LabelValue =
135
+
| '!hide'
136
+
| '!no-promote'
137
+
| '!warn'
138
+
| '!no-unauthenticated'
139
+
| 'dmca-violation'
140
+
| 'doxxing'
141
+
| 'porn'
142
+
| 'sexual'
143
+
| 'nudity'
144
+
| 'nsfl'
145
+
| 'gore'
146
+
| (string & {})
+31
src/lexicon/types/com/atproto/repo/strongRef.ts
+31
src/lexicon/types/com/atproto/repo/strongRef.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+
import { CID } from 'multiformats/cid'
6
+
import { validate as _validate } from '../../../../lexicons'
7
+
import {
8
+
type $Typed,
9
+
is$typed as _is$typed,
10
+
type OmitKey,
11
+
} from '../../../../util'
12
+
13
+
const is$typed = _is$typed,
14
+
validate = _validate
15
+
const id = 'com.atproto.repo.strongRef'
16
+
17
+
export interface Main {
18
+
$type?: 'com.atproto.repo.strongRef'
19
+
uri: string
20
+
cid: string
21
+
}
22
+
23
+
const hashMain = 'main'
24
+
25
+
export function isMain<V>(v: V) {
26
+
return is$typed(v, id, hashMain)
27
+
}
28
+
29
+
export function validateMain<V>(v: V) {
30
+
return validate<Main & V>(v, id, hashMain)
31
+
}
+42
src/lexicon/types/org/atmosphereconf/profile.ts
+42
src/lexicon/types/org/atmosphereconf/profile.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+
import { CID } from 'multiformats/cid'
6
+
import { validate as _validate } from '../../../lexicons'
7
+
import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
8
+
import type * as ComAtprotoLabelDefs from '../../com/atproto/label/defs.js'
9
+
10
+
const is$typed = _is$typed,
11
+
validate = _validate
12
+
const id = 'org.atmosphereconf.profile'
13
+
14
+
export interface Main {
15
+
$type: 'org.atmosphereconf.profile'
16
+
displayName?: string
17
+
/** Free-form profile description text. */
18
+
description?: string
19
+
/** Profile picture for conference attendee */
20
+
avatar?: BlobRef
21
+
/** Larger horizontal image to display behind profile view. */
22
+
banner?: BlobRef
23
+
labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }
24
+
createdAt?: string
25
+
[k: string]: unknown
26
+
}
27
+
28
+
const hashMain = 'main'
29
+
30
+
export function isMain<V>(v: V) {
31
+
return is$typed(v, id, hashMain)
32
+
}
33
+
34
+
export function validateMain<V>(v: V) {
35
+
return validate<Main & V>(v, id, hashMain, true)
36
+
}
37
+
38
+
export {
39
+
type Main as Record,
40
+
isMain as isRecord,
41
+
validateMain as validateRecord,
42
+
}
+82
src/lexicon/util.ts
+82
src/lexicon/util.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
5
+
import { type ValidationResult } from '@atproto/lexicon'
6
+
7
+
export type OmitKey<T, K extends keyof T> = {
8
+
[K2 in keyof T as K2 extends K ? never : K2]: T[K2]
9
+
}
10
+
11
+
export type $Typed<V, T extends string = string> = V & { $type: T }
12
+
export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>
13
+
14
+
export type $Type<Id extends string, Hash extends string> = Hash extends 'main'
15
+
? Id
16
+
: `${Id}#${Hash}`
17
+
18
+
function isObject<V>(v: V): v is V & object {
19
+
return v != null && typeof v === 'object'
20
+
}
21
+
22
+
function is$type<Id extends string, Hash extends string>(
23
+
$type: unknown,
24
+
id: Id,
25
+
hash: Hash,
26
+
): $type is $Type<Id, Hash> {
27
+
return hash === 'main'
28
+
? $type === id
29
+
: // $type === `${id}#${hash}`
30
+
typeof $type === 'string' &&
31
+
$type.length === id.length + 1 + hash.length &&
32
+
$type.charCodeAt(id.length) === 35 /* '#' */ &&
33
+
$type.startsWith(id) &&
34
+
$type.endsWith(hash)
35
+
}
36
+
37
+
export type $TypedObject<
38
+
V,
39
+
Id extends string,
40
+
Hash extends string,
41
+
> = V extends {
42
+
$type: $Type<Id, Hash>
43
+
}
44
+
? V
45
+
: V extends { $type?: string }
46
+
? V extends { $type?: infer T extends $Type<Id, Hash> }
47
+
? V & { $type: T }
48
+
: never
49
+
: V & { $type: $Type<Id, Hash> }
50
+
51
+
export function is$typed<V, Id extends string, Hash extends string>(
52
+
v: V,
53
+
id: Id,
54
+
hash: Hash,
55
+
): v is $TypedObject<V, Id, Hash> {
56
+
return isObject(v) && '$type' in v && is$type(v.$type, id, hash)
57
+
}
58
+
59
+
export function maybe$typed<V, Id extends string, Hash extends string>(
60
+
v: V,
61
+
id: Id,
62
+
hash: Hash,
63
+
): v is V & object & { $type?: $Type<Id, Hash> } {
64
+
return (
65
+
isObject(v) &&
66
+
('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)
67
+
)
68
+
}
69
+
70
+
export type Validator<R = unknown> = (v: unknown) => ValidationResult<R>
71
+
export type ValidatorParam<V extends Validator> =
72
+
V extends Validator<infer R> ? R : never
73
+
74
+
/**
75
+
* Utility function that allows to convert a "validate*" utility function into a
76
+
* type predicate.
77
+
*/
78
+
export function asPredicate<V extends Validator>(validate: V) {
79
+
return function <T>(v: T): v is T & ValidatorParam<V> {
80
+
return validate(v).success
81
+
}
82
+
}
+104
src/pages/api/profile/create.ts
+104
src/pages/api/profile/create.ts
···
1
+
import type { APIRoute } from 'astro'
2
+
import { getOAuthClient } from '../../../lib/context'
3
+
import { getSession } from '../../../lib/session'
4
+
import { Agent, BlobRef } from '@atproto/api'
5
+
import type { Main as ProfileRecord } from '../../../lexicon/types/org/atmosphereconf/profile'
6
+
7
+
async function fileToBlob(agent: Agent, file: File): Promise<BlobRef> {
8
+
const arrayBuffer = await file.arrayBuffer()
9
+
const uint8Array = new Uint8Array(arrayBuffer)
10
+
11
+
const response = await agent.com.atproto.repo.uploadBlob(uint8Array, {
12
+
encoding: file.type,
13
+
})
14
+
15
+
return response.data.blob
16
+
}
17
+
18
+
export const POST: APIRoute = async ({ request, cookies, redirect }) => {
19
+
try {
20
+
const session = getSession(cookies)
21
+
const oauthClient = getOAuthClient(cookies)
22
+
23
+
if (!session.did) {
24
+
return new Response('Unauthorized', { status: 401 })
25
+
}
26
+
27
+
const oauthSession = await oauthClient.restore(session.did)
28
+
if (!oauthSession) {
29
+
return new Response('Session expired', { status: 401 })
30
+
}
31
+
32
+
const agent = new Agent(oauthSession)
33
+
const formData = await request.formData()
34
+
35
+
// Extract form data
36
+
const displayName = formData.get('displayName')
37
+
const description = formData.get('description')
38
+
const avatarFile = formData.get('avatar')
39
+
const bannerFile = formData.get('banner')
40
+
41
+
if (!displayName || typeof displayName !== 'string') {
42
+
return new Response('Display name is required', { status: 400 })
43
+
}
44
+
45
+
// Validate file sizes
46
+
if (avatarFile instanceof File && avatarFile.size > 0 && avatarFile.size > 1000000) {
47
+
return new Response('Avatar file size must be less than 1MB', { status: 400 })
48
+
}
49
+
50
+
if (bannerFile instanceof File && bannerFile.size > 0 && bannerFile.size > 1000000) {
51
+
return new Response('Banner file size must be less than 1MB', { status: 400 })
52
+
}
53
+
54
+
// Build the profile record
55
+
const record: Omit<ProfileRecord, '$type'> = {
56
+
displayName: displayName.slice(0, 64),
57
+
description: typeof description === 'string' ? description.slice(0, 256) : undefined,
58
+
createdAt: new Date().toISOString(),
59
+
}
60
+
61
+
// Upload avatar if provided
62
+
if (avatarFile instanceof File && avatarFile.size > 0) {
63
+
try {
64
+
record.avatar = await fileToBlob(agent, avatarFile)
65
+
} catch (err) {
66
+
console.error('Failed to upload avatar:', err)
67
+
return new Response('Failed to upload avatar', { status: 500 })
68
+
}
69
+
}
70
+
71
+
// Upload banner if provided
72
+
if (bannerFile instanceof File && bannerFile.size > 0) {
73
+
try {
74
+
record.banner = await fileToBlob(agent, bannerFile)
75
+
} catch (err) {
76
+
console.error('Failed to upload banner:', err)
77
+
return new Response('Failed to upload banner', { status: 500 })
78
+
}
79
+
}
80
+
81
+
// Create or update the profile record
82
+
try {
83
+
await agent.com.atproto.repo.putRecord({
84
+
repo: agent.assertDid,
85
+
collection: 'org.atmosphereconf.profile',
86
+
rkey: 'self',
87
+
record: {
88
+
$type: 'org.atmosphereconf.profile',
89
+
...record,
90
+
},
91
+
})
92
+
93
+
return redirect('/')
94
+
} catch (err) {
95
+
console.error('Failed to create profile:', err)
96
+
const error = err instanceof Error ? err.message : 'unexpected error'
97
+
return new Response(`Failed to create profile: ${error}`, { status: 500 })
98
+
}
99
+
} catch (err) {
100
+
console.error('Profile creation failed:', err)
101
+
const error = err instanceof Error ? err.message : 'unexpected error'
102
+
return new Response(`Profile creation failed: ${error}`, { status: 500 })
103
+
}
104
+
}
+45
-35
src/pages/index.astro
+45
-35
src/pages/index.astro
···
1
1
---
2
-
import "../styles.css";
3
-
import { getSession } from "../lib/session";
4
-
import { getOAuthClient } from "../lib/context";
5
-
import { Agent } from "@atproto/api";
2
+
import '../styles.css'
3
+
import { getSession } from '../lib/session'
4
+
import { getOAuthClient } from '../lib/context'
5
+
import { Agent } from '@atproto/api'
6
6
7
-
const session = getSession(Astro.cookies);
8
-
const oauthClient = getOAuthClient(Astro.cookies);
7
+
const session = getSession(Astro.cookies)
8
+
const oauthClient = getOAuthClient(Astro.cookies)
9
9
10
-
let agent: Agent | null = null;
11
-
let profile: any = null;
10
+
let agent: Agent | null = null
11
+
let profile: any = null
12
12
13
13
if (session.did) {
14
14
try {
15
-
const oauthSession = await oauthClient.restore(session.did);
15
+
const oauthSession = await oauthClient.restore(session.did)
16
16
if (oauthSession) {
17
-
agent = new Agent(oauthSession);
17
+
agent = new Agent(oauthSession)
18
18
19
19
try {
20
-
const profileResponse = await agent.com.atproto.repo.getRecord({
21
-
repo: agent.assertDid,
22
-
collection: "app.bsky.actor.profile",
23
-
rkey: "self",
24
-
});
25
-
profile = profileResponse.data;
20
+
const profileResponse = await agent.app.bsky.actor.getProfile({
21
+
actor: agent.assertDid,
22
+
})
23
+
profile = profileResponse.data
26
24
} catch (err) {
27
-
console.warn("Failed to fetch profile:", err);
25
+
console.warn('Failed to fetch profile:', err)
28
26
}
29
27
}
30
28
} catch (err) {
31
-
console.warn("OAuth restore failed:", err);
32
-
session.destroy();
29
+
console.warn('OAuth restore failed:', err)
30
+
session.destroy()
33
31
}
34
32
}
35
33
36
-
const displayName = profile?.value?.displayName || agent?.assertDid || "User";
37
-
const handle = agent?.assertDid || "";
34
+
const displayName = profile?.displayName || agent?.assertDid || 'User'
35
+
const handle = profile?.handle || agent?.assertDid || ''
36
+
const avatar = profile?.avatar
37
+
const description = profile?.description
38
38
---
39
39
40
40
<html lang="en" data-theme="dracula">
···
43
43
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
44
44
<meta name="viewport" content="width=device-width" />
45
45
<meta name="generator" content={Astro.generator} />
46
-
<title>ATProto Login</title>
46
+
<title>ATmosphere Login</title>
47
47
</head>
48
48
<body>
49
49
<div class="min-h-screen flex items-center justify-center">
50
-
<div class="card w-96 bg-base-100 shadow-xl">
50
+
<div class="card w-96 bg-base-200 shadow-xl p-8">
51
51
<div class="card-body">
52
52
{
53
53
agent ? (
54
54
<>
55
55
<h2 class="card-title justify-center mb-4">Welcome!</h2>
56
56
<div class="space-y-4">
57
-
<div class="text-center">
57
+
<div class="flex flex-col items-center text-center">
58
+
{avatar && (
59
+
<div class="avatar mb-4">
60
+
<div class="w-24 rounded-full">
61
+
<img src={avatar} alt={displayName} />
62
+
</div>
63
+
</div>
64
+
)}
58
65
<p class="text-lg font-semibold">{displayName}</p>
59
66
<p class="text-sm opacity-70">{handle}</p>
67
+
{description && <p class="text-sm mt-2 opacity-80">{description}</p>}
60
68
</div>
61
-
<form method="POST" action="/api/logout" class="w-full">
62
-
<button type="submit" class="btn btn-error w-full">
63
-
Logout
64
-
</button>
65
-
</form>
69
+
<div class="space-y-2 w-full">
70
+
<a href={`/profile/${handle}`} class="btn btn-primary w-full">
71
+
View Profile
72
+
</a>
73
+
<form method="POST" action="/api/logout" class="w-full">
74
+
<button type="submit" class="btn btn-error w-full">
75
+
Logout
76
+
</button>
77
+
</form>
78
+
</div>
66
79
</div>
67
80
</>
68
81
) : (
69
82
<>
70
-
<h2 class="card-title justify-center mb-6">ATProto Login</h2>
83
+
<h2 class="card-title justify-center mb-6">ATmosphere Login</h2>
71
84
<form method="POST" action="/api/login">
72
85
<div class="join join-vertical w-full">
73
86
<input
74
87
type="text"
75
88
placeholder="Enter your handle (e.g. alice.bsky.social)"
76
-
class="input input-bordered join-item"
89
+
class="input input-bordered join-item w-full"
77
90
name="handle"
78
91
required
79
92
/>
80
-
<button
81
-
type="submit"
82
-
class="btn btn-primary btn-wide join-item"
83
-
>
93
+
<button type="submit" class="btn btn-primary join-item w-full">
84
94
Login
85
95
</button>
86
96
</div>
+208
src/pages/profile/[handle].astro
+208
src/pages/profile/[handle].astro
···
1
+
---
2
+
import '../../styles.css'
3
+
import { getSession } from '../../lib/session'
4
+
import { getOAuthClient } from '../../lib/context'
5
+
import { Agent } from '@atproto/api'
6
+
7
+
const { handle } = Astro.params
8
+
9
+
if (!handle) {
10
+
return Astro.redirect('/')
11
+
}
12
+
13
+
const session = getSession(Astro.cookies)
14
+
const oauthClient = getOAuthClient(Astro.cookies)
15
+
16
+
let agent: Agent | null = null
17
+
let profile: any = null
18
+
let conferenceProfile: any = null
19
+
let did: string | null = null
20
+
let isOwnProfile = false
21
+
22
+
// Get agent if authenticated
23
+
if (session.did) {
24
+
try {
25
+
const oauthSession = await oauthClient.restore(session.did)
26
+
if (oauthSession) {
27
+
agent = new Agent(oauthSession)
28
+
isOwnProfile = agent.assertDid === session.did
29
+
}
30
+
} catch (err) {
31
+
console.warn('OAuth restore failed:', err)
32
+
}
33
+
}
34
+
35
+
// Create a public agent to resolve the profile if we don't have an authenticated one
36
+
const publicAgent = agent || new Agent({ service: 'https://public.api.bsky.app' })
37
+
38
+
// Resolve handle to DID and get profile
39
+
try {
40
+
const resolveResponse = await publicAgent.resolveHandle({ handle })
41
+
did = resolveResponse.data.did
42
+
43
+
// Get Bluesky profile for basic info
44
+
try {
45
+
const profileResponse = await publicAgent.app.bsky.actor.getProfile({
46
+
actor: did,
47
+
})
48
+
profile = profileResponse.data
49
+
} catch (err) {
50
+
console.warn('Failed to fetch Bluesky profile:', err)
51
+
}
52
+
53
+
// Get conference profile
54
+
try {
55
+
const response = await publicAgent.com.atproto.repo.getRecord({
56
+
repo: did,
57
+
collection: 'org.atmosphereconf.profile',
58
+
rkey: 'self'
59
+
})
60
+
conferenceProfile = response.data.value
61
+
} catch (err) {
62
+
console.log('No conference profile found for this user')
63
+
}
64
+
} catch (err) {
65
+
console.error('Failed to resolve handle:', err)
66
+
return new Response('Profile not found', { status: 404 })
67
+
}
68
+
69
+
// Helper function to convert blob refs to URLs
70
+
function blobRefToUrl(blobRef: any, did: string): string {
71
+
if (!blobRef || typeof blobRef !== 'object') return ''
72
+
73
+
// Handle BlobRef object with CID
74
+
if (blobRef.ref) {
75
+
const cid = blobRef.ref.toString()
76
+
return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}@jpeg`
77
+
}
78
+
79
+
return ''
80
+
}
81
+
82
+
const displayName = conferenceProfile?.displayName || profile?.displayName || handle
83
+
const description = conferenceProfile?.description || profile?.description || ''
84
+
85
+
// Handle both blob refs and direct URLs
86
+
let avatar = ''
87
+
if (conferenceProfile?.avatar) {
88
+
avatar = blobRefToUrl(conferenceProfile.avatar, did)
89
+
} else if (profile?.avatar) {
90
+
avatar = profile.avatar
91
+
}
92
+
93
+
let banner = ''
94
+
if (conferenceProfile?.banner) {
95
+
banner = blobRefToUrl(conferenceProfile.banner, did)
96
+
} else if (profile?.banner) {
97
+
banner = profile.banner
98
+
}
99
+
100
+
const hasConferenceProfile = !!conferenceProfile
101
+
---
102
+
103
+
<html lang="en" data-theme="dracula">
104
+
<head>
105
+
<meta charset="utf-8" />
106
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
107
+
<meta name="viewport" content="width=device-width" />
108
+
<meta name="generator" content={Astro.generator} />
109
+
<title>{displayName} - ATmosphere</title>
110
+
</head>
111
+
<body>
112
+
<div class="min-h-screen bg-base-300">
113
+
{banner && (
114
+
<div class="w-full h-48 md:h-64 bg-base-200">
115
+
<img
116
+
src={banner}
117
+
alt="Profile banner"
118
+
class="w-full h-full object-cover"
119
+
/>
120
+
</div>
121
+
)}
122
+
123
+
<div class="container mx-auto px-4 -mt-16 relative z-10 max-w-4xl">
124
+
<div class="card bg-base-200 shadow-xl">
125
+
<div class="card-body">
126
+
<div class="flex flex-col md:flex-row gap-6">
127
+
<div class="flex-shrink-0">
128
+
{avatar ? (
129
+
<div class="avatar">
130
+
<div class="w-32 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
131
+
<img src={avatar} alt={displayName} />
132
+
</div>
133
+
</div>
134
+
) : (
135
+
<div class="avatar placeholder">
136
+
<div class="bg-neutral text-neutral-content rounded-full w-32">
137
+
<span class="text-3xl">{displayName[0]?.toUpperCase()}</span>
138
+
</div>
139
+
</div>
140
+
)}
141
+
</div>
142
+
143
+
<div class="flex-grow">
144
+
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
145
+
<div>
146
+
<h1 class="text-3xl font-bold">{displayName}</h1>
147
+
<p class="text-sm opacity-70">@{handle}</p>
148
+
{did && (
149
+
<p class="text-xs opacity-50 mt-1 font-mono break-all">
150
+
{did}
151
+
</p>
152
+
)}
153
+
</div>
154
+
155
+
{isOwnProfile && (
156
+
<a href="/profile/create" class="btn btn-primary btn-sm">
157
+
Edit Profile
158
+
</a>
159
+
)}
160
+
</div>
161
+
162
+
{description && (
163
+
<p class="mt-4 text-base whitespace-pre-wrap">{description}</p>
164
+
)}
165
+
166
+
<div class="mt-4">
167
+
{hasConferenceProfile ? (
168
+
<div class="badge badge-success gap-2">
169
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-4 h-4 stroke-current">
170
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
171
+
</svg>
172
+
Conference Attendee
173
+
</div>
174
+
) : (
175
+
<div class="badge badge-ghost gap-2">
176
+
No Conference Profile
177
+
</div>
178
+
)}
179
+
</div>
180
+
</div>
181
+
</div>
182
+
183
+
{!hasConferenceProfile && isOwnProfile && (
184
+
<div class="alert alert-warning mt-6">
185
+
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
186
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
187
+
</svg>
188
+
<span>You haven't set up your conference profile yet.</span>
189
+
<div>
190
+
<a href="/profile/create" class="btn btn-sm btn-primary">Create Now</a>
191
+
</div>
192
+
</div>
193
+
)}
194
+
</div>
195
+
</div>
196
+
197
+
<div class="mt-6 mb-12">
198
+
<a href="/" class="btn btn-ghost">
199
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
200
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
201
+
</svg>
202
+
Back to Home
203
+
</a>
204
+
</div>
205
+
</div>
206
+
</div>
207
+
</body>
208
+
</html>
+87
src/pages/profile/create.astro
+87
src/pages/profile/create.astro
···
1
+
---
2
+
import '../../styles.css'
3
+
import ProfileForm from '../../components/ProfileForm.astro'
4
+
import { getSession } from '../../lib/session'
5
+
import { getOAuthClient } from '../../lib/context'
6
+
import { Agent } from '@atproto/api'
7
+
8
+
const session = getSession(Astro.cookies)
9
+
const oauthClient = getOAuthClient(Astro.cookies)
10
+
11
+
// Redirect to login if not authenticated
12
+
if (!session.did) {
13
+
return Astro.redirect('/')
14
+
}
15
+
16
+
let agent: Agent | null = null
17
+
let existingProfile: any = null
18
+
19
+
try {
20
+
const oauthSession = await oauthClient.restore(session.did)
21
+
if (oauthSession) {
22
+
agent = new Agent(oauthSession)
23
+
24
+
// Check if profile already exists
25
+
try {
26
+
// Try to get the profile record from the repo
27
+
const did = agent.assertDid
28
+
const response = await agent.com.atproto.repo.getRecord({
29
+
repo: did,
30
+
collection: 'org.atmosphereconf.profile',
31
+
rkey: 'self'
32
+
})
33
+
existingProfile = response.data.value
34
+
} catch (err) {
35
+
// Profile doesn't exist yet, which is fine
36
+
console.log('No existing profile found')
37
+
}
38
+
}
39
+
} catch (err) {
40
+
console.warn('OAuth restore failed:', err)
41
+
session.destroy()
42
+
return Astro.redirect('/')
43
+
}
44
+
45
+
const displayName = existingProfile?.displayName || ''
46
+
const description = existingProfile?.description || ''
47
+
---
48
+
49
+
<html lang="en" data-theme="dracula">
50
+
<head>
51
+
<meta charset="utf-8" />
52
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
53
+
<meta name="viewport" content="width=device-width" />
54
+
<meta name="generator" content={Astro.generator} />
55
+
<title>Create Profile - ATmosphere</title>
56
+
</head>
57
+
<body>
58
+
<div class="min-h-screen flex items-center justify-center p-4">
59
+
<div class="card w-full max-w-2xl bg-base-200 shadow-xl">
60
+
<div class="card-body">
61
+
<h2 class="card-title justify-center text-2xl mb-6">
62
+
{existingProfile ? 'Update Your Profile' : 'Create Your Profile'}
63
+
</h2>
64
+
65
+
<div class="alert alert-info mb-4">
66
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
67
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
68
+
</svg>
69
+
<span>Set up your conference attendee profile</span>
70
+
</div>
71
+
72
+
<ProfileForm
73
+
displayName={displayName}
74
+
description={description}
75
+
submitLabel={existingProfile ? 'Update Profile' : 'Create Profile'}
76
+
/>
77
+
78
+
<div class="divider">OR</div>
79
+
80
+
<a href="/" class="btn btn-ghost w-full">
81
+
Back to Home
82
+
</a>
83
+
</div>
84
+
</div>
85
+
</div>
86
+
</body>
87
+
</html>
+2
-3
src/styles.css
+2
-3
src/styles.css