@atproto/syntax 0.4.3 → 0.5.1

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.
package/src/aturi.ts CHANGED
@@ -1,9 +1,24 @@
1
- import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'
1
+ import {
2
+ AtIdentifierString,
3
+ ensureValidAtIdentifier,
4
+ isDidIdentifier,
5
+ } from './at-identifier.js'
2
6
  import { AtUriString } from './aturi_validation.js'
3
- import { ensureValidNsid } from './nsid.js'
7
+ import { DidString, InvalidDidError } from './did.js'
8
+ import { NsidString, ensureValidNsid } from './nsid.js'
9
+ import { RecordKeyString, ensureValidRecordKey } from './recordkey.js'
4
10
 
5
11
  export * from './aturi_validation.js'
6
12
 
13
+ // Re-export types used in public interface
14
+ export type {
15
+ AtIdentifierString,
16
+ AtUriString,
17
+ DidString,
18
+ NsidString,
19
+ RecordKeyString,
20
+ }
21
+
7
22
  export const ATP_URI_REGEX =
8
23
  // proto- --did-------------- --name---------------- --path---- --query-- --hash--
9
24
  /^(at:\/\/)?((?:did:[a-z0-9:%-]+)|(?:[a-z0-9][a-z0-9.:-]*))(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i
@@ -47,7 +62,13 @@ export class AtUri {
47
62
  return `at://${this.host}` as const
48
63
  }
49
64
 
50
- get hostname() {
65
+ get did(): DidString {
66
+ const { host } = this
67
+ if (isDidIdentifier(host)) return host
68
+ throw new InvalidDidError(`AtUri "${this}" does not have a DID hostname`)
69
+ }
70
+
71
+ get hostname(): AtIdentifierString {
51
72
  return this.host
52
73
  }
53
74
 
@@ -68,8 +89,18 @@ export class AtUri {
68
89
  return this.pathname.split('/').filter(Boolean)[0] || ''
69
90
  }
70
91
 
92
+ get collectionSafe(): NsidString {
93
+ const { collection } = this
94
+ ensureValidNsid(collection)
95
+ return collection
96
+ }
97
+
71
98
  set collection(v: string) {
72
99
  ensureValidNsid(v)
100
+ this.unsafelySetCollection(v)
101
+ }
102
+
103
+ unsafelySetCollection(v: string) {
73
104
  const parts = this.pathname.split('/').filter(Boolean)
74
105
  parts[0] = v
75
106
  this.pathname = parts.join('/')
@@ -79,7 +110,18 @@ export class AtUri {
79
110
  return this.pathname.split('/').filter(Boolean)[1] || ''
80
111
  }
81
112
 
113
+ get rkeySafe(): RecordKeyString {
114
+ const { rkey } = this
115
+ ensureValidRecordKey(rkey)
116
+ return rkey
117
+ }
118
+
82
119
  set rkey(v: string) {
120
+ ensureValidRecordKey(v)
121
+ this.unsafelySetRkey(v)
122
+ }
123
+
124
+ unsafelySetRkey(v: string) {
83
125
  const parts = this.pathname.split('/').filter(Boolean)
84
126
  parts[0] ||= 'undefined'
85
127
  parts[1] = v
package/src/datetime.ts CHANGED
@@ -1,124 +1,304 @@
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}`}`
1
+ /**
2
+ * Indicates a date or string is not a valid representation of a datetime
3
+ * according to the atproto
4
+ * {@link https://atproto.com/specs/lexicon#datetime specification}.
5
+ */
6
+ export class InvalidDatetimeError extends Error {}
7
+
8
+ /**
9
+ * A subset of {@link DatetimeString} that represent valid datetime strings with
10
+ * the format: `YYYY-MM-DDTHH:mm:ss.sssZ`, as returned by `Date.toISOString()
11
+ * for dates between the years 0000 and 9999.
12
+ *
13
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString}
14
+ */
15
+ export type ISODatetimeString =
16
+ // @TODO Switch to branded types for more accurate type safety.
17
+ `${string}-${string}-${string}T${string}:${string}:${string}.${string}Z`
4
18
 
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`
19
+ /**
20
+ * Represents a {@link Date} that can be safely stringified into a valid atproto
21
+ * {@link DatetimeString} using the {@link Date.toISOString toISOString()}
22
+ * method.
23
+ */
24
+ export interface AtprotoDate extends Date {
25
+ toISOString(): ISODatetimeString
26
+ }
27
+
28
+ /**
29
+ * @see {@link AtprotoDate}
30
+ */
31
+ export function assertAtprotoDate(date: Date): asserts date is AtprotoDate {
32
+ const res = parseDate(date)
33
+ if (!res.success) {
34
+ throw new InvalidDatetimeError(res.message)
9
35
  }
10
36
  }
11
37
 
12
- /* Validates datetime string against atproto Lexicon 'datetime' format.
13
- * Syntax is described at: https://atproto.com/specs/lexicon#datetime
38
+ /**
39
+ * @see {@link AtprotoDate}
14
40
  */
15
- export function ensureValidDatetime<I extends string>(
41
+ export function asAtprotoDate(date: Date): AtprotoDate {
42
+ assertAtprotoDate(date)
43
+ return date
44
+ }
45
+
46
+ /**
47
+ * @see {@link AtprotoDate}
48
+ */
49
+ export function isAtprotoDate(date: Date): date is AtprotoDate {
50
+ return parseDate(date).success
51
+ }
52
+
53
+ /**
54
+ * @see {@link AtprotoDate}
55
+ */
56
+ export function ifAtprotoDate(date: Date): AtprotoDate | undefined {
57
+ return isAtprotoDate(date) ? date : undefined
58
+ }
59
+
60
+ /**
61
+ * Datetime strings in atproto data structures and API calls should meet the
62
+ * {@link https://ijmacd.github.io/rfc3339-iso8601/ intersecting} requirements
63
+ * of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards.
64
+ *
65
+ * @note This literal template type is not accurate enough to ensure that a
66
+ * string is a valid atproto datetime. The {@link DatetimeString} validation
67
+ * functions ({@link assertDatetimeString}, {@link isDatetimeString}, etc)
68
+ * should be used to validate that a string meets the atproto datetime
69
+ * requirements, and the {@link toDatetimeString} function should be used to
70
+ * convert a {@link Date} object into a valid {@link DatetimeString} string.
71
+ *
72
+ * @example "2024-01-15T12:30:00Z"
73
+ * @example "2024-01-15T12:30:00.000Z"
74
+ * @example "2024-01-15T12:30:00+00:00"
75
+ * @example "2024-01-15T11:30:00-01:00"
76
+ * @see {@link https://atproto.com/specs/lexicon#datetime atproto Lexicon datetime format}
77
+ * @see {@link https://www.rfc-editor.org/rfc/rfc3339 RFC 3339}
78
+ * @see {@link https://www.iso.org/iso-8601-date-and-time-format.html ISO 8601}
79
+ */
80
+ export type DatetimeString =
81
+ // @TODO Switch to branded types for more accurate type safety?
82
+ | `${string}-${string}-${string}T${string}:${string}:${string}Z`
83
+ | `${string}-${string}-${string}T${string}:${string}:${string}${'+' | '-'}${string}:${string}`
84
+
85
+ /**
86
+ * Validates that a string is a valid {@link DatetimeString} format string,
87
+ * throwing an error if it is not.
88
+ *
89
+ * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.
90
+ * @see {@link DatetimeString}
91
+ */
92
+ export function assertDatetimeString<I>(
16
93
  input: I,
17
94
  ): asserts input is I & DatetimeString {
18
- const date = new Date(input)
19
- // must parse as ISO 8601; this also verifies semantics like month is not 13 or 00
20
- if (isNaN(date.getTime())) {
21
- throw new InvalidDatetimeError('datetime did not parse as ISO 8601')
22
- }
23
- if (date.toISOString().startsWith('-')) {
24
- throw new InvalidDatetimeError('datetime normalized to a negative time')
25
- }
26
- // regex and other checks for RFC-3339
27
- if (
28
- !/^[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-6][0-9]:[0-6][0-9](.[0-9]{1,20})?(Z|([+-][0-2][0-9]:[0-5][0-9]))$/.test(
29
- input,
30
- )
31
- ) {
32
- throw new InvalidDatetimeError("datetime didn't validate via regex")
33
- }
34
- if (input.length > 64) {
35
- throw new InvalidDatetimeError('datetime is too long (64 chars max)')
36
- }
37
- if (input.endsWith('-00:00')) {
38
- throw new InvalidDatetimeError(
39
- 'datetime can not use "-00:00" for UTC timezone',
40
- )
41
- }
42
- if (input.startsWith('000')) {
43
- throw new InvalidDatetimeError('datetime so close to year zero not allowed')
95
+ const result = parseString(input)
96
+ if (!result.success) {
97
+ throw new InvalidDatetimeError(result.message)
44
98
  }
45
99
  }
46
100
 
47
- /* Same logic as ensureValidDatetime(), but returns a boolean instead of throwing an exception.
101
+ /**
102
+ * Casts a string to a {@link DatetimeString} if it is a valid datetime format
103
+ * string, throwing an error if it is not.
104
+ *
105
+ * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.
106
+ * @see {@link DatetimeString}
107
+ */
108
+ export function asDatetimeString<I>(input: I): I & DatetimeString {
109
+ assertDatetimeString(input)
110
+ return input
111
+ }
112
+
113
+ /**
114
+ * Checks if a string is a valid {@link DatetimeString} format string.
115
+ *
116
+ * @see {@link DatetimeString}
48
117
  */
49
- export function isValidDatetime<I extends string>(
118
+ export function isDatetimeString<I>(input: I): input is I & DatetimeString {
119
+ return parseString(input).success
120
+ }
121
+
122
+ /**
123
+ * Returns the input if it is a valid {@link DatetimeString} format string, or
124
+ * `undefined` if it is not.
125
+ *
126
+ * @see {@link DatetimeString}
127
+ */
128
+ export function ifDatetimeString<I>(
50
129
  input: I,
51
- ): input is I & DatetimeString {
52
- try {
53
- ensureValidDatetime(input)
54
- } catch (err) {
55
- return false
56
- }
130
+ ): undefined | (I & DatetimeString) {
131
+ return isDatetimeString(input) ? input : undefined
132
+ }
57
133
 
58
- return true
134
+ /**
135
+ * Returns the current date and time as a {@link DatetimeString}.
136
+ *
137
+ * @see {@link DatetimeString}
138
+ */
139
+ export function currentDatetimeString(): DatetimeString {
140
+ return toDatetimeString(new Date())
59
141
  }
60
142
 
61
- /* Takes a flexible datetime string and normalizes representation.
143
+ /**
144
+ * Converts any {@link Date} into a {@link DatetimeString} if possible, throwing
145
+ * an error if the date is not a valid atproto datetime.
62
146
  *
63
- * This function will work with any valid atproto datetime (eg, anything which isValidDatetime() is true for). It *additionally* is more flexible about accepting datetimes that don't comply to RFC 3339, or are missing timezone information, and normalizing them to a valid datetime.
147
+ * This is short-hand for `asAtprotoDate(date).toISOString()`.
64
148
  *
65
- * One use-case is a consistent, sortable string. Another is to work with older invalid createdAt datetimes.
149
+ * @throws InvalidDatetimeError if the input date is not a valid atproto datetime (eg, it is too far in the future or past, or it normalizes to a negative year).
150
+ * @see {@link DatetimeString}
151
+ */
152
+ export function toDatetimeString(date: Date): DatetimeString {
153
+ return asAtprotoDate(date).toISOString()
154
+ }
155
+
156
+ /**
157
+ * Takes a flexible datetime string and normalizes its representation.
158
+ *
159
+ * This function will work with any valid value that can be parsed as a date. It
160
+ * *additionally* is more flexible about accepting datetimes that are missing
161
+ * timezone information, and normalizing them to a valid atproto datetime.
66
162
  *
67
- * Successful output will be a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax. Throws `InvalidDatetimeError` if the input string could not be parsed as a datetime, even with permissive parsing.
163
+ * One use-case is a consistent, sortable string. Another is to work with older
164
+ * invalid createdAt datetimes.
68
165
  *
69
- * Expected output format: YYYY-MM-DDTHH:mm:ss.sssZ
166
+ * @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.
167
+ * @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.
70
168
  */
71
- export function normalizeDatetime(dtStr: string): DatetimeString {
72
- if (isValidDatetime(dtStr)) {
73
- const outStr = new Date(dtStr).toISOString()
74
- if (isValidDatetime(outStr)) {
75
- return outStr
76
- }
169
+ export function normalizeDatetime(dtStr: string): ISODatetimeString {
170
+ // Parse the string as is
171
+ const date = new Date(dtStr)
172
+ if (isAtprotoDate(date)) {
173
+ return date.toISOString()
77
174
  }
78
175
 
79
- // check if this permissive datetime is missing a timezone
80
- if (!/.*(([+-]\d\d:?\d\d)|[a-zA-Z])$/.test(dtStr)) {
81
- const date = new Date(dtStr + 'Z')
82
- if (!isNaN(date.getTime())) {
83
- const tzStr = date.toISOString()
84
- if (isValidDatetime(tzStr)) {
85
- return tzStr
86
- }
176
+ // if dtStr is not a valid date, try parsing again with a timezone
177
+ if (isNaN(date.getTime()) && !/.*(([+-]\d\d:?\d\d)|[a-zA-Z])$/.test(dtStr)) {
178
+ const date = new Date(`${dtStr}Z`)
179
+ if (isAtprotoDate(date)) {
180
+ return date.toISOString()
87
181
  }
88
182
  }
89
183
 
90
- // finally try parsing as simple datetime
91
- const date = new Date(dtStr)
92
- if (isNaN(date.getTime())) {
93
- throw new InvalidDatetimeError(
94
- 'datetime did not parse as any timestamp format',
95
- )
96
- }
97
- const isoStr = date.toISOString()
98
- if (isValidDatetime(isoStr)) {
99
- return isoStr
100
- } else {
101
- throw new InvalidDatetimeError(
102
- 'datetime normalized to invalid timestamp string',
103
- )
104
- }
184
+ throw new InvalidDatetimeError(
185
+ 'datetime did not parse as any timestamp format',
186
+ )
105
187
  }
106
188
 
107
- /* Variant of normalizeDatetime() which always returns a valid datetime strings.
189
+ /**
190
+ * Variant of {@link normalizeDatetime} which always returns a valid datetime
191
+ * string.
192
+ *
193
+ * If a {@link InvalidDatetimeError} is encountered, returns the UNIX epoch time
194
+ * as a UTC datetime (`1970-01-01T00:00:00.000Z`).
108
195
  *
109
- * If a InvalidDatetimeError is encountered, returns the UNIX epoch time as a UTC datetime (1970-01-01T00:00:00.000Z).
196
+ * @see {@link normalizeDatetime}
110
197
  */
111
- export const normalizeDatetimeAlways = (dtStr: string): DatetimeString => {
198
+ export function normalizeDatetimeAlways(dtStr: string): ISODatetimeString {
112
199
  try {
113
200
  return normalizeDatetime(dtStr)
114
201
  } catch (err) {
115
- if (err instanceof InvalidDatetimeError) {
116
- return new Date(0).toISOString()
117
- }
118
- throw err
202
+ return '1970-01-01T00:00:00.000Z'
203
+ }
204
+ }
205
+
206
+ // Legacy exports (should we deprecate these ?)
207
+ export {
208
+ assertDatetimeString as ensureValidDatetime,
209
+ isDatetimeString as isValidDatetime,
210
+ }
211
+
212
+ // -----------------------------------------------------------------------------
213
+ // ------------------------- Internal validation logic -------------------------
214
+ // -----------------------------------------------------------------------------
215
+
216
+ // Validation utils that allow avoiding try/catch for control flow (performance
217
+ // optimization). Other syntax formats should also use this pattern to avoid
218
+ // try/catch in their validation logic, at which point these utils can be moved
219
+ // to a common internal utils.
220
+ type FailureResult = { success: false; message: string }
221
+ const failure = (m: string): FailureResult => ({ success: false, message: m })
222
+ type SuccessResult<V> = { success: true; value: V }
223
+ const success = <V>(v: V): SuccessResult<V> => ({ success: true, value: v })
224
+ type Result<V> = FailureResult | SuccessResult<V>
225
+
226
+ /**
227
+ * @see {@link https://www.rfc-editor.org/rfc/rfc3339#section-5.6 Internet Date/Time Format}
228
+ *
229
+ * @example
230
+ * ```abnf
231
+ * date-fullyear = 4DIGIT
232
+ * date-month = 2DIGIT ; 01-12
233
+ * date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
234
+ * ; month/year
235
+ * time-hour = 2DIGIT ; 00-23
236
+ * time-minute = 2DIGIT ; 00-59
237
+ * time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
238
+ * ; rules
239
+ * time-secfrac = "." 1*DIGIT
240
+ * time-numoffset = ("+" / "-") time-hour ":" time-minute
241
+ * time-offset = "Z" / time-numoffset
242
+ * partial-time = time-hour ":" time-minute ":" time-second
243
+ * [time-secfrac]
244
+ * full-date = date-fullyear "-" date-month "-" date-mday
245
+ * full-time = partial-time time-offset
246
+ * date-time = full-date "T" full-time
247
+ * ```
248
+ */
249
+ const DATETIME_REGEX =
250
+ /^(?<full_year>[0-9]{4})-(?<date_month>0[1-9]|1[012])-(?<date_mday>[0-2][0-9]|3[01])T(?<time_hour>[0-1][0-9]|2[0-3]):(?<time_minute>[0-5][0-9]):(?<time_second>[0-5][0-9]|60)(?<time_secfrac>\.[0-9]+)?(?<time_offset>Z|(?<time_numoffset>[+-](?:[0-1][0-9]|2[0-3]):[0-5][0-9]))$/
251
+
252
+ /**
253
+ * Validates that the input is a datetime string according to atproto Lexicon
254
+ * rules, and parses it into a Date object.
255
+ */
256
+ function parseString(input: unknown): Result<AtprotoDate> {
257
+ // @NOTE Performing cheap tests first
258
+ if (typeof input !== 'string') {
259
+ return failure('datetime must be a string')
260
+ }
261
+ if (input.length > 64) {
262
+ return failure('datetime is too long (64 chars max)')
263
+ }
264
+ if (input.endsWith('-00:00')) {
265
+ return failure('datetime can not use "-00:00" for UTC timezone')
119
266
  }
267
+ if (!DATETIME_REGEX.test(input)) {
268
+ return failure(
269
+ "datetime is not in a valid format (must match RFC 3339 & ISO 8601 with 'Z' or ±hh:mm timezone)",
270
+ )
271
+ }
272
+
273
+ // must parse as ISO 8601; this also verifies semantics like leap seconds and
274
+ // correct number of days in month, which the regex does not check for
275
+ const date = new Date(input)
276
+
277
+ return parseDate(date)
120
278
  }
121
279
 
122
- /* Indicates a datetime string did not pass full atproto Lexicon datetime string format checks.
280
+ /**
281
+ * Ensures that a Date object represents a valid datetime according to atproto
282
+ * Lexicon rules. This ensures that `date.toISOString()` will produce a valid
283
+ * datetime string that can be used where {@link DatetimeString} is expected.
123
284
  */
124
- export class InvalidDatetimeError extends Error {}
285
+ function parseDate(date: Date): Result<AtprotoDate> {
286
+ const fullYear = date.getUTCFullYear()
287
+ // Ensures that the date is valid. We could check isNaN(date.getTime()) here
288
+ // but since we'll check the year anyway, we just use that for the validity
289
+ // check since an invalid date will have NaN year.
290
+ if (Number.isNaN(fullYear)) {
291
+ return failure('datetime did not parse as ISO 8601')
292
+ }
293
+ // Ensure that the ISO string representation does not start with ±YYYYYY
294
+ if (fullYear < 0) {
295
+ return failure('datetime normalized to a negative time')
296
+ }
297
+ if (fullYear > 9999) {
298
+ return failure('datetime year is too far in the future')
299
+ }
300
+ if (fullYear < 10) {
301
+ return failure('datetime so close to year zero not allowed')
302
+ }
303
+ return success(date as AtprotoDate)
304
+ }
package/src/handle.ts CHANGED
@@ -92,6 +92,8 @@ export function ensureValidHandleRegex<I extends string>(
92
92
  }
93
93
  }
94
94
 
95
+ export function normalizeHandle(handle: HandleString): HandleString
96
+ export function normalizeHandle(handle: string): string
95
97
  export function normalizeHandle(handle: string): string {
96
98
  return handle.toLowerCase()
97
99
  }
package/src/nsid.ts CHANGED
@@ -48,15 +48,15 @@ export class NSID {
48
48
  return this.segments
49
49
  .slice(0, this.segments.length - 1)
50
50
  .reverse()
51
- .join('.')
51
+ .join('.') as `${string}.${string}`
52
52
  }
53
53
 
54
54
  get name() {
55
55
  return this.segments.at(this.segments.length - 1)
56
56
  }
57
57
 
58
- toString() {
59
- return this.segments.join('.')
58
+ toString(): NsidString {
59
+ return this.segments.join('.') as NsidString
60
60
  }
61
61
  }
62
62
 
@@ -197,6 +197,8 @@ export function validateNsidRegex(value: string): ValidateResult<NsidString> {
197
197
  }
198
198
 
199
199
  if (
200
+ // Fast check for small values
201
+ value.length < 5 ||
200
202
  !/^[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(
201
203
  value,
202
204
  )
@@ -522,5 +522,52 @@ describe('AtUri validation', () => {
522
522
  })
523
523
  })
524
524
 
525
+ it('properly checks that the did property is a valid did', () => {
526
+ const urip = new AtUri('at://did:example:123')
527
+ expect(urip.did).toBe('did:example:123')
528
+ urip.host = 'did:example:456'
529
+ expect(urip.did).toBe('did:example:456')
530
+ urip.host = 'foo.com'
531
+ expect(() => urip.did).toThrow()
532
+ })
533
+
534
+ it('properly checks that the collection is a valid nsid', () => {
535
+ const urip = new AtUri('at://foo.com')
536
+ expect(urip.collection).toBe('')
537
+ expect(() => urip.collectionSafe).toThrow()
538
+
539
+ urip.collection = 'com.example.foo'
540
+ expect(urip.collection).toBe('com.example.foo')
541
+ expect(urip.collectionSafe).toBe('com.example.foo')
542
+
543
+ urip.collection = 'com.other.foo'
544
+ expect(urip.collection).toBe('com.other.foo')
545
+ expect(urip.collectionSafe).toBe('com.other.foo')
546
+
547
+ expect(() => (urip.collection = 'not a valid nsid')).toThrow()
548
+ expect(urip.collection).toBe('com.other.foo') // unchanged after failed set
549
+
550
+ urip.unsafelySetCollection('not-a-valid-nsid')
551
+ expect(urip.collection).toBe('not-a-valid-nsid')
552
+ expect(() => urip.collectionSafe).toThrow()
553
+ })
554
+
555
+ it('properly checks that the rkey is a valid record key', () => {
556
+ const urip = new AtUri('at://foo.com')
557
+ expect(urip.rkey).toBe('')
558
+ expect(() => urip.rkeySafe).toThrow()
559
+
560
+ urip.rkey = 'valid_rkey-123'
561
+ expect(urip.rkey).toBe('valid_rkey-123')
562
+ expect(urip.rkeySafe).toBe('valid_rkey-123')
563
+
564
+ expect(() => (urip.rkey = 'not a valid rkey')).toThrow()
565
+ expect(urip.rkey).toBe('valid_rkey-123') // unchanged after failed set
566
+
567
+ urip.unsafelySetRkey('not a valid rkey')
568
+ expect(urip.rkey).toBe('not a valid rkey')
569
+ expect(() => urip.rkeySafe).toThrow()
570
+ })
571
+
525
572
  // NOTE: this package is currently more permissive than spec about AT URIs, so invalid cases are not errors
526
573
  })