@atproto/syntax 0.3.4 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atproto/syntax
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `NSID.from(stringifiable)` utility
8
+
9
+ - [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performance of NSID validation
10
+
11
+ ## 0.4.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#3635](https://github.com/bluesky-social/atproto/pull/3635) [`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee) Thanks [@bnewbold](https://github.com/bnewbold)! - update NSID syntax to allow non-leading digits
16
+
3
17
  ## 0.3.4
4
18
 
5
19
  ### Patch Changes
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  Dual MIT/Apache-2.0 License
2
2
 
3
- Copyright (c) 2022-2025 Bluesky PBC, and Contributors
3
+ Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
4
4
 
5
5
  Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
6
6
 
package/benchmark.js ADDED
@@ -0,0 +1,208 @@
1
+ /* eslint-env node, commonjs */
2
+
3
+ const { validateNsid, validateNsidRegex } = require('.')
4
+
5
+ // $ node benchmark.js
6
+ // valid NSIDs {
7
+ // parsed: 181.56524884700775,
8
+ // regexp: 77.61082607507706,
9
+ // optimized: 60.183539509773254
10
+ // }
11
+ // invalid NSIDs {
12
+ // parsed: 128.7685609459877,
13
+ // regexp: 108.75775015354156,
14
+ // optimized: 53.196488440036774
15
+ // }
16
+
17
+ bench('valid NSIDs', true, [
18
+ 'com.example.foo',
19
+ 'o'.repeat(63) + '.foo.bar',
20
+ 'com.' + 'o'.repeat(63) + '.foo',
21
+ 'com.example.' + 'o'.repeat(63),
22
+ 'com.' + 'middle.'.repeat(40) + 'foo',
23
+ 'com.example.fooBar',
24
+ 'net.users.bob.ping',
25
+ 'a.b.c',
26
+ 'm.xn--masekowski-d0b.pl',
27
+ 'one.two.three',
28
+ 'one.two.three.four-and.FiVe',
29
+ 'one.2.three',
30
+ 'a-0.b-1.c',
31
+ 'a0.b1.cc',
32
+ 'cn.8.lex.stuff',
33
+ 'test.12345.record',
34
+ 'a01.thing.record',
35
+ 'a.0.c',
36
+ 'xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two',
37
+ 'a0.b1.c3',
38
+ 'com.example.f00',
39
+ 'onion.expyuzz4wqqyqhjn.spec.getThing',
40
+ 'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
41
+ 'org.4chan.lex.getThing',
42
+ 'cn.8.lex.stuff',
43
+ 'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
44
+ 'a.'.repeat(158) + 'a',
45
+ ])
46
+
47
+ bench('invalid NSIDs', false, [
48
+ 'a.'.repeat(158) + '9',
49
+ 'a.'.repeat(154) + 'a😅.9',
50
+ 'o'.repeat(64) + '.foo.bar',
51
+ 'com.' + 'o'.repeat(64) + '.foo',
52
+ 'com.example.' + 'o'.repeat(64),
53
+ 'com.' + 'middle.'.repeat(50) + 'foo',
54
+ 'com.example.foo.*',
55
+ 'com.example.foo.blah*',
56
+ 'com.example.foo.*blah',
57
+ 'com.exa💩ple.thing',
58
+ 'a-0.b-1.c-3',
59
+ 'a-0.b-1.c-o',
60
+ '1.0.0.127.record',
61
+ '0two.example.foo',
62
+ 'example.com',
63
+ 'com.example',
64
+ 'a.',
65
+ '.one.two.three',
66
+ 'one.two.three ',
67
+ 'one.two..three',
68
+ 'one .two.three',
69
+ ' one.two.three',
70
+ 'com.atproto.feed.p@st',
71
+ 'com.atproto.feed.p_st',
72
+ 'com.atproto.feed.p*st',
73
+ 'com.atproto.feed.po#t',
74
+ 'com.atproto.feed.p!ot',
75
+ 'com.example-.foo',
76
+ 'com.-example.foo',
77
+ 'com.example.0foo',
78
+ 'com.example.f-o',
79
+ ])
80
+
81
+ function bench(name, expectedResult, cases) {
82
+ const validators = {
83
+ parsed: (nsid) => validateNsid(nsid).success,
84
+ regexp: (nsid) => validateNsidRegex(nsid).success,
85
+ optimized: (nsid) => validateNsidOptimized(nsid).success,
86
+ }
87
+
88
+ const times = Object.fromEntries(Object.keys(validators).map((k) => [k, 0]))
89
+
90
+ for (let i = 0; i < 1000; i++) {
91
+ for (const [name, fn] of Object.entries(validators)) {
92
+ const start = performance.now()
93
+ for (let j = 0; j < 20; j++) {
94
+ for (const value of cases) {
95
+ if (fn(value) !== expectedResult) {
96
+ throw new Error(`Validator ${name} gave wrong result`)
97
+ }
98
+ }
99
+ }
100
+ times[name] += performance.now() - start
101
+ }
102
+ }
103
+
104
+ console.log(
105
+ name,
106
+ Object.fromEntries(
107
+ Object.entries(times).map(([k, v]) => [k, `${v.toFixed(2)} ms`]),
108
+ ),
109
+ )
110
+ }
111
+
112
+ /** @param value {string} */
113
+ function validateNsidOptimized(value) {
114
+ const { length } = value
115
+ if (length > 253 + 1 + 63) {
116
+ return { success: false, message: 'NSID is too long (317 chars max)' }
117
+ }
118
+
119
+ let partCount = 1
120
+ let partStart = 0
121
+ let partHasLeadingDigit = false
122
+ let partHasHyphen = false
123
+
124
+ let charCode
125
+ for (let i = 0; i < length; i++) {
126
+ charCode = value.charCodeAt(i)
127
+
128
+ // Hot path: check frequent chars first
129
+ if (
130
+ (charCode >= 97 && charCode <= 122) /* a-z */ ||
131
+ (charCode >= 65 && charCode <= 90) /* A-Z */
132
+ ) {
133
+ // All good
134
+ } else if (charCode >= 48 && charCode <= 57 /* 0-9 */) {
135
+ if (i === 0) {
136
+ return {
137
+ success: false,
138
+ message: 'NSID first part may not start with a digit',
139
+ }
140
+ }
141
+
142
+ // All good
143
+
144
+ if (i === partStart) {
145
+ partHasLeadingDigit = true
146
+ }
147
+ } else if (charCode === 45 /* - */) {
148
+ if (i === partStart) {
149
+ return {
150
+ success: false,
151
+ message: 'NSID part can not start with hyphen',
152
+ }
153
+ }
154
+ if (i === length - 1 || value.charCodeAt(i + 1) === 46 /* . */) {
155
+ return { success: false, message: 'NSID part can not end with hyphen' }
156
+ }
157
+
158
+ // All good
159
+
160
+ partHasHyphen = true
161
+ } else if (charCode === 46 /* . */) {
162
+ // Check prev part size
163
+ if (i === partStart) {
164
+ return { success: false, message: 'NSID parts can not be empty' }
165
+ }
166
+ if (i - partStart > 63) {
167
+ return { success: false, message: 'NSID part too long (max 63 chars)' }
168
+ }
169
+
170
+ // All good
171
+
172
+ partCount++
173
+ partStart = i + 1
174
+ partHasHyphen = false
175
+ partHasLeadingDigit = false
176
+ } else {
177
+ return {
178
+ success: false,
179
+ message:
180
+ 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
181
+ }
182
+ }
183
+ }
184
+
185
+ // Check last part size
186
+ if (length === partStart) {
187
+ return { success: false, message: 'NSID parts can not be empty' }
188
+ }
189
+ if (length - partStart > 63) {
190
+ return { success: false, message: 'NSID part too long (max 63 chars)' }
191
+ }
192
+
193
+ // Check last part chars
194
+ if (partHasHyphen || partHasLeadingDigit) {
195
+ return {
196
+ success: false,
197
+ message:
198
+ 'NSID name part must be only letters and digits (and no leading digit)',
199
+ }
200
+ }
201
+
202
+ // Check part count
203
+ if (partCount < 3) {
204
+ return { success: false, message: 'NSID needs at least three parts' }
205
+ }
206
+
207
+ return { success: true, value }
208
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"aturi_validation.d.ts","sourceRoot":"","sources":["../src/aturi_validation.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,gBAAgB,QAAS,MAAM,SA+E3C,CAAA;AAED,eAAO,MAAM,qBAAqB,QAAS,MAAM,KAAG,IAgCnD,CAAA"}
1
+ {"version":3,"file":"aturi_validation.d.ts","sourceRoot":"","sources":["../src/aturi_validation.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,SA6E3C,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,IA4BnD,CAAA"}
@@ -51,10 +51,7 @@ const ensureValidAtUri = (uri) => {
51
51
  if (parts[3].length === 0) {
52
52
  throw new Error('ATURI can not have a slash after authority without a path segment');
53
53
  }
54
- try {
55
- (0, nsid_1.ensureValidNsid)(parts[3]);
56
- }
57
- catch {
54
+ if (!(0, nsid_1.isValidNsid)(parts[3])) {
58
55
  throw new Error('ATURI requires first path segment (if supplied) to be valid NSID');
59
56
  }
60
57
  }
@@ -104,13 +101,8 @@ const ensureValidAtUriRegex = (uri) => {
104
101
  throw new Error('ATURI authority must be a valid handle or DID');
105
102
  }
106
103
  }
107
- if (groups.collection) {
108
- try {
109
- (0, nsid_1.ensureValidNsidRegex)(groups.collection);
110
- }
111
- catch {
112
- throw new Error('ATURI collection path segment must be a valid NSID');
113
- }
104
+ if (groups.collection && !(0, nsid_1.isValidNsid)(groups.collection)) {
105
+ throw new Error('ATURI collection path segment must be a valid NSID');
114
106
  }
115
107
  if (uri.length > 8 * 1024) {
116
108
  throw new Error('ATURI is far too long');
@@ -1 +1 @@
1
- {"version":3,"file":"aturi_validation.js","sourceRoot":"","sources":["../src/aturi_validation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aturi_validation.js","sourceRoot":"","sources":["../src/aturi_validation.ts"],"names":[],"mappings":";;;AAAA,+BAA2D;AAC3D,qCAAoE;AACpE,iCAAoC;AAEpC,uCAAuC;AACvC,+DAA+D;AAC/D,oCAAoC;AACpC,6EAA6E;AAC7E,wBAAwB;AACxB,sDAAsD;AACtD,iFAAiF;AACjF,kEAAkE;AAClE,8EAA8E;AAC9E,+HAA+H;AAC/H,0CAA0C;AAC1C,0CAA0C;AAC1C,wGAAwG;AACjG,MAAM,gBAAgB,GAAG,CAAC,GAAW,EAAE,EAAE;IAC9C,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IACxC,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAEjB,wCAAwC;IACxC,IAAI,CAAC,mCAAmC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAA;IAC3D,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAA;IAClD,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;IAC1E,CAAC;IAED,IAAI,CAAC;QACH,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAChC,IAAA,oBAAc,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CACb,mEAAmE,CACpE,CAAA;QACH,CAAC;QACD,IAAI,CAAC,IAAA,kBAAW,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,kEAAkE,CACnE,CAAA;QACH,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CACb,4EAA4E,CAC7E,CAAA;QACH,CAAC;QACD,oEAAoE;IACtE,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,8DAA8D,CAC/D,CAAA;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;IAC1E,CAAC;IAED,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;QACzB,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACzD,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;QAC1E,CAAC;QACD,4EAA4E;QAC5E,IAAI,CAAC,wCAAwC,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;QACpE,CAAC;IACH,CAAC;IAED,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC1C,CAAC;AACH,CAAC,CAAA;AA7EY,QAAA,gBAAgB,oBA6E5B;AAEM,MAAM,qBAAqB,GAAG,CAAC,GAAW,EAAQ,EAAE;IACzD,sEAAsE;IACtE,0DAA0D;IAC1D,MAAM,UAAU,GACd,gLAAgL,CAAA;IAClL,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IACpD,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAA;IAExB,IAAI,CAAC;QACH,IAAA,+BAAsB,EAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,IAAA,yBAAmB,EAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,IAAI,CAAC,IAAA,kBAAW,EAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;IACvE,CAAC;IAED,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC1C,CAAC;AACH,CAAC,CAAA;AA5BY,QAAA,qBAAqB,yBA4BjC"}
@@ -1 +1 @@
1
- {"version":3,"file":"datetime.d.ts","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,UAAW,MAAM,KAAG,IA4BnD,CAAA;AAID,eAAO,MAAM,eAAe,UAAW,MAAM,KAAG,OAW/C,CAAA;AAYD,eAAO,MAAM,iBAAiB,UAAW,MAAM,KAAG,MAkCjD,CAAA;AAMD,eAAO,MAAM,uBAAuB,UAAW,MAAM,KAAG,MASvD,CAAA;AAID,qBAAa,oBAAqB,SAAQ,KAAK;CAAG"}
1
+ {"version":3,"file":"datetime.d.ts","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,KAAG,IA4BnD,CAAA;AAID,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,OAW/C,CAAA;AAYD,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,MAkCjD,CAAA;AAMD,eAAO,MAAM,uBAAuB,GAAI,OAAO,MAAM,KAAG,MASvD,CAAA;AAID,qBAAa,oBAAqB,SAAQ,KAAK;CAAG"}
package/dist/did.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"did.d.ts","sourceRoot":"","sources":["../src/did.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,cAAc,QAAS,MAAM,KAAG,IA8B5C,CAAA;AAED,eAAO,MAAM,mBAAmB,QAAS,MAAM,KAAG,IAUjD,CAAA;AAED,qBAAa,eAAgB,SAAQ,KAAK;CAAG"}
1
+ {"version":3,"file":"did.d.ts","sourceRoot":"","sources":["../src/did.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,IA8B5C,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,IAUjD,CAAA;AAED,qBAAa,eAAgB,SAAQ,KAAK;CAAG"}
@@ -1 +1 @@
1
- {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,mBAAmB,CAAA;AAM9C,eAAO,MAAM,eAAe,UAY3B,CAAA;AAqBD,eAAO,MAAM,iBAAiB,WAAY,MAAM,KAAG,IAkClD,CAAA;AAGD,eAAO,MAAM,sBAAsB,WAAY,MAAM,KAAG,IAWvD,CAAA;AAED,eAAO,MAAM,eAAe,WAAY,MAAM,KAAG,MAEhD,CAAA;AAED,eAAO,MAAM,6BAA6B,WAAY,MAAM,KAAG,MAI9D,CAAA;AAED,eAAO,MAAM,aAAa,WAAY,MAAM,KAAG,OAW9C,CAAA;AAED,eAAO,MAAM,UAAU,WAAY,MAAM,KAAG,OAE3C,CAAA;AAED,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAChD,6BAA6B;AAC7B,qBAAa,mBAAoB,SAAQ,KAAK;CAAG;AACjD,6BAA6B;AAC7B,qBAAa,sBAAuB,SAAQ,KAAK;CAAG;AACpD,6BAA6B;AAC7B,qBAAa,qBAAsB,SAAQ,KAAK;CAAG"}
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,mBAAmB,CAAA;AAM9C,eAAO,MAAM,eAAe,UAY3B,CAAA;AAqBD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG,IAkClD,CAAA;AAGD,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,KAAG,IAWvD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,MAEhD,CAAA;AAED,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,KAAG,MAI9D,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,OAW9C,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,KAAG,OAE3C,CAAA;AAED,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAChD,6BAA6B;AAC7B,qBAAa,mBAAoB,SAAQ,KAAK;CAAG;AACjD,6BAA6B;AAC7B,qBAAa,sBAAuB,SAAQ,KAAK;CAAG;AACpD,6BAA6B;AAC7B,qBAAa,qBAAsB,SAAQ,KAAK;CAAG"}
package/dist/nsid.d.ts CHANGED
@@ -1,15 +1,39 @@
1
1
  export declare class NSID {
2
- segments: string[];
3
- static parse(nsid: string): NSID;
2
+ readonly segments: readonly string[];
3
+ static parse(input: string): NSID;
4
4
  static create(authority: string, name: string): NSID;
5
5
  static isValid(nsid: string): boolean;
6
+ static from(input: {
7
+ toString: () => string;
8
+ }): NSID;
6
9
  constructor(nsid: string);
7
10
  get authority(): string;
8
11
  get name(): string | undefined;
9
12
  toString(): string;
10
13
  }
11
- export declare const ensureValidNsid: (nsid: string) => void;
12
- export declare const ensureValidNsidRegex: (nsid: string) => void;
14
+ export declare function ensureValidNsid(nsid: string): void;
15
+ export declare function parseNsid(nsid: string): string[];
16
+ export declare function isValidNsid(nsid: string): boolean;
17
+ type ValidateResult<T> = {
18
+ success: true;
19
+ value: T;
20
+ } | {
21
+ success: false;
22
+ message: string;
23
+ };
24
+ export declare function validateNsid(input: string): ValidateResult<string[]>;
25
+ /**
26
+ * @deprecated Use {@link ensureValidNsid} if you care about error details,
27
+ * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or
28
+ * {@link isValidNsid} if you just want a boolean.
29
+ */
30
+ export declare function ensureValidNsidRegex(nsid: string): void;
31
+ /**
32
+ * Regexp based validation that behaves identically to the previous code but
33
+ * provides less detailed error messages (while being 20% to 50% faster).
34
+ */
35
+ export declare function validateNsidRegex(value: string): ValidateResult<string>;
13
36
  export declare class InvalidNsidError extends Error {
14
37
  }
38
+ export {};
15
39
  //# sourceMappingURL=nsid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nsid.d.ts","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"AAaA,qBAAa,IAAI;IACf,QAAQ,EAAE,MAAM,EAAE,CAAK;IAEvB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKpD,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;gBASzB,IAAI,EAAE,MAAM;IAKxB,IAAI,SAAS,WAKZ;IAED,IAAI,IAAI,uBAEP;IAED,QAAQ;CAGT;AAKD,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,IAmC9C,CAAA;AAED,eAAO,MAAM,oBAAoB,SAAU,MAAM,KAAG,IAanD,CAAA;AAED,qBAAa,gBAAiB,SAAQ,KAAK;CAAG"}
1
+ {"version":3,"file":"nsid.d.ts","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"AAaA,qBAAa,IAAI;IACf,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAEpC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIjC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKpD,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM;IAI3B,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,MAAM,CAAA;KAAE,GAAG,IAAI;gBAWxC,IAAI,EAAE,MAAM;IAIxB,IAAI,SAAS,WAKZ;IAED,IAAI,IAAI,uBAEP;IAED,QAAQ;CAGT;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGlD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAIhD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIjD;AAED,KAAK,cAAc,CAAC,CAAC,IACjB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC3B;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAKvC,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,CAAC,CA0DpE;AA4BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGvD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAuBvE;AAED,qBAAa,gBAAiB,SAAQ,KAAK;CAAG"}
package/dist/nsid.js CHANGED
@@ -7,38 +7,47 @@ number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0"
7
7
  delim = "."
8
8
  segment = alpha *( alpha / number / "-" )
9
9
  authority = segment *( delim segment )
10
- name = alpha *( alpha )
10
+ name = alpha *( alpha / number )
11
11
  nsid = authority delim name
12
12
 
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.InvalidNsidError = exports.ensureValidNsidRegex = exports.ensureValidNsid = exports.NSID = void 0;
15
+ exports.InvalidNsidError = exports.NSID = void 0;
16
+ exports.ensureValidNsid = ensureValidNsid;
17
+ exports.parseNsid = parseNsid;
18
+ exports.isValidNsid = isValidNsid;
19
+ exports.validateNsid = validateNsid;
20
+ exports.ensureValidNsidRegex = ensureValidNsidRegex;
21
+ exports.validateNsidRegex = validateNsidRegex;
16
22
  class NSID {
17
- static parse(nsid) {
18
- return new NSID(nsid);
23
+ static parse(input) {
24
+ return new NSID(input);
19
25
  }
20
26
  static create(authority, name) {
21
- const segments = [...authority.split('.').reverse(), name].join('.');
22
- return new NSID(segments);
27
+ const input = [...authority.split('.').reverse(), name].join('.');
28
+ return new NSID(input);
23
29
  }
24
30
  static isValid(nsid) {
25
- try {
26
- NSID.parse(nsid);
27
- return true;
31
+ return isValidNsid(nsid);
32
+ }
33
+ static from(input) {
34
+ if (input instanceof NSID) {
35
+ // No need to clone, NSID is immutable
36
+ return input;
28
37
  }
29
- catch (e) {
30
- return false;
38
+ if (Array.isArray(input)) {
39
+ return new NSID(input.join('.'));
31
40
  }
41
+ return new NSID(String(input));
32
42
  }
33
43
  constructor(nsid) {
34
44
  Object.defineProperty(this, "segments", {
35
45
  enumerable: true,
36
46
  configurable: true,
37
47
  writable: true,
38
- value: []
48
+ value: void 0
39
49
  });
40
- (0, exports.ensureValidNsid)(nsid);
41
- this.segments = nsid.split('.');
50
+ this.segments = parseNsid(nsid);
42
51
  }
43
52
  get authority() {
44
53
  return this.segments
@@ -54,53 +63,134 @@ class NSID {
54
63
  }
55
64
  }
56
65
  exports.NSID = NSID;
66
+ function ensureValidNsid(nsid) {
67
+ const result = validateNsid(nsid);
68
+ if (!result.success)
69
+ throw new InvalidNsidError(result.message);
70
+ }
71
+ function parseNsid(nsid) {
72
+ const result = validateNsid(nsid);
73
+ if (!result.success)
74
+ throw new InvalidNsidError(result.message);
75
+ return result.value;
76
+ }
77
+ function isValidNsid(nsid) {
78
+ // Since the regex version is more performant for valid NSIDs, we use it when
79
+ // we don't care about error details.
80
+ return validateNsidRegex(nsid).success;
81
+ }
57
82
  // Human readable constraints on NSID:
58
83
  // - a valid domain in reversed notation
59
84
  // - followed by an additional period-separated name, which is camel-case letters
60
- const ensureValidNsid = (nsid) => {
61
- const toCheck = nsid;
62
- // check that all chars are boring ASCII
63
- if (!/^[a-zA-Z0-9.-]*$/.test(toCheck)) {
64
- throw new InvalidNsidError('Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)');
85
+ function validateNsid(input) {
86
+ if (input.length > 253 + 1 + 63) {
87
+ return {
88
+ success: false,
89
+ message: 'NSID is too long (317 chars max)',
90
+ };
65
91
  }
66
- if (toCheck.length > 253 + 1 + 63) {
67
- throw new InvalidNsidError('NSID is too long (317 chars max)');
92
+ if (hasDisallowedCharacters(input)) {
93
+ return {
94
+ success: false,
95
+ message: 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
96
+ };
68
97
  }
69
- const labels = toCheck.split('.');
70
- if (labels.length < 3) {
71
- throw new InvalidNsidError('NSID needs at least three parts');
98
+ const segments = input.split('.');
99
+ if (segments.length < 3) {
100
+ return {
101
+ success: false,
102
+ message: 'NSID needs at least three parts',
103
+ };
72
104
  }
73
- for (let i = 0; i < labels.length; i++) {
74
- const l = labels[i];
105
+ for (const l of segments) {
75
106
  if (l.length < 1) {
76
- throw new InvalidNsidError('NSID parts can not be empty');
107
+ return {
108
+ success: false,
109
+ message: 'NSID parts can not be empty',
110
+ };
77
111
  }
78
112
  if (l.length > 63) {
79
- throw new InvalidNsidError('NSID part too long (max 63 chars)');
80
- }
81
- if (l.endsWith('-') || l.startsWith('-')) {
82
- throw new InvalidNsidError('NSID parts can not start or end with hyphen');
83
- }
84
- if (/^[0-9]/.test(l) && i === 0) {
85
- throw new InvalidNsidError('NSID first part may not start with a digit');
113
+ return {
114
+ success: false,
115
+ message: 'NSID part too long (max 63 chars)',
116
+ };
86
117
  }
87
- if (!/^[a-zA-Z]+$/.test(l) && i + 1 === labels.length) {
88
- throw new InvalidNsidError('NSID name part must be only letters');
118
+ if (startsWithHyphen(l) || endsWithHyphen(l)) {
119
+ return {
120
+ success: false,
121
+ message: 'NSID parts can not start or end with hyphen',
122
+ };
89
123
  }
90
124
  }
91
- };
92
- exports.ensureValidNsid = ensureValidNsid;
93
- const ensureValidNsidRegex = (nsid) => {
94
- // simple regex to enforce most constraints via just regex and length.
95
- // hand wrote this regex based on above constraints
96
- if (!/^[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-Z]{0,61}[a-zA-Z])?)$/.test(nsid)) {
97
- throw new InvalidNsidError("NSID didn't validate via regex");
125
+ if (startsWithNumber(segments[0])) {
126
+ return {
127
+ success: false,
128
+ message: 'NSID first part may not start with a digit',
129
+ };
98
130
  }
99
- if (nsid.length > 253 + 1 + 63) {
100
- throw new InvalidNsidError('NSID is too long (317 chars max)');
131
+ if (!isValidIdentifier(segments[segments.length - 1])) {
132
+ return {
133
+ success: false,
134
+ message: 'NSID name part must be only letters and digits (and no leading digit)',
135
+ };
101
136
  }
102
- };
103
- exports.ensureValidNsidRegex = ensureValidNsidRegex;
137
+ return {
138
+ success: true,
139
+ value: segments,
140
+ };
141
+ }
142
+ function hasDisallowedCharacters(v) {
143
+ return !/^[a-zA-Z0-9.-]*$/.test(v);
144
+ }
145
+ function startsWithNumber(v) {
146
+ const charCode = v.charCodeAt(0);
147
+ return charCode >= 48 && charCode <= 57;
148
+ }
149
+ function startsWithHyphen(v) {
150
+ return v.charCodeAt(0) === 45; /* - */
151
+ }
152
+ function endsWithHyphen(v) {
153
+ return v.charCodeAt(v.length - 1) === 45; /* - */
154
+ }
155
+ function isValidIdentifier(v) {
156
+ // Note, since we already know that "v" only contains [a-zA-Z0-9-], we can
157
+ // simplify the following regex by checking only the first char and presence
158
+ // of "-".
159
+ // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v)
160
+ return !startsWithNumber(v) && !v.includes('-');
161
+ }
162
+ /**
163
+ * @deprecated Use {@link ensureValidNsid} if you care about error details,
164
+ * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or
165
+ * {@link isValidNsid} if you just want a boolean.
166
+ */
167
+ function ensureValidNsidRegex(nsid) {
168
+ const result = validateNsidRegex(nsid);
169
+ if (!result.success)
170
+ throw new InvalidNsidError(result.message);
171
+ }
172
+ /**
173
+ * Regexp based validation that behaves identically to the previous code but
174
+ * provides less detailed error messages (while being 20% to 50% faster).
175
+ */
176
+ function validateNsidRegex(value) {
177
+ if (value.length > 253 + 1 + 63) {
178
+ return {
179
+ success: false,
180
+ message: 'NSID is too long (317 chars max)',
181
+ };
182
+ }
183
+ if (!/^[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(value)) {
184
+ return {
185
+ success: false,
186
+ message: "NSID didn't validate via regex",
187
+ };
188
+ }
189
+ return {
190
+ success: true,
191
+ value,
192
+ };
193
+ }
104
194
  class InvalidNsidError extends Error {
105
195
  }
106
196
  exports.InvalidNsidError = InvalidNsidError;
package/dist/nsid.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"nsid.js","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"nsid.js","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"recordkey.d.ts","sourceRoot":"","sources":["../src/recordkey.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,SAAU,MAAM,KAAG,IAUnD,CAAA;AAED,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,OAW/C,CAAA;AAED,qBAAa,qBAAsB,SAAQ,KAAK;CAAG"}
1
+ {"version":3,"file":"recordkey.d.ts","sourceRoot":"","sources":["../src/recordkey.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,IAUnD,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,OAW/C,CAAA;AAED,qBAAa,qBAAsB,SAAQ,KAAK;CAAG"}
package/dist/tid.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tid.d.ts","sourceRoot":"","sources":["../src/tid.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,QAAS,MAAM,KAAG,IAQ5C,CAAA;AAED,eAAO,MAAM,UAAU,QAAS,MAAM,KAAG,OAExC,CAAA;AAED,qBAAa,eAAgB,SAAQ,KAAK;CAAG"}
1
+ {"version":3,"file":"tid.d.ts","sourceRoot":"","sources":["../src/tid.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,IAQ5C,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,OAExC,CAAA;AAED,qBAAa,eAAgB,SAAQ,KAAK;CAAG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/syntax",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "description": "Validation for atproto identifiers and formats: DID, handle, NSID, AT URI, etc",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  import { ensureValidDid, ensureValidDidRegex } from './did'
2
2
  import { ensureValidHandle, ensureValidHandleRegex } from './handle'
3
- import { ensureValidNsid, ensureValidNsidRegex } from './nsid'
3
+ import { isValidNsid } from './nsid'
4
4
 
5
5
  // Human-readable constraints on ATURI:
6
6
  // - following regular URLs, a 8KByte hard total length limit
@@ -53,9 +53,7 @@ export const ensureValidAtUri = (uri: string) => {
53
53
  'ATURI can not have a slash after authority without a path segment',
54
54
  )
55
55
  }
56
- try {
57
- ensureValidNsid(parts[3])
58
- } catch {
56
+ if (!isValidNsid(parts[3])) {
59
57
  throw new Error(
60
58
  'ATURI requires first path segment (if supplied) to be valid NSID',
61
59
  )
@@ -117,12 +115,8 @@ export const ensureValidAtUriRegex = (uri: string): void => {
117
115
  }
118
116
  }
119
117
 
120
- if (groups.collection) {
121
- try {
122
- ensureValidNsidRegex(groups.collection)
123
- } catch {
124
- throw new Error('ATURI collection path segment must be a valid NSID')
125
- }
118
+ if (groups.collection && !isValidNsid(groups.collection)) {
119
+ throw new Error('ATURI collection path segment must be a valid NSID')
126
120
  }
127
121
 
128
122
  if (uri.length > 8 * 1024) {
package/src/nsid.ts CHANGED
@@ -6,35 +6,40 @@ number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0"
6
6
  delim = "."
7
7
  segment = alpha *( alpha / number / "-" )
8
8
  authority = segment *( delim segment )
9
- name = alpha *( alpha )
9
+ name = alpha *( alpha / number )
10
10
  nsid = authority delim name
11
11
 
12
12
  */
13
13
 
14
14
  export class NSID {
15
- segments: string[] = []
15
+ readonly segments: readonly string[]
16
16
 
17
- static parse(nsid: string): NSID {
18
- return new NSID(nsid)
17
+ static parse(input: string): NSID {
18
+ return new NSID(input)
19
19
  }
20
20
 
21
21
  static create(authority: string, name: string): NSID {
22
- const segments = [...authority.split('.').reverse(), name].join('.')
23
- return new NSID(segments)
22
+ const input = [...authority.split('.').reverse(), name].join('.')
23
+ return new NSID(input)
24
24
  }
25
25
 
26
- static isValid(nsid: string): boolean {
27
- try {
28
- NSID.parse(nsid)
29
- return true
30
- } catch (e) {
31
- return false
26
+ static isValid(nsid: string) {
27
+ return isValidNsid(nsid)
28
+ }
29
+
30
+ static from(input: { toString: () => string }): NSID {
31
+ if (input instanceof NSID) {
32
+ // No need to clone, NSID is immutable
33
+ return input
34
+ }
35
+ if (Array.isArray(input)) {
36
+ return new NSID((input as string[]).join('.'))
32
37
  }
38
+ return new NSID(String(input))
33
39
  }
34
40
 
35
41
  constructor(nsid: string) {
36
- ensureValidNsid(nsid)
37
- this.segments = nsid.split('.')
42
+ this.segments = parseNsid(nsid)
38
43
  }
39
44
 
40
45
  get authority() {
@@ -53,58 +58,152 @@ export class NSID {
53
58
  }
54
59
  }
55
60
 
61
+ export function ensureValidNsid(nsid: string): void {
62
+ const result = validateNsid(nsid)
63
+ if (!result.success) throw new InvalidNsidError(result.message)
64
+ }
65
+
66
+ export function parseNsid(nsid: string): string[] {
67
+ const result = validateNsid(nsid)
68
+ if (!result.success) throw new InvalidNsidError(result.message)
69
+ return result.value
70
+ }
71
+
72
+ export function isValidNsid(nsid: string): boolean {
73
+ // Since the regex version is more performant for valid NSIDs, we use it when
74
+ // we don't care about error details.
75
+ return validateNsidRegex(nsid).success
76
+ }
77
+
78
+ type ValidateResult<T> =
79
+ | { success: true; value: T }
80
+ | { success: false; message: string }
81
+
56
82
  // Human readable constraints on NSID:
57
83
  // - a valid domain in reversed notation
58
84
  // - followed by an additional period-separated name, which is camel-case letters
59
- export const ensureValidNsid = (nsid: string): void => {
60
- const toCheck = nsid
61
-
62
- // check that all chars are boring ASCII
63
- if (!/^[a-zA-Z0-9.-]*$/.test(toCheck)) {
64
- throw new InvalidNsidError(
65
- 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
66
- )
85
+ export function validateNsid(input: string): ValidateResult<string[]> {
86
+ if (input.length > 253 + 1 + 63) {
87
+ return {
88
+ success: false,
89
+ message: 'NSID is too long (317 chars max)',
90
+ }
67
91
  }
68
-
69
- if (toCheck.length > 253 + 1 + 63) {
70
- throw new InvalidNsidError('NSID is too long (317 chars max)')
92
+ if (hasDisallowedCharacters(input)) {
93
+ return {
94
+ success: false,
95
+ message:
96
+ 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
97
+ }
71
98
  }
72
- const labels = toCheck.split('.')
73
- if (labels.length < 3) {
74
- throw new InvalidNsidError('NSID needs at least three parts')
99
+ const segments = input.split('.')
100
+ if (segments.length < 3) {
101
+ return {
102
+ success: false,
103
+ message: 'NSID needs at least three parts',
104
+ }
75
105
  }
76
- for (let i = 0; i < labels.length; i++) {
77
- const l = labels[i]
106
+ for (const l of segments) {
78
107
  if (l.length < 1) {
79
- throw new InvalidNsidError('NSID parts can not be empty')
108
+ return {
109
+ success: false,
110
+ message: 'NSID parts can not be empty',
111
+ }
80
112
  }
81
113
  if (l.length > 63) {
82
- throw new InvalidNsidError('NSID part too long (max 63 chars)')
114
+ return {
115
+ success: false,
116
+ message: 'NSID part too long (max 63 chars)',
117
+ }
83
118
  }
84
- if (l.endsWith('-') || l.startsWith('-')) {
85
- throw new InvalidNsidError('NSID parts can not start or end with hyphen')
119
+ if (startsWithHyphen(l) || endsWithHyphen(l)) {
120
+ return {
121
+ success: false,
122
+ message: 'NSID parts can not start or end with hyphen',
123
+ }
86
124
  }
87
- if (/^[0-9]/.test(l) && i === 0) {
88
- throw new InvalidNsidError('NSID first part may not start with a digit')
125
+ }
126
+ if (startsWithNumber(segments[0])) {
127
+ return {
128
+ success: false,
129
+ message: 'NSID first part may not start with a digit',
89
130
  }
90
- if (!/^[a-zA-Z]+$/.test(l) && i + 1 === labels.length) {
91
- throw new InvalidNsidError('NSID name part must be only letters')
131
+ }
132
+ if (!isValidIdentifier(segments[segments.length - 1])) {
133
+ return {
134
+ success: false,
135
+ message:
136
+ 'NSID name part must be only letters and digits (and no leading digit)',
92
137
  }
93
138
  }
139
+ return {
140
+ success: true,
141
+ value: segments,
142
+ }
94
143
  }
95
144
 
96
- export const ensureValidNsidRegex = (nsid: string): void => {
97
- // simple regex to enforce most constraints via just regex and length.
98
- // hand wrote this regex based on above constraints
145
+ function hasDisallowedCharacters(v) {
146
+ return !/^[a-zA-Z0-9.-]*$/.test(v)
147
+ }
148
+
149
+ function startsWithNumber(v: string) {
150
+ const charCode = v.charCodeAt(0)
151
+ return charCode >= 48 && charCode <= 57
152
+ }
153
+
154
+ function startsWithHyphen(v: string) {
155
+ return v.charCodeAt(0) === 45 /* - */
156
+ }
157
+
158
+ function endsWithHyphen(v: string) {
159
+ return v.charCodeAt(v.length - 1) === 45 /* - */
160
+ }
161
+
162
+ function isValidIdentifier(v: string) {
163
+ // Note, since we already know that "v" only contains [a-zA-Z0-9-], we can
164
+ // simplify the following regex by checking only the first char and presence
165
+ // of "-".
166
+
167
+ // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v)
168
+ return !startsWithNumber(v) && !v.includes('-')
169
+ }
170
+
171
+ /**
172
+ * @deprecated Use {@link ensureValidNsid} if you care about error details,
173
+ * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or
174
+ * {@link isValidNsid} if you just want a boolean.
175
+ */
176
+ export function ensureValidNsidRegex(nsid: string): void {
177
+ const result = validateNsidRegex(nsid)
178
+ if (!result.success) throw new InvalidNsidError(result.message)
179
+ }
180
+
181
+ /**
182
+ * Regexp based validation that behaves identically to the previous code but
183
+ * provides less detailed error messages (while being 20% to 50% faster).
184
+ */
185
+ export function validateNsidRegex(value: string): ValidateResult<string> {
186
+ if (value.length > 253 + 1 + 63) {
187
+ return {
188
+ success: false,
189
+ message: 'NSID is too long (317 chars max)',
190
+ }
191
+ }
192
+
99
193
  if (
100
- !/^[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-Z]{0,61}[a-zA-Z])?)$/.test(
101
- nsid,
194
+ !/^[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(
195
+ value,
102
196
  )
103
197
  ) {
104
- throw new InvalidNsidError("NSID didn't validate via regex")
198
+ return {
199
+ success: false,
200
+ message: "NSID didn't validate via regex",
201
+ }
105
202
  }
106
- if (nsid.length > 253 + 1 + 63) {
107
- throw new InvalidNsidError('NSID is too long (317 chars max)')
203
+
204
+ return {
205
+ success: true,
206
+ value,
108
207
  }
109
208
  }
110
209
 
@@ -1,10 +1,12 @@
1
1
  import * as fs from 'node:fs'
2
- import * as readline from 'node:readline'
3
2
  import {
4
3
  InvalidNsidError,
5
4
  NSID,
6
5
  ensureValidNsid,
7
- ensureValidNsidRegex,
6
+ isValidNsid,
7
+ parseNsid,
8
+ validateNsid,
9
+ validateNsidRegex,
8
10
  } from '../src'
9
11
 
10
12
  describe('NSID parsing & creation', () => {
@@ -39,12 +41,30 @@ describe('NSID parsing & creation', () => {
39
41
 
40
42
  describe('NSID validation', () => {
41
43
  const expectValid = (h: string) => {
44
+ expect(isValidNsid(h)).toBe(true)
42
45
  ensureValidNsid(h)
43
- ensureValidNsidRegex(h)
46
+ expect(parseNsid(h)).toEqual(h.split('.'))
47
+ expect(validateNsidRegex(h)).toMatchObject({
48
+ success: true,
49
+ value: expect.any(String),
50
+ })
51
+ expect(validateNsid(h)).toMatchObject({
52
+ success: true,
53
+ value: expect.any(Array),
54
+ })
44
55
  }
45
56
  const expectInvalid = (h: string) => {
57
+ expect(isValidNsid(h)).toBe(false)
58
+ expect(() => parseNsid(h)).toThrow(InvalidNsidError)
46
59
  expect(() => ensureValidNsid(h)).toThrow(InvalidNsidError)
47
- expect(() => ensureValidNsidRegex(h)).toThrow(InvalidNsidError)
60
+ expect(validateNsidRegex(h)).toMatchObject({
61
+ success: false,
62
+ message: expect.any(String),
63
+ })
64
+ expect(validateNsid(h)).toMatchObject({
65
+ success: false,
66
+ message: expect.any(String),
67
+ })
48
68
  }
49
69
 
50
70
  it('enforces spec details', () => {
@@ -68,91 +88,111 @@ describe('NSID validation', () => {
68
88
  const tooLongOverall = 'com.' + 'middle.'.repeat(50) + 'foo'
69
89
  expect(tooLongOverall.length).toBe(357)
70
90
  expectInvalid(tooLongOverall)
71
-
72
- expectValid('com.example.fooBar')
73
- expectValid('net.users.bob.ping')
74
- expectValid('a.b.c')
75
- expectValid('m.xn--masekowski-d0b.pl')
76
- expectValid('one.two.three')
77
- expectValid('one.two.three.four-and.FiVe')
78
- expectValid('one.2.three')
79
- expectValid('a-0.b-1.c')
80
- expectValid('a0.b1.cc')
81
- expectValid('cn.8.lex.stuff')
82
- expectValid('test.12345.record')
83
- expectValid('a01.thing.record')
84
- expectValid('a.0.c')
85
- expectValid('xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two')
86
-
87
- expectInvalid('com.example.foo.*')
88
- expectInvalid('com.example.foo.blah*')
89
- expectInvalid('com.example.foo.*blah')
90
- expectInvalid('com.example.f00')
91
- expectInvalid('com.exa💩ple.thing')
92
- expectInvalid('a-0.b-1.c-3')
93
- expectInvalid('a-0.b-1.c-o')
94
- expectInvalid('a0.b1.c3')
95
- expectInvalid('1.0.0.127.record')
96
- expectInvalid('0two.example.foo')
97
- expectInvalid('example.com')
98
- expectInvalid('com.example')
99
- expectInvalid('a.')
100
- expectInvalid('.one.two.three')
101
- expectInvalid('one.two.three ')
102
- expectInvalid('one.two..three')
103
- expectInvalid('one .two.three')
104
- expectInvalid(' one.two.three')
105
- expectInvalid('com.exa💩ple.thing')
106
- expectInvalid('com.atproto.feed.p@st')
107
- expectInvalid('com.atproto.feed.p_st')
108
- expectInvalid('com.atproto.feed.p*st')
109
- expectInvalid('com.atproto.feed.po#t')
110
- expectInvalid('com.atproto.feed.p!ot')
111
- expectInvalid('com.example-.foo')
112
91
  })
113
92
 
114
- it('allows onion (Tor) NSIDs', () => {
115
- expectValid('onion.expyuzz4wqqyqhjn.spec.getThing')
116
- expectValid(
93
+ describe('valid NSIDs', () => {
94
+ for (const validNsid of [
95
+ 'com.example.foo',
96
+ 'o'.repeat(63) + '.foo.bar',
97
+ 'com.' + 'o'.repeat(63) + '.foo',
98
+ 'com.example.' + 'o'.repeat(63),
99
+ 'com.' + 'middle.'.repeat(40) + 'foo',
100
+
101
+ 'a-0.b-1.c',
102
+ 'a.0.c',
103
+ 'a.b.c',
104
+ 'a0.b1.c3',
105
+ 'a0.b1.cc',
106
+ 'a01.thing.record',
107
+ 'cn.8.lex.stuff',
108
+ 'com.example.f00',
109
+ 'com.example.fooBar',
110
+ 'm.xn--masekowski-d0b.pl',
111
+ 'net.users.bob.ping',
112
+ 'one.2.three',
113
+ 'one.two.three',
114
+ 'one.two.three.four-and.FiVe',
115
+ 'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
116
+ 'onion.expyuzz4wqqyqhjn.spec.getThing',
117
117
  'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
118
- )
118
+ 'org.4chan.lex.getThing',
119
+ 'test.12345.record',
120
+ 'xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two',
121
+ ]) {
122
+ it(validNsid, () => {
123
+ expectValid(validNsid)
124
+ })
125
+ }
119
126
  })
120
127
 
121
- it('allows starting-with-numeric segments (same as domains)', () => {
122
- expectValid('org.4chan.lex.getThing')
123
- expectValid('cn.8.lex.stuff')
124
- expectValid(
125
- 'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
126
- )
128
+ describe('invalid NSIDs', () => {
129
+ for (const invalidNsid of [
130
+ 'o'.repeat(64) + '.foo.bar',
131
+ 'com.' + 'o'.repeat(64) + '.foo',
132
+ 'com.example.' + 'o'.repeat(64),
133
+ 'com.' + 'middle.'.repeat(50) + 'foo',
134
+ 'com.example.foo.*',
135
+ 'com.example.foo.blah*',
136
+ 'com.example.foo.*blah',
137
+ 'com.exa💩ple.thing',
138
+ 'a-0.b-1.c-3',
139
+ 'a-0.b-1.c-o',
140
+ '1.0.0.127.record',
141
+ '0two.example.foo',
142
+ 'example.com',
143
+ 'com.example',
144
+ 'a.',
145
+ '.one.two.three',
146
+ 'one.two.three ',
147
+ 'one.two..three',
148
+ 'one .two.three',
149
+ ' one.two.three',
150
+ 'com.atproto.feed.p@st',
151
+ 'com.atproto.feed.p_st',
152
+ 'com.atproto.feed.p*st',
153
+ 'com.atproto.feed.po#t',
154
+ 'com.atproto.feed.p!ot',
155
+ 'com.example-.foo',
156
+ 'com.-example.foo',
157
+ 'com.example.0foo',
158
+ 'com.example.f-o',
159
+ ]) {
160
+ it(invalidNsid, () => {
161
+ expect(validateNsid(invalidNsid)).toMatchObject({
162
+ success: false,
163
+ message: expect.any(String),
164
+ })
165
+ })
166
+ }
127
167
  })
128
168
 
129
- it('conforms to interop valid NSIDs', () => {
130
- const lineReader = readline.createInterface({
131
- input: fs.createReadStream(
132
- `${__dirname}/interop-files/nsid_syntax_valid.txt`,
133
- ),
134
- terminal: false,
135
- })
136
- lineReader.on('line', (line) => {
169
+ describe('conforms to interop valid NSIDs', () => {
170
+ for (const line of fs
171
+ .readFileSync(`${__dirname}/interop-files/nsid_syntax_valid.txt`)
172
+ .toString()
173
+ .split('\n')) {
137
174
  if (line.startsWith('#') || line.length === 0) {
138
- return
175
+ continue
139
176
  }
140
- expectValid(line)
141
- })
177
+
178
+ it(line, () => {
179
+ expectValid(line)
180
+ })
181
+ }
142
182
  })
143
183
 
144
- it('conforms to interop invalid NSIDs', () => {
145
- const lineReader = readline.createInterface({
146
- input: fs.createReadStream(
147
- `${__dirname}/interop-files/nsid_syntax_invalid.txt`,
148
- ),
149
- terminal: false,
150
- })
151
- lineReader.on('line', (line) => {
184
+ describe('conforms to interop invalid NSIDs', () => {
185
+ for (const line of fs
186
+ .readFileSync(`${__dirname}/interop-files/nsid_syntax_invalid.txt`)
187
+ .toString()
188
+ .split('\n')) {
152
189
  if (line.startsWith('#') || line.length === 0) {
153
- return
190
+ continue
154
191
  }
155
- expectInvalid(line)
156
- })
192
+
193
+ it(line, () => {
194
+ expectInvalid(line)
195
+ })
196
+ }
157
197
  })
158
198
  })
@@ -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.6.3"}
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 +1 @@
1
- {"root":["./tests/aturi.test.ts","./tests/datetime.test.ts","./tests/did.test.ts","./tests/handle.test.ts","./tests/nsid.test.ts","./tests/recordkey.test.ts","./tests/tid.test.ts"],"version":"5.6.3"}
1
+ {"root":["./tests/aturi.test.ts","./tests/datetime.test.ts","./tests/did.test.ts","./tests/handle.test.ts","./tests/nsid.test.ts","./tests/recordkey.test.ts","./tests/tid.test.ts"],"version":"5.8.3"}