Skip to content

Commit 110e2d4

Browse files
committed
packages
1 parent 78aed39 commit 110e2d4

File tree

4 files changed

+48
-14
lines changed

4 files changed

+48
-14
lines changed

packages/syntax/src/aturi.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ export class AtUri {
3737
this.searchParams = parsed.searchParams
3838
}
3939

40-
static make(handleOrDid: string, collection?: string, rkey?: string) {
40+
// space could be generalized to query params, for future proofing
41+
// but the constructor already does that, spaces are special, in terms of content addressing
42+
static make(handleOrDid: string, collection?: string, rkey?: string, space?: string) {
4143
let str = handleOrDid
4244
if (collection) str += '/' + collection
4345
if (rkey) str += '/' + rkey
44-
return new AtUri(str)
46+
const uri = new AtUri(str)
47+
if (space) uri.searchParams.set('space', space)
48+
return uri
4549
}
4650

4751
get protocol() {
@@ -68,6 +72,15 @@ export class AtUri {
6872
this.searchParams = new URLSearchParams(v)
6973
}
7074

75+
get space() {
76+
return this.searchParams.get('space') || ''
77+
}
78+
79+
set space(s: string) {
80+
if (this.searchParams) this.searchParams = new URLSearchParams()
81+
this.searchParams.set('space', s)
82+
}
83+
7184
get collection() {
7285
return this.pathname.split('/').filter(Boolean)[0] || ''
7386
}

packages/syntax/src/aturi_validation.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,29 @@ import { isValidNsid } from './nsid'
1111
// - optionally, follow "authority" with "/" and valid NSID as start of path
1212
// - optionally, if NSID given, follow that with "/" and rkey
1313
// - rkey path component can include URL-encoded ("percent encoded"), or:
14-
// ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
15-
// [a-zA-Z0-9._~:@!$&'\(\)*+,;=-]
14+
// ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "?" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
15+
// [a-zA-Z0-9._~:@!$&?'\(\)*+,;=-]
1616
// - rkey must have at least one char
1717
// - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901)
1818
export const ensureValidAtUri = (uri: string) => {
1919
// 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')
20+
let fragmentPart: string | null = null
21+
const hashIndex = uri.indexOf('#')
22+
if (hashIndex > -1) {
23+
fragmentPart = uri.slice(hashIndex + 1)
24+
uri = uri.slice(0, hashIndex)
25+
}
26+
27+
// query part is also pretty different, so split that out next
28+
let queryPart: string | null = null
29+
const queryIndex = uri.indexOf('?')
30+
if (queryIndex > -1) {
31+
queryPart = uri.slice(queryIndex + 1)
32+
uri = uri.slice(0, queryIndex)
2333
}
24-
const fragmentPart = uriParts[1] || null
25-
uri = uriParts[0]
2634

2735
// check that all chars are boring ASCII
28-
if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {
36+
if (!/^[a-zA-Z0-9._~:@!$&?')(*+,;=%/-]*$/.test(uri)) {
2937
throw new Error('Disallowed characters in ATURI (ASCII)')
3038
}
3139

@@ -75,9 +83,6 @@ export const ensureValidAtUri = (uri: string) => {
7583
)
7684
}
7785

78-
if (uriParts.length >= 2 && fragmentPart == null) {
79-
throw new Error('ATURI fragment must be non-empty and start with slash')
80-
}
8186

8287
if (fragmentPart != null) {
8388
if (fragmentPart.length === 0 || fragmentPart[0] !== '/') {
@@ -89,6 +94,16 @@ export const ensureValidAtUri = (uri: string) => {
8994
}
9095
}
9196

97+
if (queryPart != null) {
98+
if (queryPart.length === 0) {
99+
throw new Error('ATURI query must be non-empty')
100+
}
101+
// NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
102+
if (!/^[a-zA-Z0-9._~:@!$&?')(*+,;=%/-]*$/.test(queryPart)) {
103+
throw new Error('Disallowed characters in ATURI query (ASCII)')
104+
}
105+
}
106+
92107
if (uri.length > 8 * 1024) {
93108
throw new Error('ATURI is far too long')
94109
}
@@ -98,7 +113,7 @@ export const ensureValidAtUriRegex = (uri: string): void => {
98113
// simple regex to enforce most constraints via just regex and length.
99114
// hand wrote this regex based on above constraints. whew!
100115
const aturiRegex =
101-
/^at:\/\/(?<authority>[a-zA-Z0-9._:%-]+)(\/(?<collection>[a-zA-Z0-9-.]+)(\/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/
116+
/^at:\/\/(?<authority>[a-zA-Z0-9._:%-]+)(\/(?<collection>[a-zA-Z0-9-.]+)(\/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(\?(?<query>[a-zA-Z0-9._~:@!$&%')(*+,;=/-]*))?(#(?<fragment>\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/]*))?$/
102117
const rm = uri.match(aturiRegex)
103118
if (!rm || !rm.groups) {
104119
throw new Error("ATURI didn't validate via regex")

packages/syntax/src/did.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const ensureValidDidRegex = (did: string): void => {
5151
throw new InvalidDidError("DID didn't validate via regex")
5252
}
5353

54+
// TODO, check this first to avoid regex if too long?
5455
if (did.length > 2 * 1024) {
5556
throw new InvalidDidError('DID is too long (2048 chars max)')
5657
}

packages/syntax/tests/aturi.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,11 @@ describe('AtUri validation', () => {
377377
expectValid('at://did:plc:asdf123/com.atproto.feed.post')
378378
expectValid('at://did:plc:asdf123/com.atproto.feed.post/record')
379379

380+
expectValid('at://did:plc:asdf123?foo=bar')
381+
expectValid('at://user.bsky.social?foo=bar')
382+
expectValid('at://did:plc:asdf123/com.atproto.feed.post?foo=bar')
383+
expectValid('at://did:plc:asdf123/com.atproto.feed.post/record?foo=bar')
384+
380385
expectValid('at://did:plc:asdf123#/frag')
381386
expectValid('at://user.bsky.social#/frag')
382387
expectValid('at://did:plc:asdf123/com.atproto.feed.post#/frag')

0 commit comments

Comments
 (0)