@atproto/syntax 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/aturi.d.ts.map +1 -1
- package/dist/aturi.js +14 -7
- package/dist/aturi.js.map +1 -1
- package/dist/aturi_validation.d.ts +137 -4
- package/dist/aturi_validation.d.ts.map +1 -1
- package/dist/aturi_validation.js +176 -94
- package/dist/aturi_validation.js.map +1 -1
- package/dist/datetime.d.ts +5 -0
- package/dist/datetime.d.ts.map +1 -1
- package/dist/datetime.js +37 -7
- package/dist/datetime.js.map +1 -1
- package/dist/lib/result.d.ts +12 -0
- package/dist/lib/result.d.ts.map +1 -0
- package/dist/lib/result.js +11 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/nsid.d.ts +3 -10
- package/dist/nsid.d.ts.map +1 -1
- package/dist/nsid.js +15 -62
- package/dist/nsid.js.map +1 -1
- package/package.json +1 -1
- package/src/aturi.ts +13 -7
- package/src/aturi_validation.ts +279 -110
- package/src/datetime.ts +41 -8
- package/src/lib/result.ts +11 -0
- package/src/nsid.ts +20 -56
- package/tests/aturi-string.test.ts +211 -0
- package/tests/aturi.test.ts +59 -198
- package/tests/datetime.test.ts +22 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -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":"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","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"]}
|
package/dist/datetime.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/datetime.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
@@ -122,18 +122,48 @@ function toDatetimeString(date) {
|
|
|
122
122
|
* One use-case is a consistent, sortable string. Another is to work with older
|
|
123
123
|
* invalid createdAt datetimes.
|
|
124
124
|
*
|
|
125
|
+
* @note This function might return different normalized strings for the same
|
|
126
|
+
* input depending on the timezone of the machine it is run on, since it will
|
|
127
|
+
* attempt to parse the input "as is" if it fails to parse with an explicit
|
|
128
|
+
* timezone.
|
|
129
|
+
*
|
|
125
130
|
* @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.
|
|
126
131
|
* @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.
|
|
127
132
|
*/
|
|
128
133
|
function normalizeDatetime(dtStr) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
if (
|
|
135
|
+
// Explicit timezone offset
|
|
136
|
+
/[+-]\d\d:?\d\d/.test(dtStr) ||
|
|
137
|
+
// 'Z' timezone designator
|
|
138
|
+
/\dZ\b/.test(dtStr) ||
|
|
139
|
+
// Timezone abbreviation (eg. "EST", "PST", "UTC", "GMT", etc), as in:
|
|
140
|
+
// > Tue Mar 17 2026 16:38:44 PST (Pacific Standard Time)
|
|
141
|
+
/\b[A-Z]{3,4}\b/.test(dtStr)) {
|
|
142
|
+
// Since we do have a timezone designator, we can try parsing "as is" and
|
|
143
|
+
// should get consistent results regardless of local timezone.
|
|
144
|
+
// @NOTE NodeJS will reject dates with an un-recognized timezone designator
|
|
145
|
+
// (like "AFT"), even if we add a well-known timezone abbreviation like
|
|
146
|
+
// "UTC" or "Z".
|
|
147
|
+
const date = new Date(dtStr);
|
|
148
|
+
if (isAtprotoDate(date)) {
|
|
149
|
+
return date.toISOString();
|
|
150
|
+
}
|
|
133
151
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
else {
|
|
153
|
+
// If there is no timezone information, try parsing as UTC using two
|
|
154
|
+
// different syntaxes, falling back to parsing "as is".
|
|
155
|
+
const dateZ = new Date(`${dtStr}Z`);
|
|
156
|
+
if (isAtprotoDate(dateZ)) {
|
|
157
|
+
return dateZ.toISOString();
|
|
158
|
+
}
|
|
159
|
+
const dateUTC = new Date(`${dtStr} UTC`);
|
|
160
|
+
if (isAtprotoDate(dateUTC)) {
|
|
161
|
+
return dateUTC.toISOString();
|
|
162
|
+
}
|
|
163
|
+
// Despite our best efforts to parse as a consistent value, appending "Z" or
|
|
164
|
+
// " UTC" did not work, so we will try parsing "as is", which may yield
|
|
165
|
+
// different results depending on the local timezone of the machine.
|
|
166
|
+
const date = new Date(dtStr);
|
|
137
167
|
if (isAtprotoDate(date)) {
|
|
138
168
|
return date.toISOString();
|
|
139
169
|
}
|
package/dist/datetime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"datetime.js","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Indicates a date or string is not a valid representation of a datetime\n * according to the atproto\n * {@link https://atproto.com/specs/lexicon#datetime specification}.\n */\nexport class InvalidDatetimeError extends Error {}\n\n/**\n * A subset of {@link DatetimeString} that represent valid datetime strings with\n * the format: `YYYY-MM-DDTHH:mm:ss.sssZ`, as returned by `Date.toISOString()\n * for dates between the years 0000 and 9999.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString}\n */\nexport type ISODatetimeString =\n // @TODO Switch to branded types for more accurate type safety.\n `${string}-${string}-${string}T${string}:${string}:${string}.${string}Z`\n\n/**\n * Represents a {@link Date} that can be safely stringified into a valid atproto\n * {@link DatetimeString} using the {@link Date.toISOString toISOString()}\n * method.\n */\nexport interface AtprotoDate extends Date {\n toISOString(): ISODatetimeString\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function assertAtprotoDate(date: Date): asserts date is AtprotoDate {\n const res = parseDate(date)\n if (!res.success) {\n throw new InvalidDatetimeError(res.message)\n }\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function asAtprotoDate(date: Date): AtprotoDate {\n assertAtprotoDate(date)\n return date\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function isAtprotoDate(date: Date): date is AtprotoDate {\n return parseDate(date).success\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function ifAtprotoDate(date: Date): AtprotoDate | undefined {\n return isAtprotoDate(date) ? date : undefined\n}\n\n/**\n * Datetime strings in atproto data structures and API calls should meet the\n * {@link https://ijmacd.github.io/rfc3339-iso8601/ intersecting} requirements\n * of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards.\n *\n * @note This literal template type is not accurate enough to ensure that a\n * string is a valid atproto datetime. The {@link DatetimeString} validation\n * functions ({@link assertDatetimeString}, {@link isDatetimeString}, etc)\n * should be used to validate that a string meets the atproto datetime\n * requirements, and the {@link toDatetimeString} function should be used to\n * convert a {@link Date} object into a valid {@link DatetimeString} string.\n *\n * @example \"2024-01-15T12:30:00Z\"\n * @example \"2024-01-15T12:30:00.000Z\"\n * @example \"2024-01-15T12:30:00+00:00\"\n * @example \"2024-01-15T11:30:00-01:00\"\n * @see {@link https://atproto.com/specs/lexicon#datetime atproto Lexicon datetime format}\n * @see {@link https://www.rfc-editor.org/rfc/rfc3339 RFC 3339}\n * @see {@link https://www.iso.org/iso-8601-date-and-time-format.html ISO 8601}\n */\nexport type DatetimeString =\n // @TODO Switch to branded types for more accurate type safety?\n | `${string}-${string}-${string}T${string}:${string}:${string}Z`\n | `${string}-${string}-${string}T${string}:${string}:${string}${'+' | '-'}${string}:${string}`\n\n/**\n * Validates that a string is a valid {@link DatetimeString} format string,\n * throwing an error if it is not.\n *\n * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link DatetimeString}\n */\nexport function assertDatetimeString<I>(\n input: I,\n): asserts input is I & DatetimeString {\n const result = parseString(input)\n if (!result.success) {\n throw new InvalidDatetimeError(result.message)\n }\n}\n\n/**\n * Casts a string to a {@link DatetimeString} if it is a valid datetime format\n * string, throwing an error if it is not.\n *\n * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link DatetimeString}\n */\nexport function asDatetimeString<I>(input: I): I & DatetimeString {\n assertDatetimeString(input)\n return input\n}\n\n/**\n * Checks if a string is a valid {@link DatetimeString} format string.\n *\n * @see {@link DatetimeString}\n */\nexport function isDatetimeString<I>(input: I): input is I & DatetimeString {\n return parseString(input).success\n}\n\n/**\n * Returns the input if it is a valid {@link DatetimeString} format string, or\n * `undefined` if it is not.\n *\n * @see {@link DatetimeString}\n */\nexport function ifDatetimeString<I>(\n input: I,\n): undefined | (I & DatetimeString) {\n return isDatetimeString(input) ? input : undefined\n}\n\n/**\n * Returns the current date and time as a {@link DatetimeString}.\n *\n * @see {@link DatetimeString}\n */\nexport function currentDatetimeString(): DatetimeString {\n return toDatetimeString(new Date())\n}\n\n/**\n * Converts any {@link Date} into a {@link DatetimeString} if possible, throwing\n * an error if the date is not a valid atproto datetime.\n *\n * This is short-hand for `asAtprotoDate(date).toISOString()`.\n *\n * @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).\n * @see {@link DatetimeString}\n */\nexport function toDatetimeString(date: Date): DatetimeString {\n return asAtprotoDate(date).toISOString()\n}\n\n/**\n * Takes a flexible datetime string and normalizes its representation.\n *\n * This function will work with any valid value that can be parsed as a date. It\n * *additionally* is more flexible about accepting datetimes that are missing\n * timezone information, and normalizing them to a valid atproto datetime.\n *\n * One use-case is a consistent, sortable string. Another is to work with older\n * invalid createdAt datetimes.\n *\n * @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.\n * @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.\n */\nexport function normalizeDatetime(dtStr: string): ISODatetimeString {\n // Parse the string as is\n const date = new Date(dtStr)\n if (isAtprotoDate(date)) {\n return date.toISOString()\n }\n\n // if dtStr is not a valid date, try parsing again with a timezone\n if (isNaN(date.getTime()) && !/.*(([+-]\\d\\d:?\\d\\d)|[a-zA-Z])$/.test(dtStr)) {\n const date = new Date(`${dtStr}Z`)\n if (isAtprotoDate(date)) {\n return date.toISOString()\n }\n }\n\n throw new InvalidDatetimeError(\n 'datetime did not parse as any timestamp format',\n )\n}\n\n/**\n * Variant of {@link normalizeDatetime} which always returns a valid datetime\n * string.\n *\n * If a {@link InvalidDatetimeError} is encountered, returns the UNIX epoch time\n * as a UTC datetime (`1970-01-01T00:00:00.000Z`).\n *\n * @see {@link normalizeDatetime}\n */\nexport function normalizeDatetimeAlways(dtStr: string): ISODatetimeString {\n try {\n return normalizeDatetime(dtStr)\n } catch (err) {\n return '1970-01-01T00:00:00.000Z'\n }\n}\n\n// Legacy exports (should we deprecate these ?)\nexport {\n assertDatetimeString as ensureValidDatetime,\n isDatetimeString as isValidDatetime,\n}\n\n// -----------------------------------------------------------------------------\n// ------------------------- Internal validation logic -------------------------\n// -----------------------------------------------------------------------------\n\n// Validation utils that allow avoiding try/catch for control flow (performance\n// optimization). Other syntax formats should also use this pattern to avoid\n// try/catch in their validation logic, at which point these utils can be moved\n// to a common internal utils.\ntype FailureResult = { success: false; message: string }\nconst failure = (m: string): FailureResult => ({ success: false, message: m })\ntype SuccessResult<V> = { success: true; value: V }\nconst success = <V>(v: V): SuccessResult<V> => ({ success: true, value: v })\ntype Result<V> = FailureResult | SuccessResult<V>\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc3339#section-5.6 Internet Date/Time Format}\n *\n * @example\n * ```abnf\n * date-fullyear = 4DIGIT\n * date-month = 2DIGIT ; 01-12\n * date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n * ; month/year\n * time-hour = 2DIGIT ; 00-23\n * time-minute = 2DIGIT ; 00-59\n * time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second\n * ; rules\n * time-secfrac = \".\" 1*DIGIT\n * time-numoffset = (\"+\" / \"-\") time-hour \":\" time-minute\n * time-offset = \"Z\" / time-numoffset\n * partial-time = time-hour \":\" time-minute \":\" time-second\n * [time-secfrac]\n * full-date = date-fullyear \"-\" date-month \"-\" date-mday\n * full-time = partial-time time-offset\n * date-time = full-date \"T\" full-time\n * ```\n */\nconst DATETIME_REGEX =\n /^(?<full_year>[0-9]{4})-(?<date_month>0[1-9]|1[012])-(?<date_mday>[0-2][0-9]|3[01])T(?<time_hour>[0-1][0-9]|2[0-3]):(?<time_minute>[0-5][0-9]):(?<time_second>[0-5][0-9]|60)(?<time_secfrac>\\.[0-9]+)?(?<time_offset>Z|(?<time_numoffset>[+-](?:[0-1][0-9]|2[0-3]):[0-5][0-9]))$/\n\n/**\n * Validates that the input is a datetime string according to atproto Lexicon\n * rules, and parses it into a Date object.\n */\nfunction parseString(input: unknown): Result<AtprotoDate> {\n // @NOTE Performing cheap tests first\n if (typeof input !== 'string') {\n return failure('datetime must be a string')\n }\n if (input.length > 64) {\n return failure('datetime is too long (64 chars max)')\n }\n if (input.endsWith('-00:00')) {\n return failure('datetime can not use \"-00:00\" for UTC timezone')\n }\n if (!DATETIME_REGEX.test(input)) {\n return failure(\n \"datetime is not in a valid format (must match RFC 3339 & ISO 8601 with 'Z' or ±hh:mm timezone)\",\n )\n }\n\n // must parse as ISO 8601; this also verifies semantics like leap seconds and\n // correct number of days in month, which the regex does not check for\n const date = new Date(input)\n\n return parseDate(date)\n}\n\n/**\n * Ensures that a Date object represents a valid datetime according to atproto\n * Lexicon rules. This ensures that `date.toISOString()` will produce a valid\n * datetime string that can be used where {@link DatetimeString} is expected.\n */\nfunction parseDate(date: Date): Result<AtprotoDate> {\n const fullYear = date.getUTCFullYear()\n // Ensures that the date is valid. We could check isNaN(date.getTime()) here\n // but since we'll check the year anyway, we just use that for the validity\n // check since an invalid date will have NaN year.\n if (Number.isNaN(fullYear)) {\n return failure('datetime did not parse as ISO 8601')\n }\n // Ensure that the ISO string representation does not start with ±YYYYYY\n if (fullYear < 0) {\n return failure('datetime normalized to a negative time')\n }\n if (fullYear > 9999) {\n return failure('datetime year is too far in the future')\n }\n return success(date as AtprotoDate)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"datetime.js","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Indicates a date or string is not a valid representation of a datetime\n * according to the atproto\n * {@link https://atproto.com/specs/lexicon#datetime specification}.\n */\nexport class InvalidDatetimeError extends Error {}\n\n/**\n * A subset of {@link DatetimeString} that represent valid datetime strings with\n * the format: `YYYY-MM-DDTHH:mm:ss.sssZ`, as returned by `Date.toISOString()\n * for dates between the years 0000 and 9999.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString}\n */\nexport type ISODatetimeString =\n // @TODO Switch to branded types for more accurate type safety.\n `${string}-${string}-${string}T${string}:${string}:${string}.${string}Z`\n\n/**\n * Represents a {@link Date} that can be safely stringified into a valid atproto\n * {@link DatetimeString} using the {@link Date.toISOString toISOString()}\n * method.\n */\nexport interface AtprotoDate extends Date {\n toISOString(): ISODatetimeString\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function assertAtprotoDate(date: Date): asserts date is AtprotoDate {\n const res = parseDate(date)\n if (!res.success) {\n throw new InvalidDatetimeError(res.message)\n }\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function asAtprotoDate(date: Date): AtprotoDate {\n assertAtprotoDate(date)\n return date\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function isAtprotoDate(date: Date): date is AtprotoDate {\n return parseDate(date).success\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function ifAtprotoDate(date: Date): AtprotoDate | undefined {\n return isAtprotoDate(date) ? date : undefined\n}\n\n/**\n * Datetime strings in atproto data structures and API calls should meet the\n * {@link https://ijmacd.github.io/rfc3339-iso8601/ intersecting} requirements\n * of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards.\n *\n * @note This literal template type is not accurate enough to ensure that a\n * string is a valid atproto datetime. The {@link DatetimeString} validation\n * functions ({@link assertDatetimeString}, {@link isDatetimeString}, etc)\n * should be used to validate that a string meets the atproto datetime\n * requirements, and the {@link toDatetimeString} function should be used to\n * convert a {@link Date} object into a valid {@link DatetimeString} string.\n *\n * @example \"2024-01-15T12:30:00Z\"\n * @example \"2024-01-15T12:30:00.000Z\"\n * @example \"2024-01-15T12:30:00+00:00\"\n * @example \"2024-01-15T11:30:00-01:00\"\n * @see {@link https://atproto.com/specs/lexicon#datetime atproto Lexicon datetime format}\n * @see {@link https://www.rfc-editor.org/rfc/rfc3339 RFC 3339}\n * @see {@link https://www.iso.org/iso-8601-date-and-time-format.html ISO 8601}\n */\nexport type DatetimeString =\n // @TODO Switch to branded types for more accurate type safety?\n | `${string}-${string}-${string}T${string}:${string}:${string}Z`\n | `${string}-${string}-${string}T${string}:${string}:${string}${'+' | '-'}${string}:${string}`\n\n/**\n * Validates that a string is a valid {@link DatetimeString} format string,\n * throwing an error if it is not.\n *\n * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link DatetimeString}\n */\nexport function assertDatetimeString<I>(\n input: I,\n): asserts input is I & DatetimeString {\n const result = parseString(input)\n if (!result.success) {\n throw new InvalidDatetimeError(result.message)\n }\n}\n\n/**\n * Casts a string to a {@link DatetimeString} if it is a valid datetime format\n * string, throwing an error if it is not.\n *\n * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link DatetimeString}\n */\nexport function asDatetimeString<I>(input: I): I & DatetimeString {\n assertDatetimeString(input)\n return input\n}\n\n/**\n * Checks if a string is a valid {@link DatetimeString} format string.\n *\n * @see {@link DatetimeString}\n */\nexport function isDatetimeString<I>(input: I): input is I & DatetimeString {\n return parseString(input).success\n}\n\n/**\n * Returns the input if it is a valid {@link DatetimeString} format string, or\n * `undefined` if it is not.\n *\n * @see {@link DatetimeString}\n */\nexport function ifDatetimeString<I>(\n input: I,\n): undefined | (I & DatetimeString) {\n return isDatetimeString(input) ? input : undefined\n}\n\n/**\n * Returns the current date and time as a {@link DatetimeString}.\n *\n * @see {@link DatetimeString}\n */\nexport function currentDatetimeString(): DatetimeString {\n return toDatetimeString(new Date())\n}\n\n/**\n * Converts any {@link Date} into a {@link DatetimeString} if possible, throwing\n * an error if the date is not a valid atproto datetime.\n *\n * This is short-hand for `asAtprotoDate(date).toISOString()`.\n *\n * @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).\n * @see {@link DatetimeString}\n */\nexport function toDatetimeString(date: Date): DatetimeString {\n return asAtprotoDate(date).toISOString()\n}\n\n/**\n * Takes a flexible datetime string and normalizes its representation.\n *\n * This function will work with any valid value that can be parsed as a date. It\n * *additionally* is more flexible about accepting datetimes that are missing\n * timezone information, and normalizing them to a valid atproto datetime.\n *\n * One use-case is a consistent, sortable string. Another is to work with older\n * invalid createdAt datetimes.\n *\n * @note This function might return different normalized strings for the same\n * input depending on the timezone of the machine it is run on, since it will\n * attempt to parse the input \"as is\" if it fails to parse with an explicit\n * timezone.\n *\n * @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.\n * @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.\n */\nexport function normalizeDatetime(dtStr: string): ISODatetimeString {\n if (\n // Explicit timezone offset\n /[+-]\\d\\d:?\\d\\d/.test(dtStr) ||\n // 'Z' timezone designator\n /\\dZ\\b/.test(dtStr) ||\n // Timezone abbreviation (eg. \"EST\", \"PST\", \"UTC\", \"GMT\", etc), as in:\n // > Tue Mar 17 2026 16:38:44 PST (Pacific Standard Time)\n /\\b[A-Z]{3,4}\\b/.test(dtStr)\n ) {\n // Since we do have a timezone designator, we can try parsing \"as is\" and\n // should get consistent results regardless of local timezone.\n\n // @NOTE NodeJS will reject dates with an un-recognized timezone designator\n // (like \"AFT\"), even if we add a well-known timezone abbreviation like\n // \"UTC\" or \"Z\".\n const date = new Date(dtStr)\n if (isAtprotoDate(date)) {\n return date.toISOString()\n }\n } else {\n // If there is no timezone information, try parsing as UTC using two\n // different syntaxes, falling back to parsing \"as is\".\n\n const dateZ = new Date(`${dtStr}Z`)\n if (isAtprotoDate(dateZ)) {\n return dateZ.toISOString()\n }\n\n const dateUTC = new Date(`${dtStr} UTC`)\n if (isAtprotoDate(dateUTC)) {\n return dateUTC.toISOString()\n }\n\n // Despite our best efforts to parse as a consistent value, appending \"Z\" or\n // \" UTC\" did not work, so we will try parsing \"as is\", which may yield\n // different results depending on the local timezone of the machine.\n const date = new Date(dtStr)\n if (isAtprotoDate(date)) {\n return date.toISOString()\n }\n }\n\n throw new InvalidDatetimeError(\n 'datetime did not parse as any timestamp format',\n )\n}\n\n/**\n * Variant of {@link normalizeDatetime} which always returns a valid datetime\n * string.\n *\n * If a {@link InvalidDatetimeError} is encountered, returns the UNIX epoch time\n * as a UTC datetime (`1970-01-01T00:00:00.000Z`).\n *\n * @see {@link normalizeDatetime}\n */\nexport function normalizeDatetimeAlways(dtStr: string): ISODatetimeString {\n try {\n return normalizeDatetime(dtStr)\n } catch (err) {\n return '1970-01-01T00:00:00.000Z'\n }\n}\n\n// Legacy exports (should we deprecate these ?)\nexport {\n assertDatetimeString as ensureValidDatetime,\n isDatetimeString as isValidDatetime,\n}\n\n// -----------------------------------------------------------------------------\n// ------------------------- Internal validation logic -------------------------\n// -----------------------------------------------------------------------------\n\n// Validation utils that allow avoiding try/catch for control flow (performance\n// optimization). Other syntax formats should also use this pattern to avoid\n// try/catch in their validation logic, at which point these utils can be moved\n// to a common internal utils.\ntype FailureResult = { success: false; message: string }\nconst failure = (m: string): FailureResult => ({ success: false, message: m })\ntype SuccessResult<V> = { success: true; value: V }\nconst success = <V>(v: V): SuccessResult<V> => ({ success: true, value: v })\ntype Result<V> = FailureResult | SuccessResult<V>\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc3339#section-5.6 Internet Date/Time Format}\n *\n * @example\n * ```abnf\n * date-fullyear = 4DIGIT\n * date-month = 2DIGIT ; 01-12\n * date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n * ; month/year\n * time-hour = 2DIGIT ; 00-23\n * time-minute = 2DIGIT ; 00-59\n * time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second\n * ; rules\n * time-secfrac = \".\" 1*DIGIT\n * time-numoffset = (\"+\" / \"-\") time-hour \":\" time-minute\n * time-offset = \"Z\" / time-numoffset\n * partial-time = time-hour \":\" time-minute \":\" time-second\n * [time-secfrac]\n * full-date = date-fullyear \"-\" date-month \"-\" date-mday\n * full-time = partial-time time-offset\n * date-time = full-date \"T\" full-time\n * ```\n */\nconst DATETIME_REGEX =\n /^(?<full_year>[0-9]{4})-(?<date_month>0[1-9]|1[012])-(?<date_mday>[0-2][0-9]|3[01])T(?<time_hour>[0-1][0-9]|2[0-3]):(?<time_minute>[0-5][0-9]):(?<time_second>[0-5][0-9]|60)(?<time_secfrac>\\.[0-9]+)?(?<time_offset>Z|(?<time_numoffset>[+-](?:[0-1][0-9]|2[0-3]):[0-5][0-9]))$/\n\n/**\n * Validates that the input is a datetime string according to atproto Lexicon\n * rules, and parses it into a Date object.\n */\nfunction parseString(input: unknown): Result<AtprotoDate> {\n // @NOTE Performing cheap tests first\n if (typeof input !== 'string') {\n return failure('datetime must be a string')\n }\n if (input.length > 64) {\n return failure('datetime is too long (64 chars max)')\n }\n if (input.endsWith('-00:00')) {\n return failure('datetime can not use \"-00:00\" for UTC timezone')\n }\n if (!DATETIME_REGEX.test(input)) {\n return failure(\n \"datetime is not in a valid format (must match RFC 3339 & ISO 8601 with 'Z' or ±hh:mm timezone)\",\n )\n }\n\n // must parse as ISO 8601; this also verifies semantics like leap seconds and\n // correct number of days in month, which the regex does not check for\n const date = new Date(input)\n\n return parseDate(date)\n}\n\n/**\n * Ensures that a Date object represents a valid datetime according to atproto\n * Lexicon rules. This ensures that `date.toISOString()` will produce a valid\n * datetime string that can be used where {@link DatetimeString} is expected.\n */\nfunction parseDate(date: Date): Result<AtprotoDate> {\n const fullYear = date.getUTCFullYear()\n // Ensures that the date is valid. We could check isNaN(date.getTime()) here\n // but since we'll check the year anyway, we just use that for the validity\n // check since an invalid date will have NaN year.\n if (Number.isNaN(fullYear)) {\n return failure('datetime did not parse as ISO 8601')\n }\n // Ensure that the ISO string representation does not start with ±YYYYYY\n if (fullYear < 0) {\n return failure('datetime normalized to a negative time')\n }\n if (fullYear > 9999) {\n return failure('datetime year is too far in the future')\n }\n return success(date as AtprotoDate)\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Result<T> = Success<T> | Failure;
|
|
2
|
+
export type Success<T> = {
|
|
3
|
+
success: true;
|
|
4
|
+
value: T;
|
|
5
|
+
};
|
|
6
|
+
export declare function success<T>(value: T): Success<T>;
|
|
7
|
+
export type Failure = {
|
|
8
|
+
success: false;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function failure(message: string): Failure;
|
|
12
|
+
//# sourceMappingURL=result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/lib/result.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAA;AAE5C,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAA;AACpD,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAE/C;AAED,MAAM,MAAM,OAAO,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AACzD,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.success = success;
|
|
4
|
+
exports.failure = failure;
|
|
5
|
+
function success(value) {
|
|
6
|
+
return { success: true, value };
|
|
7
|
+
}
|
|
8
|
+
function failure(message) {
|
|
9
|
+
return { success: false, message };
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/lib/result.ts"],"names":[],"mappings":";;AAGA,0BAEC;AAGD,0BAEC;AAPD,SAAgB,OAAO,CAAI,KAAQ;IACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACjC,CAAC;AAGD,SAAgB,OAAO,CAAC,OAAe;IACrC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AACpC,CAAC","sourcesContent":["export type Result<T> = Success<T> | Failure\n\nexport type Success<T> = { success: true; value: T }\nexport function success<T>(value: T): Success<T> {\n return { success: true, value }\n}\n\nexport type Failure = { success: false; message: string }\nexport function failure(message: string): Failure {\n return { success: false, message }\n}\n"]}
|
package/dist/nsid.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Result } from './lib/result.js';
|
|
1
2
|
export type NsidString = `${string}.${string}.${string}`;
|
|
2
3
|
export declare class NSID {
|
|
3
4
|
readonly segments: readonly string[];
|
|
@@ -15,14 +16,7 @@ export declare class NSID {
|
|
|
15
16
|
export declare function ensureValidNsid<I extends string>(input: I): asserts input is I & NsidString;
|
|
16
17
|
export declare function parseNsid(nsid: string): string[];
|
|
17
18
|
export declare function isValidNsid<I extends string>(input: I): input is I & NsidString;
|
|
18
|
-
|
|
19
|
-
success: true;
|
|
20
|
-
value: T;
|
|
21
|
-
} | {
|
|
22
|
-
success: false;
|
|
23
|
-
message: string;
|
|
24
|
-
};
|
|
25
|
-
export declare function validateNsid(input: string): ValidateResult<string[]>;
|
|
19
|
+
export declare function validateNsid(input: string): Result<string[]>;
|
|
26
20
|
/**
|
|
27
21
|
* @deprecated Use {@link ensureValidNsid} if you care about error details,
|
|
28
22
|
* {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or
|
|
@@ -33,8 +27,7 @@ export declare function ensureValidNsidRegex(nsid: string): asserts nsid is Nsid
|
|
|
33
27
|
* Regexp based validation that behaves identically to the previous code but
|
|
34
28
|
* provides less detailed error messages (while being 20% to 50% faster).
|
|
35
29
|
*/
|
|
36
|
-
export declare function validateNsidRegex(value: string):
|
|
30
|
+
export declare function validateNsidRegex(value: string): Result<NsidString>;
|
|
37
31
|
export declare class InvalidNsidError extends Error {
|
|
38
32
|
}
|
|
39
|
-
export {};
|
|
40
33
|
//# sourceMappingURL=nsid.d.ts.map
|
package/dist/nsid.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nsid.d.ts","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nsid.d.ts","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,MAAM,iBAAiB,CAAA;AAe1D,MAAM,MAAM,UAAU,GAAG,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,EAAE,CAAA;AAExD,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,IAIK,GAAG,MAAM,IAAI,MAAM,EAAE,CACtC;IAED,IAAI,IAAI,uBAEP;IAED,QAAQ,IAAI,UAAU;CAGvB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC9C,KAAK,EAAE,CAAC,GACP,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,UAAU,CAGjC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAIhD;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC1C,KAAK,EAAE,CAAC,GACP,KAAK,IAAI,CAAC,GAAG,UAAU,CAIzB;AAKD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAiC5D;AA4BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,IAAI,UAAU,CAG7E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,CAgBnE;AAED,qBAAa,gBAAiB,SAAQ,KAAK;CAAG"}
|
package/dist/nsid.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
Grammar:
|
|
4
|
-
|
|
5
|
-
alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z"
|
|
6
|
-
number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0"
|
|
7
|
-
delim = "."
|
|
8
|
-
segment = alpha *( alpha / number / "-" )
|
|
9
|
-
authority = segment *( delim segment )
|
|
10
|
-
name = alpha *( alpha / number )
|
|
11
|
-
nsid = authority delim name
|
|
12
|
-
|
|
13
|
-
*/
|
|
14
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
3
|
exports.InvalidNsidError = exports.NSID = void 0;
|
|
16
4
|
exports.ensureValidNsid = ensureValidNsid;
|
|
@@ -19,6 +7,7 @@ exports.isValidNsid = isValidNsid;
|
|
|
19
7
|
exports.validateNsid = validateNsid;
|
|
20
8
|
exports.ensureValidNsidRegex = ensureValidNsidRegex;
|
|
21
9
|
exports.validateNsidRegex = validateNsidRegex;
|
|
10
|
+
const result_js_1 = require("./lib/result.js");
|
|
22
11
|
class NSID {
|
|
23
12
|
segments;
|
|
24
13
|
static parse(input) {
|
|
@@ -79,60 +68,33 @@ function isValidNsid(input) {
|
|
|
79
68
|
// - followed by an additional period-separated name, which is camel-case letters
|
|
80
69
|
function validateNsid(input) {
|
|
81
70
|
if (input.length > 253 + 1 + 63) {
|
|
82
|
-
return
|
|
83
|
-
success: false,
|
|
84
|
-
message: 'NSID is too long (317 chars max)',
|
|
85
|
-
};
|
|
71
|
+
return (0, result_js_1.failure)('NSID is too long (317 chars max)');
|
|
86
72
|
}
|
|
87
73
|
if (hasDisallowedCharacters(input)) {
|
|
88
|
-
return
|
|
89
|
-
success: false,
|
|
90
|
-
message: 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
|
|
91
|
-
};
|
|
74
|
+
return (0, result_js_1.failure)('Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)');
|
|
92
75
|
}
|
|
93
76
|
const segments = input.split('.');
|
|
94
77
|
if (segments.length < 3) {
|
|
95
|
-
return
|
|
96
|
-
success: false,
|
|
97
|
-
message: 'NSID needs at least three parts',
|
|
98
|
-
};
|
|
78
|
+
return (0, result_js_1.failure)('NSID needs at least three parts');
|
|
99
79
|
}
|
|
100
80
|
for (const l of segments) {
|
|
101
81
|
if (l.length < 1) {
|
|
102
|
-
return
|
|
103
|
-
success: false,
|
|
104
|
-
message: 'NSID parts can not be empty',
|
|
105
|
-
};
|
|
82
|
+
return (0, result_js_1.failure)('NSID parts can not be empty');
|
|
106
83
|
}
|
|
107
84
|
if (l.length > 63) {
|
|
108
|
-
return
|
|
109
|
-
success: false,
|
|
110
|
-
message: 'NSID part too long (max 63 chars)',
|
|
111
|
-
};
|
|
85
|
+
return (0, result_js_1.failure)('NSID part too long (max 63 chars)');
|
|
112
86
|
}
|
|
113
87
|
if (startsWithHyphen(l) || endsWithHyphen(l)) {
|
|
114
|
-
return
|
|
115
|
-
success: false,
|
|
116
|
-
message: 'NSID parts can not start or end with hyphen',
|
|
117
|
-
};
|
|
88
|
+
return (0, result_js_1.failure)('NSID parts can not start or end with hyphen');
|
|
118
89
|
}
|
|
119
90
|
}
|
|
120
91
|
if (startsWithNumber(segments[0])) {
|
|
121
|
-
return
|
|
122
|
-
success: false,
|
|
123
|
-
message: 'NSID first part may not start with a digit',
|
|
124
|
-
};
|
|
92
|
+
return (0, result_js_1.failure)('NSID first part may not start with a digit');
|
|
125
93
|
}
|
|
126
94
|
if (!isValidIdentifier(segments[segments.length - 1])) {
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
success: true,
|
|
134
|
-
value: segments,
|
|
135
|
-
};
|
|
95
|
+
return (0, result_js_1.failure)('NSID name part must be only letters and digits (and no leading digit)');
|
|
96
|
+
}
|
|
97
|
+
return (0, result_js_1.success)(segments);
|
|
136
98
|
}
|
|
137
99
|
function hasDisallowedCharacters(v) {
|
|
138
100
|
return !/^[a-zA-Z0-9.-]*$/.test(v);
|
|
@@ -170,24 +132,15 @@ function ensureValidNsidRegex(nsid) {
|
|
|
170
132
|
*/
|
|
171
133
|
function validateNsidRegex(value) {
|
|
172
134
|
if (value.length > 253 + 1 + 63) {
|
|
173
|
-
return
|
|
174
|
-
success: false,
|
|
175
|
-
message: 'NSID is too long (317 chars max)',
|
|
176
|
-
};
|
|
135
|
+
return (0, result_js_1.failure)('NSID is too long (317 chars max)');
|
|
177
136
|
}
|
|
178
137
|
if (
|
|
179
138
|
// Fast check for small values
|
|
180
139
|
value.length < 5 ||
|
|
181
140
|
!/^[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)) {
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
success: true,
|
|
189
|
-
value: value,
|
|
190
|
-
};
|
|
141
|
+
return (0, result_js_1.failure)("NSID didn't validate via regex");
|
|
142
|
+
}
|
|
143
|
+
return (0, result_js_1.success)(value);
|
|
191
144
|
}
|
|
192
145
|
class InvalidNsidError extends Error {
|
|
193
146
|
}
|
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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","sourcesContent":["/*\nGrammar:\n\nalpha = \"a\" / \"b\" / \"c\" / \"d\" / \"e\" / \"f\" / \"g\" / \"h\" / \"i\" / \"j\" / \"k\" / \"l\" / \"m\" / \"n\" / \"o\" / \"p\" / \"q\" / \"r\" / \"s\" / \"t\" / \"u\" / \"v\" / \"w\" / \"x\" / \"y\" / \"z\" / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\" / \"G\" / \"H\" / \"I\" / \"J\" / \"K\" / \"L\" / \"M\" / \"N\" / \"O\" / \"P\" / \"Q\" / \"R\" / \"S\" / \"T\" / \"U\" / \"V\" / \"W\" / \"X\" / \"Y\" / \"Z\"\nnumber = \"1\" / \"2\" / \"3\" / \"4\" / \"5\" / \"6\" / \"7\" / \"8\" / \"9\" / \"0\"\ndelim = \".\"\nsegment = alpha *( alpha / number / \"-\" )\nauthority = segment *( delim segment )\nname = alpha *( alpha / number )\nnsid = authority delim name\n\n*/\n\nexport type NsidString = `${string}.${string}.${string}`\n\nexport class NSID {\n readonly segments: readonly string[]\n\n static parse(input: string): NSID {\n return new NSID(input)\n }\n\n static create(authority: string, name: string): NSID {\n const input = [...authority.split('.').reverse(), name].join('.')\n return new NSID(input)\n }\n\n static isValid(nsid: string) {\n return isValidNsid(nsid)\n }\n\n static from(input: { toString: () => string }): NSID {\n if (input instanceof NSID) {\n // No need to clone, NSID is immutable\n return input\n }\n if (Array.isArray(input)) {\n return new NSID((input as string[]).join('.'))\n }\n return new NSID(String(input))\n }\n\n constructor(nsid: string) {\n this.segments = parseNsid(nsid)\n }\n\n get authority() {\n return this.segments\n .slice(0, this.segments.length - 1)\n .reverse()\n .join('.') as `${string}.${string}`\n }\n\n get name() {\n return this.segments.at(this.segments.length - 1)\n }\n\n toString(): NsidString {\n return this.segments.join('.') as NsidString\n }\n}\n\nexport function ensureValidNsid<I extends string>(\n input: I,\n): asserts input is I & NsidString {\n const result = validateNsid(input)\n if (!result.success) throw new InvalidNsidError(result.message)\n}\n\nexport function parseNsid(nsid: string): string[] {\n const result = validateNsid(nsid)\n if (!result.success) throw new InvalidNsidError(result.message)\n return result.value\n}\n\nexport function isValidNsid<I extends string>(\n input: I,\n): input is I & NsidString {\n // Since the regex version is more performant for valid NSIDs, we use it when\n // we don't care about error details.\n return validateNsidRegex(input).success\n}\n\ntype ValidateResult<T> =\n | { success: true; value: T }\n | { success: false; message: string }\n\n// Human readable constraints on NSID:\n// - a valid domain in reversed notation\n// - followed by an additional period-separated name, which is camel-case letters\nexport function validateNsid(input: string): ValidateResult<string[]> {\n if (input.length > 253 + 1 + 63) {\n return {\n success: false,\n message: 'NSID is too long (317 chars max)',\n }\n }\n if (hasDisallowedCharacters(input)) {\n return {\n success: false,\n message:\n 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',\n }\n }\n const segments = input.split('.')\n if (segments.length < 3) {\n return {\n success: false,\n message: 'NSID needs at least three parts',\n }\n }\n for (const l of segments) {\n if (l.length < 1) {\n return {\n success: false,\n message: 'NSID parts can not be empty',\n }\n }\n if (l.length > 63) {\n return {\n success: false,\n message: 'NSID part too long (max 63 chars)',\n }\n }\n if (startsWithHyphen(l) || endsWithHyphen(l)) {\n return {\n success: false,\n message: 'NSID parts can not start or end with hyphen',\n }\n }\n }\n if (startsWithNumber(segments[0])) {\n return {\n success: false,\n message: 'NSID first part may not start with a digit',\n }\n }\n if (!isValidIdentifier(segments[segments.length - 1])) {\n return {\n success: false,\n message:\n 'NSID name part must be only letters and digits (and no leading digit)',\n }\n }\n return {\n success: true,\n value: segments,\n }\n}\n\nfunction hasDisallowedCharacters(v: string) {\n return !/^[a-zA-Z0-9.-]*$/.test(v)\n}\n\nfunction startsWithNumber(v: string) {\n const charCode = v.charCodeAt(0)\n return charCode >= 48 && charCode <= 57\n}\n\nfunction startsWithHyphen(v: string) {\n return v.charCodeAt(0) === 45 /* - */\n}\n\nfunction endsWithHyphen(v: string) {\n return v.charCodeAt(v.length - 1) === 45 /* - */\n}\n\nfunction isValidIdentifier(v: string) {\n // Note, since we already know that \"v\" only contains [a-zA-Z0-9-], we can\n // simplify the following regex by checking only the first char and presence\n // of \"-\".\n\n // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v)\n return !startsWithNumber(v) && !v.includes('-')\n}\n\n/**\n * @deprecated Use {@link ensureValidNsid} if you care about error details,\n * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or\n * {@link isValidNsid} if you just want a boolean.\n */\nexport function ensureValidNsidRegex(nsid: string): asserts nsid is NsidString {\n const result = validateNsidRegex(nsid)\n if (!result.success) throw new InvalidNsidError(result.message)\n}\n\n/**\n * Regexp based validation that behaves identically to the previous code but\n * provides less detailed error messages (while being 20% to 50% faster).\n */\nexport function validateNsidRegex(value: string): ValidateResult<NsidString> {\n if (value.length > 253 + 1 + 63) {\n return {\n success: false,\n message: 'NSID is too long (317 chars max)',\n }\n }\n\n if (\n // Fast check for small values\n value.length < 5 ||\n !/^[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(\n value,\n )\n ) {\n return {\n success: false,\n message: \"NSID didn't validate via regex\",\n }\n }\n\n return {\n success: true,\n value: value as NsidString,\n }\n}\n\nexport class InvalidNsidError extends Error {}\n"]}
|
|
1
|
+
{"version":3,"file":"nsid.js","sourceRoot":"","sources":["../src/nsid.ts"],"names":[],"mappings":"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","sourcesContent":["import { Result, failure, success } from './lib/result.js'\n\n/*\nGrammar:\n\nalpha = \"a\" / \"b\" / \"c\" / \"d\" / \"e\" / \"f\" / \"g\" / \"h\" / \"i\" / \"j\" / \"k\" / \"l\" / \"m\" / \"n\" / \"o\" / \"p\" / \"q\" / \"r\" / \"s\" / \"t\" / \"u\" / \"v\" / \"w\" / \"x\" / \"y\" / \"z\" / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\" / \"G\" / \"H\" / \"I\" / \"J\" / \"K\" / \"L\" / \"M\" / \"N\" / \"O\" / \"P\" / \"Q\" / \"R\" / \"S\" / \"T\" / \"U\" / \"V\" / \"W\" / \"X\" / \"Y\" / \"Z\"\nnumber = \"1\" / \"2\" / \"3\" / \"4\" / \"5\" / \"6\" / \"7\" / \"8\" / \"9\" / \"0\"\ndelim = \".\"\nsegment = alpha *( alpha / number / \"-\" )\nauthority = segment *( delim segment )\nname = alpha *( alpha / number )\nnsid = authority delim name\n\n*/\n\nexport type NsidString = `${string}.${string}.${string}`\n\nexport class NSID {\n readonly segments: readonly string[]\n\n static parse(input: string): NSID {\n return new NSID(input)\n }\n\n static create(authority: string, name: string): NSID {\n const input = [...authority.split('.').reverse(), name].join('.')\n return new NSID(input)\n }\n\n static isValid(nsid: string) {\n return isValidNsid(nsid)\n }\n\n static from(input: { toString: () => string }): NSID {\n if (input instanceof NSID) {\n // No need to clone, NSID is immutable\n return input\n }\n if (Array.isArray(input)) {\n return new NSID((input as string[]).join('.'))\n }\n return new NSID(String(input))\n }\n\n constructor(nsid: string) {\n this.segments = parseNsid(nsid)\n }\n\n get authority() {\n return this.segments\n .slice(0, this.segments.length - 1)\n .reverse()\n .join('.') as `${string}.${string}`\n }\n\n get name() {\n return this.segments.at(this.segments.length - 1)\n }\n\n toString(): NsidString {\n return this.segments.join('.') as NsidString\n }\n}\n\nexport function ensureValidNsid<I extends string>(\n input: I,\n): asserts input is I & NsidString {\n const result = validateNsid(input)\n if (!result.success) throw new InvalidNsidError(result.message)\n}\n\nexport function parseNsid(nsid: string): string[] {\n const result = validateNsid(nsid)\n if (!result.success) throw new InvalidNsidError(result.message)\n return result.value\n}\n\nexport function isValidNsid<I extends string>(\n input: I,\n): input is I & NsidString {\n // Since the regex version is more performant for valid NSIDs, we use it when\n // we don't care about error details.\n return validateNsidRegex(input).success\n}\n\n// Human readable constraints on NSID:\n// - a valid domain in reversed notation\n// - followed by an additional period-separated name, which is camel-case letters\nexport function validateNsid(input: string): Result<string[]> {\n if (input.length > 253 + 1 + 63) {\n return failure('NSID is too long (317 chars max)')\n }\n if (hasDisallowedCharacters(input)) {\n return failure(\n 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',\n )\n }\n const segments = input.split('.')\n if (segments.length < 3) {\n return failure('NSID needs at least three parts')\n }\n for (const l of segments) {\n if (l.length < 1) {\n return failure('NSID parts can not be empty')\n }\n if (l.length > 63) {\n return failure('NSID part too long (max 63 chars)')\n }\n if (startsWithHyphen(l) || endsWithHyphen(l)) {\n return failure('NSID parts can not start or end with hyphen')\n }\n }\n if (startsWithNumber(segments[0])) {\n return failure('NSID first part may not start with a digit')\n }\n if (!isValidIdentifier(segments[segments.length - 1])) {\n return failure(\n 'NSID name part must be only letters and digits (and no leading digit)',\n )\n }\n return success(segments)\n}\n\nfunction hasDisallowedCharacters(v: string) {\n return !/^[a-zA-Z0-9.-]*$/.test(v)\n}\n\nfunction startsWithNumber(v: string) {\n const charCode = v.charCodeAt(0)\n return charCode >= 48 && charCode <= 57\n}\n\nfunction startsWithHyphen(v: string) {\n return v.charCodeAt(0) === 45 /* - */\n}\n\nfunction endsWithHyphen(v: string) {\n return v.charCodeAt(v.length - 1) === 45 /* - */\n}\n\nfunction isValidIdentifier(v: string) {\n // Note, since we already know that \"v\" only contains [a-zA-Z0-9-], we can\n // simplify the following regex by checking only the first char and presence\n // of \"-\".\n\n // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v)\n return !startsWithNumber(v) && !v.includes('-')\n}\n\n/**\n * @deprecated Use {@link ensureValidNsid} if you care about error details,\n * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or\n * {@link isValidNsid} if you just want a boolean.\n */\nexport function ensureValidNsidRegex(nsid: string): asserts nsid is NsidString {\n const result = validateNsidRegex(nsid)\n if (!result.success) throw new InvalidNsidError(result.message)\n}\n\n/**\n * Regexp based validation that behaves identically to the previous code but\n * provides less detailed error messages (while being 20% to 50% faster).\n */\nexport function validateNsidRegex(value: string): Result<NsidString> {\n if (value.length > 253 + 1 + 63) {\n return failure('NSID is too long (317 chars max)')\n }\n\n if (\n // Fast check for small values\n value.length < 5 ||\n !/^[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(\n value,\n )\n ) {\n return failure(\"NSID didn't validate via regex\")\n }\n\n return success(value as NsidString)\n}\n\nexport class InvalidNsidError extends Error {}\n"]}
|
package/package.json
CHANGED
package/src/aturi.ts
CHANGED
|
@@ -133,19 +133,25 @@ export class AtUri {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
toString(): AtUriString {
|
|
136
|
-
let
|
|
137
|
-
if (!
|
|
138
|
-
|
|
136
|
+
let pathname = this.pathname
|
|
137
|
+
if (pathname && !pathname.startsWith('/')) {
|
|
138
|
+
pathname = `/${pathname}`
|
|
139
|
+
}
|
|
140
|
+
while (pathname.endsWith('/')) {
|
|
141
|
+
pathname = pathname.slice(0, -1)
|
|
139
142
|
}
|
|
140
143
|
let qs = ''
|
|
141
144
|
if (this.searchParams.size) {
|
|
142
145
|
qs = `?${this.searchParams.toString()}`
|
|
143
146
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
// @NOTE We keep the hash as-is, even if it doesn't start with a '/'.
|
|
148
|
+
let fragment = this.hash
|
|
149
|
+
if (fragment === '#') {
|
|
150
|
+
fragment = ''
|
|
151
|
+
} else if (fragment && !fragment.startsWith('#')) {
|
|
152
|
+
fragment = `#${fragment}`
|
|
147
153
|
}
|
|
148
|
-
return `at://${this.host}${
|
|
154
|
+
return `at://${this.host}${pathname}${qs}${fragment}` as AtUriString
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|