···1818### [alicia_cbor](cbor/)
1919Library to encode/decode CBOR (RFC 8949) and subsets of it like DAG-CBOR.
20202121+### [alicia_core](core/)
2222+The core syntax/types of atproto.
2323+2124# License
22252326This project and its packages are all licensed under the Apache-2.0 License - see [LICENSE](LICENSE).
···11+22+ Apache License
33+ Version 2.0, January 2004
44+ http://www.apache.org/licenses/
55+66+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
77+88+ 1. Definitions.
99+1010+ "License" shall mean the terms and conditions for use, reproduction,
1111+ and distribution as defined by Sections 1 through 9 of this document.
1212+1313+ "Licensor" shall mean the copyright owner or entity authorized by
1414+ the copyright owner that is granting the License.
1515+1616+ "Legal Entity" shall mean the union of the acting entity and all
1717+ other entities that control, are controlled by, or are under common
1818+ control with that entity. For the purposes of this definition,
1919+ "control" means (i) the power, direct or indirect, to cause the
2020+ direction or management of such entity, whether by contract or
2121+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
2222+ outstanding shares, or (iii) beneficial ownership of such entity.
2323+2424+ "You" (or "Your") shall mean an individual or Legal Entity
2525+ exercising permissions granted by this License.
2626+2727+ "Source" form shall mean the preferred form for making modifications,
2828+ including but not limited to software source code, documentation
2929+ source, and configuration files.
3030+3131+ "Object" form shall mean any form resulting from mechanical
3232+ transformation or translation of a Source form, including but
3333+ not limited to compiled object code, generated documentation,
3434+ and conversions to other media types.
3535+3636+ "Work" shall mean the work of authorship, whether in Source or
3737+ Object form, made available under the License, as indicated by a
3838+ copyright notice that is included in or attached to the work
3939+ (an example is provided in the Appendix below).
4040+4141+ "Derivative Works" shall mean any work, whether in Source or Object
4242+ form, that is based on (or derived from) the Work and for which the
4343+ editorial revisions, annotations, elaborations, or other modifications
4444+ represent, as a whole, an original work of authorship. For the purposes
4545+ of this License, Derivative Works shall not include works that remain
4646+ separable from, or merely link (or bind by name) to the interfaces of,
4747+ the Work and Derivative Works thereof.
4848+4949+ "Contribution" shall mean any work of authorship, including
5050+ the original version of the Work and any modifications or additions
5151+ to that Work or Derivative Works thereof, that is intentionally
5252+ submitted to Licensor for inclusion in the Work by the copyright owner
5353+ or by an individual or Legal Entity authorized to submit on behalf of
5454+ the copyright owner. For the purposes of this definition, "submitted"
5555+ means any form of electronic, verbal, or written communication sent
5656+ to the Licensor or its representatives, including but not limited to
5757+ communication on electronic mailing lists, source code control systems,
5858+ and issue tracking systems that are managed by, or on behalf of, the
5959+ Licensor for the purpose of discussing and improving the Work, but
6060+ excluding communication that is conspicuously marked or otherwise
6161+ designated in writing by the copyright owner as "Not a Contribution."
6262+6363+ "Contributor" shall mean Licensor and any individual or Legal Entity
6464+ on behalf of whom a Contribution has been received by Licensor and
6565+ subsequently incorporated within the Work.
6666+6767+ 2. Grant of Copyright License. Subject to the terms and conditions of
6868+ this License, each Contributor hereby grants to You a perpetual,
6969+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
7070+ copyright license to reproduce, prepare Derivative Works of,
7171+ publicly display, publicly perform, sublicense, and distribute the
7272+ Work and such Derivative Works in Source or Object form.
7373+7474+ 3. Grant of Patent License. Subject to the terms and conditions of
7575+ this License, each Contributor hereby grants to You a perpetual,
7676+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
7777+ (except as stated in this section) patent license to make, have made,
7878+ use, offer to sell, sell, import, and otherwise transfer the Work,
7979+ where such license applies only to those patent claims licensable
8080+ by such Contributor that are necessarily infringed by their
8181+ Contribution(s) alone or by combination of their Contribution(s)
8282+ with the Work to which such Contribution(s) was submitted. If You
8383+ institute patent litigation against any entity (including a
8484+ cross-claim or counterclaim in a lawsuit) alleging that the Work
8585+ or a Contribution incorporated within the Work constitutes direct
8686+ or contributory patent infringement, then any patent licenses
8787+ granted to You under this License for that Work shall terminate
8888+ as of the date such litigation is filed.
8989+9090+ 4. Redistribution. You may reproduce and distribute copies of the
9191+ Work or Derivative Works thereof in any medium, with or without
9292+ modifications, and in Source or Object form, provided that You
9393+ meet the following conditions:
9494+9595+ (a) You must give any other recipients of the Work or
9696+ Derivative Works a copy of this License; and
9797+9898+ (b) You must cause any modified files to carry prominent notices
9999+ stating that You changed the files; and
100100+101101+ (c) You must retain, in the Source form of any Derivative Works
102102+ that You distribute, all copyright, patent, trademark, and
103103+ attribution notices from the Source form of the Work,
104104+ excluding those notices that do not pertain to any part of
105105+ the Derivative Works; and
106106+107107+ (d) If the Work includes a "NOTICE" text file as part of its
108108+ distribution, then any Derivative Works that You distribute must
109109+ include a readable copy of the attribution notices contained
110110+ within such NOTICE file, excluding those notices that do not
111111+ pertain to any part of the Derivative Works, in at least one
112112+ of the following places: within a NOTICE text file distributed
113113+ as part of the Derivative Works; within the Source form or
114114+ documentation, if provided along with the Derivative Works; or,
115115+ within a display generated by the Derivative Works, if and
116116+ wherever such third-party notices normally appear. The contents
117117+ of the NOTICE file are for informational purposes only and
118118+ do not modify the License. You may add Your own attribution
119119+ notices within Derivative Works that You distribute, alongside
120120+ or as an addendum to the NOTICE text from the Work, provided
121121+ that such additional attribution notices cannot be construed
122122+ as modifying the License.
123123+124124+ You may add Your own copyright statement to Your modifications and
125125+ may provide additional or different license terms and conditions
126126+ for use, reproduction, or distribution of Your modifications, or
127127+ for any such Derivative Works as a whole, provided Your use,
128128+ reproduction, and distribution of the Work otherwise complies with
129129+ the conditions stated in this License.
130130+131131+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132132+ any Contribution intentionally submitted for inclusion in the Work
133133+ by You to the Licensor shall be under the terms and conditions of
134134+ this License, without any additional terms or conditions.
135135+ Notwithstanding the above, nothing herein shall supersede or modify
136136+ the terms of any separate license agreement you may have executed
137137+ with Licensor regarding such Contributions.
138138+139139+ 6. Trademarks. This License does not grant permission to use the trade
140140+ names, trademarks, service marks, or product names of the Licensor,
141141+ except as required for reasonable and customary use in describing the
142142+ origin of the Work and reproducing the content of the NOTICE file.
143143+144144+ 7. Disclaimer of Warranty. Unless required by applicable law or
145145+ agreed to in writing, Licensor provides the Work (and each
146146+ Contributor provides its Contributions) on an "AS IS" BASIS,
147147+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148148+ implied, including, without limitation, any warranties or conditions
149149+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150150+ PARTICULAR PURPOSE. You are solely responsible for determining the
151151+ appropriateness of using or redistributing the Work and assume any
152152+ risks associated with Your exercise of permissions under this License.
153153+154154+ 8. Limitation of Liability. In no event and under no legal theory,
155155+ whether in tort (including negligence), contract, or otherwise,
156156+ unless required by applicable law (such as deliberate and grossly
157157+ negligent acts) or agreed to in writing, shall any Contributor be
158158+ liable to You for damages, including any direct, indirect, special,
159159+ incidental, or consequential damages of any character arising as a
160160+ result of this License or out of the use or inability to use the
161161+ Work (including but not limited to damages for loss of goodwill,
162162+ work stoppage, computer failure or malfunction, or any and all
163163+ other commercial damages or losses), even if such Contributor
164164+ has been advised of the possibility of such damages.
165165+166166+ 9. Accepting Warranty or Additional Liability. While redistributing
167167+ the Work or Derivative Works thereof, You may choose to offer,
168168+ and charge a fee for, acceptance of support, warranty, indemnity,
169169+ or other liability obligations and/or rights consistent with this
170170+ License. However, in accepting such obligations, You may act only
171171+ on Your own behalf and on Your sole responsibility, not on behalf
172172+ of any other Contributor, and only if You agree to indemnify,
173173+ defend, and hold each Contributor harmless for any liability
174174+ incurred by, or claims asserted against, such Contributor by reason
175175+ of your accepting any such warranty or additional liability.
176176+177177+ END OF TERMS AND CONDITIONS
178178+179179+ APPENDIX: How to apply the Apache License to your work.
180180+181181+ To apply the Apache License to your work, attach the following
182182+ boilerplate notice, with the fields enclosed by brackets "[]"
183183+ replaced with your own identifying information. (Don't include
184184+ the brackets!) The text should be enclosed in the appropriate
185185+ comment syntax for the file format. We also recommend that a
186186+ file or class name and description of purpose be included on the
187187+ same "printed page" as the copyright notice for easier
188188+ identification within third-party archives.
189189+190190+ Copyright [yyyy] [name of copyright owner]
191191+192192+ Licensed under the Apache License, Version 2.0 (the "License");
193193+ you may not use this file except in compliance with the License.
194194+ You may obtain a copy of the License at
195195+196196+ http://www.apache.org/licenses/LICENSE-2.0
197197+198198+ Unless required by applicable law or agreed to in writing, software
199199+ distributed under the License is distributed on an "AS IS" BASIS,
200200+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201201+ See the License for the specific language governing permissions and
202202+ limitations under the License.
+27
core/README.md
···11+# alicia_core
22+33+[](https://hex.pm/packages/alicia_core)
44+[](https://hexdocs.pm/alicia_core/)
55+66+The core syntax/types of atproto.
77+88+```sh
99+gleam add alicia_core@1
1010+```
1111+```gleam
1212+import alicia/core/nsid
1313+import gleam/io
1414+1515+pub fn main() -> Nil {
1616+ let assert Ok(parsed) = nsid.parse("app.bsky.feed.post")
1717+ io.println(nsid.to_string(parsed))
1818+}
1919+```
2020+2121+Further documentation can be found at <https://hexdocs.pm/alicia_core>.
2222+2323+## Development
2424+2525+```sh
2626+gleam test # Run the tests
2727+```
···11+# Do not manually edit this file, it is managed by Gleam.
22+#
33+# This file locks the dependency versions used, to make your build
44+# deterministic and to prevent unexpected versions from being included
55+# in your application.
66+#
77+# You should check this file into your source control repository.
88+99+packages = [
1010+ { name = "alicia_identity", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../identity" },
1111+ { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" },
1212+ { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" },
1313+]
1414+1515+[requirements]
1616+alicia_identity = { path = "../identity" }
1717+gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" }
1818+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+31
core/src/alicia/core/internal/ascii.gleam
···11+pub fn every(bytes: BitArray, predicate: fn(Int) -> Bool) -> Bool {
22+ case bytes {
33+ <<>> -> True
44+ <<b, rest:bits>> ->
55+ case predicate(b) {
66+ True -> every(rest, predicate)
77+ False -> False
88+ }
99+ _ -> False
1010+ }
1111+}
1212+1313+pub fn is_ascii(b: Int) -> Bool {
1414+ b <= 0x7f
1515+}
1616+1717+pub fn is_digit(b: Int) -> Bool {
1818+ b >= 0x30 && b <= 0x39
1919+}
2020+2121+pub fn is_alnum(b: Int) -> Bool {
2222+ is_alpha(b) || is_digit(b)
2323+}
2424+2525+fn is_alpha(b: Int) -> Bool {
2626+ case b {
2727+ _ if b >= 0x61 && b <= 0x7a -> True
2828+ _ if b >= 0x41 && b <= 0x5a -> True
2929+ _ -> False
3030+ }
3131+}
+161
core/src/alicia/core/nsid.gleam
···11+//// # NSID (Namespaced Identifier)
22+////
33+//// Official Documentation:
44+//// [atproto.com/specs/nsid](https://atproto.com/specs/nsid)
55+////
66+//// NSIDs identify Lexicon schemas: record types, XRPC endpoints, & more.
77+//// An NSID is a domain authority followed by one final name-segment,
88+//// where the authority is a hostname in reverse domain-name order.
99+//// In `com.atproto.sync.getRecord` the authority is `com.atproto.sync`
1010+//// and the name is `getRecord`. As the reader can see,
1111+//// it is very Java-flavored.
1212+////
1313+//// `parse` normalizes the authority to lowercase.
1414+//// ...A name-segment gets to keep its casing though!
1515+//// The 253-byte hostname limit does not apply to the authority.
1616+//// Only the 317-byte total limit and the 63-byte per segment apply,
1717+//// and the interop vectors seem to accept a 283-byte authority.
1818+////
1919+//// Interop vectors:
2020+//// [github.com/bluesky-social/atproto-interop-tests](https://github.com/bluesky-social/atproto-interop-tests)
2121+2222+import alicia/core/internal/ascii
2323+import alicia/identity/domain
2424+import gleam/bit_array
2525+import gleam/bool
2626+import gleam/list
2727+import gleam/result
2828+import gleam/string
2929+3030+/// A parsed, syntactically valid NSID.
3131+pub opaque type Nsid {
3232+ Nsid(authority: domain.Domain, name: Name)
3333+}
3434+3535+/// The final segment of an NSID.
3636+/// ASCII letters and digits only, no hyphens,
3737+/// no leading digit, max 63 bytes.
3838+/// Case-sensitive and never normalized.
3939+pub opaque type Name {
4040+ Name(String)
4141+}
4242+4343+/// Every way in which the `parse` function can reject an input.
4444+pub type ParseError {
4545+ /// Beyond the 317-byte limit.
4646+ TooLong
4747+ /// NSID needs at least three segments,
4848+ /// two for the authority and one for the name.
4949+ TooFewSegments
5050+ /// Label inside the authority failed with the given `domain.LabelError`.
5151+ InvalidAuthority(reason: domain.LabelError)
5252+ /// Name segment is missing entirely.
5353+ EmptyName
5454+ /// Name over 63 bytes.
5555+ NameTooLong
5656+ /// Name starts with a digit.
5757+ NameLeadsWithDigit
5858+ /// Name byte which isn't an ASCII letter or digit.
5959+ InvalidNameCharacter
6060+}
6161+6262+/// The overall NSID byte limit from the spec.
6363+pub const max_byte_length = 317
6464+6565+const max_name_length = 63
6666+6767+/// Constructing an NSID.
6868+/// Lowercases the authority but keeps the name-segment as-is.
6969+/// `domain.parse_segments` checks the authority segments without the
7070+/// 253-byte hostname limit.
7171+///
7272+/// For example:
7373+/// - `parse("cafe.oyster.beepBoop")` -> `Ok(...)`
7474+/// - `parse("cafe.oyster")` -> `Error(TooFewSegments)`
7575+/// - `parse("cafe.oyster.3")` -> `Error(NameLeadsWithDigit)`
7676+pub fn parse(raw: String) -> Result(Nsid, ParseError) {
7777+ use <- bool.guard(string.byte_size(raw) > max_byte_length, Error(TooLong))
7878+ case list.reverse(string.split(raw, ".")) {
7979+ [final, second, third, ..rest] -> {
8080+ use name <- result.try(parse_name(final))
8181+ use authority <- result.try(parse_authority(second, third, rest))
8282+ Ok(Nsid(authority:, name:))
8383+ }
8484+ _ -> Error(TooFewSegments)
8585+ }
8686+}
8787+8888+fn parse_name(raw: String) -> Result(Name, ParseError) {
8989+ let bytes = bit_array.from_string(raw)
9090+ use <- bool.guard(
9191+ !ascii.every(bytes, ascii.is_alnum),
9292+ Error(InvalidNameCharacter),
9393+ )
9494+ use <- bool.guard(
9595+ bit_array.byte_size(bytes) > max_name_length,
9696+ Error(NameTooLong),
9797+ )
9898+ case bytes {
9999+ <<first, _:bits>> ->
100100+ case ascii.is_digit(first) {
101101+ True -> Error(NameLeadsWithDigit)
102102+ False -> Ok(Name(raw))
103103+ }
104104+ _ -> Error(EmptyName)
105105+ }
106106+}
107107+108108+fn parse_authority(
109109+ first: String,
110110+ next: String,
111111+ rest: List(String),
112112+) -> Result(domain.Domain, ParseError) {
113113+ let ascii_only =
114114+ list.all([first, next, ..rest], fn(segment) {
115115+ ascii.every(bit_array.from_string(segment), ascii.is_ascii)
116116+ })
117117+ use <- bool.guard(
118118+ !ascii_only,
119119+ Error(InvalidAuthority(domain.InvalidCharacter)),
120120+ )
121121+ domain.parse_segments(
122122+ string.lowercase(first),
123123+ string.lowercase(next),
124124+ list.map(rest, string.lowercase),
125125+ )
126126+ |> result.map_error(InvalidAuthority)
127127+}
128128+129129+/// The inverse of `parse`.
130130+/// An uppercase authority will render lowercased.
131131+pub fn to_string(nsid: Nsid) -> String {
132132+ authority_to_string(nsid.authority) <> "." <> name_to_string(nsid.name)
133133+}
134134+135135+/// Accessor for domain authority, in hostname order.
136136+pub fn authority(nsid: Nsid) -> domain.Domain {
137137+ nsid.authority
138138+}
139139+140140+/// Accessor for the name-segment.
141141+pub fn name(nsid: Nsid) -> Name {
142142+ nsid.name
143143+}
144144+145145+/// Renders a `domain.Domain` in reverse domain-name order.
146146+/// Java-ify.
147147+/// `oyster.cafe` becomes `cafe.oyster`.
148148+pub fn authority_to_string(authority: domain.Domain) -> String {
149149+ let labels =
150150+ list.map(domain.leading_labels(authority), domain.label_to_string)
151151+ string.join(
152152+ [domain.tld_to_string(domain.tld(authority)), ..list.reverse(labels)],
153153+ ".",
154154+ )
155155+}
156156+157157+/// Unwraps a `Name` to its text.
158158+pub fn name_to_string(name: Name) -> String {
159159+ let Name(value) = name
160160+ value
161161+}