+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
+
}
+205
package-lock.json
+205
package-lock.json
···
16
16
"daisyui": "^5.3.9",
17
17
"dotenv": "^17.2.3",
18
18
"tailwindcss": "^4.1.16"
19
+
},
20
+
"devDependencies": {
21
+
"@atproto/lex-cli": "^0.9.6"
19
22
}
20
23
},
21
24
"node_modules/@astrojs/compiler": {
···
265
268
"@atproto/jwk": "0.6.0",
266
269
"@atproto/jwk-jose": "0.1.11",
267
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"
268
327
}
269
328
},
270
329
"node_modules/@atproto/lexicon": {
···
1958
2017
"vite": "^5.2.0 || ^6 || ^7"
1959
2018
}
1960
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
+
},
1961
2032
"node_modules/@types/debug": {
1962
2033
"version": "4.1.12",
1963
2034
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
···
2356
2427
"url": "https://github.com/sponsors/wooorm"
2357
2428
}
2358
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
+
},
2359
2437
"node_modules/base-64": {
2360
2438
"version": "1.0.0",
2361
2439
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
···
2402
2480
},
2403
2481
"funding": {
2404
2482
"url": "https://github.com/sponsors/sindresorhus"
2483
+
}
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"
2405
2493
}
2406
2494
},
2407
2495
"node_modules/brotli": {
···
2537
2625
"node": ">=6"
2538
2626
}
2539
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
+
},
2540
2655
"node_modules/comma-separated-tokens": {
2541
2656
"version": "2.0.3",
2542
2657
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
···
2545
2660
"funding": {
2546
2661
"type": "github",
2547
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"
2548
2673
}
2549
2674
},
2550
2675
"node_modules/common-ancestor-path": {
···
3032
3157
"radix3": "^1.1.2",
3033
3158
"ufo": "^1.6.1",
3034
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"
3035
3170
}
3036
3171
},
3037
3172
"node_modules/hast-util-from-html": {
···
4526
4661
"node": ">= 0.6"
4527
4662
}
4528
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
+
},
4529
4680
"node_modules/mrmime": {
4530
4681
"version": "2.0.1",
4531
4682
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
···
4739
4890
"url": "https://github.com/inikulin/parse5?sponsor=1"
4740
4891
}
4741
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
+
},
4742
4900
"node_modules/picocolors": {
4743
4901
"version": "1.1.1",
4744
4902
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
···
4783
4941
},
4784
4942
"engines": {
4785
4943
"node": "^10 || ^12 || >=14"
4944
+
}
4945
+
},
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,
4951
+
"license": "MIT",
4952
+
"bin": {
4953
+
"prettier": "bin/prettier.cjs"
4954
+
},
4955
+
"engines": {
4956
+
"node": ">=14"
4957
+
},
4958
+
"funding": {
4959
+
"url": "https://github.com/prettier/prettier?sponsor=1"
4786
4960
}
4787
4961
},
4788
4962
"node_modules/prismjs": {
···
5316
5490
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
5317
5491
}
5318
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
+
},
5319
5506
"node_modules/tailwindcss": {
5320
5507
"version": "4.1.16",
5321
5508
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
···
5399
5586
"funding": {
5400
5587
"type": "github",
5401
5588
"url": "https://github.com/sponsors/wooorm"
5589
+
}
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"
5402
5600
}
5403
5601
},
5404
5602
"node_modules/tsconfck": {
···
5965
6163
"engines": {
5966
6164
"node": ">=12"
5967
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"
5968
6173
},
5969
6174
"node_modules/yocto-queue": {
5970
6175
"version": "1.2.1",
+5
-1
package.json
+5
-1
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",
···
17
18
"daisyui": "^5.3.9",
18
19
"dotenv": "^17.2.3",
19
20
"tailwindcss": "^4.1.16"
21
+
},
22
+
"devDependencies": {
23
+
"@atproto/lex-cli": "^0.9.6"
20
24
}
21
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
+
}
+13
-10
src/pages/index.astro
+13
-10
src/pages/index.astro
···
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">
···
64
64
)}
65
65
<p class="text-lg font-semibold">{displayName}</p>
66
66
<p class="text-sm opacity-70">{handle}</p>
67
-
{description && (
68
-
<p class="text-sm mt-2 opacity-80">{description}</p>
69
-
)}
67
+
{description && <p class="text-sm mt-2 opacity-80">{description}</p>}
68
+
</div>
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>
70
78
</div>
71
-
<form method="POST" action="/api/logout" class="w-full">
72
-
<button type="submit" class="btn btn-error w-full">
73
-
Logout
74
-
</button>
75
-
</form>
76
79
</div>
77
80
</>
78
81
) : (
79
82
<>
80
-
<h2 class="card-title justify-center mb-6">ATProto Login</h2>
83
+
<h2 class="card-title justify-center mb-6">ATmosphere Login</h2>
81
84
<form method="POST" action="/api/login">
82
85
<div class="join join-vertical w-full">
83
86
<input
+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>