@atcute/lexicons 1.2.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/interfaces/blob.d.ts +25 -2
  2. package/dist/interfaces/blob.d.ts.map +1 -1
  3. package/dist/interfaces/blob.js +54 -0
  4. package/dist/interfaces/blob.js.map +1 -1
  5. package/dist/interfaces/bytes.d.ts.map +1 -1
  6. package/dist/interfaces/bytes.js.map +1 -1
  7. package/dist/interfaces/cid-link.d.ts.map +1 -1
  8. package/dist/interfaces/cid-link.js.map +1 -1
  9. package/dist/interfaces/index.d.ts +1 -1
  10. package/dist/interfaces/index.d.ts.map +1 -1
  11. package/dist/interfaces/index.js +1 -1
  12. package/dist/interfaces/index.js.map +1 -1
  13. package/dist/syntax/at-identifier.d.ts.map +1 -1
  14. package/dist/syntax/at-identifier.js.map +1 -1
  15. package/dist/syntax/at-uri.d.ts +1 -1
  16. package/dist/syntax/at-uri.d.ts.map +1 -1
  17. package/dist/syntax/at-uri.js.map +1 -1
  18. package/dist/syntax/cid.d.ts +1 -1
  19. package/dist/syntax/cid.d.ts.map +1 -1
  20. package/dist/syntax/cid.js.map +1 -1
  21. package/dist/syntax/datetime.d.ts +1 -1
  22. package/dist/syntax/datetime.d.ts.map +1 -1
  23. package/dist/syntax/datetime.js.map +1 -1
  24. package/dist/syntax/did.d.ts +1 -1
  25. package/dist/syntax/did.d.ts.map +1 -1
  26. package/dist/syntax/did.js.map +1 -1
  27. package/dist/syntax/handle.d.ts +1 -1
  28. package/dist/syntax/handle.d.ts.map +1 -1
  29. package/dist/syntax/handle.js.map +1 -1
  30. package/dist/syntax/language.d.ts +1 -1
  31. package/dist/syntax/language.d.ts.map +1 -1
  32. package/dist/syntax/language.js.map +1 -1
  33. package/dist/syntax/nsid.d.ts +1 -1
  34. package/dist/syntax/nsid.d.ts.map +1 -1
  35. package/dist/syntax/nsid.js.map +1 -1
  36. package/dist/syntax/record-key.d.ts +1 -1
  37. package/dist/syntax/record-key.d.ts.map +1 -1
  38. package/dist/syntax/record-key.js.map +1 -1
  39. package/dist/syntax/tid.d.ts +1 -1
  40. package/dist/syntax/tid.d.ts.map +1 -1
  41. package/dist/syntax/tid.js.map +1 -1
  42. package/dist/syntax/uri.d.ts +1 -1
  43. package/dist/syntax/uri.d.ts.map +1 -1
  44. package/dist/syntax/uri.js.map +1 -1
  45. package/dist/syntax/utils/ascii.d.ts.map +1 -1
  46. package/dist/syntax/utils/ascii.js.map +1 -1
  47. package/dist/utils.d.ts +1 -1
  48. package/dist/utils.d.ts.map +1 -1
  49. package/dist/utils.js.map +1 -1
  50. package/dist/validations/index.d.ts +41 -11
  51. package/dist/validations/index.d.ts.map +1 -1
  52. package/dist/validations/index.js +89 -9
  53. package/dist/validations/index.js.map +1 -1
  54. package/dist/validations/utils.d.ts.map +1 -1
  55. package/dist/validations/utils.js.map +1 -1
  56. package/lib/interfaces/blob.ts +78 -0
  57. package/lib/interfaces/index.ts +9 -1
  58. package/lib/validations/index.ts +125 -9
  59. package/package.json +5 -5
@@ -68,6 +68,8 @@ export type IssueLeaf = { ok: false; msg: IssueFormatter } & (
68
68
  | { code: 'invalid_string_length'; minLength: number; maxLength: number }
69
69
  | { code: 'invalid_array_length'; minLength: number; maxLength: number }
70
70
  | { code: 'invalid_bytes_size'; minSize: number; maxSize: number }
71
+ | { code: 'invalid_blob_size'; maxSize: number }
72
+ | { code: 'invalid_blob_mime_type'; accept: readonly string[] }
71
73
  );
72
74
 
73
75
  export type IssueTree =
@@ -85,7 +87,9 @@ export type Issue =
85
87
  | { code: 'invalid_string_graphemes'; path: Key[]; minGraphemes: number; maxGraphemes: number }
86
88
  | { code: 'invalid_string_length'; path: Key[]; minLength: number; maxLength: number }
87
89
  | { code: 'invalid_array_length'; path: Key[]; minLength: number; maxLength: number }
88
- | { code: 'invalid_bytes_size'; path: Key[]; minSize: number; maxSize: number };
90
+ | { code: 'invalid_bytes_size'; path: Key[]; minSize: number; maxSize: number }
91
+ | { code: 'invalid_blob_size'; path: Key[]; maxSize: number }
92
+ | { code: 'invalid_blob_mime_type'; path: Key[]; accept: readonly string[] };
89
93
 
90
94
  // #__NO_SIDE_EFFECTS__
91
95
  const joinIssues = (left: IssueTree | undefined, right: IssueTree): IssueTree => {
@@ -127,6 +131,8 @@ type kType = typeof kType;
127
131
  export const FLAG_EMPTY = 0;
128
132
  // Don't continue validation if an error is encountered
129
133
  export const FLAG_ABORT_EARLY = 1 << 0;
134
+ // Enable strict blob validation (size, MIME type constraints, reject legacy blobs)
135
+ export const FLAG_STRICT = 1 << 1;
130
136
 
131
137
  type MatcherResult = undefined | Ok<unknown> | IssueTree;
132
138
  type Matcher = (input: unknown, flags: number) => MatcherResult;
@@ -313,12 +319,22 @@ class ErrImpl implements Err {
313
319
  }
314
320
  }
315
321
 
322
+ export interface ValidationOptions {
323
+ /** enable strict blob validation (size, MIME type constraints, reject legacy blobs) */
324
+ strict?: boolean;
325
+ }
326
+
316
327
  // #__NO_SIDE_EFFECTS__
317
328
  export const is = <const TSchema extends BaseSchema>(
318
329
  schema: TSchema,
319
330
  input: unknown,
331
+ options?: ValidationOptions,
320
332
  ): input is InferInput<TSchema> => {
321
- const r = schema['~run'](input, FLAG_ABORT_EARLY);
333
+ let flags = FLAG_ABORT_EARLY;
334
+ if (options?.strict) {
335
+ flags |= FLAG_STRICT;
336
+ }
337
+ const r = schema['~run'](input, flags);
322
338
  return r === undefined || r.ok;
323
339
  };
324
340
 
@@ -326,8 +342,13 @@ export const is = <const TSchema extends BaseSchema>(
326
342
  export const safeParse = <const TSchema extends BaseSchema>(
327
343
  schema: TSchema,
328
344
  input: unknown,
345
+ options?: ValidationOptions,
329
346
  ): ValidationResult<InferOutput<TSchema>> => {
330
- const r = schema['~run'](input, FLAG_EMPTY);
347
+ let flags = FLAG_EMPTY;
348
+ if (options?.strict) {
349
+ flags |= FLAG_STRICT;
350
+ }
351
+ const r = schema['~run'](input, flags);
331
352
 
332
353
  if (r === undefined) {
333
354
  return ok(input as InferOutput<TSchema>);
@@ -343,8 +364,13 @@ export const safeParse = <const TSchema extends BaseSchema>(
343
364
  export const parse = <const TSchema extends BaseSchema>(
344
365
  schema: TSchema,
345
366
  input: unknown,
367
+ options?: ValidationOptions,
346
368
  ): InferOutput<TSchema> => {
347
- const r = schema['~run'](input, FLAG_EMPTY);
369
+ let flags = FLAG_EMPTY;
370
+ if (options?.strict) {
371
+ flags |= FLAG_STRICT;
372
+ }
373
+ const r = schema['~run'](input, flags);
348
374
 
349
375
  if (r === undefined) {
350
376
  return input as InferOutput<TSchema>;
@@ -886,7 +912,7 @@ const ISSUE_EXPECTED_BLOB: IssueLeaf = {
886
912
  const BLOB_SCHEMA: BlobSchema = {
887
913
  kind: 'schema',
888
914
  type: 'blob',
889
- '~run'(input, _flags) {
915
+ '~run'(input, flags) {
890
916
  if (typeof input !== 'object' || input === null) {
891
917
  return ISSUE_EXPECTED_BLOB;
892
918
  }
@@ -895,7 +921,7 @@ const BLOB_SCHEMA: BlobSchema = {
895
921
  return undefined;
896
922
  }
897
923
 
898
- if (interfaces.isLegacyBlob(input)) {
924
+ if (!(flags & FLAG_STRICT) && interfaces.isLegacyBlob(input)) {
899
925
  const blob: interfaces.Blob = {
900
926
  $type: 'blob',
901
927
  mimeType: input.mimeType,
@@ -918,6 +944,94 @@ export const blob = (): BlobSchema => {
918
944
  return BLOB_SCHEMA;
919
945
  };
920
946
 
947
+ // #region Blob constraints
948
+
949
+ export interface BlobSizeConstraint<
950
+ TMaxSize extends number = number,
951
+ > extends BaseConstraint<interfaces.Blob> {
952
+ readonly type: 'blob_size';
953
+ readonly maxSize: TMaxSize;
954
+ }
955
+
956
+ // #__NO_SIDE_EFFECTS__
957
+ export const blobSize = <const TMaxSize extends number>(maxSize: TMaxSize): BlobSizeConstraint<TMaxSize> => {
958
+ const issue: IssueLeaf = {
959
+ ok: false,
960
+ code: 'invalid_blob_size',
961
+ maxSize: maxSize,
962
+ msg() {
963
+ return `blob size must not exceed ${maxSize} bytes`;
964
+ },
965
+ };
966
+
967
+ return {
968
+ kind: 'constraint',
969
+ type: 'blob_size',
970
+ maxSize: maxSize,
971
+ '~run'(input, flags) {
972
+ if (!(flags & FLAG_STRICT)) {
973
+ return undefined;
974
+ }
975
+ if ((input as interfaces.Blob).size > maxSize) {
976
+ return issue;
977
+ }
978
+ return undefined;
979
+ },
980
+ };
981
+ };
982
+
983
+ export interface BlobAcceptConstraint extends BaseConstraint<interfaces.Blob> {
984
+ readonly type: 'blob_accept';
985
+ readonly accept: readonly string[];
986
+ }
987
+
988
+ // #__NO_SIDE_EFFECTS__
989
+ export const blobAccept = (accept: readonly string[]): BlobAcceptConstraint => {
990
+ const normalized = accept.map((p) => p.toLowerCase());
991
+
992
+ const issue: IssueLeaf = {
993
+ ok: false,
994
+ code: 'invalid_blob_mime_type',
995
+ accept: accept,
996
+ msg() {
997
+ return `blob MIME type must match: ${accept.join(', ')}`;
998
+ },
999
+ };
1000
+
1001
+ return {
1002
+ kind: 'constraint',
1003
+ type: 'blob_accept',
1004
+ accept: accept,
1005
+ '~run'(input, flags) {
1006
+ if (!(flags & FLAG_STRICT)) {
1007
+ return undefined;
1008
+ }
1009
+ const mimeType = (input as interfaces.Blob).mimeType.toLowerCase();
1010
+
1011
+ for (let idx = 0, len = normalized.length; idx < len; idx++) {
1012
+ const pattern = normalized[idx];
1013
+
1014
+ if (pattern === '*/*') {
1015
+ return undefined;
1016
+ }
1017
+ if (pattern.endsWith('/*')) {
1018
+ if (mimeType.startsWith(pattern.slice(0, -1))) {
1019
+ return undefined;
1020
+ }
1021
+ } else {
1022
+ if (mimeType === pattern) {
1023
+ return undefined;
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ return issue;
1029
+ },
1030
+ };
1031
+ };
1032
+
1033
+ // #endregion
1034
+
921
1035
  // #region IPLD bytes schema
922
1036
 
923
1037
  export interface BytesSchema extends BaseSchema<interfaces.Bytes, interfaces.Bytes> {
@@ -1601,7 +1715,8 @@ export const record = <TKey extends RecordKeySchema, TObject extends ObjectSchem
1601
1715
 
1602
1716
  // #region Variant schema
1603
1717
 
1604
- type VariantTuple = readonly ObjectSchema<any>[];
1718
+ type VariantMember = ObjectSchema<any> | RecordSchema<ObjectSchema<any>, RecordKeySchema>;
1719
+ type VariantTuple = readonly VariantMember[];
1605
1720
 
1606
1721
  type InferVariantInput<TMembers extends VariantTuple> = $type.enforce<InferInput<TMembers[number]>>;
1607
1722
 
@@ -1629,7 +1744,7 @@ export const variant: {
1629
1744
  members: TMembers,
1630
1745
  closed: TClosed,
1631
1746
  ): VariantSchema<TMembers, TClosed>;
1632
- } = (members: ObjectSchema[], closed: boolean = false): VariantSchema<any, any> => {
1747
+ } = (members: VariantMember[], closed: boolean = false): VariantSchema<any, any> => {
1633
1748
  return {
1634
1749
  kind: 'schema',
1635
1750
  type: 'variant',
@@ -1640,7 +1755,8 @@ export const variant: {
1640
1755
  const schemas: ObjectSchema[] = [];
1641
1756
 
1642
1757
  for (let idx = 0, len = members.length; idx < len; idx++) {
1643
- const member = members[idx]!;
1758
+ const raw = members[idx]!;
1759
+ const member = raw.type === 'record' ? raw.object : raw;
1644
1760
  const shape = member.shape;
1645
1761
 
1646
1762
  let t = shape.$type as MaybeOptional<LiteralSchema<syntax.Nsid>> | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atcute/lexicons",
3
- "version": "1.2.9",
3
+ "version": "1.3.0",
4
4
  "description": "AT Protocol core lexicon types and schema validations",
5
5
  "license": "0BSD",
6
6
  "repository": {
@@ -29,13 +29,13 @@
29
29
  "@standard-schema/spec": "^1.1.0",
30
30
  "esm-env": "^1.2.2",
31
31
  "@atcute/uint8array": "^1.1.1",
32
- "@atcute/util-text": "^1.1.1"
32
+ "@atcute/util-text": "^1.2.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@vitest/coverage-v8": "^4.0.18",
36
- "vitest": "^4.0.18",
35
+ "@vitest/coverage-v8": "^4.1.2",
36
+ "vitest": "^4.1.2",
37
37
  "@atcute/cbor": "^2.3.2",
38
- "@atcute/multibase": "^1.1.8"
38
+ "@atcute/multibase": "^1.2.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsgo --project tsconfig.build.json",