@atproto/syntax 0.5.3 → 0.6.0-next.0

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 (53) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/at-identifier.js +15 -23
  3. package/dist/at-identifier.js.map +1 -1
  4. package/dist/aturi.d.ts.map +1 -1
  5. package/dist/aturi.js +30 -32
  6. package/dist/aturi.js.map +1 -1
  7. package/dist/aturi_validation.d.ts +137 -4
  8. package/dist/aturi_validation.d.ts.map +1 -1
  9. package/dist/aturi_validation.js +172 -102
  10. package/dist/aturi_validation.js.map +1 -1
  11. package/dist/datetime.d.ts +5 -0
  12. package/dist/datetime.d.ts.map +1 -1
  13. package/dist/datetime.js +52 -38
  14. package/dist/datetime.js.map +1 -1
  15. package/dist/did.js +4 -11
  16. package/dist/did.js.map +1 -1
  17. package/dist/handle.js +13 -26
  18. package/dist/handle.js.map +1 -1
  19. package/dist/index.js +10 -13
  20. package/dist/index.js.map +1 -1
  21. package/dist/language.js +2 -6
  22. package/dist/language.js.map +1 -1
  23. package/dist/lib/result.d.ts +12 -0
  24. package/dist/lib/result.d.ts.map +1 -0
  25. package/dist/lib/result.js +7 -0
  26. package/dist/lib/result.js.map +1 -0
  27. package/dist/nsid.d.ts +3 -10
  28. package/dist/nsid.d.ts.map +1 -1
  29. package/dist/nsid.js +23 -82
  30. package/dist/nsid.js.map +1 -1
  31. package/dist/recordkey.js +3 -9
  32. package/dist/recordkey.js.map +1 -1
  33. package/dist/tid.js +3 -9
  34. package/dist/tid.js.map +1 -1
  35. package/dist/uri.js +1 -4
  36. package/dist/uri.js.map +1 -1
  37. package/package.json +12 -4
  38. package/src/aturi.ts +13 -7
  39. package/src/aturi_validation.ts +279 -110
  40. package/src/datetime.ts +41 -8
  41. package/src/lib/result.ts +11 -0
  42. package/src/nsid.ts +20 -56
  43. package/tests/aturi-string.test.ts +72 -40
  44. package/tests/aturi.test.ts +5 -5
  45. package/tests/datetime.test.ts +23 -1
  46. package/tests/did.test.ts +5 -1
  47. package/tests/handle.test.ts +1 -1
  48. package/tests/language.test.ts +1 -1
  49. package/tests/nsid.test.ts +1 -1
  50. package/tests/recordkey.test.ts +1 -1
  51. package/tests/tid.test.ts +1 -1
  52. package/tsconfig.build.tsbuildinfo +1 -1
  53. /package/{benchmark.js → benchmark.cjs} +0 -0
@@ -1,118 +1,188 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureValidAtUri = ensureValidAtUri;
4
- exports.ensureValidAtUriRegex = ensureValidAtUriRegex;
5
- exports.isValidAtUri = isValidAtUri;
6
- const at_identifier_js_1 = require("./at-identifier.js");
7
- const did_js_1 = require("./did.js");
8
- const handle_js_1 = require("./handle.js");
9
- const nsid_js_1 = require("./nsid.js");
10
- // Human-readable constraints on ATURI:
11
- // - following regular URLs, a 8KByte hard total length limit
12
- // - follows ATURI docs on website
13
- // - all ASCII characters, no whitespace. non-ASCII could be URL-encoded
14
- // - starts "at://"
15
- // - "authority" is a valid DID or a valid handle
16
- // - optionally, follow "authority" with "/" and valid NSID as start of path
17
- // - optionally, if NSID given, follow that with "/" and rkey
18
- // - rkey path component can include URL-encoded ("percent encoded"), or:
19
- // ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
20
- // [a-zA-Z0-9._~:@!$&'\(\)*+,;=-]
21
- // - rkey must have at least one char
22
- // - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901)
23
- function ensureValidAtUri(input) {
24
- const fragmentIndex = input.indexOf('#');
25
- if (fragmentIndex !== -1) {
26
- if (input.charCodeAt(fragmentIndex + 1) !== 47) {
27
- throw new Error('ATURI fragment must be non-empty and start with slash');
1
+ import { isAtIdentifierString } from './at-identifier.js';
2
+ import { failure, success } from './lib/result.js';
3
+ import { isValidNsid } from './nsid.js';
4
+ import { isValidRecordKey } from './recordkey.js';
5
+ /**
6
+ * Type guard that checks if a value is a valid {@link AtUriString}
7
+ *
8
+ * @see {@link AtUriString}
9
+ */
10
+ export function isAtUriString(input, options) {
11
+ return parseAtUriString(input, options).success;
12
+ }
13
+ /**
14
+ * Returns the input if it is a valid {@link AtUriString} format string, or
15
+ * `undefined` if it is not.
16
+ *
17
+ * @see {@link AtUriString}
18
+ */
19
+ export function ifAtUriString(input, options) {
20
+ return isAtUriString(input, options) ? input : undefined;
21
+ }
22
+ /**
23
+ * Casts a string to an {@link AtUriString} if it is a valid AT URI format
24
+ * string, throwing an error if it is not.
25
+ *
26
+ * @throws InvalidAtUriError if the input string does not meet the atproto AT URI format requirements.
27
+ * @see {@link AtUriString}
28
+ */
29
+ export function asAtUriString(input, options) {
30
+ assertAtUriString(input, options);
31
+ return input;
32
+ }
33
+ /**
34
+ * Assert the validity of an {@link AtUriString}, throwing an error if the
35
+ * {@link input} is not a valid AT URI.
36
+ *
37
+ * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}
38
+ */
39
+ export function assertAtUriString(input, options) {
40
+ // Optimistically use faster isAtUriString(), throwing a detailed error only
41
+ // in case of failure. This check, and the fact that the code after it always
42
+ // throws, also ensures that isAtUriString() and assertAtUriString()'s
43
+ // behavior are always consistent.
44
+ const result = parseAtUriString(input, options);
45
+ if (!result.success) {
46
+ throw new InvalidAtUriError(result.message);
47
+ }
48
+ }
49
+ /**
50
+ * Assert the **non-strict** validity of an {@link AtUriString}, throwing a
51
+ * detailed error if the {@link input} is not a valid AT URI.
52
+ *
53
+ * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}
54
+ * @deprecated use {@link assertAtUriString} with `{ strict: false }` option instead
55
+ */
56
+ export function ensureValidAtUri(input) {
57
+ assertAtUriString(input, { strict: false, detailed: true });
58
+ }
59
+ /**
60
+ * Assert the (non-strict!) validity of an {@link AtUriString}, throwing an
61
+ * error if the {@link input} is not a valid AT URI.
62
+ *
63
+ * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}
64
+ * @deprecated use {@link assertAtUriString} with `{ strict: false }` option instead
65
+ */
66
+ export function ensureValidAtUriRegex(input) {
67
+ assertAtUriString(input, { strict: false, detailed: false });
68
+ }
69
+ /**
70
+ * Type guard that checks if a value is a valid {@link AtUriString} format
71
+ * string, without enforcing strict record key validation. This is useful for
72
+ * cases where you want to allow a wider range of valid ATURIs, such as when
73
+ * validating user input or when the record key is not relevant.
74
+ *
75
+ * @deprecated use {@link isAtUriString} with `{ strict: false }` option instead
76
+ */
77
+ export function isValidAtUri(input) {
78
+ return isAtUriString(input, { strict: false });
79
+ }
80
+ export class InvalidAtUriError extends Error {
81
+ }
82
+ const INVALID_CHAR_REGEXP = /[^a-zA-Z0-9._~:@!$&'()*+,;=%/\\[\]#?-]/;
83
+ const AT_URI_REGEXP = /^(?<uri>at:\/\/(?<authority>[^/?#\s]+)(?:\/(?<collection>[^/?#\s]+)(?:\/(?<rkey>[^/?#\s]+))?)?(?<trailingSlash>\/)?)(?:\?(?<query>[^#\s]*))?(?:#(?<hash>[^\s]*))?$/;
84
+ /**
85
+ * Parses a valid {@link AtUriString} into a {@link AtUriParts} object, or
86
+ * returns a failure with a detailed error message if the string is not a valid
87
+ * {@link AtUriString}.
88
+ */
89
+ export function parseAtUriString(input, options) {
90
+ if (typeof input !== 'string') {
91
+ return failure('ATURI must be a string');
92
+ }
93
+ if (input.length > 8192) {
94
+ return failure('ATURI exceeds maximum length');
95
+ }
96
+ const invalidChar = input.match(INVALID_CHAR_REGEXP);
97
+ if (invalidChar) {
98
+ return failure('Disallowed characters in ATURI (ASCII)');
99
+ }
100
+ const match = input.match(AT_URI_REGEXP);
101
+ const groups = match?.groups;
102
+ if (!groups) {
103
+ // Regex validation failed, but we don't know exactly why. Provide more
104
+ // detailed error messages if the "detailed" option is set, falling back to
105
+ // a generic error.
106
+ if (options?.detailed) {
107
+ if (!input.startsWith('at://')) {
108
+ return failure('ATURI must start with "at://"');
109
+ }
110
+ if (input.includes(' ')) {
111
+ return failure('ATURI can not contain spaces');
112
+ }
113
+ if (input.includes('//', 5)) {
114
+ return failure('ATURI can not have empty path segments');
115
+ }
116
+ const pathStart = input.indexOf('/', 5); // after "at://"
117
+ if (pathStart !== -1) {
118
+ const fragmentIndex = input.indexOf('#');
119
+ const pathEnd = fragmentIndex !== -1 ? fragmentIndex : input.length;
120
+ const secondSlash = input.indexOf('/', pathStart + 1);
121
+ if (secondSlash !== -1 && secondSlash !== pathEnd - 1) {
122
+ return failure('ATURI can not have more than two path segments');
123
+ }
124
+ }
28
125
  }
29
- if (input.includes('#', fragmentIndex + 1)) {
30
- throw new Error('ATURI can have at most one "#", separating fragment out');
126
+ return failure('ATURI does not match expected format');
127
+ }
128
+ // @NOTE Percent-encoding is allowed by the AT URI specification, but any
129
+ // percent-encoded characters appearing in the collection NSID or record key
130
+ // will effectively be rejected by the isValidNsid and isValidRecordKey
131
+ // validators. Since these values are defined to be plain ASCII identifiers,
132
+ // this legacy behavior is beneficial: it ensures that normalized
133
+ // (non-percent-encoded) values are always used, as prescribed by the spec.
134
+ if (!isAtIdentifierString(groups.authority)) {
135
+ return failure('ATURI has invalid authority');
136
+ }
137
+ if (groups.collection != null && !isValidNsid(groups.collection)) {
138
+ return failure('ATURI has invalid collection');
139
+ }
140
+ if (groups.hash != null) {
141
+ const result = parseJsonPointer(groups.hash, options);
142
+ if (result.success) {
143
+ groups.hash = result.value;
31
144
  }
32
- // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
33
- const fragment = input.slice(fragmentIndex + 1);
34
- if (!/^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/.test(fragment)) {
35
- throw new Error('Disallowed characters in ATURI fragment (ASCII)');
145
+ else {
146
+ return failure(`ATURI has invalid fragment (${result.message})`);
36
147
  }
37
148
  }
38
- const uri = fragmentIndex === -1 ? input : input.slice(0, fragmentIndex);
39
- if (uri.length > 8 * 1024) {
40
- throw new Error('ATURI is far too long');
41
- }
42
- if (!uri.startsWith('at://')) {
43
- throw new Error('ATURI must start with "at://"');
44
- }
45
- // check that all chars are boring ASCII
46
- if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {
47
- throw new Error('Disallowed characters in ATURI (ASCII)');
48
- }
49
- const authorityEnd = uri.indexOf('/', 5);
50
- const authority = authorityEnd === -1 ? uri.slice(5) : uri.slice(5, authorityEnd);
51
- try {
52
- (0, at_identifier_js_1.ensureValidAtIdentifier)(authority);
53
- }
54
- catch (cause) {
55
- throw new Error('ATURI authority must be a valid handle or DID', { cause });
56
- }
57
- const collectionStart = authorityEnd === -1 ? -1 : authorityEnd + 1;
58
- const collectionEnd = collectionStart === -1 ? -1 : uri.indexOf('/', collectionStart);
59
- if (collectionStart !== -1) {
60
- const collection = collectionEnd === -1
61
- ? uri.slice(collectionStart)
62
- : uri.slice(collectionStart, collectionEnd);
63
- if (collection.length === 0) {
64
- throw new Error('ATURI can not have a slash after authority without a path segment');
149
+ if (options?.strict !== false) {
150
+ if (groups.trailingSlash != null) {
151
+ return failure('ATURI can not have a trailing slash');
65
152
  }
66
- if (!(0, nsid_js_1.isValidNsid)(collection)) {
67
- throw new Error('ATURI requires first path segment (if supplied) to be valid NSID');
153
+ if (groups.query != null) {
154
+ return failure('ATURI query part is not allowed');
68
155
  }
69
- }
70
- const recordKeyStart = collectionEnd === -1 ? -1 : collectionEnd + 1;
71
- const recordKeyEnd = recordKeyStart === -1 ? -1 : uri.indexOf('/', recordKeyStart);
72
- if (recordKeyStart !== -1) {
73
- if (recordKeyStart === uri.length) {
74
- throw new Error('ATURI can not have a slash after collection, unless record key is provided');
156
+ if (groups.rkey != null && !isValidRecordKey(groups.rkey)) {
157
+ return failure('ATURI has invalid record key');
75
158
  }
76
- // would validate rkey here, but there are basically no constraints!
77
- }
78
- if (recordKeyEnd !== -1) {
79
- throw new Error('ATURI path can have at most two parts, and no trailing slash');
80
159
  }
160
+ return success(groups);
81
161
  }
82
- function ensureValidAtUriRegex(input) {
83
- // simple regex to enforce most constraints via just regex and length.
84
- // hand wrote this regex based on above constraints. whew!
85
- const aturiRegex = /^at:\/\/(?<authority>[a-zA-Z0-9._:%-]+)(\/(?<collection>[a-zA-Z0-9-.]+)(\/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/;
86
- const rm = input.match(aturiRegex);
87
- if (!rm || !rm.groups) {
88
- throw new Error("ATURI didn't validate via regex");
89
- }
90
- const groups = rm.groups;
91
- try {
92
- (0, handle_js_1.ensureValidHandleRegex)(groups.authority);
93
- }
94
- catch {
95
- try {
96
- (0, did_js_1.ensureValidDidRegex)(groups.authority);
97
- }
98
- catch {
99
- throw new Error('ATURI authority must be a valid handle or DID');
100
- }
101
- }
102
- if (groups.collection && !(0, nsid_js_1.isValidNsid)(groups.collection)) {
103
- throw new Error('ATURI collection path segment must be a valid NSID');
104
- }
105
- if (input.length > 8 * 1024) {
106
- throw new Error('ATURI is far too long');
107
- }
162
+ const BASIC_JSON_POINTER_REGEXP = /^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/;
163
+ /**
164
+ * Checks if a string is a valid JSON pointer (RFC-6901) with the allowed chars
165
+ * for ATURI fragments. This is a very loose validation that only checks the
166
+ * basic syntax and charset.
167
+ */
168
+ function parseJsonPointer(value, options) {
169
+ if (!BASIC_JSON_POINTER_REGEXP.test(value)) {
170
+ return failure('Invalid JSON pointer');
171
+ }
172
+ const result = parsePercentEncoding(value);
173
+ // In non-strict mode, we allow invalid percent-encoding in the fragment
174
+ if (!result.success && options?.strict === false) {
175
+ return success(value);
176
+ }
177
+ return result;
108
178
  }
109
- function isValidAtUri(input) {
179
+ function parsePercentEncoding(value) {
110
180
  try {
111
- ensureValidAtUri(input);
181
+ return success(decodeURIComponent(value));
112
182
  }
113
183
  catch {
114
- return false;
184
+ // decodeURIComponent throws if the percent-encoding is invalid (e.g. "%FF")
185
+ return failure('Invalid percent-encoding');
115
186
  }
116
- return true;
117
187
  }
118
188
  //# sourceMappingURL=aturi_validation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"aturi_validation.js","sourceRoot":"","sources":["../src/aturi_validation.ts"],"names":[],"mappings":"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","sourcesContent":["import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'\nimport { ensureValidDidRegex } from './did.js'\nimport { ensureValidHandleRegex } from './handle.js'\nimport { NsidString, isValidNsid } from './nsid.js'\n\nexport type AtUriString =\n | `at://${AtIdentifierString}`\n | `at://${AtIdentifierString}/${NsidString}`\n | `at://${AtIdentifierString}/${NsidString}/${string}`\n\n// Human-readable constraints on ATURI:\n// - following regular URLs, a 8KByte hard total length limit\n// - follows ATURI docs on website\n// - all ASCII characters, no whitespace. non-ASCII could be URL-encoded\n// - starts \"at://\"\n// - \"authority\" is a valid DID or a valid handle\n// - optionally, follow \"authority\" with \"/\" and valid NSID as start of path\n// - optionally, if NSID given, follow that with \"/\" and rkey\n// - rkey path component can include URL-encoded (\"percent encoded\"), or:\n// ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\" / \":\" / \"@\" / \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n// [a-zA-Z0-9._~:@!$&'\\(\\)*+,;=-]\n// - rkey must have at least one char\n// - regardless of path component, a fragment can follow as \"#\" and then a JSON pointer (RFC-6901)\n\nexport function ensureValidAtUri<I extends string>(\n input: I,\n): asserts input is I & AtUriString {\n const fragmentIndex = input.indexOf('#')\n if (fragmentIndex !== -1) {\n if (input.charCodeAt(fragmentIndex + 1) !== 47) {\n throw new Error('ATURI fragment must be non-empty and start with slash')\n }\n if (input.includes('#', fragmentIndex + 1)) {\n throw new Error('ATURI can have at most one \"#\", separating fragment out')\n }\n\n // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace\n const fragment = input.slice(fragmentIndex + 1)\n if (!/^\\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\\]/-]*$/.test(fragment)) {\n throw new Error('Disallowed characters in ATURI fragment (ASCII)')\n }\n }\n\n const uri = fragmentIndex === -1 ? input : input.slice(0, fragmentIndex)\n\n if (uri.length > 8 * 1024) {\n throw new Error('ATURI is far too long')\n }\n\n if (!uri.startsWith('at://')) {\n throw new Error('ATURI must start with \"at://\"')\n }\n\n // check that all chars are boring ASCII\n if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {\n throw new Error('Disallowed characters in ATURI (ASCII)')\n }\n\n const authorityEnd = uri.indexOf('/', 5)\n const authority =\n authorityEnd === -1 ? uri.slice(5) : uri.slice(5, authorityEnd)\n try {\n ensureValidAtIdentifier(authority)\n } catch (cause) {\n throw new Error('ATURI authority must be a valid handle or DID', { cause })\n }\n\n const collectionStart = authorityEnd === -1 ? -1 : authorityEnd + 1\n const collectionEnd =\n collectionStart === -1 ? -1 : uri.indexOf('/', collectionStart)\n\n if (collectionStart !== -1) {\n const collection =\n collectionEnd === -1\n ? uri.slice(collectionStart)\n : uri.slice(collectionStart, collectionEnd)\n\n if (collection.length === 0) {\n throw new Error(\n 'ATURI can not have a slash after authority without a path segment',\n )\n }\n if (!isValidNsid(collection)) {\n throw new Error(\n 'ATURI requires first path segment (if supplied) to be valid NSID',\n )\n }\n }\n\n const recordKeyStart = collectionEnd === -1 ? -1 : collectionEnd + 1\n const recordKeyEnd =\n recordKeyStart === -1 ? -1 : uri.indexOf('/', recordKeyStart)\n\n if (recordKeyStart !== -1) {\n if (recordKeyStart === uri.length) {\n throw new Error(\n 'ATURI can not have a slash after collection, unless record key is provided',\n )\n }\n // would validate rkey here, but there are basically no constraints!\n }\n\n if (recordKeyEnd !== -1) {\n throw new Error(\n 'ATURI path can have at most two parts, and no trailing slash',\n )\n }\n}\n\nexport function ensureValidAtUriRegex<I extends string>(\n input: I,\n): asserts input is I & AtUriString {\n // simple regex to enforce most constraints via just regex and length.\n // hand wrote this regex based on above constraints. whew!\n const aturiRegex =\n /^at:\\/\\/(?<authority>[a-zA-Z0-9._:%-]+)(\\/(?<collection>[a-zA-Z0-9-.]+)(\\/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>\\/[a-zA-Z0-9._~:@!$&%')(*+,;=\\-[\\]/\\\\]*))?$/\n const rm = input.match(aturiRegex)\n if (!rm || !rm.groups) {\n throw new Error(\"ATURI didn't validate via regex\")\n }\n const groups = rm.groups\n\n try {\n ensureValidHandleRegex(groups.authority)\n } catch {\n try {\n ensureValidDidRegex(groups.authority)\n } catch {\n throw new Error('ATURI authority must be a valid handle or DID')\n }\n }\n\n if (groups.collection && !isValidNsid(groups.collection)) {\n throw new Error('ATURI collection path segment must be a valid NSID')\n }\n\n if (input.length > 8 * 1024) {\n throw new Error('ATURI is far too long')\n }\n}\n\nexport function isValidAtUri<I extends string>(\n input: I,\n): input is I & AtUriString {\n try {\n ensureValidAtUri(input)\n } catch {\n return false\n }\n\n return true\n}\n"]}
1
+ {"version":3,"file":"aturi_validation.js","sourceRoot":"","sources":["../src/aturi_validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EAAU,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAc,WAAW,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAuDjD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAQ,EACR,OAAmD;IAEnD,OAAO,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,OAAO,CAAA;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAQ,EACR,OAAmD;IAEnD,OAAO,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAQ,EACR,OAAiC;IAEjC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACjC,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAQ,EACR,OAAiC;IAEjC,4EAA4E;IAC5E,6EAA6E;IAC7E,sEAAsE;IACtE,kCAAkC;IAClC,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC7C,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAQ;IAER,iBAAiB,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAQ;IAER,iBAAiB,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAI,KAAQ;IACtC,OAAO,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;AAChD,CAAC;AAED,MAAM,OAAO,iBAAkB,SAAQ,KAAK;CAAG;AAgC/C,MAAM,mBAAmB,GAAG,wCAAwC,CAAA;AACpE,MAAM,aAAa,GACjB,oKAAoK,CAAA;AAEtK;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAc,EACd,OAAiC;IAEjC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAC,wBAAwB,CAAC,CAAA;IAC1C,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC,8BAA8B,CAAC,CAAA;IAChD,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAA;IACpD,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,OAAO,CAAC,wCAAwC,CAAC,CAAA;IAC1D,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,KAAK,EAAE,MAAM,CAAA;IAC5B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,uEAAuE;QACvE,2EAA2E;QAC3E,mBAAmB;QACnB,IAAI,OAAO,EAAE,QAAQ,EAAE,CAAC;YACtB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/B,OAAO,OAAO,CAAC,+BAA+B,CAAC,CAAA;YACjD,CAAC;YAED,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxB,OAAO,OAAO,CAAC,8BAA8B,CAAC,CAAA;YAChD,CAAC;YAED,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,OAAO,OAAO,CAAC,wCAAwC,CAAC,CAAA;YAC1D,CAAC;YAED,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA,CAAC,gBAAgB;YACxD,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;gBACrB,MAAM,aAAa,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;gBACxC,MAAM,OAAO,GAAG,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAA;gBACnE,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,CAAA;gBACrD,IAAI,WAAW,KAAK,CAAC,CAAC,IAAI,WAAW,KAAK,OAAO,GAAG,CAAC,EAAE,CAAC;oBACtD,OAAO,OAAO,CAAC,gDAAgD,CAAC,CAAA;gBAClE,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC,sCAAsC,CAAC,CAAA;IACxD,CAAC;IAED,yEAAyE;IACzE,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,iEAAiE;IACjE,2EAA2E;IAE3E,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5C,OAAO,OAAO,CAAC,6BAA6B,CAAC,CAAA;IAC/C,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACjE,OAAO,OAAO,CAAC,8BAA8B,CAAC,CAAA;IAChD,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QACrD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,KAAK,CAAA;QAC5B,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAC,+BAA+B,MAAM,CAAC,OAAO,GAAG,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;IAED,IAAI,OAAO,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC;QAC9B,IAAI,MAAM,CAAC,aAAa,IAAI,IAAI,EAAE,CAAC;YACjC,OAAO,OAAO,CAAC,qCAAqC,CAAC,CAAA;QACvD,CAAC;QAED,IAAI,MAAM,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,OAAO,OAAO,CAAC,iCAAiC,CAAC,CAAA;QACnD,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1D,OAAO,OAAO,CAAC,8BAA8B,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,MAAoB,CAAC,CAAA;AACtC,CAAC;AAED,MAAM,yBAAyB,GAAG,wCAAwC,CAAA;AAE1E;;;;GAIG;AACH,SAAS,gBAAgB,CACvB,KAAa,EACb,OAA8B;IAE9B,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACxC,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;IAE1C,wEAAwE;IACxE,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC;QACjD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAA;IACvB,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa;IACzC,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,OAAO,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC5C,CAAC;AACH,CAAC","sourcesContent":["import { AtIdentifierString, isAtIdentifierString } from './at-identifier.js'\nimport { Result, failure, success } from './lib/result.js'\nimport { NsidString, isValidNsid } from './nsid.js'\nimport { isValidRecordKey } from './recordkey.js'\n\nexport type AtUriStringBase =\n | `at://${AtIdentifierString}`\n | `at://${AtIdentifierString}/${NsidString}`\n | `at://${AtIdentifierString}/${NsidString}/${string}`\n\nexport type AtUriStringFragment = `#/${string}`\n\n/**\n * A URI string as used to point at resources in the AT Protocol\n *\n * The full, general structure of an AT URI is:\n *\n * ```bnf\n * AT-URI = \"at://\" AUTHORITY [ PATH ] [ \"?\" QUERY ] [ \"#\" FRAGMENT ]\n * ```\n *\n * The authority part of the URI can be either a handle or a DID, indicating the\n * identity associated with the repository. In current atproto Lexicon use, the\n * query and fragment parts are not yet supported, and only a fixed pattern of\n * paths are allowed:\n *\n * ```bnf\n * AT-URI = \"at://\" AUTHORITY [ \"/\" COLLECTION [ \"/\" RKEY ] ]\n *\n * AUTHORITY = HANDLE | DID\n * COLLECTION = NSID\n * RKEY = RECORD-KEY\n * ```\n *\n * The authority section is required, and should be normalized.\n *\n * AT URI strings must respect the following syntax (as prescribed by the AT\n * protocol specification):\n *\n * - The overall URI is restricted to a subset of ASCII characters\n * - 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 (`.-_~`)\n * - Maximum overall length is 8 kilobytes (which may be shortened in the future)\n * - Hex-encoding of characters is permitted (but in practice not necessary and should be avoided to keep the URI normalized and human-readable)\n * - The URI scheme is `at`, and an authority part preceded with double slashes is always required. AT URIs always start with `at://`.\n * - 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.\n * - Note that none of the current \"blessed\" DID methods for atproto allow these characters in DID identifiers\n * - 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.\n * - An optional query part is allowed, following generic URI syntax restrictions\n * - An optional fragment part is allowed, using JSON Path syntax\n *\n * @example \"at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.actor.profile/self\"\n *\n * @see {@link https://atproto.com/specs/at-uri-scheme AT protocol - AT URI Scheme}\n */\nexport type AtUriString =\n | AtUriStringBase\n | `${AtUriStringBase}${AtUriStringFragment}`\n\n/**\n * Type guard that checks if a value is a valid {@link AtUriString}\n *\n * @see {@link AtUriString}\n */\nexport function isAtUriString<I>(\n input: I,\n options?: Omit<ParseAtUriStringOptions, 'detailed'>,\n): input is I & AtUriString {\n return parseAtUriString(input, options).success\n}\n\n/**\n * Returns the input if it is a valid {@link AtUriString} format string, or\n * `undefined` if it is not.\n *\n * @see {@link AtUriString}\n */\nexport function ifAtUriString<I>(\n input: I,\n options?: Omit<ParseAtUriStringOptions, 'detailed'>,\n): undefined | (I & AtUriString) {\n return isAtUriString(input, options) ? input : undefined\n}\n\n/**\n * Casts a string to an {@link AtUriString} if it is a valid AT URI format\n * string, throwing an error if it is not.\n *\n * @throws InvalidAtUriError if the input string does not meet the atproto AT URI format requirements.\n * @see {@link AtUriString}\n */\nexport function asAtUriString<I>(\n input: I,\n options?: ParseAtUriStringOptions,\n): I & AtUriString {\n assertAtUriString(input, options)\n return input\n}\n\n/**\n * Assert the validity of an {@link AtUriString}, throwing an error if the\n * {@link input} is not a valid AT URI.\n *\n * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}\n */\nexport function assertAtUriString<I>(\n input: I,\n options?: ParseAtUriStringOptions,\n): asserts input is I & AtUriString {\n // Optimistically use faster isAtUriString(), throwing a detailed error only\n // in case of failure. This check, and the fact that the code after it always\n // throws, also ensures that isAtUriString() and assertAtUriString()'s\n // behavior are always consistent.\n const result = parseAtUriString(input, options)\n if (!result.success) {\n throw new InvalidAtUriError(result.message)\n }\n}\n\n/**\n * Assert the **non-strict** validity of an {@link AtUriString}, throwing a\n * detailed error if the {@link input} is not a valid AT URI.\n *\n * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}\n * @deprecated use {@link assertAtUriString} with `{ strict: false }` option instead\n */\nexport function ensureValidAtUri<I>(\n input: I,\n): asserts input is I & AtUriString {\n assertAtUriString(input, { strict: false, detailed: true })\n}\n\n/**\n * Assert the (non-strict!) validity of an {@link AtUriString}, throwing an\n * error if the {@link input} is not a valid AT URI.\n *\n * @throws InvalidAtUriError if the {@link input} is not a valid {@link AtUriString}\n * @deprecated use {@link assertAtUriString} with `{ strict: false }` option instead\n */\nexport function ensureValidAtUriRegex<I>(\n input: I,\n): asserts input is I & AtUriString {\n assertAtUriString(input, { strict: false, detailed: false })\n}\n\n/**\n * Type guard that checks if a value is a valid {@link AtUriString} format\n * string, without enforcing strict record key validation. This is useful for\n * cases where you want to allow a wider range of valid ATURIs, such as when\n * validating user input or when the record key is not relevant.\n *\n * @deprecated use {@link isAtUriString} with `{ strict: false }` option instead\n */\nexport function isValidAtUri<I>(input: I): input is I & AtUriString {\n return isAtUriString(input, { strict: false })\n}\n\nexport class InvalidAtUriError extends Error {}\n\nexport type ParseAtUriStringOptions = {\n /**\n * If true, the parser will enforce that the record key (rkey) part of the URI\n * is a valid record key (validated by {@link isValidRecordKey}). If false,\n * any non-empty string of allowed chars will be accepted as a record key.\n *\n * @default true\n */\n strict?: boolean\n\n /**\n * If true, the parser will return detailed error messages for why a string is\n * not a valid AT URI. This option has no effect on the behavior of\n * {@link isAtUriString}, which will always return false for invalid strings\n * regardless of this option.\n *\n * @default false\n */\n detailed?: boolean\n}\n\nexport type AtUriParts = {\n authority: AtIdentifierString\n query?: string\n hash?: string\n} & (\n | { collection?: NsidString; rkey?: undefined }\n | { collection: NsidString; rkey?: string }\n)\n\nconst INVALID_CHAR_REGEXP = /[^a-zA-Z0-9._~:@!$&'()*+,;=%/\\\\[\\]#?-]/\nconst AT_URI_REGEXP =\n /^(?<uri>at:\\/\\/(?<authority>[^/?#\\s]+)(?:\\/(?<collection>[^/?#\\s]+)(?:\\/(?<rkey>[^/?#\\s]+))?)?(?<trailingSlash>\\/)?)(?:\\?(?<query>[^#\\s]*))?(?:#(?<hash>[^\\s]*))?$/\n\n/**\n * Parses a valid {@link AtUriString} into a {@link AtUriParts} object, or\n * returns a failure with a detailed error message if the string is not a valid\n * {@link AtUriString}.\n */\nexport function parseAtUriString(\n input: unknown,\n options?: ParseAtUriStringOptions,\n): Result<AtUriParts> {\n if (typeof input !== 'string') {\n return failure('ATURI must be a string')\n }\n\n if (input.length > 8192) {\n return failure('ATURI exceeds maximum length')\n }\n\n const invalidChar = input.match(INVALID_CHAR_REGEXP)\n if (invalidChar) {\n return failure('Disallowed characters in ATURI (ASCII)')\n }\n\n const match = input.match(AT_URI_REGEXP)\n const groups = match?.groups\n if (!groups) {\n // Regex validation failed, but we don't know exactly why. Provide more\n // detailed error messages if the \"detailed\" option is set, falling back to\n // a generic error.\n if (options?.detailed) {\n if (!input.startsWith('at://')) {\n return failure('ATURI must start with \"at://\"')\n }\n\n if (input.includes(' ')) {\n return failure('ATURI can not contain spaces')\n }\n\n if (input.includes('//', 5)) {\n return failure('ATURI can not have empty path segments')\n }\n\n const pathStart = input.indexOf('/', 5) // after \"at://\"\n if (pathStart !== -1) {\n const fragmentIndex = input.indexOf('#')\n const pathEnd = fragmentIndex !== -1 ? fragmentIndex : input.length\n const secondSlash = input.indexOf('/', pathStart + 1)\n if (secondSlash !== -1 && secondSlash !== pathEnd - 1) {\n return failure('ATURI can not have more than two path segments')\n }\n }\n }\n\n return failure('ATURI does not match expected format')\n }\n\n // @NOTE Percent-encoding is allowed by the AT URI specification, but any\n // percent-encoded characters appearing in the collection NSID or record key\n // will effectively be rejected by the isValidNsid and isValidRecordKey\n // validators. Since these values are defined to be plain ASCII identifiers,\n // this legacy behavior is beneficial: it ensures that normalized\n // (non-percent-encoded) values are always used, as prescribed by the spec.\n\n if (!isAtIdentifierString(groups.authority)) {\n return failure('ATURI has invalid authority')\n }\n\n if (groups.collection != null && !isValidNsid(groups.collection)) {\n return failure('ATURI has invalid collection')\n }\n\n if (groups.hash != null) {\n const result = parseJsonPointer(groups.hash, options)\n if (result.success) {\n groups.hash = result.value\n } else {\n return failure(`ATURI has invalid fragment (${result.message})`)\n }\n }\n\n if (options?.strict !== false) {\n if (groups.trailingSlash != null) {\n return failure('ATURI can not have a trailing slash')\n }\n\n if (groups.query != null) {\n return failure('ATURI query part is not allowed')\n }\n\n if (groups.rkey != null && !isValidRecordKey(groups.rkey)) {\n return failure('ATURI has invalid record key')\n }\n }\n\n return success(groups as AtUriParts)\n}\n\nconst BASIC_JSON_POINTER_REGEXP = /^\\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\\]/-]*$/\n\n/**\n * Checks if a string is a valid JSON pointer (RFC-6901) with the allowed chars\n * for ATURI fragments. This is a very loose validation that only checks the\n * basic syntax and charset.\n */\nfunction parseJsonPointer(\n value: string,\n options?: { strict?: boolean },\n): Result<string> {\n if (!BASIC_JSON_POINTER_REGEXP.test(value)) {\n return failure('Invalid JSON pointer')\n }\n\n const result = parsePercentEncoding(value)\n\n // In non-strict mode, we allow invalid percent-encoding in the fragment\n if (!result.success && options?.strict === false) {\n return success(value)\n }\n\n return result\n}\n\nfunction parsePercentEncoding(value: string): Result<string> {\n try {\n return success(decodeURIComponent(value))\n } catch {\n // decodeURIComponent throws if the percent-encoding is invalid (e.g. \"%FF\")\n return failure('Invalid percent-encoding')\n }\n}\n"]}
@@ -113,6 +113,11 @@ export declare function toDatetimeString(date: Date): DatetimeString;
113
113
  * One use-case is a consistent, sortable string. Another is to work with older
114
114
  * invalid createdAt datetimes.
115
115
  *
116
+ * @note This function might return different normalized strings for the same
117
+ * input depending on the timezone of the machine it is run on, since it will
118
+ * attempt to parse the input "as is" if it fails to parse with an explicit
119
+ * timezone.
120
+ *
116
121
  * @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.
117
122
  * @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.
118
123
  */
@@ -1 +1 @@
1
- {"version":3,"file":"datetime.d.ts","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAE3B,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,CAAA;AAE1E;;;;GAIG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI;IACvC,WAAW,IAAI,iBAAiB,CAAA;CACjC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,CAKzE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,CAGrD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,WAAW,CAE7D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,SAAS,CAEjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,cAAc,GAEtB,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,GAC9D,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAA;AAEhG;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,GACP,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,cAAc,CAKrC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,cAAc,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,cAAc,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,KAAK,EAAE,CAAC,GACP,SAAS,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC,CAElC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAEtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,CAE3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAkBlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAMxE;AAGD,OAAO,EACL,oBAAoB,IAAI,mBAAmB,EAC3C,gBAAgB,IAAI,eAAe,GACpC,CAAA"}
1
+ {"version":3,"file":"datetime.d.ts","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAE3B,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,CAAA;AAE1E;;;;GAIG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI;IACvC,WAAW,IAAI,iBAAiB,CAAA;CACjC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,CAKzE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,CAGrD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,WAAW,CAE7D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,SAAS,CAEjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,cAAc,GAEtB,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,GAC9D,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAA;AAEhG;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,GACP,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,cAAc,CAKrC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,cAAc,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,cAAc,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,KAAK,EAAE,CAAC,GACP,SAAS,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC,CAElC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAEtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,CAE3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CA8ClE;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAMxE;AAGD,OAAO,EACL,oBAAoB,IAAI,mBAAmB,EAC3C,gBAAgB,IAAI,eAAe,GACpC,CAAA"}
package/dist/datetime.js CHANGED
@@ -1,32 +1,14 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.InvalidDatetimeError = void 0;
4
- exports.assertAtprotoDate = assertAtprotoDate;
5
- exports.asAtprotoDate = asAtprotoDate;
6
- exports.isAtprotoDate = isAtprotoDate;
7
- exports.ifAtprotoDate = ifAtprotoDate;
8
- exports.assertDatetimeString = assertDatetimeString;
9
- exports.ensureValidDatetime = assertDatetimeString;
10
- exports.asDatetimeString = asDatetimeString;
11
- exports.isDatetimeString = isDatetimeString;
12
- exports.isValidDatetime = isDatetimeString;
13
- exports.ifDatetimeString = ifDatetimeString;
14
- exports.currentDatetimeString = currentDatetimeString;
15
- exports.toDatetimeString = toDatetimeString;
16
- exports.normalizeDatetime = normalizeDatetime;
17
- exports.normalizeDatetimeAlways = normalizeDatetimeAlways;
18
1
  /**
19
2
  * Indicates a date or string is not a valid representation of a datetime
20
3
  * according to the atproto
21
4
  * {@link https://atproto.com/specs/lexicon#datetime specification}.
22
5
  */
23
- class InvalidDatetimeError extends Error {
6
+ export class InvalidDatetimeError extends Error {
24
7
  }
25
- exports.InvalidDatetimeError = InvalidDatetimeError;
26
8
  /**
27
9
  * @see {@link AtprotoDate}
28
10
  */
29
- function assertAtprotoDate(date) {
11
+ export function assertAtprotoDate(date) {
30
12
  const res = parseDate(date);
31
13
  if (!res.success) {
32
14
  throw new InvalidDatetimeError(res.message);
@@ -35,20 +17,20 @@ function assertAtprotoDate(date) {
35
17
  /**
36
18
  * @see {@link AtprotoDate}
37
19
  */
38
- function asAtprotoDate(date) {
20
+ export function asAtprotoDate(date) {
39
21
  assertAtprotoDate(date);
40
22
  return date;
41
23
  }
42
24
  /**
43
25
  * @see {@link AtprotoDate}
44
26
  */
45
- function isAtprotoDate(date) {
27
+ export function isAtprotoDate(date) {
46
28
  return parseDate(date).success;
47
29
  }
48
30
  /**
49
31
  * @see {@link AtprotoDate}
50
32
  */
51
- function ifAtprotoDate(date) {
33
+ export function ifAtprotoDate(date) {
52
34
  return isAtprotoDate(date) ? date : undefined;
53
35
  }
54
36
  /**
@@ -58,7 +40,7 @@ function ifAtprotoDate(date) {
58
40
  * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.
59
41
  * @see {@link DatetimeString}
60
42
  */
61
- function assertDatetimeString(input) {
43
+ export function assertDatetimeString(input) {
62
44
  const result = parseString(input);
63
45
  if (!result.success) {
64
46
  throw new InvalidDatetimeError(result.message);
@@ -71,7 +53,7 @@ function assertDatetimeString(input) {
71
53
  * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.
72
54
  * @see {@link DatetimeString}
73
55
  */
74
- function asDatetimeString(input) {
56
+ export function asDatetimeString(input) {
75
57
  assertDatetimeString(input);
76
58
  return input;
77
59
  }
@@ -80,7 +62,7 @@ function asDatetimeString(input) {
80
62
  *
81
63
  * @see {@link DatetimeString}
82
64
  */
83
- function isDatetimeString(input) {
65
+ export function isDatetimeString(input) {
84
66
  return parseString(input).success;
85
67
  }
86
68
  /**
@@ -89,7 +71,7 @@ function isDatetimeString(input) {
89
71
  *
90
72
  * @see {@link DatetimeString}
91
73
  */
92
- function ifDatetimeString(input) {
74
+ export function ifDatetimeString(input) {
93
75
  return isDatetimeString(input) ? input : undefined;
94
76
  }
95
77
  /**
@@ -97,7 +79,7 @@ function ifDatetimeString(input) {
97
79
  *
98
80
  * @see {@link DatetimeString}
99
81
  */
100
- function currentDatetimeString() {
82
+ export function currentDatetimeString() {
101
83
  return toDatetimeString(new Date());
102
84
  }
103
85
  /**
@@ -109,7 +91,7 @@ function currentDatetimeString() {
109
91
  * @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).
110
92
  * @see {@link DatetimeString}
111
93
  */
112
- function toDatetimeString(date) {
94
+ export function toDatetimeString(date) {
113
95
  return asAtprotoDate(date).toISOString();
114
96
  }
115
97
  /**
@@ -122,18 +104,48 @@ function toDatetimeString(date) {
122
104
  * One use-case is a consistent, sortable string. Another is to work with older
123
105
  * invalid createdAt datetimes.
124
106
  *
107
+ * @note This function might return different normalized strings for the same
108
+ * input depending on the timezone of the machine it is run on, since it will
109
+ * attempt to parse the input "as is" if it fails to parse with an explicit
110
+ * timezone.
111
+ *
125
112
  * @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.
126
113
  * @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.
127
114
  */
128
- function normalizeDatetime(dtStr) {
129
- // Parse the string as is
130
- const date = new Date(dtStr);
131
- if (isAtprotoDate(date)) {
132
- return date.toISOString();
115
+ export function normalizeDatetime(dtStr) {
116
+ if (
117
+ // Explicit timezone offset
118
+ /[+-]\d\d:?\d\d/.test(dtStr) ||
119
+ // 'Z' timezone designator
120
+ /\dZ\b/.test(dtStr) ||
121
+ // Timezone abbreviation (eg. "EST", "PST", "UTC", "GMT", etc), as in:
122
+ // > Tue Mar 17 2026 16:38:44 PST (Pacific Standard Time)
123
+ /\b[A-Z]{3,4}\b/.test(dtStr)) {
124
+ // Since we do have a timezone designator, we can try parsing "as is" and
125
+ // should get consistent results regardless of local timezone.
126
+ // @NOTE NodeJS will reject dates with an un-recognized timezone designator
127
+ // (like "AFT"), even if we add a well-known timezone abbreviation like
128
+ // "UTC" or "Z".
129
+ const date = new Date(dtStr);
130
+ if (isAtprotoDate(date)) {
131
+ return date.toISOString();
132
+ }
133
133
  }
134
- // if dtStr is not a valid date, try parsing again with a timezone
135
- if (isNaN(date.getTime()) && !/.*(([+-]\d\d:?\d\d)|[a-zA-Z])$/.test(dtStr)) {
136
- const date = new Date(`${dtStr}Z`);
134
+ else {
135
+ // If there is no timezone information, try parsing as UTC using two
136
+ // different syntaxes, falling back to parsing "as is".
137
+ const dateZ = new Date(`${dtStr}Z`);
138
+ if (isAtprotoDate(dateZ)) {
139
+ return dateZ.toISOString();
140
+ }
141
+ const dateUTC = new Date(`${dtStr} UTC`);
142
+ if (isAtprotoDate(dateUTC)) {
143
+ return dateUTC.toISOString();
144
+ }
145
+ // Despite our best efforts to parse as a consistent value, appending "Z" or
146
+ // " UTC" did not work, so we will try parsing "as is", which may yield
147
+ // different results depending on the local timezone of the machine.
148
+ const date = new Date(dtStr);
137
149
  if (isAtprotoDate(date)) {
138
150
  return date.toISOString();
139
151
  }
@@ -149,7 +161,7 @@ function normalizeDatetime(dtStr) {
149
161
  *
150
162
  * @see {@link normalizeDatetime}
151
163
  */
152
- function normalizeDatetimeAlways(dtStr) {
164
+ export function normalizeDatetimeAlways(dtStr) {
153
165
  try {
154
166
  return normalizeDatetime(dtStr);
155
167
  }
@@ -157,6 +169,8 @@ function normalizeDatetimeAlways(dtStr) {
157
169
  return '1970-01-01T00:00:00.000Z';
158
170
  }
159
171
  }
172
+ // Legacy exports (should we deprecate these ?)
173
+ export { assertDatetimeString as ensureValidDatetime, isDatetimeString as isValidDatetime, };
160
174
  const failure = (m) => ({ success: false, message: m });
161
175
  const success = (v) => ({ success: true, value: v });
162
176
  /**