@atproto/lex 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +78 -19
  3. package/package.json +9 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @atproto/lex
2
2
 
3
+ ## 0.0.23
4
+
5
+ ### Patch Changes
6
+
7
+ - 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)]:
8
+ - @atproto/lex-schema@0.0.17
9
+ - @atproto/lex-client@0.0.18
10
+ - @atproto/lex-builder@0.0.20
11
+ - @atproto/lex-installer@0.0.23
12
+
13
+ ## 0.0.22
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:
18
+ - @atproto/lex-client@0.0.17
19
+ - @atproto/lex-schema@0.0.16
20
+ - @atproto/lex-data@0.0.14
21
+ - @atproto/lex-builder@0.0.19
22
+ - @atproto/lex-installer@0.0.22
23
+ - @atproto/lex-json@0.0.14
24
+
3
25
  ## 0.0.21
4
26
 
5
27
  ### 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!',
@@ -222,7 +222,7 @@ function renderPost(p: app.bsky.feed.post.Main) {
222
222
 
223
223
  ### Building data
224
224
 
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.
225
+ 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
226
 
227
227
  ```typescript
228
228
  import { l } from '@atproto/lex'
@@ -234,6 +234,10 @@ const post = app.bsky.feed.post.$build({
234
234
  text: 'Hello, world!',
235
235
  createdAt: l.toDatetimeString(new Date()),
236
236
  })
237
+
238
+ // For runtime validation, use $parse()/$validate() instead
239
+ const postWithDefaults = app.bsky.feed.post.$parse(post)
240
+ app.bsky.feed.post.$validate(post)
237
241
  ```
238
242
 
239
243
  ### Validation Helpers
@@ -326,7 +330,7 @@ const result = app.bsky.feed.post.$validate(value)
326
330
  value === result // true
327
331
  ```
328
332
 
329
- #### `$safeParse(data)` - Parse a value against a schema and get the resulting value
333
+ #### `$safeParse(data, options?)` - Parse a value against a schema and get the resulting value
330
334
 
331
335
  Returns a detailed validation result object without throwing:
332
336
 
@@ -347,9 +351,19 @@ if (result.success) {
347
351
  }
348
352
  ```
349
353
 
354
+ 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:
355
+
356
+ ```typescript
357
+ // Strict mode (default) - rejects datetime without timezone
358
+ app.bsky.feed.post.$safeParse(data) // { strict: true } is the default
359
+
360
+ // Non-strict mode - accepts more lenient data
361
+ app.bsky.feed.post.$safeParse(data, { strict: false })
362
+ ```
363
+
350
364
  #### `$build(data)` - Build with Defaults
351
365
 
352
- Builds data without needing to specify the `$type` property, and properly types the result:
366
+ 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:
353
367
 
354
368
  ```typescript
355
369
  import { l } from '@atproto/lex'
@@ -506,6 +520,8 @@ if (result.success) {
506
520
  }
507
521
  ```
508
522
 
523
+ Both `xrpc()` and `xrpcSafe()` accept `validateRequest`, `validateResponse`, and `strictResponseProcessing` options to control validation and strictness per-call. See [Validation and Strictness Options](#validation-and-strictness-options) for details.
524
+
509
525
  ## Client API
510
526
 
511
527
  The `Client` class provides high-level helpers for common AT Protocol "repo" operations: `create()`, `get()`, `put()`, `delete()`, `list()`, `uploadBlob()`, and more. A `Client` instance is typically useful for making requests in the context of an authenticated user session, as it automatically handles headers and provides default values based on the authenticated user's DID.
@@ -574,6 +590,29 @@ const client = new Client(session, {
574
590
  })
575
591
  ```
576
592
 
593
+ #### Validation and Strictness Options
594
+
595
+ The `Client` constructor accepts options to control request/response validation and how invalid Lex data is handled. These defaults apply to all XRPC calls made through the client, and can be overridden per-call via `client.call()`, `client.xrpc()` or `client.xrpcSafe()`.
596
+
597
+ ```typescript
598
+ const client = new Client(session, {
599
+ // Validate requests against the method's input schema (default: false)
600
+ validateRequest: true,
601
+
602
+ // Validate responses against the method's output schema (default: true)
603
+ validateResponse: true,
604
+
605
+ // Strictly process responses according to Lex encoding rules. When set to
606
+ // false, accepts responses containing invalid Lex data such as floats or
607
+ // malformed $bytes/$link objects (default: true)
608
+ strictResponseProcessing: false,
609
+ })
610
+ ```
611
+
612
+ - **`validateRequest`** — When `true`, outgoing request bodies are validated against the Lexicon input schema before sending. Useful in development to catch errors early. Default: `false`.
613
+ - **`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`.
614
+ - **`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`.
615
+
577
616
  ### Core Methods
578
617
 
579
618
  #### `client.call()`
@@ -603,7 +642,6 @@ const timeline = await client.call(
603
642
  },
604
643
  {
605
644
  signal: abortSignal,
606
- headers: { 'custom-header': 'value' },
607
645
  },
608
646
  )
609
647
  ```
@@ -628,7 +666,8 @@ console.log(result.cid)
628
666
  Options:
629
667
 
630
668
  - `rkey` - Custom record key (auto-generated if not provided)
631
- - `validate` - Validate record against schema before creating
669
+ - `validate` - Asks the PDS to validate the record against schema when processing the request
670
+ - `validateRequest` - Validate the record locally against schema before submitting the request
632
671
  - `swapCommit` - CID for optimistic concurrency control
633
672
 
634
673
  #### `client.get()`
@@ -669,6 +708,8 @@ await client.put(app.bsky.actor.profile, {
669
708
  Options:
670
709
 
671
710
  - `rkey` - Record key (required for non-literal keys)
711
+ - `validate` - Validate record against schema before updating (falls back to `validateRequest` option if not specified)
712
+ - `validateRequest` - Alternative way to enable validation (used if `validate` is not specified)
672
713
  - `swapCommit` - Expected repo commit CID
673
714
  - `swapRecord` - Expected record CID
674
715
 
@@ -725,7 +766,7 @@ The `xrpcSafe()` method returns a union type that includes the success case (`Xr
725
766
  import {
726
767
  Client,
727
768
  XrpcResponseError,
728
- XrpcUpstreamError,
769
+ XrpcInvalidResponseError,
729
770
  XrpcInternalError,
730
771
  } from '@atproto/lex'
731
772
  import * as com from './lexicons/com.js'
@@ -743,20 +784,26 @@ if (result.success) {
743
784
  } else {
744
785
  // Handle failure - result is an XrpcFailure
745
786
  if (result instanceof XrpcResponseError) {
746
- // The server returned a valid XRPC error response
747
- result.error // string (e.g. "HandleNotFound", "AuthenticationRequired", etc.)
787
+ // The server responded with an error status code (4xx or 5xx).
788
+ // This is used for all error responses, whether or not they have a valid XRPC error payload.
789
+
790
+ result.error // string (e.g. "HandleNotFound", "AuthenticationRequired", "UpstreamFailure", etc.)
748
791
  result.message // string
749
792
  result.response.status // number
750
793
  result.response.headers // Headers
751
- result.payload // { body: { error: string, message?: string }; encoding: string }
752
- } else if (result instanceof XrpcUpstreamError) {
753
- // The response was not a valid XRPC response (e.g. malformed JSON,
754
- // data does not match schema, connection dropped)
794
+ result.payload // undefined | { body: unknown; encoding: string }
795
+
796
+ // Coerce to a valid XRPC error payload using toJSON():
797
+ result.toJSON() // { error: string, message?: string }
798
+ } else if (result instanceof XrpcInvalidResponseError) {
799
+ // The response was truly invalid (3xx redirect, malformed JSON, schema mismatch, etc.).
800
+ // This is a more specific error for responses that are not processable.
801
+
755
802
  result.error // "UpstreamFailure"
756
803
  result.message // string
757
804
  result.response.status // number
758
805
  result.response.headers // Headers
759
- result.payload // null | { body: unknown; encoding: string }
806
+ result.payload // undefined | { body: unknown; encoding: string }
760
807
  } else if (result instanceof XrpcInternalError) {
761
808
  // Something went wrong on the client side (network error, etc.)
762
809
  result.error // "InternalServerError"
@@ -776,9 +823,9 @@ if (result.success) {
776
823
 
777
824
  The `XrpcFailure<M>` type is a union of three error classes:
778
825
 
779
- 1. **`XrpcResponseError`** - The server returned a valid XRPC error response (non-2xx with proper error payload)
826
+ 1. **`XrpcResponseError`** - The server responded with a 4xx/5xx error status code. This is used for all error responses from the upstream server.
780
827
 
781
- 2. **`XrpcUpstreamError`** - The response was invalid or unprocessable (malformed JSON, schema mismatch, incomplete response)
828
+ 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.
782
829
 
783
830
  3. **`XrpcInternalError`** - Client-side errors (network failures, timeouts, etc.)
784
831
 
@@ -857,6 +904,16 @@ console.log(response.headers)
857
904
  console.log(response.body)
858
905
  ```
859
906
 
907
+ Validation and strictness options (`validateRequest`, `validateResponse`, `strictResponseProcessing`) can also be passed per-call to override the client defaults:
908
+
909
+ ```typescript
910
+ const response = await client.xrpc(app.bsky.feed.getTimeline, {
911
+ params: { limit: 50 },
912
+ strictResponseProcessing: false, // Accept non-strict Lex data for this call
913
+ validateResponse: false, // Skip schema validation for this call
914
+ })
915
+ ```
916
+
860
917
  ## Utilities
861
918
 
862
919
  Various utilities for working with CIDs, datetime strings, string lengths, language tags, and low-level JSON encoding are exported from the package:
@@ -903,7 +960,7 @@ isLanguageString('en-US') // true
903
960
 
904
961
  ### Datetime Strings
905
962
 
906
- 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`:
963
+ 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`:
907
964
 
908
965
  - **`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.
909
966
  - **`asDatetimeString(input: string)`** - Validates and casts an arbitrary string to `DatetimeString`, throwing an `InvalidDatetimeError` if the string does not conform.
@@ -1001,12 +1058,14 @@ if (isBlobRef(blobRef)) {
1001
1058
  >
1002
1059
  > ```typescript
1003
1060
  > type LegacyBlobRef = {
1004
- > ref: string
1061
+ > cid: string
1005
1062
  > mimeType: string
1006
1063
  > }
1007
1064
  > ```
1008
1065
  >
1009
1066
  > 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.
1067
+ >
1068
+ > 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`.
1010
1069
 
1011
1070
  ### Actions
1012
1071
 
@@ -1028,7 +1087,7 @@ Actions receive:
1028
1087
 
1029
1088
  - `client` - The Client instance (to make XRPC calls)
1030
1089
  - `input` - The input data for the action
1031
- - `options` - Call options (signal, headers)
1090
+ - `options` - Call options (signal)
1032
1091
 
1033
1092
  #### Using Actions
1034
1093
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
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.18",
42
- "@atproto/lex-client": "^0.0.16",
43
- "@atproto/lex-data": "^0.0.13",
44
- "@atproto/lex-json": "^0.0.13",
45
- "@atproto/lex-installer": "^0.0.21",
46
- "@atproto/lex-schema": "^0.0.15"
41
+ "@atproto/lex-builder": "^0.0.20",
42
+ "@atproto/lex-client": "^0.0.18",
43
+ "@atproto/lex-data": "^0.0.14",
44
+ "@atproto/lex-json": "^0.0.14",
45
+ "@atproto/lex-installer": "^0.0.23",
46
+ "@atproto/lex-schema": "^0.0.17"
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
  }