@atproto/syntax 0.4.1 → 0.4.2

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 (54) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/at-identifier.d.ts +5 -0
  3. package/dist/at-identifier.d.ts.map +1 -0
  4. package/dist/at-identifier.js +19 -0
  5. package/dist/at-identifier.js.map +1 -0
  6. package/dist/aturi.d.ts +8 -6
  7. package/dist/aturi.d.ts.map +1 -1
  8. package/dist/aturi.js +36 -45
  9. package/dist/aturi.js.map +1 -1
  10. package/dist/aturi_validation.d.ts +5 -2
  11. package/dist/aturi_validation.d.ts.map +1 -1
  12. package/dist/aturi_validation.js +53 -57
  13. package/dist/aturi_validation.js.map +1 -1
  14. package/dist/datetime.d.ts +11 -4
  15. package/dist/datetime.d.ts.map +1 -1
  16. package/dist/datetime.js +16 -16
  17. package/dist/datetime.js.map +1 -1
  18. package/dist/did.d.ts +3 -2
  19. package/dist/did.d.ts.map +1 -1
  20. package/dist/did.js +8 -8
  21. package/dist/did.js.map +1 -1
  22. package/dist/handle.d.ts +7 -6
  23. package/dist/handle.d.ts.map +1 -1
  24. package/dist/handle.js +28 -31
  25. package/dist/handle.js.map +1 -1
  26. package/dist/index.d.ts +8 -7
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8 -7
  29. package/dist/index.js.map +1 -1
  30. package/dist/nsid.d.ts +6 -5
  31. package/dist/nsid.d.ts.map +1 -1
  32. package/dist/nsid.js +1 -1
  33. package/dist/nsid.js.map +1 -1
  34. package/dist/recordkey.d.ts +3 -2
  35. package/dist/recordkey.d.ts.map +1 -1
  36. package/dist/recordkey.js +33 -22
  37. package/dist/recordkey.js.map +1 -1
  38. package/dist/tid.d.ts +3 -2
  39. package/dist/tid.d.ts.map +1 -1
  40. package/dist/tid.js +7 -7
  41. package/dist/tid.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/at-identifier.ts +22 -0
  44. package/src/aturi.ts +59 -46
  45. package/src/aturi_validation.ts +60 -49
  46. package/src/datetime.ts +17 -4
  47. package/src/did.ts +5 -2
  48. package/src/handle.ts +23 -22
  49. package/src/index.ts +8 -7
  50. package/src/nsid.ts +8 -6
  51. package/src/recordkey.ts +40 -17
  52. package/src/tid.ts +4 -2
  53. package/tsconfig.build.json +1 -0
  54. package/tsconfig.build.tsbuildinfo +1 -1
package/src/aturi.ts CHANGED
@@ -1,4 +1,8 @@
1
- export * from './aturi_validation'
1
+ import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'
2
+ import { AtUriString } from './aturi_validation.js'
3
+ import { ensureValidNsid } from './nsid.js'
4
+
5
+ export * from './aturi_validation.js'
2
6
 
3
7
  export const ATP_URI_REGEX =
4
8
  // proto- --did-------------- --name---------------- --path---- --query-- --hash--
@@ -8,32 +12,23 @@ const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i
8
12
 
9
13
  export class AtUri {
10
14
  hash: string
11
- host: string
15
+ host: AtIdentifierString
12
16
  pathname: string
13
17
  searchParams: URLSearchParams
14
18
 
15
- constructor(uri: string, base?: string) {
16
- let parsed
17
- if (base) {
18
- parsed = parse(base)
19
- if (!parsed) {
20
- throw new Error(`Invalid at uri: ${base}`)
21
- }
22
- const relativep = parseRelative(uri)
23
- if (!relativep) {
24
- throw new Error(`Invalid path: ${uri}`)
25
- }
26
- Object.assign(parsed, relativep)
27
- } else {
28
- parsed = parse(uri)
29
- if (!parsed) {
30
- throw new Error(`Invalid at uri: ${uri}`)
31
- }
32
- }
19
+ constructor(uri: string, base?: string | AtUri) {
20
+ const parsed =
21
+ base !== undefined
22
+ ? typeof base === 'string'
23
+ ? Object.assign(parse(base), parseRelative(uri))
24
+ : Object.assign({ host: base.host }, parseRelative(uri))
25
+ : parse(uri)
33
26
 
34
- this.hash = parsed.hash
27
+ ensureValidAtIdentifier(parsed.host)
28
+
29
+ this.hash = parsed.hash ?? ''
35
30
  this.host = parsed.host
36
- this.pathname = parsed.pathname
31
+ this.pathname = parsed.pathname ?? ''
37
32
  this.searchParams = parsed.searchParams
38
33
  }
39
34
 
@@ -49,7 +44,7 @@ export class AtUri {
49
44
  }
50
45
 
51
46
  get origin() {
52
- return `at://${this.host}`
47
+ return `at://${this.host}` as const
53
48
  }
54
49
 
55
50
  get hostname() {
@@ -57,6 +52,7 @@ export class AtUri {
57
52
  }
58
53
 
59
54
  set hostname(v: string) {
55
+ ensureValidAtIdentifier(v)
60
56
  this.host = v
61
57
  }
62
58
 
@@ -73,6 +69,7 @@ export class AtUri {
73
69
  }
74
70
 
75
71
  set collection(v: string) {
72
+ ensureValidNsid(v)
76
73
  const parts = this.pathname.split('/').filter(Boolean)
77
74
  parts[0] = v
78
75
  this.pathname = parts.join('/')
@@ -84,7 +81,7 @@ export class AtUri {
84
81
 
85
82
  set rkey(v: string) {
86
83
  const parts = this.pathname.split('/').filter(Boolean)
87
- if (!parts[0]) parts[0] = 'undefined'
84
+ parts[0] ||= 'undefined'
88
85
  parts[1] = v
89
86
  this.pathname = parts.join('/')
90
87
  }
@@ -93,44 +90,60 @@ export class AtUri {
93
90
  return this.toString()
94
91
  }
95
92
 
96
- toString() {
93
+ toString(): AtUriString {
97
94
  let path = this.pathname || '/'
98
95
  if (!path.startsWith('/')) {
99
96
  path = `/${path}`
100
97
  }
101
- let qs = this.searchParams.toString()
102
- if (qs && !qs.startsWith('?')) {
103
- qs = `?${qs}`
98
+ let qs = ''
99
+ if (this.searchParams.size) {
100
+ qs = `?${this.searchParams.toString()}`
104
101
  }
105
102
  let hash = this.hash
106
103
  if (hash && !hash.startsWith('#')) {
107
104
  hash = `#${hash}`
108
105
  }
109
- return `at://${this.host}${path}${qs}${hash}`
106
+ return `at://${this.host}${path}${qs}${hash}` as AtUriString
110
107
  }
111
108
  }
112
109
 
113
110
  function parse(str: string) {
114
- const match = ATP_URI_REGEX.exec(str)
115
- if (match) {
116
- return {
117
- hash: match[5] || '',
118
- host: match[2] || '',
119
- pathname: match[3] || '',
120
- searchParams: new URLSearchParams(match[4] || ''),
121
- }
111
+ const match = str.match(ATP_URI_REGEX) as null | {
112
+ 0: string
113
+ 1: string | undefined // proto
114
+ 2: string // host
115
+ 3: string | undefined // path
116
+ 4: string | undefined // query
117
+ 5: string | undefined // hash
118
+ }
119
+
120
+ if (!match) {
121
+ throw new Error(`Invalid AT uri: ${str}`)
122
+ }
123
+
124
+ return {
125
+ host: match[2],
126
+ hash: match[5],
127
+ pathname: match[3],
128
+ searchParams: new URLSearchParams(match[4]),
122
129
  }
123
- return undefined
124
130
  }
125
131
 
126
132
  function parseRelative(str: string) {
127
- const match = RELATIVE_REGEX.exec(str)
128
- if (match) {
129
- return {
130
- hash: match[3] || '',
131
- pathname: match[1] || '',
132
- searchParams: new URLSearchParams(match[2] || ''),
133
- }
133
+ const match = str.match(RELATIVE_REGEX) as null | {
134
+ 0: string
135
+ 1: string | undefined // path
136
+ 2: string | undefined // query
137
+ 3: string | undefined // hash
138
+ }
139
+
140
+ if (!match) {
141
+ throw new Error(`Invalid path: ${str}`)
142
+ }
143
+
144
+ return {
145
+ hash: match[3],
146
+ pathname: match[1],
147
+ searchParams: new URLSearchParams(match[2]),
134
148
  }
135
- return undefined
136
149
  }
@@ -1,6 +1,12 @@
1
- import { ensureValidDid, ensureValidDidRegex } from './did'
2
- import { ensureValidHandle, ensureValidHandleRegex } from './handle'
3
- import { isValidNsid } from './nsid'
1
+ import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'
2
+ import { ensureValidDidRegex } from './did.js'
3
+ import { ensureValidHandleRegex } from './handle.js'
4
+ import { NsidString, isValidNsid } from './nsid.js'
5
+
6
+ export type AtUriString =
7
+ | `at://${AtIdentifierString}`
8
+ | `at://${AtIdentifierString}/${NsidString}`
9
+ | `at://${AtIdentifierString}/${NsidString}/${string}`
4
10
 
5
11
  // Human-readable constraints on ATURI:
6
12
  // - following regular URLs, a 8KByte hard total length limit
@@ -15,53 +21,76 @@ import { isValidNsid } from './nsid'
15
21
  // [a-zA-Z0-9._~:@!$&'\(\)*+,;=-]
16
22
  // - rkey must have at least one char
17
23
  // - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901)
18
- export const ensureValidAtUri = (uri: string) => {
19
- // JSON pointer is pretty different from rest of URI, so split that out first
20
- const uriParts = uri.split('#')
21
- if (uriParts.length > 2) {
22
- throw new Error('ATURI can have at most one "#", separating fragment out')
24
+
25
+ export function ensureValidAtUri(input: string): asserts input is AtUriString {
26
+ const fragmentIndex = input.indexOf('#')
27
+ if (fragmentIndex !== -1) {
28
+ if (input.charCodeAt(fragmentIndex + 1) !== 47) {
29
+ throw new Error('ATURI fragment must be non-empty and start with slash')
30
+ }
31
+ if (input.includes('#', fragmentIndex + 1)) {
32
+ throw new Error('ATURI can have at most one "#", separating fragment out')
33
+ }
34
+
35
+ // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
36
+ const fragment = input.slice(fragmentIndex + 1)
37
+ if (!/^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/.test(fragment)) {
38
+ throw new Error('Disallowed characters in ATURI fragment (ASCII)')
39
+ }
23
40
  }
24
- const fragmentPart = uriParts[1] || null
25
- uri = uriParts[0]
26
41
 
27
- // check that all chars are boring ASCII
28
- if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {
29
- throw new Error('Disallowed characters in ATURI (ASCII)')
42
+ const uri = fragmentIndex === -1 ? input : input.slice(0, fragmentIndex)
43
+
44
+ if (uri.length > 8 * 1024) {
45
+ throw new Error('ATURI is far too long')
30
46
  }
31
47
 
32
- const parts = uri.split('/')
33
- if (parts.length >= 3 && (parts[0] !== 'at:' || parts[1].length !== 0)) {
48
+ if (!uri.startsWith('at://')) {
34
49
  throw new Error('ATURI must start with "at://"')
35
50
  }
36
- if (parts.length < 3) {
37
- throw new Error('ATURI requires at least method and authority sections')
51
+
52
+ // check that all chars are boring ASCII
53
+ if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {
54
+ throw new Error('Disallowed characters in ATURI (ASCII)')
38
55
  }
39
56
 
57
+ const authorityEnd = uri.indexOf('/', 5)
58
+ const authority =
59
+ authorityEnd === -1 ? uri.slice(5) : uri.slice(5, authorityEnd)
40
60
  try {
41
- if (parts[2].startsWith('did:')) {
42
- ensureValidDid(parts[2])
43
- } else {
44
- ensureValidHandle(parts[2])
45
- }
46
- } catch {
47
- throw new Error('ATURI authority must be a valid handle or DID')
61
+ ensureValidAtIdentifier(authority)
62
+ } catch (cause) {
63
+ throw new Error('ATURI authority must be a valid handle or DID', { cause })
48
64
  }
49
65
 
50
- if (parts.length >= 4) {
51
- if (parts[3].length === 0) {
66
+ const collectionStart = authorityEnd === -1 ? -1 : authorityEnd + 1
67
+ const collectionEnd =
68
+ collectionStart === -1 ? -1 : uri.indexOf('/', collectionStart)
69
+
70
+ if (collectionStart !== -1) {
71
+ const collection =
72
+ collectionEnd === -1
73
+ ? uri.slice(collectionStart)
74
+ : uri.slice(collectionStart, collectionEnd)
75
+
76
+ if (collection.length === 0) {
52
77
  throw new Error(
53
78
  'ATURI can not have a slash after authority without a path segment',
54
79
  )
55
80
  }
56
- if (!isValidNsid(parts[3])) {
81
+ if (!isValidNsid(collection)) {
57
82
  throw new Error(
58
83
  'ATURI requires first path segment (if supplied) to be valid NSID',
59
84
  )
60
85
  }
61
86
  }
62
87
 
63
- if (parts.length >= 5) {
64
- if (parts[4].length === 0) {
88
+ const recordKeyStart = collectionEnd === -1 ? -1 : collectionEnd + 1
89
+ const recordKeyEnd =
90
+ recordKeyStart === -1 ? -1 : uri.indexOf('/', recordKeyStart)
91
+
92
+ if (recordKeyStart !== -1) {
93
+ if (recordKeyStart === uri.length) {
65
94
  throw new Error(
66
95
  'ATURI can not have a slash after collection, unless record key is provided',
67
96
  )
@@ -69,32 +98,14 @@ export const ensureValidAtUri = (uri: string) => {
69
98
  // would validate rkey here, but there are basically no constraints!
70
99
  }
71
100
 
72
- if (parts.length >= 6) {
101
+ if (recordKeyEnd !== -1) {
73
102
  throw new Error(
74
103
  'ATURI path can have at most two parts, and no trailing slash',
75
104
  )
76
105
  }
77
-
78
- if (uriParts.length >= 2 && fragmentPart == null) {
79
- throw new Error('ATURI fragment must be non-empty and start with slash')
80
- }
81
-
82
- if (fragmentPart != null) {
83
- if (fragmentPart.length === 0 || fragmentPart[0] !== '/') {
84
- throw new Error('ATURI fragment must be non-empty and start with slash')
85
- }
86
- // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
87
- if (!/^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/.test(fragmentPart)) {
88
- throw new Error('Disallowed characters in ATURI fragment (ASCII)')
89
- }
90
- }
91
-
92
- if (uri.length > 8 * 1024) {
93
- throw new Error('ATURI is far too long')
94
- }
95
106
  }
96
107
 
97
- export const ensureValidAtUriRegex = (uri: string): void => {
108
+ export function ensureValidAtUriRegex(uri: string): asserts uri is AtUriString {
98
109
  // simple regex to enforce most constraints via just regex and length.
99
110
  // hand wrote this regex based on above constraints. whew!
100
111
  const aturiRegex =
package/src/datetime.ts CHANGED
@@ -1,7 +1,20 @@
1
+ /** An ISO 8601 formatted datetime string (YYYY-MM-DDTHH:mm:ss.sssZ) */
2
+ export type DatetimeString =
3
+ `${string}-${string}-${string}T${string}:${string}:${string}${'Z' | `+${string}` | `-${string}`}`
4
+
5
+ // Allow date.toISOString() to be used where datetime format is expected
6
+ declare global {
7
+ interface Date {
8
+ toISOString(): `${string}-${string}-${string}T${string}:${string}:${string}Z`
9
+ }
10
+ }
11
+
1
12
  /* Validates datetime string against atproto Lexicon 'datetime' format.
2
13
  * Syntax is described at: https://atproto.com/specs/lexicon#datetime
3
14
  */
4
- export const ensureValidDatetime = (dtStr: string): void => {
15
+ export function ensureValidDatetime(
16
+ dtStr: string,
17
+ ): asserts dtStr is DatetimeString {
5
18
  const date = new Date(dtStr)
6
19
  // must parse as ISO 8601; this also verifies semantics like month is not 13 or 00
7
20
  if (isNaN(date.getTime())) {
@@ -33,7 +46,7 @@ export const ensureValidDatetime = (dtStr: string): void => {
33
46
 
34
47
  /* Same logic as ensureValidDatetime(), but returns a boolean instead of throwing an exception.
35
48
  */
36
- export const isValidDatetime = (dtStr: string): boolean => {
49
+ export function isValidDatetime(dtStr: string): dtStr is DatetimeString {
37
50
  try {
38
51
  ensureValidDatetime(dtStr)
39
52
  } catch (err) {
@@ -56,7 +69,7 @@ export const isValidDatetime = (dtStr: string): boolean => {
56
69
  *
57
70
  * Expected output format: YYYY-MM-DDTHH:mm:ss.sssZ
58
71
  */
59
- export const normalizeDatetime = (dtStr: string): string => {
72
+ export function normalizeDatetime(dtStr: string): DatetimeString {
60
73
  if (isValidDatetime(dtStr)) {
61
74
  const outStr = new Date(dtStr).toISOString()
62
75
  if (isValidDatetime(outStr)) {
@@ -96,7 +109,7 @@ export const normalizeDatetime = (dtStr: string): string => {
96
109
  *
97
110
  * If a InvalidDatetimeError is encountered, returns the UNIX epoch time as a UTC datetime (1970-01-01T00:00:00.000Z).
98
111
  */
99
- export const normalizeDatetimeAlways = (dtStr: string): string => {
112
+ export const normalizeDatetimeAlways = (dtStr: string): DatetimeString => {
100
113
  try {
101
114
  return normalizeDatetime(dtStr)
102
115
  } catch (err) {
package/src/did.ts CHANGED
@@ -11,7 +11,10 @@
11
11
  // - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer
12
12
  // - hard length limit of 8KBytes
13
13
  // - not going to validate "percent encoding" here
14
- export const ensureValidDid = (did: string): void => {
14
+
15
+ export type DidString<M extends string = string> = `did:${M}:${string}`
16
+
17
+ export function ensureValidDid(did: string): asserts did is DidString {
15
18
  if (!did.startsWith('did:')) {
16
19
  throw new InvalidDidError('DID requires "did:" prefix')
17
20
  }
@@ -43,7 +46,7 @@ export const ensureValidDid = (did: string): void => {
43
46
  }
44
47
  }
45
48
 
46
- export const ensureValidDidRegex = (did: string): void => {
49
+ export function ensureValidDidRegex(did: string): asserts did is DidString {
47
50
  // simple regex to enforce most constraints via just regex and length.
48
51
  // hand wrote this regex based on above constraints
49
52
  if (!/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(did)) {
package/src/handle.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export const INVALID_HANDLE = 'handle.invalid'
2
2
 
3
+ export type HandleString = `${string}.${string}`
4
+
3
5
  // Currently these are registration-time restrictions, not protocol-level
4
6
  // restrictions. We have a couple accounts in the wild that we need to clean up
5
7
  // before hard-disallow.
@@ -37,7 +39,9 @@ export const DISALLOWED_TLDS = [
37
39
  // - does not validate whether domain or TLD exists, or is a reserved or
38
40
  // special TLD (eg, .onion or .local)
39
41
  // - does not validate punycode
40
- export const ensureValidHandle = (handle: string): void => {
42
+ export function ensureValidHandle(
43
+ handle: string,
44
+ ): asserts handle is HandleString {
41
45
  // check that all chars are boring ASCII
42
46
  if (!/^[a-zA-Z0-9.-]*$/.test(handle)) {
43
47
  throw new InvalidHandleError(
@@ -74,46 +78,43 @@ export const ensureValidHandle = (handle: string): void => {
74
78
  }
75
79
 
76
80
  // simple regex translation of above constraints
77
- export const ensureValidHandleRegex = (handle: string): void => {
78
- if (
79
- !/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(
80
- handle,
81
- )
82
- ) {
83
- throw new InvalidHandleError("Handle didn't validate via regex")
84
- }
81
+ const HANDLE_REGEX =
82
+ /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
83
+
84
+ export function ensureValidHandleRegex(
85
+ handle: string,
86
+ ): asserts handle is HandleString {
85
87
  if (handle.length > 253) {
86
88
  throw new InvalidHandleError('Handle is too long (253 chars max)')
87
89
  }
90
+ if (!HANDLE_REGEX.test(handle)) {
91
+ throw new InvalidHandleError("Handle didn't validate via regex")
92
+ }
88
93
  }
89
94
 
90
- export const normalizeHandle = (handle: string): string => {
95
+ export function normalizeHandle(handle: string): string {
91
96
  return handle.toLowerCase()
92
97
  }
93
98
 
94
- export const normalizeAndEnsureValidHandle = (handle: string): string => {
99
+ export function normalizeAndEnsureValidHandle(handle: string): HandleString {
95
100
  const normalized = normalizeHandle(handle)
96
101
  ensureValidHandle(normalized)
97
102
  return normalized
98
103
  }
99
104
 
100
- export const isValidHandle = (handle: string): boolean => {
101
- try {
102
- ensureValidHandle(handle)
103
- } catch (err) {
104
- if (err instanceof InvalidHandleError) {
105
+ export function isValidHandle(handle: string): handle is HandleString {
106
+ return handle.length <= 253 && HANDLE_REGEX.test(handle)
107
+ }
108
+
109
+ export function isValidTld(handle: string): boolean {
110
+ for (const tld of DISALLOWED_TLDS) {
111
+ if (handle.endsWith(tld)) {
105
112
  return false
106
113
  }
107
- throw err
108
114
  }
109
-
110
115
  return true
111
116
  }
112
117
 
113
- export const isValidTld = (handle: string): boolean => {
114
- return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain))
115
- }
116
-
117
118
  export class InvalidHandleError extends Error {}
118
119
  /** @deprecated Never used */
119
120
  export class ReservedHandleError extends Error {}
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
- export * from './handle'
2
- export * from './did'
3
- export * from './nsid'
4
- export * from './aturi'
5
- export * from './tid'
6
- export * from './recordkey'
7
- export * from './datetime'
1
+ export * from './handle.js'
2
+ export * from './did.js'
3
+ export * from './nsid.js'
4
+ export * from './aturi.js'
5
+ export * from './at-identifier.js'
6
+ export * from './tid.js'
7
+ export * from './recordkey.js'
8
+ export * from './datetime.js'
package/src/nsid.ts CHANGED
@@ -11,6 +11,8 @@ nsid = authority delim name
11
11
 
12
12
  */
13
13
 
14
+ export type NsidString = `${string}.${string}.${string}`
15
+
14
16
  export class NSID {
15
17
  readonly segments: readonly string[]
16
18
 
@@ -58,7 +60,7 @@ export class NSID {
58
60
  }
59
61
  }
60
62
 
61
- export function ensureValidNsid(nsid: string): void {
63
+ export function ensureValidNsid(nsid: string): asserts nsid is NsidString {
62
64
  const result = validateNsid(nsid)
63
65
  if (!result.success) throw new InvalidNsidError(result.message)
64
66
  }
@@ -69,7 +71,7 @@ export function parseNsid(nsid: string): string[] {
69
71
  return result.value
70
72
  }
71
73
 
72
- export function isValidNsid(nsid: string): boolean {
74
+ export function isValidNsid(nsid: string): nsid is NsidString {
73
75
  // Since the regex version is more performant for valid NSIDs, we use it when
74
76
  // we don't care about error details.
75
77
  return validateNsidRegex(nsid).success
@@ -142,7 +144,7 @@ export function validateNsid(input: string): ValidateResult<string[]> {
142
144
  }
143
145
  }
144
146
 
145
- function hasDisallowedCharacters(v) {
147
+ function hasDisallowedCharacters(v: string) {
146
148
  return !/^[a-zA-Z0-9.-]*$/.test(v)
147
149
  }
148
150
 
@@ -173,7 +175,7 @@ function isValidIdentifier(v: string) {
173
175
  * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or
174
176
  * {@link isValidNsid} if you just want a boolean.
175
177
  */
176
- export function ensureValidNsidRegex(nsid: string): void {
178
+ export function ensureValidNsidRegex(nsid: string): asserts nsid is NsidString {
177
179
  const result = validateNsidRegex(nsid)
178
180
  if (!result.success) throw new InvalidNsidError(result.message)
179
181
  }
@@ -182,7 +184,7 @@ export function ensureValidNsidRegex(nsid: string): void {
182
184
  * Regexp based validation that behaves identically to the previous code but
183
185
  * provides less detailed error messages (while being 20% to 50% faster).
184
186
  */
185
- export function validateNsidRegex(value: string): ValidateResult<string> {
187
+ export function validateNsidRegex(value: string): ValidateResult<NsidString> {
186
188
  if (value.length > 253 + 1 + 63) {
187
189
  return {
188
190
  success: false,
@@ -203,7 +205,7 @@ export function validateNsidRegex(value: string): ValidateResult<string> {
203
205
 
204
206
  return {
205
207
  success: true,
206
- value,
208
+ value: value as NsidString,
207
209
  }
208
210
  }
209
211
 
package/src/recordkey.ts CHANGED
@@ -1,26 +1,49 @@
1
- export const ensureValidRecordKey = (rkey: string): void => {
2
- if (rkey.length > 512 || rkey.length < 1) {
3
- throw new InvalidRecordKeyError('record key must be 1 to 512 characters')
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(
21
+ rkey: string,
22
+ ): asserts rkey is RecordKeyString {
23
+ if (
24
+ rkey.length > RECORD_KEY_MAX_LENGTH ||
25
+ rkey.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(rkey)) {
32
+ throw new InvalidRecordKeyError('record key can not be "." or ".."')
4
33
  }
5
34
  // simple regex to enforce most constraints via just regex and length.
6
- if (!/^[a-zA-Z0-9_~.:-]{1,512}$/.test(rkey)) {
35
+ if (!RECORD_KEY_REGEX.test(rkey)) {
7
36
  throw new InvalidRecordKeyError('record key syntax not valid (regex)')
8
37
  }
9
- if (rkey === '.' || rkey === '..')
10
- throw new InvalidRecordKeyError('record key can not be "." or ".."')
11
38
  }
12
39
 
13
- export const isValidRecordKey = (rkey: string): boolean => {
14
- try {
15
- ensureValidRecordKey(rkey)
16
- } catch (err) {
17
- if (err instanceof InvalidRecordKeyError) {
18
- return false
19
- }
20
- throw err
21
- }
22
-
23
- return true
40
+ export function isValidRecordKey(rkey: string): rkey is RecordKeyString {
41
+ return (
42
+ rkey.length >= RECORD_KEY_MIN_LENGTH &&
43
+ rkey.length <= RECORD_KEY_MAX_LENGTH &&
44
+ RECORD_KEY_REGEX.test(rkey) &&
45
+ !RECORD_KEY_INVALID_VALUES.has(rkey)
46
+ )
24
47
  }
25
48
 
26
49
  export class InvalidRecordKeyError extends Error {}
package/src/tid.ts CHANGED
@@ -1,7 +1,9 @@
1
+ export type TidString = string
2
+
1
3
  const TID_LENGTH = 13
2
4
  const TID_REGEX = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/
3
5
 
4
- export const ensureValidTid = (tid: string): void => {
6
+ export function ensureValidTid(tid: string): asserts tid is TidString {
5
7
  if (tid.length !== TID_LENGTH) {
6
8
  throw new InvalidTidError(`TID must be ${TID_LENGTH} characters`)
7
9
  }
@@ -11,7 +13,7 @@ export const ensureValidTid = (tid: string): void => {
11
13
  }
12
14
  }
13
15
 
14
- export const isValidTid = (tid: string): boolean => {
16
+ export function isValidTid(tid: string): tid is TidString {
15
17
  return tid.length === TID_LENGTH && TID_REGEX.test(tid)
16
18
  }
17
19
 
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "extends": "../../tsconfig/isomorphic.json",
3
3
  "compilerOptions": {
4
+ "noImplicitAny": true,
4
5
  "rootDir": "./src",
5
6
  "outDir": "./dist"
6
7
  },
@@ -1 +1 @@
1
- {"root":["./src/aturi.ts","./src/aturi_validation.ts","./src/datetime.ts","./src/did.ts","./src/handle.ts","./src/index.ts","./src/nsid.ts","./src/recordkey.ts","./src/tid.ts"],"version":"5.8.2"}
1
+ {"root":["./src/at-identifier.ts","./src/aturi.ts","./src/aturi_validation.ts","./src/datetime.ts","./src/did.ts","./src/handle.ts","./src/index.ts","./src/nsid.ts","./src/recordkey.ts","./src/tid.ts"],"version":"5.8.2"}