@atproto/lex 0.0.22 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @atproto/lex
2
2
 
3
+ ## 0.0.24
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4828](https://github.com/bluesky-social/atproto/pull/4828) [`c62651d`](https://github.com/bluesky-social/atproto/commit/c62651dd69f1e18bd854b66e499b91fee9eaa856) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Accept legacy blob references in non-strict mode. Legacy blob references (objects with `cid` and `mimeType` properties) are now accepted when `strict: false`, which is the default behavior when `strictResponseProcessing` is disabled on the Client.
8
+
9
+ BREAKING: The `allowLegacy` option has been removed from the blob schema builder, and legacy blobs are now handled automatically based on the strictness mode: in strict mode they are rejected, and in non-strict mode they are accepted. Consumers should stop passing `allowLegacy` and rely on strictness configuration instead. Likewise, CLI consumers should stop using the removed `--allowLegacyBlobs` flag and use the default strict/non-strict behavior.
10
+
11
+ - Updated dependencies [[`c62651d`](https://github.com/bluesky-social/atproto/commit/c62651dd69f1e18bd854b66e499b91fee9eaa856), [`f6f100c`](https://github.com/bluesky-social/atproto/commit/f6f100c33700a7ff58a1458109cc7420131feed0), [`b6b231f`](https://github.com/bluesky-social/atproto/commit/b6b231f9c05cf90239d4a29aa0ae2592ea5ce928), [`c62651d`](https://github.com/bluesky-social/atproto/commit/c62651dd69f1e18bd854b66e499b91fee9eaa856), [`c62651d`](https://github.com/bluesky-social/atproto/commit/c62651dd69f1e18bd854b66e499b91fee9eaa856), [`f6f100c`](https://github.com/bluesky-social/atproto/commit/f6f100c33700a7ff58a1458109cc7420131feed0), [`f6f100c`](https://github.com/bluesky-social/atproto/commit/f6f100c33700a7ff58a1458109cc7420131feed0)]:
12
+ - @atproto/lex-data@0.0.15
13
+ - @atproto/lex-client@0.0.19
14
+ - @atproto/lex-schema@0.0.18
15
+ - @atproto/lex-json@0.0.15
16
+ - @atproto/lex-installer@0.0.24
17
+ - @atproto/lex-builder@0.0.21
18
+
19
+ ## 0.0.23
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [[`527f5d4`](https://github.com/bluesky-social/atproto/commit/527f5d4c5d0c9264c2ff6f23ad06a41163fc6809), [`527f5d4`](https://github.com/bluesky-social/atproto/commit/527f5d4c5d0c9264c2ff6f23ad06a41163fc6809), [`c4df84c`](https://github.com/bluesky-social/atproto/commit/c4df84cd78df68ee8cb7289e7b61b3a032ad484e), [`527f5d4`](https://github.com/bluesky-social/atproto/commit/527f5d4c5d0c9264c2ff6f23ad06a41163fc6809), [`a99dd58`](https://github.com/bluesky-social/atproto/commit/a99dd58b5fe1995e571cf5e7b0105355583efa93), [`e5e5bcf`](https://github.com/bluesky-social/atproto/commit/e5e5bcf85fbc0d418f05724d684e7265be6a0be9), [`ac6bd18`](https://github.com/bluesky-social/atproto/commit/ac6bd18f1dc3397dd29008eff2a1e40702a4e138), [`c5c6c7d`](https://github.com/bluesky-social/atproto/commit/c5c6c7dac3b08e5f63cc918f57705573028ad797)]:
24
+ - @atproto/lex-schema@0.0.17
25
+ - @atproto/lex-client@0.0.18
26
+ - @atproto/lex-builder@0.0.20
27
+ - @atproto/lex-installer@0.0.23
28
+
3
29
  ## 0.0.22
4
30
 
5
31
  ### Patch Changes
package/README.md CHANGED
@@ -10,7 +10,7 @@ Type-safe Lexicon tooling for AT Protocol data.
10
10
  - Tree-shaking and composition friendly
11
11
 
12
12
  ```typescript
13
- // Build data with generated builders and validators
13
+ // Build and validate data with generated utilities
14
14
 
15
15
  const newPost = app.bsky.feed.post.$build({
16
16
  text: 'Hello, world!',
@@ -194,7 +194,6 @@ Options:
194
194
  - `--exclude <patterns...>` - List of strings or regex patterns to exclude lexicon documents by their IDs
195
195
  - `--include <patterns...>` - List of strings or regex patterns to include lexicon documents by their IDs
196
196
  - `--lib <package>` - Package name of the library to import the lex schema utility "l" from (default: `@atproto/lex`)
197
- - `--allowLegacyBlobs` - Allow generating schemas that accept legacy blob references (disabled by default; enable this if you encounter issues while processing records created a long time ago)
198
197
  - `--importExt <ext>` - File extension to use for import statements in generated files (default: `.js`). Use `--importExt ""` to generate extension-less imports
199
198
  - `--fileExt <ext>` - File extension to use for generated files (default: `.ts`)
200
199
  - `--indexFile` - Generate an "index" file that re-exports all root-level namespaces (disabled by default)
@@ -222,7 +221,7 @@ function renderPost(p: app.bsky.feed.post.Main) {
222
221
 
223
222
  ### Building data
224
223
 
225
- It is recommended to use the generated builders to create data that conforms to the schema. This ensures that all required fields are present.
224
+ It is recommended to use the generated builders to create data that conforms to the schema. TypeScript ensures that all required fields are present at compile time.
226
225
 
227
226
  ```typescript
228
227
  import { l } from '@atproto/lex'
@@ -234,6 +233,10 @@ const post = app.bsky.feed.post.$build({
234
233
  text: 'Hello, world!',
235
234
  createdAt: l.toDatetimeString(new Date()),
236
235
  })
236
+
237
+ // For runtime validation, use $parse()/$validate() instead
238
+ const postWithDefaults = app.bsky.feed.post.$parse(post)
239
+ app.bsky.feed.post.$validate(post)
237
240
  ```
238
241
 
239
242
  ### Validation Helpers
@@ -347,7 +350,7 @@ if (result.success) {
347
350
  }
348
351
  ```
349
352
 
350
- All schema methods that perform validation (`$parse`, `$safeParse`, `$validate`, `$safeValidate`) accept an optional `{ strict }` option. When `strict` is `false`, validation becomes more lenient: datetime string format checks are relaxed (e.g. datetimes without timezones are accepted; other string formats remain strict), blob MIME type and size constraints are not enforced, and non-raw CIDs are allowed in blob references. This is primarily used internally by the XRPC client when `strictResponseProcessing` is disabled, but can also be used directly:
353
+ All schema methods that perform validation (`$parse`, `$safeParse`, `$validate`, `$safeValidate`) accept an optional `{ strict }` option. When `strict` is `false`, validation becomes more lenient: datetime string format checks are relaxed (e.g. datetimes without timezones are accepted; other string formats remain strict), blob MIME type and size constraints are not enforced, non-raw CIDs are allowed in blob references, and legacy blob reference format (objects with `cid` and `mimeType` properties) is accepted. This is primarily used internally by the XRPC client when `strictResponseProcessing` is disabled, but can also be used directly:
351
354
 
352
355
  ```typescript
353
356
  // Strict mode (default) - rejects datetime without timezone
@@ -359,7 +362,7 @@ app.bsky.feed.post.$safeParse(data, { strict: false })
359
362
 
360
363
  #### `$build(data)` - Build with Defaults
361
364
 
362
- Builds data without needing to specify the `$type` property, and properly types the result:
365
+ Builds data by adding the `$type` property and properly types the result. Note that `$build()` does not perform validation - use `$parse()` if you need validation:
363
366
 
364
367
  ```typescript
365
368
  import { l } from '@atproto/lex'
@@ -607,7 +610,7 @@ const client = new Client(session, {
607
610
 
608
611
  - **`validateRequest`** — When `true`, outgoing request bodies are validated against the Lexicon input schema before sending. Useful in development to catch errors early. Default: `false`.
609
612
  - **`validateResponse`** — When `true`, incoming response bodies are validated against the Lexicon output schema. Disabling this can improve performance when you trust the upstream service. Default: `true`.
610
- - **`strictResponseProcessing`** — When `true` (default), the client will strictly process responses according to Lex encoding rules, rejecting responses containing invalid Lex data (e.g. floating-point numbers, malformed `$bytes` or `$link` objects). When `false`, the client accepts such responses in a lenient mode: invalid values are returned as-is rather than being rejected or converted, `datetime` string format checks become more lenient (e.g. datetimes without timezones are accepted) while other string formats remain strict, blob MIME type and size constraints are not enforced, and legacy blob references are coerced into standard `BlobRef` objects. Default: `true`.
613
+ - **`strictResponseProcessing`** — When `true` (default), the client will strictly process responses according to Lex encoding rules, rejecting responses containing invalid Lex data (e.g. floating-point numbers, malformed `$bytes` or `$link` objects). When `false`, the client accepts such responses in a lenient mode: invalid values are returned as-is rather than being rejected or converted, `datetime` string format checks become more lenient (e.g. datetimes without timezones are accepted) while other string formats remain strict, blob MIME type and size constraints are not enforced, and legacy blob reference format (objects with `cid` and `mimeType` properties) is accepted. Default: `true`.
611
614
 
612
615
  ### Core Methods
613
616
 
@@ -662,7 +665,8 @@ console.log(result.cid)
662
665
  Options:
663
666
 
664
667
  - `rkey` - Custom record key (auto-generated if not provided)
665
- - `validate` - Validate record against schema before creating
668
+ - `validate` - Asks the PDS to validate the record against schema when processing the request
669
+ - `validateRequest` - Validate the record locally against schema before submitting the request
666
670
  - `swapCommit` - CID for optimistic concurrency control
667
671
 
668
672
  #### `client.get()`
@@ -703,6 +707,8 @@ await client.put(app.bsky.actor.profile, {
703
707
  Options:
704
708
 
705
709
  - `rkey` - Record key (required for non-literal keys)
710
+ - `validate` - Validate record against schema before updating (falls back to `validateRequest` option if not specified)
711
+ - `validateRequest` - Alternative way to enable validation (used if `validate` is not specified)
706
712
  - `swapCommit` - Expected repo commit CID
707
713
  - `swapRecord` - Expected record CID
708
714
 
@@ -759,7 +765,7 @@ The `xrpcSafe()` method returns a union type that includes the success case (`Xr
759
765
  import {
760
766
  Client,
761
767
  XrpcResponseError,
762
- XrpcUpstreamError,
768
+ XrpcInvalidResponseError,
763
769
  XrpcInternalError,
764
770
  } from '@atproto/lex'
765
771
  import * as com from './lexicons/com.js'
@@ -777,20 +783,26 @@ if (result.success) {
777
783
  } else {
778
784
  // Handle failure - result is an XrpcFailure
779
785
  if (result instanceof XrpcResponseError) {
780
- // The server returned a valid XRPC error response
781
- result.error // string (e.g. "HandleNotFound", "AuthenticationRequired", etc.)
786
+ // The server responded with an error status code (4xx or 5xx).
787
+ // This is used for all error responses, whether or not they have a valid XRPC error payload.
788
+
789
+ result.error // string (e.g. "HandleNotFound", "AuthenticationRequired", "UpstreamFailure", etc.)
782
790
  result.message // string
783
791
  result.response.status // number
784
792
  result.response.headers // Headers
785
- result.payload // { body: { error: string, message?: string }; encoding: string }
786
- } else if (result instanceof XrpcUpstreamError) {
787
- // The response was not a valid XRPC response (e.g. malformed JSON,
788
- // data does not match schema, connection dropped)
793
+ result.payload // undefined | { body: unknown; encoding: string }
794
+
795
+ // Coerce to a valid XRPC error payload using toJSON():
796
+ result.toJSON() // { error: string, message?: string }
797
+ } else if (result instanceof XrpcInvalidResponseError) {
798
+ // The response was truly invalid (3xx redirect, malformed JSON, schema mismatch, etc.).
799
+ // This is a more specific error for responses that are not processable.
800
+
789
801
  result.error // "UpstreamFailure"
790
802
  result.message // string
791
803
  result.response.status // number
792
804
  result.response.headers // Headers
793
- result.payload // null | { body: unknown; encoding: string }
805
+ result.payload // undefined | { body: unknown; encoding: string }
794
806
  } else if (result instanceof XrpcInternalError) {
795
807
  // Something went wrong on the client side (network error, etc.)
796
808
  result.error // "InternalServerError"
@@ -810,9 +822,9 @@ if (result.success) {
810
822
 
811
823
  The `XrpcFailure<M>` type is a union of three error classes:
812
824
 
813
- 1. **`XrpcResponseError`** - The server returned a valid XRPC error response (non-2xx with proper error payload)
825
+ 1. **`XrpcResponseError`** - The server responded with a 4xx/5xx error status code. This is used for all error responses from the upstream server.
814
826
 
815
- 2. **`XrpcUpstreamError`** - The response was invalid or unprocessable (malformed JSON, schema mismatch, incomplete response)
827
+ 2. **`XrpcInvalidResponseError`** - The upstream server returned a 2xx/3xx that does not comply with XRPC specifications for successful responses. A sub-class, `XrpcResponseValidationError`, is used for payload schema validation failures specifically.
816
828
 
817
829
  3. **`XrpcInternalError`** - Client-side errors (network failures, timeouts, etc.)
818
830
 
@@ -919,8 +931,16 @@ import {
919
931
  ifDatetimeString, // Returns DatetimeString or undefined
920
932
 
921
933
  // Blob references
922
- BlobRef, // { $type: 'blob', ref: Cid, mimeType: string, size: number }
923
- isBlobRef, // Type guard for BlobRef objects
934
+ BlobRef, // TypedBlobRef | LegacyBlobRef
935
+ LegacyBlobRef, // { cid: string, mimeType: string }
936
+ TypedBlobRef, // { $type: 'blob', ref: Cid, mimeType: string, size: number }
937
+ isBlobRef, // Type guard for BlobRef (accepts both TypedBlobRef and LegacyBlobRef)
938
+ isLegacyBlobRef, // Type guard for LegacyBlobRef objects
939
+ isTypedBlobRef, // Type guard for TypedBlobRef objects
940
+ getBlobCid, // Extract Cid from BlobRef or LegacyBlobRef
941
+ getBlobCidString, // Extract CID string from BlobRef or LegacyBlobRef
942
+ getBlobMime, // Extract MIME type from BlobRef or LegacyBlobRef
943
+ getBlobSize, // Extract size from BlobRef (returns undefined for LegacyBlobRef)
924
944
 
925
945
  // Equality
926
946
  lexEquals, // Deep equality (handles CIDs and bytes)
@@ -947,7 +967,7 @@ isLanguageString('en-US') // true
947
967
 
948
968
  ### Datetime Strings
949
969
 
950
- Many AT Protocol records (such as posts, likes, and follows) include a `createdAt` field that expects a valid `DatetimeString`. While `new Date().toISOString()` produces a string that looks like a valid datetime, it is not guaranteed to always conform to the AT Protocol's [datetime format requirements](https://atproto.com/specs/lexicon#datetime) (for example, `Date` objects representing dates before year 10 or after year 9999 will produce non-conforming strings). To ensure correctness and type safety, use the `DatetimeString` utilities exported from `@atproto/lex`:
970
+ Many AT Protocol records (such as posts, likes, and follows) include a `createdAt` field that expects a valid `DatetimeString`. While `new Date().toISOString()` produces a string that looks like a valid datetime, it is not guaranteed to always conform to the AT Protocol's [datetime format requirements](https://atproto.com/specs/lexicon#datetime) (for example, `Date` objects representing dates before year 0 or after year 9999 will produce non-conforming strings). To ensure correctness and type safety, use the `DatetimeString` utilities exported from `@atproto/lex`:
951
971
 
952
972
  - **`toDatetimeString(date: Date)`** - Converts a `Date` object into a valid `DatetimeString`, throwing an `InvalidDatetimeError` if the date cannot be represented as a valid AT Protocol datetime.
953
973
  - **`asDatetimeString(input: string)`** - Validates and casts an arbitrary string to `DatetimeString`, throwing an `InvalidDatetimeError` if the string does not conform.
@@ -1022,37 +1042,96 @@ This will make the generated code more easily tree-shakeable from places that im
1022
1042
 
1023
1043
  ### Blob references
1024
1044
 
1025
- In AT Protocol, binary data (blobs) are referenced using `BlobRef`, which include metadata like MIME type and size. These references are what allow PDSs to determine which binary data ("files") is referenced by records.
1045
+ In AT Protocol, binary data (blobs) are referenced using blob references, which include metadata like MIME type and size. These references allow PDSs to determine which binary data ("files") is referenced by records.
1046
+
1047
+ #### TypedBlobRef: The Current Standard
1048
+
1049
+ The current standard format for blob references is `TypedBlobRef`:
1026
1050
 
1027
1051
  ```typescript
1028
- import { BlobRef, isBlobRef } from '@atproto/lex'
1052
+ import { TypedBlobRef } from '@atproto/lex'
1029
1053
 
1030
- const blobRef: BlobRef = {
1054
+ const blobRef: TypedBlobRef = {
1031
1055
  $type: 'blob',
1032
1056
  ref: parseCid('bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'),
1033
1057
  mimeType: 'image/png',
1034
1058
  size: 12345,
1035
1059
  }
1060
+ ```
1061
+
1062
+ **When creating new blobs**, always use the `TypedBlobRef` format. This is the format returned by `client.uploadBlob()` and expected by PDS endpoints.
1063
+
1064
+ #### LegacyBlobRef: Historical Format
1065
+
1066
+ Historically, blob references used a simpler format without the `$type` property:
1036
1067
 
1037
- if (isBlobRef(blobRef)) {
1038
- console.log('Valid BlobRef:', blobRef.mimeType, blobRef.size)
1068
+ ```typescript
1069
+ type LegacyBlobRef = {
1070
+ cid: string // CID as a string (not a Cid object)
1071
+ mimeType: string // No size property
1039
1072
  }
1040
1073
  ```
1041
1074
 
1042
- > [!NOTE]
1075
+ **Legacy blob references still exist in the AT Protocol network** in older records created before the format migration. While new blobs should always be created as `TypedBlobRef`, your code must be prepared to handle both formats when reading existing data.
1076
+
1077
+ #### Working with Both Formats
1078
+
1079
+ The `BlobRef` type is a union that accepts both formats:
1080
+
1081
+ ```typescript
1082
+ import {
1083
+ BlobRef,
1084
+ isBlobRef,
1085
+ isTypedBlobRef,
1086
+ isLegacyBlobRef,
1087
+ } from '@atproto/lex'
1088
+
1089
+ // When reading data, always use BlobRef to handle both formats
1090
+ function processBlobRef(blob: BlobRef) {
1091
+ if (isTypedBlobRef(blob)) {
1092
+ console.log('Modern blob:', blob.ref, blob.mimeType, blob.size)
1093
+ } else if (isLegacyBlobRef(blob)) {
1094
+ console.log('Legacy blob:', blob.cid, blob.mimeType)
1095
+ }
1096
+ }
1097
+
1098
+ // Or use the isBlobRef type guard which accepts both
1099
+ if (isBlobRef(value)) {
1100
+ // value is BlobRef (either TypedBlobRef or LegacyBlobRef)
1101
+ }
1102
+ ```
1103
+
1104
+ Helper functions work with both formats:
1105
+
1106
+ ```typescript
1107
+ import {
1108
+ getBlobCid,
1109
+ getBlobCidString,
1110
+ getBlobMime,
1111
+ getBlobSize,
1112
+ } from '@atproto/lex'
1113
+
1114
+ // These utilities work with both TypedBlobRef and LegacyBlobRef
1115
+ const cid = getBlobCid(blobRef) // Returns Cid object
1116
+ const cidStr = getBlobCidString(blobRef) // Returns string (optimized)
1117
+ const mime = getBlobMime(blobRef) // Returns mimeType
1118
+ const size = getBlobSize(blobRef) // Returns number | undefined (legacy refs lack size)
1119
+ ```
1120
+
1121
+ > [!IMPORTANT]
1043
1122
  >
1044
- > Historically, references to blobs were represented as simple objects with the following structure:
1123
+ > **Validation behavior with legacy blobs:**
1045
1124
  >
1046
- > ```typescript
1047
- > type LegacyBlobRef = {
1048
- > cid: string
1049
- > mimeType: string
1050
- > }
1051
- > ```
1125
+ > - In **strict mode** (`strict: true`, the default): Legacy blob references are rejected during validation. Use this mode when you control the data source and expect only modern blobs.
1126
+ > - In **non-strict mode** (`strict: false`): Legacy blob references are accepted. This mode is used automatically when `strictResponseProcessing: false` is set on the Client, allowing your application to handle older records from the network gracefully.
1052
1127
  >
1053
- > These should no longer be used for new records, but existing records using this format might still be encountered. To handle legacy blob references when validating data, enable the `--allowLegacyBlobs` flag when generating TypeScript schemas with `lex build`. You can use `isLegacyBlobRef()` from `@atproto/lex` to discriminate legacy blob references.
1128
+ > ```typescript
1129
+ > // Strict mode (default) - rejects legacy blobs
1130
+ > schema.$safeParse(data) // { strict: true }
1054
1131
  >
1055
- > When using non-strict validation (e.g. `$safeParse(data, { strict: false })`), legacy blob references are automatically coerced into standard `BlobRef` objects with `size: -1`, even without `--allowLegacyBlobs`.
1132
+ > // Non-strict mode - accepts legacy blobs
1133
+ > schema.$safeParse(data, { strict: false })
1134
+ > ```
1056
1135
 
1057
1136
  ### Actions
1058
1137
 
package/bin/lex CHANGED
@@ -76,12 +76,6 @@ yargs(hideBin(process.argv))
76
76
  describe:
77
77
  'package name of the library to import the lex schema utility "l" from',
78
78
  },
79
- allowLegacyBlobs: {
80
- type: 'boolean',
81
- default: false,
82
- describe:
83
- 'generate schemas that accept legacy blob references (disabled by default; enable this if you encounter issues while processing records created a long time ago)',
84
- },
85
79
  importExt: {
86
80
  type: 'string',
87
81
  default: '.js',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "license": "MIT",
5
5
  "description": "Lexicon tooling for AT",
6
6
  "keywords": [
@@ -38,19 +38,20 @@
38
38
  "dependencies": {
39
39
  "tslib": "^2.8.1",
40
40
  "yargs": "^17.0.0",
41
- "@atproto/lex-builder": "^0.0.19",
42
- "@atproto/lex-client": "^0.0.17",
43
- "@atproto/lex-data": "^0.0.14",
44
- "@atproto/lex-json": "^0.0.14",
45
- "@atproto/lex-installer": "^0.0.22",
46
- "@atproto/lex-schema": "^0.0.16"
41
+ "@atproto/lex-builder": "^0.0.21",
42
+ "@atproto/lex-client": "^0.0.19",
43
+ "@atproto/lex-data": "^0.0.15",
44
+ "@atproto/lex-json": "^0.0.15",
45
+ "@atproto/lex-installer": "^0.0.24",
46
+ "@atproto/lex-schema": "^0.0.18"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/yargs": "^17.0.33",
50
50
  "vitest": "^4.0.16"
51
51
  },
52
52
  "scripts": {
53
- "prebuild": "./bin/lex build --clear --lexicons ./lexicons --out ./tests/lexicons --lib @atproto/lex-schema -- ignore additional npm args",
53
+ "codegen": "./bin/lex build --override --indexFile --lexicons ./lexicons --out ./tests/lexicons --lib @atproto/lex-schema",
54
+ "prebuild": "pnpm run codegen",
54
55
  "build": "tsc --build tsconfig.build.json",
55
56
  "test": "vitest run"
56
57
  }