@atproto/syntax 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/at-identifier.d.ts +2 -2
  3. package/dist/at-identifier.d.ts.map +1 -1
  4. package/dist/at-identifier.js.map +1 -1
  5. package/dist/aturi.d.ts +5 -5
  6. package/dist/aturi.d.ts.map +1 -1
  7. package/dist/aturi.js +1 -1
  8. package/dist/aturi.js.map +1 -1
  9. package/dist/aturi_validation.d.ts +3 -3
  10. package/dist/aturi_validation.d.ts.map +1 -1
  11. package/dist/aturi_validation.js.map +1 -1
  12. package/dist/language.js +1 -1
  13. package/dist/language.js.map +1 -1
  14. package/dist/nsid.d.ts +1 -1
  15. package/dist/nsid.d.ts.map +1 -1
  16. package/dist/nsid.js.map +1 -1
  17. package/package.json +15 -14
  18. package/benchmark.cjs +0 -208
  19. package/src/at-identifier.ts +0 -104
  20. package/src/aturi.ts +0 -197
  21. package/src/aturi_validation.ts +0 -321
  22. package/src/datetime.ts +0 -369
  23. package/src/did.ts +0 -71
  24. package/src/handle.ts +0 -128
  25. package/src/index.ts +0 -10
  26. package/src/language.ts +0 -39
  27. package/src/lib/result.ts +0 -11
  28. package/src/nsid.ts +0 -182
  29. package/src/recordkey.ts +0 -51
  30. package/src/tid.ts +0 -22
  31. package/src/uri.ts +0 -5
  32. package/tests/aturi-string.test.ts +0 -223
  33. package/tests/aturi.test.ts +0 -428
  34. package/tests/datetime.test.ts +0 -280
  35. package/tests/did.test.ts +0 -104
  36. package/tests/handle.test.ts +0 -239
  37. package/tests/language.test.ts +0 -88
  38. package/tests/nsid.test.ts +0 -174
  39. package/tests/recordkey.test.ts +0 -43
  40. package/tests/tid.test.ts +0 -43
  41. package/tsconfig.build.json +0 -12
  42. package/tsconfig.build.tsbuildinfo +0 -1
  43. package/tsconfig.json +0 -7
  44. package/tsconfig.tests.json +0 -8
  45. package/vitest.config.ts +0 -5
package/src/nsid.ts DELETED
@@ -1,182 +0,0 @@
1
- import { Result, failure, success } from './lib/result.js'
2
-
3
- /*
4
- Grammar:
5
-
6
- alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z"
7
- number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0"
8
- delim = "."
9
- segment = alpha *( alpha / number / "-" )
10
- authority = segment *( delim segment )
11
- name = alpha *( alpha / number )
12
- nsid = authority delim name
13
-
14
- */
15
-
16
- export type NsidString = `${string}.${string}.${string}`
17
-
18
- export class NSID {
19
- readonly segments: readonly string[]
20
-
21
- static parse(input: string): NSID {
22
- return new NSID(input)
23
- }
24
-
25
- static create(authority: string, name: string): NSID {
26
- const input = [...authority.split('.').reverse(), name].join('.')
27
- return new NSID(input)
28
- }
29
-
30
- static isValid(nsid: string) {
31
- return isValidNsid(nsid)
32
- }
33
-
34
- static from(input: { toString: () => string }): NSID {
35
- if (input instanceof NSID) {
36
- // No need to clone, NSID is immutable
37
- return input
38
- }
39
- if (Array.isArray(input)) {
40
- return new NSID((input as string[]).join('.'))
41
- }
42
- return new NSID(String(input))
43
- }
44
-
45
- constructor(nsid: string) {
46
- this.segments = parseNsid(nsid)
47
- }
48
-
49
- get authority() {
50
- return this.segments
51
- .slice(0, this.segments.length - 1)
52
- .reverse()
53
- .join('.') as `${string}.${string}`
54
- }
55
-
56
- get name() {
57
- return this.segments.at(this.segments.length - 1)
58
- }
59
-
60
- toString(): NsidString {
61
- return this.segments.join('.') as NsidString
62
- }
63
- }
64
-
65
- export function ensureValidNsid<I extends string>(
66
- input: I,
67
- ): asserts input is I & NsidString {
68
- const result = validateNsid(input)
69
- if (!result.success) throw new InvalidNsidError(result.message)
70
- }
71
-
72
- export function parseNsid(nsid: string): string[] {
73
- const result = validateNsid(nsid)
74
- if (!result.success) throw new InvalidNsidError(result.message)
75
- return result.value
76
- }
77
-
78
- export function isValidNsid<I extends string>(
79
- input: I,
80
- ): input is I & NsidString {
81
- // Since the regex version is more performant for valid NSIDs, we use it when
82
- // we don't care about error details.
83
- return validateNsidRegex(input).success
84
- }
85
-
86
- // Human readable constraints on NSID:
87
- // - a valid domain in reversed notation
88
- // - followed by an additional period-separated name, which is camel-case letters
89
- export function validateNsid(input: string): Result<string[]> {
90
- if (input.length > 253 + 1 + 63) {
91
- return failure('NSID is too long (317 chars max)')
92
- }
93
- if (hasDisallowedCharacters(input)) {
94
- return failure(
95
- 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
96
- )
97
- }
98
- const segments = input.split('.')
99
- if (segments.length < 3) {
100
- return failure('NSID needs at least three parts')
101
- }
102
- for (const l of segments) {
103
- if (l.length < 1) {
104
- return failure('NSID parts can not be empty')
105
- }
106
- if (l.length > 63) {
107
- return failure('NSID part too long (max 63 chars)')
108
- }
109
- if (startsWithHyphen(l) || endsWithHyphen(l)) {
110
- return failure('NSID parts can not start or end with hyphen')
111
- }
112
- }
113
- if (startsWithNumber(segments[0])) {
114
- return failure('NSID first part may not start with a digit')
115
- }
116
- if (!isValidIdentifier(segments[segments.length - 1])) {
117
- return failure(
118
- 'NSID name part must be only letters and digits (and no leading digit)',
119
- )
120
- }
121
- return success(segments)
122
- }
123
-
124
- function hasDisallowedCharacters(v: string) {
125
- return !/^[a-zA-Z0-9.-]*$/.test(v)
126
- }
127
-
128
- function startsWithNumber(v: string) {
129
- const charCode = v.charCodeAt(0)
130
- return charCode >= 48 && charCode <= 57
131
- }
132
-
133
- function startsWithHyphen(v: string) {
134
- return v.charCodeAt(0) === 45 /* - */
135
- }
136
-
137
- function endsWithHyphen(v: string) {
138
- return v.charCodeAt(v.length - 1) === 45 /* - */
139
- }
140
-
141
- function isValidIdentifier(v: string) {
142
- // Note, since we already know that "v" only contains [a-zA-Z0-9-], we can
143
- // simplify the following regex by checking only the first char and presence
144
- // of "-".
145
-
146
- // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v)
147
- return !startsWithNumber(v) && !v.includes('-')
148
- }
149
-
150
- /**
151
- * @deprecated Use {@link ensureValidNsid} if you care about error details,
152
- * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or
153
- * {@link isValidNsid} if you just want a boolean.
154
- */
155
- export function ensureValidNsidRegex(nsid: string): asserts nsid is NsidString {
156
- const result = validateNsidRegex(nsid)
157
- if (!result.success) throw new InvalidNsidError(result.message)
158
- }
159
-
160
- /**
161
- * Regexp based validation that behaves identically to the previous code but
162
- * provides less detailed error messages (while being 20% to 50% faster).
163
- */
164
- export function validateNsidRegex(value: string): Result<NsidString> {
165
- if (value.length > 253 + 1 + 63) {
166
- return failure('NSID is too long (317 chars max)')
167
- }
168
-
169
- if (
170
- // Fast check for small values
171
- value.length < 5 ||
172
- !/^[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:\.[a-zA-Z](?:[a-zA-Z0-9]{0,62})?)$/.test(
173
- value,
174
- )
175
- ) {
176
- return failure("NSID didn't validate via regex")
177
- }
178
-
179
- return success(value as NsidString)
180
- }
181
-
182
- export class InvalidNsidError extends Error {}
package/src/recordkey.ts DELETED
@@ -1,51 +0,0 @@
1
- export type RecordKeyString = string
2
-
3
- const RECORD_KEY_MAX_LENGTH = 512
4
- const RECORD_KEY_MIN_LENGTH = 1
5
- const RECORD_KEY_INVALID_VALUES = new Set(['.', '..'])
6
- const RECORD_KEY_REGEX = /^[a-zA-Z0-9_~.:-]{1,512}$/
7
-
8
- // https://atproto.com/specs/record-key#record-key-syntax
9
- // Regardless of the type, Record Keys must fulfill some baseline syntax constraints:
10
- // - restricted to a subset of ASCII characters -- the allowed characters are
11
- // alphanumeric (A-Za-z0-9), period, dash, underscore, colon, or tilde (.-_:~)
12
- // - must have at least 1 and at most 512 characters
13
- // - the specific record key values . and .. are not allowed
14
- // - must be a permissible part of repository MST path string (the above
15
- // constraints satisfy this condition)
16
- // - must be permissible to include in a path component of a URI (following
17
- // RFC-3986, section 3.3). The above constraints satisfy this condition, by
18
- // matching the "unreserved" characters allowed in generic URI paths.
19
-
20
- export function ensureValidRecordKey<I extends string>(
21
- input: I,
22
- ): asserts input is I & RecordKeyString {
23
- if (
24
- input.length > RECORD_KEY_MAX_LENGTH ||
25
- input.length < RECORD_KEY_MIN_LENGTH
26
- ) {
27
- throw new InvalidRecordKeyError(
28
- `record key must be ${RECORD_KEY_MIN_LENGTH} to ${RECORD_KEY_MAX_LENGTH} characters`,
29
- )
30
- }
31
- if (RECORD_KEY_INVALID_VALUES.has(input)) {
32
- throw new InvalidRecordKeyError('record key can not be "." or ".."')
33
- }
34
- // simple regex to enforce most constraints via just regex and length.
35
- if (!RECORD_KEY_REGEX.test(input)) {
36
- throw new InvalidRecordKeyError('record key syntax not valid (regex)')
37
- }
38
- }
39
-
40
- export function isValidRecordKey<I extends string>(
41
- input: I,
42
- ): input is I & RecordKeyString {
43
- return (
44
- input.length >= RECORD_KEY_MIN_LENGTH &&
45
- input.length <= RECORD_KEY_MAX_LENGTH &&
46
- RECORD_KEY_REGEX.test(input) &&
47
- !RECORD_KEY_INVALID_VALUES.has(input)
48
- )
49
- }
50
-
51
- export class InvalidRecordKeyError extends Error {}
package/src/tid.ts DELETED
@@ -1,22 +0,0 @@
1
- export type TidString = string
2
-
3
- const TID_LENGTH = 13
4
- const TID_REGEX = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/
5
-
6
- export function ensureValidTid<I extends string>(
7
- input: I,
8
- ): asserts input is I & TidString {
9
- if (input.length !== TID_LENGTH) {
10
- throw new InvalidTidError(`TID must be ${TID_LENGTH} characters`)
11
- }
12
- // simple regex to enforce most constraints via just regex and length.
13
- if (!TID_REGEX.test(input)) {
14
- throw new InvalidTidError('TID syntax not valid (regex)')
15
- }
16
- }
17
-
18
- export function isValidTid<I extends string>(input: I): input is I & TidString {
19
- return input.length === TID_LENGTH && TID_REGEX.test(input)
20
- }
21
-
22
- export class InvalidTidError extends Error {}
package/src/uri.ts DELETED
@@ -1,5 +0,0 @@
1
- export type UriString = `${string}:${string}`
2
-
3
- export function isValidUri<I extends string>(input: I): input is I & UriString {
4
- return /^\w+:(?:\/\/)?[^\s/][^\s]*$/.test(input)
5
- }
@@ -1,223 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { describe, expect, test } from 'vitest'
3
- import {
4
- InvalidAtUriError,
5
- assertAtUriString,
6
- isAtUriString,
7
- } from '../src/index.js'
8
-
9
- describe('valid interop', () => {
10
- test.each(
11
- readLines(
12
- `${__dirname}/../../../interop-test-files/syntax/aturi_syntax_valid.txt`,
13
- ),
14
- )('%s', (value) => {
15
- expect(isAtUriString(value)).toBe(true)
16
- expect(isAtUriString(value, { strict: false })).toBe(true)
17
- expect(() => assertAtUriString(value)).not.toThrow()
18
- expect(() => assertAtUriString(value, { strict: false })).not.toThrow()
19
- })
20
- })
21
-
22
- describe('invalid interop', () => {
23
- test.each(
24
- readLines(
25
- `${__dirname}/../../../interop-test-files/syntax/aturi_syntax_invalid.txt`,
26
- ),
27
- )('%s', (value) => {
28
- expect(isAtUriString(value)).toBe(false)
29
- expect(() => assertAtUriString(value)).toThrow(InvalidAtUriError)
30
- })
31
- })
32
-
33
- describe('custom cases', () => {
34
- describe('valid spec basics', () => {
35
- testValid('at://did:plc:asdf123')
36
- testValid('at://user.bsky.social')
37
- testValid('at://did:plc:asdf123/com.atproto.feed.post')
38
- testValid('at://did:plc:asdf123/com.atproto.feed.post/record')
39
-
40
- testValid('at://did:plc:asdf123#/frag')
41
- testValid('at://user.bsky.social#/frag')
42
- testValid('at://did:plc:asdf123/com.atproto.feed.post#/frag')
43
- testValid('at://did:plc:asdf123/com.atproto.feed.post/record#/frag')
44
- })
45
-
46
- describe('invalid spec basics', () => {
47
- testInvalid('a://did:plc:asdf123')
48
- testInvalid('at//did:plc:asdf123')
49
- testInvalid('at:/a/did:plc:asdf123')
50
- testInvalid('at:/did:plc:asdf123')
51
- testInvalid('AT://did:plc:asdf123')
52
- testInvalid('http://did:plc:asdf123')
53
- testInvalid('://did:plc:asdf123')
54
- testInvalid('at:did:plc:asdf123')
55
- testInvalid('at:/did:plc:asdf123')
56
- testInvalid('at:///did:plc:asdf123')
57
- testInvalid('at://:/did:plc:asdf123')
58
- testInvalid('at:/ /did:plc:asdf123')
59
- testInvalid('at://did:plc:asdf123 ')
60
- testInvalid('at://did:plc:asdf123/ ')
61
- testInvalid(' at://did:plc:asdf123')
62
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post ')
63
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post# ')
64
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post#/ ')
65
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post#/frag ')
66
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post#fr ag')
67
- testInvalid('//did:plc:asdf123')
68
- testInvalid('at://name')
69
- testInvalid('at://name.0')
70
- testInvalid('at://diD:plc:asdf123')
71
- testInvalid('at://did:plc:asdf123/com.atproto.feed.p@st')
72
- testInvalid('at://did:plc:asdf123/com.atproto.feed.p$st')
73
- testInvalid('at://did:plc:asdf123/com.atproto.feed.p%st')
74
- testInvalid('at://did:plc:asdf123/com.atproto.feed.p&st')
75
- testInvalid('at://did:plc:asdf123/com.atproto.feed.p()t')
76
- testInvalid('at://did:plc:asdf123/com.atproto.feed_post')
77
- testInvalid('at://did:plc:asdf123/-com.atproto.feed.post')
78
- testInvalid('at://did:plc:asdf@123/com.atproto.feed.post')
79
-
80
- testInvalid('at://did:plc:asdf123?a')
81
- testInvalid('at://user.bsky.social?a=B')
82
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post?foo=bar')
83
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/record?q=3')
84
-
85
- testInvalid('at://did:plc:asdf123?a=b#/frag')
86
- testInvalid('at://user.bsky.social?a=b#/frag')
87
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post?a=b#/frag')
88
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/record?a=b#/frag')
89
-
90
- testInvalid('at://DID:plc:asdf123')
91
- testInvalid('at://user.bsky.123')
92
- testInvalid('at://bsky')
93
- testInvalid('at://did:plc:')
94
- testInvalid('at://did:plc:')
95
- testInvalid('at://frag')
96
- })
97
-
98
- describe('very long strings', () => {
99
- testValid('at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(512))
100
- testValid(`at://did:web:x${'.y'.repeat(100)}/com.atproto.feed.post/record`)
101
- testInvalid(`at://did:plc:${'o'.repeat(8200)}/com.atproto.feed.post/record`)
102
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(513))
103
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(800))
104
- })
105
-
106
- describe('invalid collection', () => {
107
- testInvalid('at://did:plc:asdf123/short/stuff')
108
- testInvalid('at://did:plc:asdf123/12345')
109
- })
110
-
111
- describe('invalid repeated slashes', () => {
112
- testInvalid('at://user.bsky.social//')
113
- testInvalid('at://user.bsky.social//com.atproto.feed.post')
114
- testInvalid('at://user.bsky.social/com.atproto.feed.post//')
115
- })
116
-
117
- describe('invalid trailing slashes', () => {
118
- testInvalid('at://did:plc:asdf123/')
119
- testInvalid('at://user.bsky.social/')
120
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/')
121
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/')
122
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/#/frag')
123
- })
124
-
125
- describe('invalid segment count', () => {
126
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf')
127
- testInvalid('at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more')
128
- })
129
-
130
- describe('valid record key', () => {
131
- testValid('at://did:plc:asdf123/com.atproto.feed.post/a')
132
- testValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123')
133
- })
134
-
135
- describe('loosely valid trailing slash', () => {
136
- testLoose('at://did:plc:asdf123/')
137
- testLoose('at://user.bsky.social/')
138
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/')
139
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/record/')
140
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/record/#/frag')
141
- })
142
-
143
- describe('loosely valid record keys', () => {
144
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/%23')
145
-
146
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123')
147
- testLoose("at://did:plc:asdf123/com.atproto.feed.post/~'sdf123")
148
-
149
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/$')
150
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/@')
151
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/!')
152
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/*')
153
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/(')
154
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/,')
155
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/;')
156
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/abc%30123')
157
-
158
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/%30')
159
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/%3')
160
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/%')
161
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/%zz')
162
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/%%%')
163
-
164
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/[]')
165
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/foo[')
166
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/bar]')
167
- testLoose('at://did:plc:asdf123/com.atproto.feed.post/[baz]')
168
- })
169
-
170
- describe('valid fragment', () => {
171
- testValid('at://did:plc:asdf123#/frac')
172
- })
173
-
174
- describe('invalid fragment', () => {
175
- testValid('at://did:plc:asdf123#/com.atproto.feed.post')
176
- testValid('at://did:plc:asdf123#/com.atproto.feed.post/')
177
- testValid('at://did:plc:asdf123#/asdf/')
178
-
179
- testValid('at://did:plc:asdf123/com.atproto.feed.post#/$@!*():,;~.sdf123')
180
- testValid('at://did:plc:asdf123#/[asfd]')
181
-
182
- testValid('at://did:plc:asdf123#/$')
183
- testValid('at://did:plc:asdf123#/*')
184
- testValid('at://did:plc:asdf123#/;')
185
- testValid('at://did:plc:asdf123#/,')
186
-
187
- testInvalid('at://did:plc:asdf123#')
188
- testInvalid('at://did:plc:asdf123##')
189
- testInvalid('#at://did:plc:asdf123')
190
- testInvalid('at://did:plc:asdf123#/asdf#/asdf')
191
- })
192
- })
193
-
194
- function testValid(value: string) {
195
- test(value, () => {
196
- expect(isAtUriString(value)).toBe(true)
197
- expect(isAtUriString(value, { strict: false })).toBe(true)
198
- expect(() => assertAtUriString(value)).not.toThrow()
199
- expect(() => assertAtUriString(value, { strict: false })).not.toThrow()
200
- })
201
- }
202
-
203
- function testInvalid(value: string) {
204
- test(value, () => {
205
- expect(isAtUriString(value)).toBe(false)
206
- expect(() => assertAtUriString(value)).toThrow(InvalidAtUriError)
207
- })
208
- }
209
-
210
- function testLoose(value: string) {
211
- test(value, () => {
212
- expect(isAtUriString(value)).toBe(false)
213
- expect(isAtUriString(value, { strict: false })).toBe(true)
214
- expect(() => assertAtUriString(value)).toThrow()
215
- expect(() => assertAtUriString(value, { strict: false })).not.toThrow()
216
- })
217
- }
218
-
219
- function readLines(filePath: string): string[] {
220
- return readFileSync(filePath, 'utf-8')
221
- .split(/\r?\n/)
222
- .filter((line) => !line.startsWith('#') && line.length > 0)
223
- }