@atproto/syntax 0.6.3 → 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 +17 -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
@@ -1,104 +0,0 @@
1
- import { DidString, ensureValidDidRegex, isValidDid } from './did.js'
2
- import {
3
- HandleString,
4
- InvalidHandleError,
5
- ensureValidHandleRegex,
6
- isValidHandle,
7
- } from './handle.js'
8
-
9
- /**
10
- * An "at-identifier" string - either a {@link DidString} or a {@link HandleString}
11
- *
12
- * @example `"did:plc:1234..."`, `"did:web:example.com"` or `"alice.bsky.social"`
13
- */
14
- export type AtIdentifierString = DidString | HandleString
15
-
16
- /**
17
- * Discriminates {@link HandleString} from a valid {@link AtIdentifierString}.
18
- *
19
- * @return `true` if the identifier is a handle, `false` otherwise
20
- */
21
- export function isHandleIdentifier(id: AtIdentifierString): id is HandleString {
22
- return !isDidIdentifier(id)
23
- }
24
-
25
- /**
26
- * Discriminates {@link DidString} from a valid {@link AtIdentifierString}.
27
- *
28
- * @return `true` if the identifier is a DID, `false` otherwise
29
- */
30
- export function isDidIdentifier(id: AtIdentifierString): id is DidString {
31
- return id.startsWith('did:')
32
- }
33
-
34
- /**
35
- * Validates that a string is a valid {@link AtIdentifierString} format string,
36
- * throwing an error if it is not.
37
- *
38
- * @throws InvalidHandleError if the input string does not meet the atproto 'datetime' format requirements.
39
- * @see {@link AtIdentifierString}
40
- */
41
- export function assertAtIdentifierString<I>(
42
- input: I,
43
- ): asserts input is I & AtIdentifierString {
44
- try {
45
- if (!input || typeof input !== 'string') {
46
- throw new TypeError('Identifier must be a non-empty string')
47
- } else if (input.startsWith('did:')) {
48
- ensureValidDidRegex(input)
49
- } else {
50
- ensureValidHandleRegex(input)
51
- }
52
- } catch (cause) {
53
- throw new InvalidHandleError('Invalid DID or handle', { cause })
54
- }
55
- }
56
-
57
- /**
58
- * Casts a string to a {@link AtIdentifierString} if it is a valid at-identifier
59
- * string, throwing an error if it is not.
60
- *
61
- * @throws InvalidHandleError if the input string does not meet the atproto 'at-identifier' format requirements.
62
- * @see {@link AtIdentifierString}
63
- */
64
- export function asAtIdentifierString<I>(input: I): I & AtIdentifierString {
65
- assertAtIdentifierString(input)
66
- return input
67
- }
68
-
69
- /**
70
- * Type guard that checks if a value is a valid AT identifier (DID or handle).
71
- *
72
- * @param value - The value to check
73
- * @returns `true` if the value is a valid AT identifier
74
- * @see {@link AtIdentifierString}
75
- */
76
- export function isAtIdentifierString<I>(
77
- input: I,
78
- ): input is I & AtIdentifierString {
79
- if (!input || typeof input !== 'string') {
80
- return false
81
- } else if (input.startsWith('did:')) {
82
- return isValidDid(input)
83
- } else {
84
- return isValidHandle(input)
85
- }
86
- }
87
-
88
- /**
89
- * Returns the input if it is a valid {@link AtIdentifierString} format string, or
90
- * `undefined` if it is not.
91
- *
92
- * @see {@link AtIdentifierString}
93
- */
94
- export function ifAtIdentifierString<I>(
95
- input: I,
96
- ): undefined | (I & AtIdentifierString) {
97
- return isAtIdentifierString(input) ? input : undefined
98
- }
99
-
100
- // Legacy exports (should we deprecate these ?)
101
- export {
102
- assertAtIdentifierString as ensureValidAtIdentifier,
103
- isAtIdentifierString as isValidAtIdentifier,
104
- }
package/src/aturi.ts DELETED
@@ -1,197 +0,0 @@
1
- import {
2
- AtIdentifierString,
3
- ensureValidAtIdentifier,
4
- isDidIdentifier,
5
- } from './at-identifier.js'
6
- import { AtUriString } from './aturi_validation.js'
7
- import { DidString, InvalidDidError } from './did.js'
8
- import { NsidString, ensureValidNsid } from './nsid.js'
9
- import { RecordKeyString, ensureValidRecordKey } from './recordkey.js'
10
-
11
- export * from './aturi_validation.js'
12
-
13
- // Re-export types used in public interface
14
- export type {
15
- AtIdentifierString,
16
- AtUriString,
17
- DidString,
18
- NsidString,
19
- RecordKeyString,
20
- }
21
-
22
- export const ATP_URI_REGEX =
23
- // proto- --did-------------- --name---------------- --path---- --query-- --hash--
24
- /^(at:\/\/)?((?:did:[a-z0-9:%-]+)|(?:[a-z0-9][a-z0-9.:-]*))(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i
25
- // --path----- --query-- --hash--
26
- const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i
27
-
28
- export class AtUri {
29
- hash: string
30
- host: AtIdentifierString
31
- pathname: string
32
- searchParams: URLSearchParams
33
-
34
- constructor(uri: string, base?: string | AtUri) {
35
- const parsed =
36
- base !== undefined
37
- ? typeof base === 'string'
38
- ? Object.assign(parse(base), parseRelative(uri))
39
- : Object.assign({ host: base.host }, parseRelative(uri))
40
- : parse(uri)
41
-
42
- ensureValidAtIdentifier(parsed.host)
43
-
44
- this.hash = parsed.hash ?? ''
45
- this.host = parsed.host
46
- this.pathname = parsed.pathname ?? ''
47
- this.searchParams = parsed.searchParams
48
- }
49
-
50
- static make(handleOrDid: string, collection?: string, rkey?: string) {
51
- let str = handleOrDid
52
- if (collection) str += '/' + collection
53
- if (rkey) str += '/' + rkey
54
- return new AtUri(str)
55
- }
56
-
57
- get protocol() {
58
- return 'at:'
59
- }
60
-
61
- get origin() {
62
- return `at://${this.host}` as const
63
- }
64
-
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 {
72
- return this.host
73
- }
74
-
75
- set hostname(v: string) {
76
- ensureValidAtIdentifier(v)
77
- this.host = v
78
- }
79
-
80
- get search() {
81
- return this.searchParams.toString()
82
- }
83
-
84
- set search(v: string) {
85
- this.searchParams = new URLSearchParams(v)
86
- }
87
-
88
- get collection() {
89
- return this.pathname.split('/').filter(Boolean)[0] || ''
90
- }
91
-
92
- get collectionSafe(): NsidString {
93
- const { collection } = this
94
- ensureValidNsid(collection)
95
- return collection
96
- }
97
-
98
- set collection(v: string) {
99
- ensureValidNsid(v)
100
- this.unsafelySetCollection(v)
101
- }
102
-
103
- unsafelySetCollection(v: string) {
104
- const parts = this.pathname.split('/').filter(Boolean)
105
- parts[0] = v
106
- this.pathname = parts.join('/')
107
- }
108
-
109
- get rkey() {
110
- return this.pathname.split('/').filter(Boolean)[1] || ''
111
- }
112
-
113
- get rkeySafe(): RecordKeyString {
114
- const { rkey } = this
115
- ensureValidRecordKey(rkey)
116
- return rkey
117
- }
118
-
119
- set rkey(v: string) {
120
- ensureValidRecordKey(v)
121
- this.unsafelySetRkey(v)
122
- }
123
-
124
- unsafelySetRkey(v: string) {
125
- const parts = this.pathname.split('/').filter(Boolean)
126
- parts[0] ||= 'undefined'
127
- parts[1] = v
128
- this.pathname = parts.join('/')
129
- }
130
-
131
- get href() {
132
- return this.toString()
133
- }
134
-
135
- toString(): AtUriString {
136
- let pathname = this.pathname
137
- if (pathname && !pathname.startsWith('/')) {
138
- pathname = `/${pathname}`
139
- }
140
- while (pathname.endsWith('/')) {
141
- pathname = pathname.slice(0, -1)
142
- }
143
- let qs = ''
144
- if (this.searchParams.size) {
145
- qs = `?${this.searchParams.toString()}`
146
- }
147
- // @NOTE We keep the hash as-is, even if it doesn't start with a '/'.
148
- let fragment = this.hash
149
- if (fragment === '#') {
150
- fragment = ''
151
- } else if (fragment && !fragment.startsWith('#')) {
152
- fragment = `#${fragment}`
153
- }
154
- return `at://${this.host}${pathname}${qs}${fragment}` as AtUriString
155
- }
156
- }
157
-
158
- function parse(str: string) {
159
- const match = str.match(ATP_URI_REGEX) as null | {
160
- 0: string
161
- 1: string | undefined // proto
162
- 2: string // host
163
- 3: string | undefined // path
164
- 4: string | undefined // query
165
- 5: string | undefined // hash
166
- }
167
-
168
- if (!match) {
169
- throw new Error(`Invalid AT uri: ${str}`)
170
- }
171
-
172
- return {
173
- host: match[2],
174
- hash: match[5],
175
- pathname: match[3],
176
- searchParams: new URLSearchParams(match[4]),
177
- }
178
- }
179
-
180
- function parseRelative(str: string) {
181
- const match = str.match(RELATIVE_REGEX) as null | {
182
- 0: string
183
- 1: string | undefined // path
184
- 2: string | undefined // query
185
- 3: string | undefined // hash
186
- }
187
-
188
- if (!match) {
189
- throw new Error(`Invalid path: ${str}`)
190
- }
191
-
192
- return {
193
- hash: match[3],
194
- pathname: match[1],
195
- searchParams: new URLSearchParams(match[2]),
196
- }
197
- }
@@ -1,321 +0,0 @@
1
- import { AtIdentifierString, isAtIdentifierString } from './at-identifier.js'
2
- import { Result, failure, success } from './lib/result.js'
3
- import { NsidString, isValidNsid } from './nsid.js'
4
- import { isValidRecordKey } from './recordkey.js'
5
-
6
- export type AtUriStringBase =
7
- | `at://${AtIdentifierString}`
8
- | `at://${AtIdentifierString}/${NsidString}`
9
- | `at://${AtIdentifierString}/${NsidString}/${string}`
10
-
11
- export type AtUriStringFragment = `#/${string}`
12
-
13
- /**
14
- * A URI string as used to point at resources in the AT Protocol
15
- *
16
- * The full, general structure of an AT URI is:
17
- *
18
- * ```bnf
19
- * AT-URI = "at://" AUTHORITY [ PATH ] [ "?" QUERY ] [ "#" FRAGMENT ]
20
- * ```
21
- *
22
- * The authority part of the URI can be either a handle or a DID, indicating the
23
- * identity associated with the repository. In current atproto Lexicon use, the
24
- * query and fragment parts are not yet supported, and only a fixed pattern of
25
- * paths are allowed:
26
- *
27
- * ```bnf
28
- * AT-URI = "at://" AUTHORITY [ "/" COLLECTION [ "/" RKEY ] ]
29
- *
30
- * AUTHORITY = HANDLE | DID
31
- * COLLECTION = NSID
32
- * RKEY = RECORD-KEY
33
- * ```
34
- *
35
- * The authority section is required, and should be normalized.
36
- *
37
- * AT URI strings must respect the following syntax (as prescribed by the AT
38
- * protocol specification):
39
- *
40
- * - The overall URI is restricted to a subset of ASCII characters
41
- * - For reference below, the set of unreserved characters, as defined in [RFC-3986](https://www.rfc-editor.org/rfc/rfc3986), includes alphanumeric (`A-Za-z0-9`), period, hyphen, underscore, and tilde (`.-_~`)
42
- * - Maximum overall length is 8 kilobytes (which may be shortened in the future)
43
- * - Hex-encoding of characters is permitted (but in practice not necessary and should be avoided to keep the URI normalized and human-readable)
44
- * - The URI scheme is `at`, and an authority part preceded with double slashes is always required. AT URIs always start with `at://`.
45
- * - An authority section is required and must be non-empty. the authority can be either an atproto Handle, or a DID meeting the restrictions for use with atproto. The authority part can *not* be interpreted as a host:port pair, because of the use of colon characters (`:`) in DIDs. Colons and unreserved characters should not be escaped in DIDs, but other reserved characters (including `#`, `/`, `$`, `&`, `@`) must be escaped.
46
- * - Note that none of the current "blessed" DID methods for atproto allow these characters in DID identifiers
47
- * - An optional path section may follow the authority. The path may contain multiple segments separated by a single slash (`/`). Generic URI path normalization rules may be used.
48
- * - An optional query part is allowed, following generic URI syntax restrictions
49
- * - An optional fragment part is allowed, using JSON Path syntax
50
- *
51
- * @example "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.actor.profile/self"
52
- *
53
- * @see {@link https://atproto.com/specs/at-uri-scheme AT protocol - AT URI Scheme}
54
- */
55
- export type AtUriString =
56
- | AtUriStringBase
57
- | `${AtUriStringBase}${AtUriStringFragment}`
58
-
59
- /**
60
- * Type guard that checks if a value is a valid {@link AtUriString}
61
- *
62
- * @see {@link AtUriString}
63
- */
64
- export function isAtUriString<I>(
65
- input: I,
66
- options?: Omit<ParseAtUriStringOptions, 'detailed'>,
67
- ): input is I & AtUriString {
68
- return parseAtUriString(input, options).success
69
- }
70
-
71
- /**
72
- * Returns the input if it is a valid {@link AtUriString} format string, or
73
- * `undefined` if it is not.
74
- *
75
- * @see {@link AtUriString}
76
- */
77
- export function ifAtUriString<I>(
78
- input: I,
79
- options?: Omit<ParseAtUriStringOptions, 'detailed'>,
80
- ): undefined | (I & AtUriString) {
81
- return isAtUriString(input, options) ? input : undefined
82
- }
83
-
84
- /**
85
- * Casts a string to an {@link AtUriString} if it is a valid AT URI format
86
- * string, throwing an error if it is not.
87
- *
88
- * @throws InvalidAtUriError if the input string does not meet the atproto AT URI format requirements.
89
- * @see {@link AtUriString}
90
- */
91
- export function asAtUriString<I>(
92
- input: I,
93
- options?: ParseAtUriStringOptions,
94
- ): I & AtUriString {
95
- assertAtUriString(input, options)
96
- return input
97
- }
98
-
99
- /**
100
- * Assert the validity of an {@link AtUriString}, throwing an error if the
101
- * {@link input} is not a valid AT URI.
102
- *
103
- * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}
104
- */
105
- export function assertAtUriString<I>(
106
- input: I,
107
- options?: ParseAtUriStringOptions,
108
- ): asserts input is I & AtUriString {
109
- // Optimistically use faster isAtUriString(), throwing a detailed error only
110
- // in case of failure. This check, and the fact that the code after it always
111
- // throws, also ensures that isAtUriString() and assertAtUriString()'s
112
- // behavior are always consistent.
113
- const result = parseAtUriString(input, options)
114
- if (!result.success) {
115
- throw new InvalidAtUriError(result.message)
116
- }
117
- }
118
-
119
- /**
120
- * Assert the **non-strict** validity of an {@link AtUriString}, throwing a
121
- * detailed error if the {@link input} is not a valid AT URI.
122
- *
123
- * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}
124
- * @deprecated use {@link assertAtUriString} with `{ strict: false }` option instead
125
- */
126
- export function ensureValidAtUri<I>(
127
- input: I,
128
- ): asserts input is I & AtUriString {
129
- assertAtUriString(input, { strict: false, detailed: true })
130
- }
131
-
132
- /**
133
- * Assert the (non-strict!) validity of an {@link AtUriString}, throwing an
134
- * error if the {@link input} is not a valid AT URI.
135
- *
136
- * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}
137
- * @deprecated use {@link assertAtUriString} with `{ strict: false }` option instead
138
- */
139
- export function ensureValidAtUriRegex<I>(
140
- input: I,
141
- ): asserts input is I & AtUriString {
142
- assertAtUriString(input, { strict: false, detailed: false })
143
- }
144
-
145
- /**
146
- * Type guard that checks if a value is a valid {@link AtUriString} format
147
- * string, without enforcing strict record key validation. This is useful for
148
- * cases where you want to allow a wider range of valid ATURIs, such as when
149
- * validating user input or when the record key is not relevant.
150
- *
151
- * @deprecated use {@link isAtUriString} with `{ strict: false }` option instead
152
- */
153
- export function isValidAtUri<I>(input: I): input is I & AtUriString {
154
- return isAtUriString(input, { strict: false })
155
- }
156
-
157
- export class InvalidAtUriError extends Error {}
158
-
159
- export type ParseAtUriStringOptions = {
160
- /**
161
- * If true, the parser will enforce that the record key (rkey) part of the URI
162
- * is a valid record key (validated by {@link isValidRecordKey}). If false,
163
- * any non-empty string of allowed chars will be accepted as a record key.
164
- *
165
- * @default true
166
- */
167
- strict?: boolean
168
-
169
- /**
170
- * If true, the parser will return detailed error messages for why a string is
171
- * not a valid AT URI. This option has no effect on the behavior of
172
- * {@link isAtUriString}, which will always return false for invalid strings
173
- * regardless of this option.
174
- *
175
- * @default false
176
- */
177
- detailed?: boolean
178
- }
179
-
180
- export type AtUriParts = {
181
- authority: AtIdentifierString
182
- query?: string
183
- hash?: string
184
- } & (
185
- | { collection?: NsidString; rkey?: undefined }
186
- | { collection: NsidString; rkey?: string }
187
- )
188
-
189
- const INVALID_CHAR_REGEXP = /[^a-zA-Z0-9._~:@!$&'()*+,;=%/\\[\]#?-]/
190
- const AT_URI_REGEXP =
191
- /^(?<uri>at:\/\/(?<authority>[^/?#\s]+)(?:\/(?<collection>[^/?#\s]+)(?:\/(?<rkey>[^/?#\s]+))?)?(?<trailingSlash>\/)?)(?:\?(?<query>[^#\s]*))?(?:#(?<hash>[^\s]*))?$/
192
-
193
- /**
194
- * Parses a valid {@link AtUriString} into a {@link AtUriParts} object, or
195
- * returns a failure with a detailed error message if the string is not a valid
196
- * {@link AtUriString}.
197
- */
198
- export function parseAtUriString(
199
- input: unknown,
200
- options?: ParseAtUriStringOptions,
201
- ): Result<AtUriParts> {
202
- if (typeof input !== 'string') {
203
- return failure('ATURI must be a string')
204
- }
205
-
206
- if (input.length > 8192) {
207
- return failure('ATURI exceeds maximum length')
208
- }
209
-
210
- const invalidChar = input.match(INVALID_CHAR_REGEXP)
211
- if (invalidChar) {
212
- return failure('Disallowed characters in ATURI (ASCII)')
213
- }
214
-
215
- const match = input.match(AT_URI_REGEXP)
216
- const groups = match?.groups
217
- if (!groups) {
218
- // Regex validation failed, but we don't know exactly why. Provide more
219
- // detailed error messages if the "detailed" option is set, falling back to
220
- // a generic error.
221
- if (options?.detailed) {
222
- if (!input.startsWith('at://')) {
223
- return failure('ATURI must start with "at://"')
224
- }
225
-
226
- if (input.includes(' ')) {
227
- return failure('ATURI can not contain spaces')
228
- }
229
-
230
- if (input.includes('//', 5)) {
231
- return failure('ATURI can not have empty path segments')
232
- }
233
-
234
- const pathStart = input.indexOf('/', 5) // after "at://"
235
- if (pathStart !== -1) {
236
- const fragmentIndex = input.indexOf('#')
237
- const pathEnd = fragmentIndex !== -1 ? fragmentIndex : input.length
238
- const secondSlash = input.indexOf('/', pathStart + 1)
239
- if (secondSlash !== -1 && secondSlash !== pathEnd - 1) {
240
- return failure('ATURI can not have more than two path segments')
241
- }
242
- }
243
- }
244
-
245
- return failure('ATURI does not match expected format')
246
- }
247
-
248
- // @NOTE Percent-encoding is allowed by the AT URI specification, but any
249
- // percent-encoded characters appearing in the collection NSID or record key
250
- // will effectively be rejected by the isValidNsid and isValidRecordKey
251
- // validators. Since these values are defined to be plain ASCII identifiers,
252
- // this legacy behavior is beneficial: it ensures that normalized
253
- // (non-percent-encoded) values are always used, as prescribed by the spec.
254
-
255
- if (!isAtIdentifierString(groups.authority)) {
256
- return failure('ATURI has invalid authority')
257
- }
258
-
259
- if (groups.collection != null && !isValidNsid(groups.collection)) {
260
- return failure('ATURI has invalid collection')
261
- }
262
-
263
- if (groups.hash != null) {
264
- const result = parseJsonPointer(groups.hash, options)
265
- if (result.success) {
266
- groups.hash = result.value
267
- } else {
268
- return failure(`ATURI has invalid fragment (${result.message})`)
269
- }
270
- }
271
-
272
- if (options?.strict !== false) {
273
- if (groups.trailingSlash != null) {
274
- return failure('ATURI can not have a trailing slash')
275
- }
276
-
277
- if (groups.query != null) {
278
- return failure('ATURI query part is not allowed')
279
- }
280
-
281
- if (groups.rkey != null && !isValidRecordKey(groups.rkey)) {
282
- return failure('ATURI has invalid record key')
283
- }
284
- }
285
-
286
- return success(groups as AtUriParts)
287
- }
288
-
289
- const BASIC_JSON_POINTER_REGEXP = /^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/
290
-
291
- /**
292
- * Checks if a string is a valid JSON pointer (RFC-6901) with the allowed chars
293
- * for ATURI fragments. This is a very loose validation that only checks the
294
- * basic syntax and charset.
295
- */
296
- function parseJsonPointer(
297
- value: string,
298
- options?: { strict?: boolean },
299
- ): Result<string> {
300
- if (!BASIC_JSON_POINTER_REGEXP.test(value)) {
301
- return failure('Invalid JSON pointer')
302
- }
303
-
304
- const result = parsePercentEncoding(value)
305
-
306
- // In non-strict mode, we allow invalid percent-encoding in the fragment
307
- if (!result.success && options?.strict === false) {
308
- return success(value)
309
- }
310
-
311
- return result
312
- }
313
-
314
- function parsePercentEncoding(value: string): Result<string> {
315
- try {
316
- return success(decodeURIComponent(value))
317
- } catch {
318
- // decodeURIComponent throws if the percent-encoding is invalid (e.g. "%FF")
319
- return failure('Invalid percent-encoding')
320
- }
321
- }